mirror of
https://github.com/home-assistant/core.git
synced 2026-09-24 23:41:48 -05:00
Bump midea-local to 11.0.1 (#181405)
This commit is contained in:
@@ -251,18 +251,15 @@ class MideaClimate(MideaEntity, ClimateEntity):
|
||||
if hvac_mode == HVACMode.OFF:
|
||||
self.turn_off()
|
||||
else:
|
||||
mode = None
|
||||
if hvac_mode:
|
||||
if hvac_mode not in self.hvac_modes:
|
||||
raise ServiceValidationError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="unsupported_hvac_mode",
|
||||
translation_placeholders={"hvac_mode": hvac_mode},
|
||||
)
|
||||
mode = self.hvac_modes.index(hvac_mode)
|
||||
self._device.set_target_temperature(
|
||||
if hvac_mode and hvac_mode not in self.hvac_modes:
|
||||
raise ServiceValidationError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="unsupported_hvac_mode",
|
||||
translation_placeholders={"hvac_mode": hvac_mode},
|
||||
)
|
||||
self._device.set_raw_target_temperature(
|
||||
target_temperature=temperature,
|
||||
mode=mode,
|
||||
hvac_mode=hvac_mode,
|
||||
zone=self._zone,
|
||||
)
|
||||
|
||||
@@ -355,31 +352,13 @@ class MideaACClimate(MideaClimate):
|
||||
@property
|
||||
@override
|
||||
def hvac_modes(self) -> list[HVACMode]:
|
||||
"""Midea AC Climate hvac modes."""
|
||||
return (
|
||||
[HVACMode.OFF]
|
||||
+ (
|
||||
[HVACMode.AUTO]
|
||||
if self._device.capabilities.get("auto_mode", True)
|
||||
else []
|
||||
)
|
||||
+ (
|
||||
[HVACMode.COOL]
|
||||
if self._device.capabilities.get("cool_mode", True)
|
||||
else []
|
||||
)
|
||||
+ (
|
||||
[HVACMode.DRY]
|
||||
if self._device.capabilities.get("dry_mode", True)
|
||||
else []
|
||||
)
|
||||
+ (
|
||||
[HVACMode.HEAT]
|
||||
if self._device.capabilities.get("heat_mode", True)
|
||||
else []
|
||||
)
|
||||
+ [HVACMode.FAN_ONLY]
|
||||
)
|
||||
"""Midea AC Climate hvac modes.
|
||||
|
||||
The device reports the generic mode names in protocol-index order
|
||||
(``off`` first, ``fan_only`` last), already filtered by its B5
|
||||
capabilities.
|
||||
"""
|
||||
return [HVACMode(mode) for mode in self._device.raw_hvac_modes]
|
||||
|
||||
@property
|
||||
@override
|
||||
@@ -439,13 +418,7 @@ class MideaACClimate(MideaClimate):
|
||||
@override
|
||||
def set_swing_mode(self, swing_mode: str) -> None:
|
||||
"""Midea AC Climate set swing mode."""
|
||||
swing_vertical, swing_horizontal = _SWING_MODE_MAP.get(
|
||||
swing_mode, (False, False)
|
||||
)
|
||||
self._device.set_swing(
|
||||
swing_vertical=swing_vertical,
|
||||
swing_horizontal=swing_horizontal,
|
||||
)
|
||||
self._device.set_raw_swing_mode(swing_mode)
|
||||
|
||||
|
||||
class MideaCCClimate(MideaClimate):
|
||||
@@ -481,16 +454,13 @@ class MideaCCClimate(MideaClimate):
|
||||
@override
|
||||
def fan_modes(self) -> list[str] | None:
|
||||
"""Midea CC Climate fan modes."""
|
||||
return self._device.fan_modes
|
||||
return list(self._device.raw_fan_modes)
|
||||
|
||||
@property
|
||||
@override
|
||||
def fan_mode(self) -> str | None:
|
||||
"""Midea CC Climate fan mode."""
|
||||
fan_mode = self._device.get_attribute(CCAttributes.fan_speed)
|
||||
if not isinstance(fan_mode, str):
|
||||
return None
|
||||
return fan_mode
|
||||
return self._device.raw_fan_mode
|
||||
|
||||
@property
|
||||
@override
|
||||
@@ -510,7 +480,7 @@ class MideaCCClimate(MideaClimate):
|
||||
@override
|
||||
def set_fan_mode(self, fan_mode: str) -> None:
|
||||
"""Midea CC Climate set fan mode."""
|
||||
self._device.set_attribute(attr=CCAttributes.fan_speed, value=fan_mode)
|
||||
self._device.set_raw_fan_mode(fan_mode)
|
||||
|
||||
@override
|
||||
def set_swing_mode(self, swing_mode: str) -> None:
|
||||
@@ -554,10 +524,9 @@ class MideaCFClimate(MideaClimate):
|
||||
if hvac_mode == HVACMode.OFF:
|
||||
self.turn_off()
|
||||
else:
|
||||
target_temperature = self.target_temperature or self.min_temp
|
||||
self._device.set_target_temperature(
|
||||
target_temperature=target_temperature,
|
||||
mode=self._hvac_to_protocol_mode(hvac_mode),
|
||||
self._device.set_raw_target_temperature(
|
||||
target_temperature=self.target_temperature or self.min_temp,
|
||||
hvac_mode=hvac_mode,
|
||||
)
|
||||
|
||||
@property
|
||||
@@ -709,7 +678,7 @@ class MideaC3Climate(MideaClimate):
|
||||
if hvac_mode == HVACMode.OFF:
|
||||
self.turn_off()
|
||||
else:
|
||||
self._device.set_mode(self._zone, self._hvac_to_protocol_mode(hvac_mode))
|
||||
self._device.set_raw_hvac_mode(hvac_mode, zone=self._zone)
|
||||
|
||||
|
||||
class MideaFBClimate(MideaClimate):
|
||||
|
||||
@@ -14,6 +14,7 @@ from midealocal.const import DeviceType, ProtocolVersion
|
||||
from midealocal.device import MideaDevice
|
||||
from midealocal.devices import device_selector
|
||||
from midealocal.discover import discover
|
||||
from midealocal.exceptions import MideaCloudError
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
|
||||
@@ -112,6 +113,8 @@ class MideaConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
self.supports: dict = {}
|
||||
self.cloud: MideaCloud | None = None
|
||||
self._login_data: dict[str, str] | None = None
|
||||
self._cloud_error: str | None = None
|
||||
self._cloud_error_code: int | None = None
|
||||
unsorted = dict(MIDEA_DEVICE_NAMES)
|
||||
|
||||
# sort and assign supports
|
||||
@@ -127,10 +130,33 @@ class MideaConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
self.preset_cloud_name: str = preset_account["cloud_name"]
|
||||
|
||||
def _clear_login_state(self) -> None:
|
||||
"""Clear flow-scoped credentials and cloud."""
|
||||
"""Clear flow-scoped credentials and cloud.
|
||||
|
||||
The pending cloud error/code are intentionally left in place: this is
|
||||
called right before re-showing a form that still needs to render that
|
||||
error. They are refreshed on the next cloud call (``_check_cloud_login``
|
||||
resets them) or when ``async_step_auto`` re-runs with input.
|
||||
"""
|
||||
self._login_data = None
|
||||
self.cloud = None
|
||||
|
||||
def _reset_cloud_error(self) -> None:
|
||||
"""Forget any cloud error carried over from a previous submit."""
|
||||
self._cloud_error = None
|
||||
self._cloud_error_code = None
|
||||
|
||||
def _form_error(self, error: str | None) -> dict[str, Any]:
|
||||
"""Build async_show_form error kwargs, adding the cloud error code if known."""
|
||||
if not error:
|
||||
return {"errors": None}
|
||||
result: dict[str, Any] = {"errors": {"base": error}}
|
||||
# only the cloud error slug carries a numeric code to show alongside it
|
||||
if error == self._cloud_error and self._cloud_error_code is not None:
|
||||
result["description_placeholders"] = {
|
||||
"error_code": str(self._cloud_error_code)
|
||||
}
|
||||
return result
|
||||
|
||||
def _already_configured(self, device_id: str, ip_address: str) -> bool:
|
||||
"""Check device from json with device_id or ip address."""
|
||||
for entry in self._async_current_entries():
|
||||
@@ -197,7 +223,7 @@ class MideaConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
cloud_server_options,
|
||||
default_server,
|
||||
user_input=user_input,
|
||||
error="login_failed",
|
||||
error=self._cloud_error or "login_failed",
|
||||
)
|
||||
# user not login, show login form in UI
|
||||
return self._show_login_credentials_form(
|
||||
@@ -234,7 +260,7 @@ class MideaConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
return self.async_show_form(
|
||||
step_id="login_credentials",
|
||||
data_schema=schema,
|
||||
errors={"base": error} if error else None,
|
||||
**self._form_error(error),
|
||||
)
|
||||
|
||||
async def async_step_auth_method(
|
||||
@@ -261,7 +287,7 @@ class MideaConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
)
|
||||
|
||||
return await self.async_step_auth_method(
|
||||
error="preset_login_failed",
|
||||
error=self._cloud_error or "preset_login_failed",
|
||||
)
|
||||
|
||||
return self.async_show_form(
|
||||
@@ -282,7 +308,7 @@ class MideaConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
),
|
||||
}
|
||||
),
|
||||
errors={"base": error} if error else None,
|
||||
**self._form_error(error),
|
||||
)
|
||||
|
||||
async def async_step_list(
|
||||
@@ -385,8 +411,16 @@ class MideaConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
account,
|
||||
password,
|
||||
)
|
||||
self._reset_cloud_error()
|
||||
# check cloud login after self.cloud exist
|
||||
if await self.cloud.login():
|
||||
try:
|
||||
logged_in = await self.cloud.login()
|
||||
except MideaCloudError as err:
|
||||
LOGGER.debug("Cloud login to %s failed: %s", cloud_name, err)
|
||||
self._cloud_error = err.translation_key
|
||||
self._cloud_error_code = err.code
|
||||
return False
|
||||
if logged_in:
|
||||
LOGGER.debug(
|
||||
"Cloud login succeeded for %s",
|
||||
cloud_name,
|
||||
@@ -410,7 +444,20 @@ class MideaConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
assert self.cloud is not None
|
||||
|
||||
# get device token/key from cloud, plus the well-known default keys
|
||||
keys = await self.cloud.get_cloud_keys(appliance_id)
|
||||
try:
|
||||
keys = await self.cloud.get_cloud_keys(appliance_id)
|
||||
except MideaCloudError as err:
|
||||
# A cloud rejection (e.g. code 3201) is not fatal: a V3 device may
|
||||
# still authenticate with a built-in default key. Remember the error
|
||||
# for display and keep going with the default keys only.
|
||||
LOGGER.debug(
|
||||
"Cloud rejected the token request for device %s: %s",
|
||||
appliance_id,
|
||||
err,
|
||||
)
|
||||
self._cloud_error = err.translation_key
|
||||
self._cloud_error_code = err.code
|
||||
keys = {}
|
||||
if default_key:
|
||||
keys = {**keys, **(await MideaCloud.get_default_keys())}
|
||||
error = "connect_error"
|
||||
@@ -447,7 +494,10 @@ class MideaConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
LOGGER.debug(
|
||||
"Unable to connect device with all the token/key",
|
||||
)
|
||||
return {"error": error}
|
||||
result: dict[str, Any] = {"error": error}
|
||||
if self._cloud_error is not None:
|
||||
result["cloud_error"] = self._cloud_error
|
||||
return result
|
||||
|
||||
async def async_step_auto(
|
||||
self,
|
||||
@@ -457,6 +507,7 @@ class MideaConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
"""Discovery device detail info."""
|
||||
# input device exist
|
||||
if user_input is not None:
|
||||
self._reset_cloud_error()
|
||||
device_id = user_input[CONF_DEVICE]
|
||||
device = self.devices[device_id]
|
||||
self.found_device = {
|
||||
@@ -486,6 +537,10 @@ class MideaConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
|
||||
# phase 1, try with user input login data
|
||||
keys = await self._check_key_from_cloud(device_id)
|
||||
# _check_key_from_cloud sets the pending cloud error/code on a
|
||||
# cloud rejection; keep phase 1's in case phase 2 is less specific
|
||||
phase1_error = self._cloud_error
|
||||
phase1_error_code = self._cloud_error_code
|
||||
|
||||
# no available key, continue the phase 2
|
||||
if not keys.get("token") or not keys.get("key"):
|
||||
@@ -496,10 +551,14 @@ class MideaConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
|
||||
# get key phase 2: reinit cloud with preset account
|
||||
if not await self._check_cloud_login(force_login=True):
|
||||
# _check_cloud_login clears the pending error; if it only
|
||||
# returned False (no raise), fall back to phase 1's error.
|
||||
if not self._cloud_error and phase1_error:
|
||||
self._cloud_error = phase1_error
|
||||
self._cloud_error_code = phase1_error_code
|
||||
error = self._cloud_error or "preset_login_failed"
|
||||
self._clear_login_state()
|
||||
return await self.async_step_auto(
|
||||
error="preset_login_failed",
|
||||
)
|
||||
return await self.async_step_auto(error=error)
|
||||
# try to get a passed key, without default_key
|
||||
keys = await self._check_key_from_cloud(
|
||||
device_id,
|
||||
@@ -512,10 +571,12 @@ class MideaConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
"Can't get available token from Midea server for device %s",
|
||||
device_id,
|
||||
)
|
||||
if not self._cloud_error and phase1_error:
|
||||
self._cloud_error = phase1_error
|
||||
self._cloud_error_code = phase1_error_code
|
||||
error = self._cloud_error or "token_unavailable"
|
||||
self._clear_login_state()
|
||||
return await self.async_step_auto(
|
||||
error="token_unavailable",
|
||||
)
|
||||
return await self.async_step_auto(error=error)
|
||||
# get key pass
|
||||
self.found_device[CONF_TOKEN] = keys["token"]
|
||||
self.found_device[CONF_KEY] = keys["key"]
|
||||
@@ -539,7 +600,7 @@ class MideaConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
): vol.In(self.available_device),
|
||||
},
|
||||
),
|
||||
errors={"base": error} if error else None,
|
||||
**self._form_error(error),
|
||||
)
|
||||
|
||||
def _found_device_to_user_input(self) -> dict[str, Any]:
|
||||
@@ -678,7 +739,7 @@ class MideaConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
if not result:
|
||||
return self._show_manually_form(
|
||||
user_input,
|
||||
error="preset_login_failed",
|
||||
error=self._cloud_error or "preset_login_failed",
|
||||
)
|
||||
# try to get a passed key
|
||||
keys = await self._check_key_from_cloud(int(user_input[CONF_DEVICE_ID]))
|
||||
@@ -691,7 +752,7 @@ class MideaConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
)
|
||||
return self._show_manually_form(
|
||||
user_input,
|
||||
error="token_unavailable",
|
||||
error=keys.get("cloud_error") or "token_unavailable",
|
||||
)
|
||||
|
||||
# set token/key from preset account
|
||||
@@ -808,7 +869,7 @@ class MideaConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
return self.async_show_form(
|
||||
step_id="manually",
|
||||
data_schema=schema,
|
||||
errors={"base": error} if error else None,
|
||||
**self._form_error(error),
|
||||
)
|
||||
|
||||
@override
|
||||
|
||||
@@ -13,5 +13,5 @@
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["midealocal"],
|
||||
"quality_scale": "bronze",
|
||||
"requirements": ["midea-local==10.1.0"]
|
||||
"requirements": ["midea-local==11.0.1"]
|
||||
}
|
||||
|
||||
@@ -4,7 +4,13 @@
|
||||
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]"
|
||||
},
|
||||
"error": {
|
||||
"account_locked": "Too many failed sign-in attempts. Wait about five minutes before trying again. (err {error_code})",
|
||||
"cloud_error": "The Midea cloud returned an error. Check the logs for details and try again. (err {error_code})",
|
||||
"cloud_session_expired": "The Midea cloud sign-in session is no longer valid. Try again. (err {error_code})",
|
||||
"device_auth_failed": "Could not connect with the provided configuration",
|
||||
"device_not_registered": "This Midea cloud account is not authorized for this device. The device is registered to a different account. (err {error_code})",
|
||||
"invalid_auth": "Invalid authentication (err {error_code})",
|
||||
"invalid_cloud_server": "This account is not registered on the selected cloud server. Choose the server where the account was created. (err {error_code})",
|
||||
"invalid_device_id_for_ip": "The device ID does not match the selected IP address",
|
||||
"invalid_device_ip": "Could not find a supported device at this IP address",
|
||||
"invalid_token": "Token and key must be valid hexadecimal strings",
|
||||
@@ -14,6 +20,7 @@
|
||||
"preset_login_failed": "Could not log in with the preset account",
|
||||
"protocol_mismatch": "The protocol does not match the discovered device",
|
||||
"token_unavailable": "Could not get a valid token and key from the cloud",
|
||||
"too_many_logged_in_devices": "The Midea cloud account has too many active sign-ins. Sign out of the Midea app on other devices and try again. (err {error_code})",
|
||||
"type_mismatch": "The type does not match the discovered device"
|
||||
},
|
||||
"step": {
|
||||
|
||||
Generated
+1
-1
@@ -1620,7 +1620,7 @@ micloud==0.5
|
||||
microBeesPy==0.3.5
|
||||
|
||||
# homeassistant.components.midea
|
||||
midea-local==10.1.0
|
||||
midea-local==11.0.1
|
||||
|
||||
# homeassistant.components.mill
|
||||
mill-local==0.5.0
|
||||
|
||||
@@ -46,11 +46,12 @@ class DummyDevice:
|
||||
self.subtype = TEST_SUBTYPE
|
||||
self.available = False
|
||||
self.attributes = attributes or {}
|
||||
self.capabilities: dict[str, Any] = {}
|
||||
self._callbacks: list[Callable] = []
|
||||
self.calls: list[tuple] = []
|
||||
self.temperature_step = 1
|
||||
self.fan_modes = ["Low", "Medium", "High", "Auto"]
|
||||
self.raw_hvac_modes = ["off", "auto", "cool", "dry", "heat", "fan_only"]
|
||||
self.raw_fan_modes = ["low", "medium", "high", "auto"]
|
||||
self.raw_fan_mode: str | None = "high"
|
||||
self.modes = [
|
||||
"Auto",
|
||||
"ECO",
|
||||
@@ -88,17 +89,21 @@ class DummyDevice:
|
||||
self.notify_update({attr: value})
|
||||
self.calls.append(("set_attribute", attr, value))
|
||||
|
||||
def set_target_temperature(self, **kwargs: Any) -> None:
|
||||
def set_raw_target_temperature(self, **kwargs: Any) -> None:
|
||||
"""Record set target temperature call."""
|
||||
self.calls.append(("set_target_temperature", kwargs))
|
||||
self.calls.append(("set_raw_target_temperature", kwargs))
|
||||
|
||||
def set_swing(self, **kwargs: Any) -> None:
|
||||
"""Record set swing call."""
|
||||
self.calls.append(("set_swing", kwargs))
|
||||
def set_raw_swing_mode(self, swing_mode: str) -> None:
|
||||
"""Record set swing mode call."""
|
||||
self.calls.append(("set_raw_swing_mode", swing_mode))
|
||||
|
||||
def set_mode(self, zone: int, mode: int) -> None:
|
||||
"""Record set mode call."""
|
||||
self.calls.append(("set_mode", zone, mode))
|
||||
def set_raw_fan_mode(self, fan_mode: str) -> None:
|
||||
"""Record set fan mode call."""
|
||||
self.calls.append(("set_raw_fan_mode", fan_mode))
|
||||
|
||||
def set_raw_hvac_mode(self, hvac_mode: str, zone: int | None = None) -> None:
|
||||
"""Record set hvac mode call."""
|
||||
self.calls.append(("set_raw_hvac_mode", hvac_mode, zone))
|
||||
|
||||
def start_work(self) -> None:
|
||||
"""Record start_work call."""
|
||||
|
||||
@@ -275,10 +275,10 @@
|
||||
'area_id': None,
|
||||
'capabilities': dict({
|
||||
<ClimateEntityCapabilityAttribute.FAN_MODES: 'fan_modes'>: list([
|
||||
'Low',
|
||||
'Medium',
|
||||
'High',
|
||||
'Auto',
|
||||
'low',
|
||||
'medium',
|
||||
'high',
|
||||
'auto',
|
||||
]),
|
||||
<ClimateEntityCapabilityAttribute.HVAC_MODES: 'hvac_modes'>: list([
|
||||
<HVACMode.OFF: 'off'>,
|
||||
@@ -335,12 +335,12 @@
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
<ClimateEntityStateAttribute.CURRENT_TEMPERATURE: 'current_temperature'>: None,
|
||||
<ClimateEntityStateAttribute.FAN_MODE: 'fan_mode'>: 'High',
|
||||
<ClimateEntityStateAttribute.FAN_MODE: 'fan_mode'>: 'high',
|
||||
<ClimateEntityCapabilityAttribute.FAN_MODES: 'fan_modes'>: list([
|
||||
'Low',
|
||||
'Medium',
|
||||
'High',
|
||||
'Auto',
|
||||
'low',
|
||||
'medium',
|
||||
'high',
|
||||
'auto',
|
||||
]),
|
||||
<EntityStateAttribute.FRIENDLY_NAME: 'friendly_name'>: 'MDV Wi-Fi Controller',
|
||||
<ClimateEntityCapabilityAttribute.HVAC_MODES: 'hvac_modes'>: list([
|
||||
|
||||
@@ -147,8 +147,12 @@ async def test_midea_ac_climate_setup_and_services(
|
||||
{ATTR_TEMPERATURE: 23.1, "hvac_mode": HVACMode.COOL},
|
||||
[
|
||||
(
|
||||
"set_target_temperature",
|
||||
{"target_temperature": 23.1, "mode": 2, "zone": None},
|
||||
"set_raw_target_temperature",
|
||||
{
|
||||
"target_temperature": 23.1,
|
||||
"hvac_mode": HVACMode.COOL,
|
||||
"zone": None,
|
||||
},
|
||||
)
|
||||
],
|
||||
device,
|
||||
@@ -174,7 +178,7 @@ async def test_midea_ac_climate_setup_and_services(
|
||||
entity_entry.entity_id,
|
||||
SERVICE_SET_SWING_MODE,
|
||||
{ATTR_SWING_MODE: SWING_VERTICAL},
|
||||
[("set_swing", {"swing_vertical": True, "swing_horizontal": False})],
|
||||
[("set_raw_swing_mode", SWING_VERTICAL)],
|
||||
device,
|
||||
)
|
||||
await _assert_service_calls(
|
||||
@@ -260,7 +264,6 @@ async def test_midea_cc_climate_setup_and_services(
|
||||
attributes={
|
||||
CCAttributes.power: True,
|
||||
CCAttributes.mode: 5,
|
||||
CCAttributes.fan_speed: "High",
|
||||
CCAttributes.temperature_precision: 0.5,
|
||||
CCAttributes.swing: True,
|
||||
},
|
||||
@@ -272,7 +275,7 @@ async def test_midea_cc_climate_setup_and_services(
|
||||
state = hass.states.get(entity_entry.entity_id)
|
||||
assert state is not None
|
||||
assert state.state == HVACMode.AUTO
|
||||
assert state.attributes[ATTR_FAN_MODE] == "High"
|
||||
assert state.attributes[ATTR_FAN_MODE] == "high"
|
||||
assert state.attributes[ATTR_HVAC_MODES] == [
|
||||
HVACMode.OFF,
|
||||
HVACMode.FAN_ONLY,
|
||||
@@ -289,8 +292,8 @@ async def test_midea_cc_climate_setup_and_services(
|
||||
hass,
|
||||
entity_entry.entity_id,
|
||||
SERVICE_SET_FAN_MODE,
|
||||
{ATTR_FAN_MODE: "Low"},
|
||||
[("set_attribute", CCAttributes.fan_speed, "Low")],
|
||||
{ATTR_FAN_MODE: "low"},
|
||||
[("set_raw_fan_mode", "low")],
|
||||
device,
|
||||
)
|
||||
await _assert_service_calls(
|
||||
@@ -352,8 +355,8 @@ async def test_midea_cf_climate_setup_and_services(
|
||||
{"hvac_mode": HVACMode.HEAT},
|
||||
[
|
||||
(
|
||||
"set_target_temperature",
|
||||
{"target_temperature": 20.0, "mode": 3},
|
||||
"set_raw_target_temperature",
|
||||
{"target_temperature": 20.0, "hvac_mode": HVACMode.HEAT},
|
||||
)
|
||||
],
|
||||
device,
|
||||
@@ -408,8 +411,12 @@ async def test_midea_c3_climate_setup_and_services(
|
||||
{ATTR_TEMPERATURE: 21.4, "hvac_mode": HVACMode.COOL},
|
||||
[
|
||||
(
|
||||
"set_target_temperature",
|
||||
{"target_temperature": 21.4, "mode": 2, "zone": 0},
|
||||
"set_raw_target_temperature",
|
||||
{
|
||||
"target_temperature": 21.4,
|
||||
"hvac_mode": HVACMode.COOL,
|
||||
"zone": 0,
|
||||
},
|
||||
)
|
||||
],
|
||||
device,
|
||||
@@ -427,7 +434,7 @@ async def test_midea_c3_climate_setup_and_services(
|
||||
zone1.entity_id,
|
||||
SERVICE_SET_HVAC_MODE,
|
||||
{"hvac_mode": HVACMode.HEAT},
|
||||
[("set_mode", 0, 3)],
|
||||
[("set_raw_hvac_mode", HVACMode.HEAT, 0)],
|
||||
device,
|
||||
)
|
||||
|
||||
@@ -597,8 +604,8 @@ async def test_ac_set_temperature_without_hvac_mode(
|
||||
{ATTR_TEMPERATURE: 23.0},
|
||||
[
|
||||
(
|
||||
"set_target_temperature",
|
||||
{"target_temperature": 23.0, "mode": None, "zone": None},
|
||||
"set_raw_target_temperature",
|
||||
{"target_temperature": 23.0, "hvac_mode": None, "zone": None},
|
||||
)
|
||||
],
|
||||
device,
|
||||
@@ -1133,8 +1140,8 @@ async def test_cf_set_hvac_mode_falls_back_to_min_temp(
|
||||
{"hvac_mode": HVACMode.HEAT},
|
||||
[
|
||||
(
|
||||
"set_target_temperature",
|
||||
{"target_temperature": 16.0, "mode": 3},
|
||||
"set_raw_target_temperature",
|
||||
{"target_temperature": 16.0, "hvac_mode": HVACMode.HEAT},
|
||||
)
|
||||
],
|
||||
device,
|
||||
@@ -1193,8 +1200,12 @@ async def test_fb_set_temperature_with_heat_mode_turns_on_when_off(
|
||||
[
|
||||
("set_attribute", FBAttributes.power, True),
|
||||
(
|
||||
"set_target_temperature",
|
||||
{"target_temperature": 25.0, "mode": 1, "zone": None},
|
||||
"set_raw_target_temperature",
|
||||
{
|
||||
"target_temperature": 25.0,
|
||||
"hvac_mode": HVACMode.HEAT,
|
||||
"zone": None,
|
||||
},
|
||||
),
|
||||
],
|
||||
device,
|
||||
@@ -1258,11 +1269,11 @@ async def test_cc_fan_and_swing_invalid_types_return_none(
|
||||
attributes={
|
||||
CCAttributes.power: True,
|
||||
CCAttributes.mode: 5,
|
||||
CCAttributes.fan_speed: 1,
|
||||
CCAttributes.temperature_precision: 0.5,
|
||||
CCAttributes.swing: "on",
|
||||
},
|
||||
)
|
||||
device.raw_fan_mode = None
|
||||
config_entry = mock_config_entry(device)
|
||||
await setup_integration(hass, config_entry, device)
|
||||
entity_entry = entity_entries(hass, config_entry)[f"{TEST_DEVICE_ID}_climate"]
|
||||
@@ -1302,8 +1313,12 @@ async def test_c3_zone2_service_calls_address_zone_two(
|
||||
{ATTR_TEMPERATURE: 24.0, "hvac_mode": HVACMode.COOL},
|
||||
[
|
||||
(
|
||||
"set_target_temperature",
|
||||
{"target_temperature": 24.0, "mode": 2, "zone": 1},
|
||||
"set_raw_target_temperature",
|
||||
{
|
||||
"target_temperature": 24.0,
|
||||
"hvac_mode": HVACMode.COOL,
|
||||
"zone": 1,
|
||||
},
|
||||
)
|
||||
],
|
||||
device,
|
||||
@@ -1313,7 +1328,7 @@ async def test_c3_zone2_service_calls_address_zone_two(
|
||||
zone2.entity_id,
|
||||
SERVICE_SET_HVAC_MODE,
|
||||
{"hvac_mode": HVACMode.HEAT},
|
||||
[("set_mode", 1, 3)],
|
||||
[("set_raw_hvac_mode", HVACMode.HEAT, 1)],
|
||||
device,
|
||||
)
|
||||
await _assert_service_calls(
|
||||
@@ -1402,7 +1417,6 @@ async def test_fb_invalid_attribute_types_return_none(
|
||||
attributes={
|
||||
CCAttributes.power: True,
|
||||
CCAttributes.mode: 5,
|
||||
CCAttributes.fan_speed: "High",
|
||||
CCAttributes.temperature_precision: 0.5,
|
||||
CCAttributes.swing: True,
|
||||
},
|
||||
|
||||
@@ -6,6 +6,7 @@ from unittest.mock import AsyncMock, MagicMock, patch
|
||||
|
||||
from midealocal.const import DeviceType, ProtocolVersion
|
||||
from midealocal.device import MideaDevice
|
||||
from midealocal.exceptions import CloudLoginError, MideaCloudError, NoDeviceRegistered
|
||||
import pytest
|
||||
|
||||
from homeassistant.components.midea.config_flow import (
|
||||
@@ -169,6 +170,7 @@ async def test_manual_flow_duplicate_unique_id(hass: HomeAssistant) -> None:
|
||||
"connect_return",
|
||||
"cloud_login_return",
|
||||
"cloud_keys_return",
|
||||
"cloud_keys_side_effect",
|
||||
"default_keys_return",
|
||||
"pre_input",
|
||||
"expected_error",
|
||||
@@ -180,6 +182,7 @@ async def test_manual_flow_duplicate_unique_id(hass: HomeAssistant) -> None:
|
||||
None,
|
||||
True,
|
||||
{},
|
||||
None,
|
||||
{},
|
||||
None,
|
||||
"invalid_token",
|
||||
@@ -191,6 +194,7 @@ async def test_manual_flow_duplicate_unique_id(hass: HomeAssistant) -> None:
|
||||
None,
|
||||
True,
|
||||
{},
|
||||
None,
|
||||
{},
|
||||
None,
|
||||
"invalid_device_ip",
|
||||
@@ -202,6 +206,7 @@ async def test_manual_flow_duplicate_unique_id(hass: HomeAssistant) -> None:
|
||||
None,
|
||||
True,
|
||||
{},
|
||||
None,
|
||||
{},
|
||||
None,
|
||||
"invalid_device_id_for_ip",
|
||||
@@ -219,6 +224,7 @@ async def test_manual_flow_duplicate_unique_id(hass: HomeAssistant) -> None:
|
||||
None,
|
||||
True,
|
||||
{},
|
||||
None,
|
||||
{},
|
||||
None,
|
||||
"ip_address_mismatch",
|
||||
@@ -236,6 +242,7 @@ async def test_manual_flow_duplicate_unique_id(hass: HomeAssistant) -> None:
|
||||
None,
|
||||
True,
|
||||
{},
|
||||
None,
|
||||
{},
|
||||
None,
|
||||
"protocol_mismatch",
|
||||
@@ -252,6 +259,7 @@ async def test_manual_flow_duplicate_unique_id(hass: HomeAssistant) -> None:
|
||||
None,
|
||||
True,
|
||||
{},
|
||||
None,
|
||||
{},
|
||||
None,
|
||||
"type_mismatch",
|
||||
@@ -263,6 +271,7 @@ async def test_manual_flow_duplicate_unique_id(hass: HomeAssistant) -> None:
|
||||
False,
|
||||
True,
|
||||
{},
|
||||
None,
|
||||
{},
|
||||
None,
|
||||
"device_auth_failed",
|
||||
@@ -274,6 +283,7 @@ async def test_manual_flow_duplicate_unique_id(hass: HomeAssistant) -> None:
|
||||
None,
|
||||
False,
|
||||
{},
|
||||
None,
|
||||
{},
|
||||
None,
|
||||
"preset_login_failed",
|
||||
@@ -285,11 +295,24 @@ async def test_manual_flow_duplicate_unique_id(hass: HomeAssistant) -> None:
|
||||
None,
|
||||
True,
|
||||
{},
|
||||
None,
|
||||
{},
|
||||
None,
|
||||
"token_unavailable",
|
||||
id="no_token_from_cloud",
|
||||
),
|
||||
pytest.param(
|
||||
{**EXTENDED_DATA, CONF_TOKEN: "", CONF_KEY: ""},
|
||||
{TEST_DEVICE_ID: {**BASE_DATA, CONF_TYPE: TEST_TYPE}},
|
||||
None,
|
||||
True,
|
||||
{},
|
||||
NoDeviceRegistered(3201, "no permission"),
|
||||
{},
|
||||
None,
|
||||
"device_not_registered",
|
||||
id="cloud_rejects_token_request",
|
||||
),
|
||||
],
|
||||
)
|
||||
async def test_manual_step_errors(
|
||||
@@ -299,6 +322,7 @@ async def test_manual_step_errors(
|
||||
connect_return: bool | None,
|
||||
cloud_login_return: bool,
|
||||
cloud_keys_return: dict[str, dict[str, str]],
|
||||
cloud_keys_side_effect: Exception | None,
|
||||
default_keys_return: dict[str, dict[str, str]],
|
||||
pre_input: dict[str, object] | None,
|
||||
expected_error: str,
|
||||
@@ -323,7 +347,9 @@ async def test_manual_step_errors(
|
||||
|
||||
cloud = MagicMock()
|
||||
cloud.login = AsyncMock(return_value=cloud_login_return)
|
||||
cloud.get_cloud_keys = AsyncMock(return_value=cloud_keys_return)
|
||||
cloud.get_cloud_keys = AsyncMock(
|
||||
return_value=cloud_keys_return, side_effect=cloud_keys_side_effect
|
||||
)
|
||||
|
||||
with (
|
||||
patch(
|
||||
@@ -702,6 +728,150 @@ async def test_auto_flow_v3_preset_phase1_default_key_success(
|
||||
assert result["data"][CONF_KEY] == TEST_KEY
|
||||
|
||||
|
||||
async def test_auto_flow_v3_default_key_success_after_cloud_error(
|
||||
hass: HomeAssistant,
|
||||
) -> None:
|
||||
"""Test a cloud rejection still lets a device connect with a built-in key.
|
||||
|
||||
``get_cloud_keys`` now raises for known failure codes; that must not skip
|
||||
the established well-known default-key fallback.
|
||||
"""
|
||||
mock_devices = {TEST_DEVICE_ID: {**BASE_DATA, CONF_TYPE: TEST_TYPE}}
|
||||
|
||||
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": "search"},
|
||||
)
|
||||
with patch(
|
||||
"homeassistant.components.midea.config_flow.discover",
|
||||
return_value=mock_devices,
|
||||
):
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
flow_id,
|
||||
user_input={CONF_IP_ADDRESS: "auto"},
|
||||
)
|
||||
assert result["step_id"] == "auto"
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
flow_id,
|
||||
user_input={CONF_DEVICE: TEST_DEVICE_ID},
|
||||
)
|
||||
assert result["step_id"] == "auth_method"
|
||||
|
||||
cloud = MagicMock()
|
||||
cloud.login = AsyncMock(return_value=True)
|
||||
cloud.get_device_info = AsyncMock(return_value=None)
|
||||
cloud.get_cloud_keys = AsyncMock(
|
||||
side_effect=NoDeviceRegistered(3201, "no permission")
|
||||
)
|
||||
|
||||
dm = MagicMock()
|
||||
dm.connect.return_value = True
|
||||
|
||||
with (
|
||||
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={"builtin": {"token": TEST_TOKEN, "key": TEST_KEY}}),
|
||||
),
|
||||
patch(
|
||||
"homeassistant.components.midea.config_flow.device_selector",
|
||||
return_value=dm,
|
||||
),
|
||||
):
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
flow_id,
|
||||
user_input={"login_mode": LOGIN_MODE_PRESET},
|
||||
)
|
||||
|
||||
assert result["type"] is FlowResultType.CREATE_ENTRY
|
||||
assert result["data"][CONF_TOKEN] == TEST_TOKEN
|
||||
assert result["data"][CONF_KEY] == TEST_KEY
|
||||
|
||||
|
||||
async def test_auto_flow_phase2_login_false_keeps_phase1_cloud_error(
|
||||
hass: HomeAssistant,
|
||||
) -> None:
|
||||
"""Test a specific phase-1 cloud error survives a plain phase-2 login failure.
|
||||
|
||||
Phase 1 raises ``NoDeviceRegistered`` (3201); phase 2's preset login only
|
||||
returns ``False`` (no exception), which resets the pending error - the flow
|
||||
must still report the actionable 3201 error, not ``preset_login_failed``.
|
||||
"""
|
||||
mock_devices = {TEST_DEVICE_ID: {**BASE_DATA, CONF_TYPE: TEST_TYPE}}
|
||||
|
||||
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": "search"},
|
||||
)
|
||||
with patch(
|
||||
"homeassistant.components.midea.config_flow.discover",
|
||||
return_value=mock_devices,
|
||||
):
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
flow_id,
|
||||
user_input={CONF_IP_ADDRESS: "auto"},
|
||||
)
|
||||
assert result["step_id"] == "auto"
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
flow_id,
|
||||
user_input={CONF_DEVICE: TEST_DEVICE_ID},
|
||||
)
|
||||
assert result["step_id"] == "auth_method"
|
||||
|
||||
cloud = MagicMock()
|
||||
# phase 1 login succeeds, phase 2 (force_login) login just returns False
|
||||
cloud.login = AsyncMock(side_effect=[True, False])
|
||||
cloud.get_device_info = AsyncMock(return_value=None)
|
||||
cloud.get_cloud_keys = AsyncMock(
|
||||
side_effect=NoDeviceRegistered(3201, "no permission")
|
||||
)
|
||||
|
||||
with (
|
||||
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={}),
|
||||
),
|
||||
):
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
flow_id,
|
||||
user_input={"login_mode": LOGIN_MODE_PRESET},
|
||||
)
|
||||
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["step_id"] == "auto"
|
||||
assert result["errors"] == {"base": "device_not_registered"}
|
||||
assert result["description_placeholders"] == {"error_code": "3201"}
|
||||
|
||||
|
||||
async def test_auto_flow_v3_token_retrieval_exhausted(hass: HomeAssistant) -> None:
|
||||
"""Test both phase 1 and phase 2 key retrieval failing surfaces token_unavailable."""
|
||||
mock_devices = {TEST_DEVICE_ID: {**BASE_DATA, CONF_TYPE: TEST_TYPE}}
|
||||
@@ -1253,6 +1423,191 @@ async def test_login_credentials_step_login_failed_sets_error(
|
||||
assert get_schema_suggested_value(data_schema, CONF_SERVER) == DEFAULT_CLOUD
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("login_error", "expected_error", "expected_code"),
|
||||
[
|
||||
(CloudLoginError(7610, "locked"), "account_locked", "7610"),
|
||||
(MideaCloudError(9999, "system error"), "cloud_error", "9999"),
|
||||
],
|
||||
ids=["specific_login_error", "generic_cloud_error"],
|
||||
)
|
||||
async def test_login_credentials_step_maps_cloud_error(
|
||||
hass: HomeAssistant,
|
||||
login_error: MideaCloudError,
|
||||
expected_error: str,
|
||||
expected_code: str,
|
||||
) -> None:
|
||||
"""Test a cloud error raised by login() surfaces its translation_key and code.
|
||||
|
||||
The exhaustive code-to-slug matrix is covered in the midea-local library
|
||||
tests; here we only check the config flow forwards ``err.translation_key``
|
||||
and the numeric code.
|
||||
"""
|
||||
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": "search"},
|
||||
)
|
||||
with patch(
|
||||
"homeassistant.components.midea.config_flow.discover",
|
||||
return_value=DISCOVERY_RESULT,
|
||||
):
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
flow_id,
|
||||
user_input={CONF_IP_ADDRESS: "auto"},
|
||||
)
|
||||
assert result["step_id"] == "auto"
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
flow_id,
|
||||
user_input={CONF_DEVICE: TEST_DEVICE_ID},
|
||||
)
|
||||
assert result["step_id"] == "auth_method"
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
flow_id,
|
||||
user_input={"login_mode": LOGIN_MODE_ACCOUNT},
|
||||
)
|
||||
assert result["step_id"] == "login_credentials"
|
||||
|
||||
cloud = MagicMock()
|
||||
cloud.login = AsyncMock(side_effect=login_error)
|
||||
|
||||
with (
|
||||
patch(
|
||||
"homeassistant.components.midea.config_flow.MideaCloud.get_cloud_servers",
|
||||
AsyncMock(return_value={1: DEFAULT_CLOUD}),
|
||||
),
|
||||
patch(
|
||||
"homeassistant.components.midea.config_flow.async_get_clientsession",
|
||||
return_value=object(),
|
||||
),
|
||||
patch(
|
||||
"homeassistant.components.midea.config_flow.get_midea_cloud",
|
||||
return_value=cloud,
|
||||
),
|
||||
):
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
flow_id,
|
||||
user_input={
|
||||
CONF_SERVER: DEFAULT_CLOUD,
|
||||
CONF_ACCOUNT: "user",
|
||||
CONF_PASSWORD: "pass",
|
||||
},
|
||||
)
|
||||
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["step_id"] == "login_credentials"
|
||||
assert result["errors"] == {"base": expected_error}
|
||||
assert result["description_placeholders"] == {"error_code": expected_code}
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
(
|
||||
"login_side_effect",
|
||||
"cloud_keys_side_effect",
|
||||
"expected_step",
|
||||
"expected_error",
|
||||
"expected_code",
|
||||
),
|
||||
[
|
||||
pytest.param(
|
||||
CloudLoginError(7610, "locked"),
|
||||
None,
|
||||
"auth_method",
|
||||
"account_locked",
|
||||
"7610",
|
||||
id="preset_login_rejected",
|
||||
),
|
||||
pytest.param(
|
||||
None,
|
||||
[NoDeviceRegistered(3201, "no permission"), {}],
|
||||
"auto",
|
||||
"device_not_registered",
|
||||
"3201",
|
||||
id="device_bound_to_other_account",
|
||||
),
|
||||
],
|
||||
)
|
||||
async def test_auto_flow_preset_auth_maps_cloud_error(
|
||||
hass: HomeAssistant,
|
||||
login_side_effect: MideaCloudError | None,
|
||||
cloud_keys_side_effect: list[object] | None,
|
||||
expected_step: str,
|
||||
expected_error: str,
|
||||
expected_code: str,
|
||||
) -> None:
|
||||
"""Test cloud API errors on the preset auth path surface a specific message.
|
||||
|
||||
Either the preset login itself is rejected (stays on auth_method), or the
|
||||
login succeeds but the cloud refuses to issue a token/key for the device
|
||||
(falls back to the auto step).
|
||||
"""
|
||||
mock_devices = {TEST_DEVICE_ID: {**BASE_DATA, CONF_TYPE: TEST_TYPE}}
|
||||
|
||||
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": "search"},
|
||||
)
|
||||
with patch(
|
||||
"homeassistant.components.midea.config_flow.discover",
|
||||
return_value=mock_devices,
|
||||
):
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
flow_id,
|
||||
user_input={CONF_IP_ADDRESS: "auto"},
|
||||
)
|
||||
assert result["step_id"] == "auto"
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
flow_id,
|
||||
user_input={CONF_DEVICE: TEST_DEVICE_ID},
|
||||
)
|
||||
assert result["step_id"] == "auth_method"
|
||||
|
||||
cloud = MagicMock()
|
||||
cloud.login = AsyncMock(return_value=True, side_effect=login_side_effect)
|
||||
cloud.get_device_info = AsyncMock(return_value=None)
|
||||
cloud.get_cloud_keys = AsyncMock(
|
||||
return_value={}, side_effect=cloud_keys_side_effect
|
||||
)
|
||||
|
||||
with (
|
||||
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={}),
|
||||
),
|
||||
):
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
flow_id,
|
||||
user_input={"login_mode": LOGIN_MODE_PRESET},
|
||||
)
|
||||
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["step_id"] == expected_step
|
||||
assert result["errors"] == {"base": expected_error}
|
||||
assert result["description_placeholders"] == {"error_code": expected_code}
|
||||
|
||||
|
||||
async def test_login_credentials_step_recovers_after_failed_login(
|
||||
hass: HomeAssistant,
|
||||
) -> None:
|
||||
|
||||
Reference in New Issue
Block a user