1
0
mirror of https://github.com/home-assistant/core.git synced 2026-08-06 13:26:29 +01:00
Files
core/tests/components/midea/test_climate.py
T
2026-08-01 00:36:39 +02:00

1468 lines
48 KiB
Python

"""Tests for midea climate.py."""
from collections.abc import Callable
from typing import Any
from unittest.mock import patch
from midealocal.const import DeviceType
from midealocal.devices.ac import DeviceAttributes as ACAttributes
from midealocal.devices.c3.const import DeviceAttributes as C3Attributes
from midealocal.devices.cc import DeviceAttributes as CCAttributes
from midealocal.devices.cf import DeviceAttributes as CFAttributes
from midealocal.devices.fb import DeviceAttributes as FBAttributes
import pytest
from syrupy.assertion import SnapshotAssertion
from homeassistant.components.climate import (
ATTR_CURRENT_HUMIDITY,
ATTR_CURRENT_TEMPERATURE,
ATTR_FAN_MODE,
ATTR_HVAC_MODES,
ATTR_MAX_TEMP,
ATTR_MIN_TEMP,
ATTR_PRESET_MODE,
ATTR_SWING_MODE,
ATTR_TARGET_TEMP_STEP,
ATTR_TEMPERATURE,
DOMAIN as CLIMATE_DOMAIN,
FAN_AUTO,
FAN_HIGH,
FAN_LOW,
FAN_MEDIUM,
PRESET_COMFORT,
PRESET_ECO,
PRESET_NONE,
SERVICE_SET_FAN_MODE,
SERVICE_SET_HVAC_MODE,
SERVICE_SET_PRESET_MODE,
SERVICE_SET_SWING_MODE,
SERVICE_SET_TEMPERATURE,
SERVICE_TURN_OFF,
SERVICE_TURN_ON,
SWING_BOTH,
SWING_ON,
SWING_VERTICAL,
HVACMode,
)
from homeassistant.components.midea.climate import FAN_FULL_SPEED, FAN_SILENT
from homeassistant.const import ATTR_ENTITY_ID, Platform
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ServiceValidationError
from homeassistant.helpers import entity_registry as er
from . import setup_integration
from .conftest import DummyDevice, entity_entries
from .const import TEST_DEVICE_ID
from tests.common import MockConfigEntry, snapshot_platform
async def _assert_service_calls(
hass: HomeAssistant,
entity_id: str,
service: str,
service_data: dict,
expected_calls: list[tuple],
device: DummyDevice,
) -> None:
"""Call a climate service and assert the fake device recorded the right call."""
device.calls.clear()
await hass.services.async_call(
CLIMATE_DOMAIN,
service,
{ATTR_ENTITY_ID: entity_id, **service_data},
blocking=True,
)
assert device.calls == expected_calls
async def test_midea_ac_climate_setup_and_services(
hass: HomeAssistant,
mock_config_entry: Callable[[DummyDevice], MockConfigEntry],
) -> None:
"""Test AC climate entities are created and service calls reach the device."""
device = DummyDevice(
DeviceType.AC,
attributes={
ACAttributes.power: True,
ACAttributes.mode: 1,
ACAttributes.target_temperature: 22.0,
ACAttributes.indoor_temperature: 21.0,
ACAttributes.comfort_mode: False,
ACAttributes.eco_mode: False,
ACAttributes.boost_mode: False,
ACAttributes.sleep_mode: False,
ACAttributes.frost_protect: False,
ACAttributes.fan_speed: 103,
ACAttributes.swing_vertical: True,
ACAttributes.swing_horizontal: True,
ACAttributes.indoor_humidity: 50,
},
)
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"]
state = hass.states.get(entity_entry.entity_id)
assert state is not None
assert state.state == HVACMode.AUTO
assert state.attributes[ATTR_CURRENT_HUMIDITY] == 50.0
assert state.attributes[ATTR_CURRENT_TEMPERATURE] == 21.0
assert state.attributes[ATTR_FAN_MODE] == "auto"
assert state.attributes[ATTR_HVAC_MODES] == [
HVACMode.OFF,
HVACMode.AUTO,
HVACMode.COOL,
HVACMode.DRY,
HVACMode.HEAT,
HVACMode.FAN_ONLY,
]
assert state.attributes[ATTR_MAX_TEMP] == 30
assert state.attributes[ATTR_MIN_TEMP] == 16
assert state.attributes[ATTR_PRESET_MODE] == PRESET_NONE
assert state.attributes[ATTR_SWING_MODE] == SWING_BOTH
assert state.attributes[ATTR_TARGET_TEMP_STEP] == 1.0
assert state.attributes[ATTR_TEMPERATURE] == 22.0
await _assert_service_calls(
hass,
entity_entry.entity_id,
SERVICE_TURN_OFF,
{},
[("set_attribute", ACAttributes.power, False)],
device,
)
await _assert_service_calls(
hass,
entity_entry.entity_id,
SERVICE_TURN_ON,
{},
[("set_attribute", ACAttributes.power, True)],
device,
)
await _assert_service_calls(
hass,
entity_entry.entity_id,
SERVICE_SET_TEMPERATURE,
{ATTR_TEMPERATURE: 23.1, "hvac_mode": HVACMode.COOL},
[
(
"set_target_temperature",
{"target_temperature": 23.1, "mode": 2, "zone": None},
)
],
device,
)
await _assert_service_calls(
hass,
entity_entry.entity_id,
SERVICE_SET_HVAC_MODE,
{"hvac_mode": HVACMode.HEAT},
[("set_attribute", ACAttributes.mode, 4)],
device,
)
await _assert_service_calls(
hass,
entity_entry.entity_id,
SERVICE_SET_FAN_MODE,
{ATTR_FAN_MODE: FAN_LOW},
[("set_attribute", ACAttributes.fan_speed, 40)],
device,
)
await _assert_service_calls(
hass,
entity_entry.entity_id,
SERVICE_SET_SWING_MODE,
{ATTR_SWING_MODE: SWING_VERTICAL},
[("set_swing", {"swing_vertical": True, "swing_horizontal": False})],
device,
)
await _assert_service_calls(
hass,
entity_entry.entity_id,
SERVICE_SET_PRESET_MODE,
{ATTR_PRESET_MODE: PRESET_COMFORT},
[("set_attribute", ACAttributes.comfort_mode, True)],
device,
)
device.attributes.update(
{
ACAttributes.comfort_mode: True,
ACAttributes.eco_mode: False,
ACAttributes.boost_mode: False,
ACAttributes.sleep_mode: False,
ACAttributes.frost_protect: False,
}
)
await _assert_service_calls(
hass,
entity_entry.entity_id,
SERVICE_SET_PRESET_MODE,
{ATTR_PRESET_MODE: PRESET_ECO},
[("set_attribute", ACAttributes.eco_mode, True)],
device,
)
device.attributes.update(
{
ACAttributes.comfort_mode: True,
ACAttributes.eco_mode: False,
ACAttributes.boost_mode: False,
ACAttributes.sleep_mode: False,
ACAttributes.frost_protect: False,
}
)
await _assert_service_calls(
hass,
entity_entry.entity_id,
SERVICE_SET_PRESET_MODE,
{ATTR_PRESET_MODE: PRESET_NONE},
[("set_attribute", ACAttributes.comfort_mode, False)],
device,
)
async def test_ac_min_max_temperature_from_device(
hass: HomeAssistant,
mock_config_entry: Callable[[DummyDevice], MockConfigEntry],
) -> None:
"""Test AC min/max temperature are read from the device attributes."""
device = DummyDevice(
DeviceType.AC,
attributes={
ACAttributes.power: True,
ACAttributes.mode: 1,
ACAttributes.target_temperature: 22.0,
ACAttributes.indoor_temperature: 21.0,
ACAttributes.fan_speed: 103,
ACAttributes.swing_vertical: True,
ACAttributes.swing_horizontal: True,
ACAttributes.min_temperature: 17,
ACAttributes.max_temperature: 26,
},
)
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"]
entity = hass.data[CLIMATE_DOMAIN].get_entity(entity_entry.entity_id)
assert entity is not None
assert entity.min_temp == 17.0
assert entity.max_temp == 26.0
async def test_midea_cc_climate_setup_and_services(
hass: HomeAssistant,
mock_config_entry: Callable[[DummyDevice], MockConfigEntry],
) -> None:
"""Test CC climate entities are created and exposed through hass.states."""
device = DummyDevice(
DeviceType.CC,
attributes={
CCAttributes.power: True,
CCAttributes.mode: 5,
CCAttributes.fan_speed: "High",
CCAttributes.temperature_precision: 0.5,
CCAttributes.swing: True,
},
)
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"]
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_HVAC_MODES] == [
HVACMode.OFF,
HVACMode.FAN_ONLY,
HVACMode.DRY,
HVACMode.HEAT,
HVACMode.COOL,
HVACMode.AUTO,
]
assert state.attributes[ATTR_PRESET_MODE] == PRESET_NONE
assert state.attributes[ATTR_SWING_MODE] == SWING_ON
assert state.attributes[ATTR_TARGET_TEMP_STEP] == 0.5
await _assert_service_calls(
hass,
entity_entry.entity_id,
SERVICE_SET_FAN_MODE,
{ATTR_FAN_MODE: "Low"},
[("set_attribute", CCAttributes.fan_speed, "Low")],
device,
)
await _assert_service_calls(
hass,
entity_entry.entity_id,
SERVICE_SET_SWING_MODE,
{ATTR_SWING_MODE: SWING_ON},
[("set_attribute", CCAttributes.swing, True)],
device,
)
async def test_midea_cf_climate_setup_and_services(
hass: HomeAssistant,
mock_config_entry: Callable[[DummyDevice], MockConfigEntry],
) -> None:
"""Test CF climate entities are created and control calls are routed."""
device = DummyDevice(
DeviceType.CF,
attributes={
"power": True,
"mode": 2,
CFAttributes.min_temperature: 16,
CFAttributes.max_temperature: 30,
CFAttributes.current_temperature: 22,
CFAttributes.target_temperature: 20,
},
)
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"]
state = hass.states.get(entity_entry.entity_id)
assert state is not None
assert state.state == HVACMode.COOL
assert state.attributes[ATTR_CURRENT_TEMPERATURE] == 22.0
assert state.attributes[ATTR_HVAC_MODES] == [
HVACMode.OFF,
HVACMode.AUTO,
HVACMode.COOL,
HVACMode.HEAT,
]
assert state.attributes[ATTR_MAX_TEMP] == 30
assert state.attributes[ATTR_MIN_TEMP] == 16
assert state.attributes[ATTR_TARGET_TEMP_STEP] == 1.0
await _assert_service_calls(
hass,
entity_entry.entity_id,
SERVICE_SET_TEMPERATURE,
{ATTR_TEMPERATURE: 24.2, "hvac_mode": HVACMode.OFF},
[("set_attribute", CFAttributes.power, False)],
device,
)
await _assert_service_calls(
hass,
entity_entry.entity_id,
SERVICE_SET_HVAC_MODE,
{"hvac_mode": HVACMode.HEAT},
[
(
"set_target_temperature",
{"target_temperature": 20.0, "mode": 3},
)
],
device,
)
async def test_midea_c3_climate_setup_and_services(
hass: HomeAssistant,
mock_config_entry: Callable[[DummyDevice], MockConfigEntry],
) -> None:
"""Test C3 climate entities are created and zone-specific services work."""
device = DummyDevice(
DeviceType.C3,
attributes={
C3Attributes.zone_temp_type: [True, False],
C3Attributes.temperature_min: [16, 17],
C3Attributes.temperature_max: [30, 29],
C3Attributes.mode: 1,
C3Attributes.zone1_power: True,
C3Attributes.target_temperature: [22, 23],
C3Attributes.temp_tw_out: 21.5,
},
)
config_entry = mock_config_entry(device)
await setup_integration(hass, config_entry, device)
entries_by_unique_id = entity_entries(hass, config_entry)
zone1 = entries_by_unique_id[f"{TEST_DEVICE_ID}_climate_zone1"]
zone2 = entries_by_unique_id[f"{TEST_DEVICE_ID}_climate_zone2"]
assert zone2.disabled_by == er.RegistryEntryDisabler.INTEGRATION
assert hass.states.get(zone2.entity_id) is None
state = hass.states.get(zone1.entity_id)
assert state is not None
assert state.state == HVACMode.AUTO
assert state.attributes[ATTR_CURRENT_TEMPERATURE] == 21.5
assert state.attributes[ATTR_HVAC_MODES] == [
HVACMode.OFF,
HVACMode.AUTO,
HVACMode.COOL,
HVACMode.HEAT,
]
assert state.attributes[ATTR_MAX_TEMP] == 30
assert state.attributes[ATTR_MIN_TEMP] == 16
assert state.attributes[ATTR_TARGET_TEMP_STEP] == 1.0
assert state.attributes[ATTR_TEMPERATURE] == 22.0
await _assert_service_calls(
hass,
zone1.entity_id,
SERVICE_SET_TEMPERATURE,
{ATTR_TEMPERATURE: 21.4, "hvac_mode": HVACMode.COOL},
[
(
"set_target_temperature",
{"target_temperature": 21.4, "mode": 2, "zone": 0},
)
],
device,
)
await _assert_service_calls(
hass,
zone1.entity_id,
SERVICE_SET_HVAC_MODE,
{"hvac_mode": HVACMode.OFF},
[("set_attribute", C3Attributes.zone1_power, False)],
device,
)
await _assert_service_calls(
hass,
zone1.entity_id,
SERVICE_SET_HVAC_MODE,
{"hvac_mode": HVACMode.HEAT},
[("set_mode", 0, 3)],
device,
)
async def test_midea_fb_climate_setup_and_services(
hass: HomeAssistant,
mock_config_entry: Callable[[DummyDevice], MockConfigEntry],
) -> None:
"""Test FB climate entities are created and preset calls are routed."""
device = DummyDevice(
DeviceType.FB,
attributes={
FBAttributes.mode: "Comfort",
FBAttributes.power: True,
FBAttributes.current_temperature: 20,
},
)
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"]
state = hass.states.get(entity_entry.entity_id)
assert state is not None
assert state.state == HVACMode.HEAT
assert state.attributes[ATTR_CURRENT_TEMPERATURE] == 20.0
assert state.attributes[ATTR_HVAC_MODES] == [HVACMode.OFF, HVACMode.HEAT]
assert state.attributes[ATTR_MAX_TEMP] == 35
assert state.attributes[ATTR_MIN_TEMP] == 5
assert state.attributes[ATTR_PRESET_MODE] == "Comfort"
assert state.attributes[ATTR_TARGET_TEMP_STEP] == 1.0
await _assert_service_calls(
hass,
entity_entry.entity_id,
SERVICE_SET_TEMPERATURE,
{ATTR_TEMPERATURE: 24.2, "hvac_mode": HVACMode.OFF},
[("set_attribute", FBAttributes.power, False)],
device,
)
await _assert_service_calls(
hass,
entity_entry.entity_id,
SERVICE_SET_HVAC_MODE,
{"hvac_mode": HVACMode.HEAT},
[("set_attribute", FBAttributes.power, True)],
device,
)
await _assert_service_calls(
hass,
entity_entry.entity_id,
SERVICE_SET_PRESET_MODE,
{ATTR_PRESET_MODE: "ECO"},
[("set_attribute", FBAttributes.mode, "ECO")],
device,
)
@pytest.mark.parametrize(
("fan_speed", "expected_fan_mode"),
[
pytest.param(101, "auto", id="just_above_auto_threshold"),
pytest.param(100, "full", id="auto_threshold_falls_to_full"),
pytest.param(80, "high", id="full_threshold_falls_to_high"),
pytest.param(60, "medium", id="high_threshold_falls_to_medium"),
pytest.param(40, "low", id="medium_threshold_falls_to_low"),
pytest.param(20, "silent", id="low_threshold_falls_to_silent"),
pytest.param(0, "silent", id="silent_fallback"),
],
)
async def test_ac_fan_mode_read_thresholds(
hass: HomeAssistant,
mock_config_entry: Callable[[DummyDevice], MockConfigEntry],
fan_speed: int,
expected_fan_mode: str,
) -> None:
"""Test AC fan mode read mapping across every numeric bucket boundary."""
device = DummyDevice(
DeviceType.AC,
attributes={
ACAttributes.power: True,
ACAttributes.mode: 1,
ACAttributes.target_temperature: 22.0,
ACAttributes.indoor_temperature: 21.0,
ACAttributes.fan_speed: fan_speed,
ACAttributes.swing_vertical: True,
ACAttributes.swing_horizontal: True,
ACAttributes.indoor_humidity: 50,
},
)
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"]
assert (state := hass.states.get(entity_entry.entity_id))
assert state.attributes[ATTR_FAN_MODE] == expected_fan_mode
@pytest.mark.parametrize(
("fan_mode", "expected_speed"),
[
pytest.param(FAN_SILENT, 20, id="silent"),
pytest.param(FAN_LOW, 40, id="low"),
pytest.param(FAN_MEDIUM, 60, id="medium"),
pytest.param(FAN_HIGH, 80, id="high"),
pytest.param(FAN_FULL_SPEED, 100, id="full"),
pytest.param(FAN_AUTO, 102, id="auto"),
],
)
async def test_ac_fan_mode_write_speeds(
hass: HomeAssistant,
mock_config_entry: Callable[[DummyDevice], MockConfigEntry],
fan_mode: str,
expected_speed: int,
) -> None:
"""Test AC set_fan_mode writes the numeric speed for every fan mode."""
device = DummyDevice(
DeviceType.AC,
attributes={
ACAttributes.power: True,
ACAttributes.mode: 1,
ACAttributes.target_temperature: 22.0,
ACAttributes.indoor_temperature: 21.0,
ACAttributes.fan_speed: 103,
ACAttributes.swing_vertical: True,
ACAttributes.swing_horizontal: True,
},
)
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"]
await _assert_service_calls(
hass,
entity_entry.entity_id,
SERVICE_SET_FAN_MODE,
{ATTR_FAN_MODE: fan_mode},
[("set_attribute", ACAttributes.fan_speed, expected_speed)],
device,
)
async def test_ac_set_temperature_without_hvac_mode(
hass: HomeAssistant,
mock_config_entry: Callable[[DummyDevice], MockConfigEntry],
) -> None:
"""Test set_temperature without hvac_mode leaves the protocol mode unset."""
device = DummyDevice(
DeviceType.AC,
attributes={
ACAttributes.power: True,
ACAttributes.mode: 1,
ACAttributes.target_temperature: 22.0,
ACAttributes.indoor_temperature: 21.0,
ACAttributes.fan_speed: 103,
ACAttributes.swing_vertical: True,
ACAttributes.swing_horizontal: True,
},
)
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"]
await _assert_service_calls(
hass,
entity_entry.entity_id,
SERVICE_SET_TEMPERATURE,
{ATTR_TEMPERATURE: 23.0},
[
(
"set_target_temperature",
{"target_temperature": 23.0, "mode": None, "zone": None},
)
],
device,
)
@pytest.mark.parametrize(
("humidity", "expected_humidity"),
[
pytest.param(50, 50.0, id="normal"),
pytest.param(0, None, id="invalid_zero"),
pytest.param(0xFF, None, id="invalid_ff"),
],
)
async def test_ac_humidity_filtering(
hass: HomeAssistant,
mock_config_entry: Callable[[DummyDevice], MockConfigEntry],
humidity: int,
expected_humidity: float | None,
) -> None:
"""Test AC humidity filtering for invalid sensor values."""
device = DummyDevice(
DeviceType.AC,
attributes={
ACAttributes.power: True,
ACAttributes.mode: 1,
ACAttributes.target_temperature: 22.0,
ACAttributes.indoor_temperature: 21.0,
ACAttributes.fan_speed: 103,
ACAttributes.swing_vertical: True,
ACAttributes.swing_horizontal: True,
ACAttributes.indoor_humidity: humidity,
},
)
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"]
assert (state := hass.states.get(entity_entry.entity_id))
assert state.attributes.get(ATTR_CURRENT_HUMIDITY) == expected_humidity
async def test_base_set_temperature_without_target_noop(
hass: HomeAssistant,
mock_config_entry: Callable[[DummyDevice], MockConfigEntry],
) -> None:
"""Test set_temperature without ATTR_TEMPERATURE is ignored."""
device = DummyDevice(
DeviceType.AC,
attributes={
ACAttributes.power: True,
ACAttributes.mode: 1,
ACAttributes.target_temperature: 22.0,
ACAttributes.indoor_temperature: 21.0,
ACAttributes.fan_speed: 103,
ACAttributes.swing_vertical: True,
ACAttributes.swing_horizontal: True,
},
)
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"]
entity = hass.data[CLIMATE_DOMAIN].get_entity(entity_entry.entity_id)
device.calls.clear()
assert entity is not None
entity.set_temperature()
assert device.calls == []
async def test_set_preset_mode_none_when_already_none_is_noop(
hass: HomeAssistant,
mock_config_entry: Callable[[DummyDevice], MockConfigEntry],
) -> None:
"""Test setting preset mode to none while already none writes nothing."""
device = DummyDevice(
DeviceType.AC,
attributes={
ACAttributes.power: True,
ACAttributes.mode: 1,
ACAttributes.target_temperature: 22.0,
ACAttributes.indoor_temperature: 21.0,
ACAttributes.comfort_mode: False,
ACAttributes.eco_mode: False,
ACAttributes.boost_mode: False,
ACAttributes.sleep_mode: False,
ACAttributes.frost_protect: False,
ACAttributes.fan_speed: 103,
ACAttributes.swing_vertical: True,
ACAttributes.swing_horizontal: True,
},
)
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"]
await _assert_service_calls(
hass,
entity_entry.entity_id,
SERVICE_SET_PRESET_MODE,
{ATTR_PRESET_MODE: PRESET_NONE},
[],
device,
)
async def test_ac_set_hvac_mode_off_calls_power_off(
hass: HomeAssistant,
mock_config_entry: Callable[[DummyDevice], MockConfigEntry],
) -> None:
"""Test AC HVAC off delegates to turn_off."""
device = DummyDevice(
DeviceType.AC,
attributes={
ACAttributes.power: True,
ACAttributes.mode: 1,
ACAttributes.target_temperature: 22.0,
ACAttributes.indoor_temperature: 21.0,
ACAttributes.fan_speed: 103,
ACAttributes.swing_vertical: True,
ACAttributes.swing_horizontal: True,
},
)
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"]
await _assert_service_calls(
hass,
entity_entry.entity_id,
SERVICE_SET_HVAC_MODE,
{"hvac_mode": HVACMode.OFF},
[("set_attribute", ACAttributes.power, False)],
device,
)
@pytest.mark.parametrize(
("attributes", "expected_state"),
[
pytest.param(
{
ACAttributes.power: True,
ACAttributes.mode: 999,
ACAttributes.target_temperature: 22.0,
ACAttributes.indoor_temperature: 21.0,
ACAttributes.fan_speed: 103,
ACAttributes.swing_vertical: True,
ACAttributes.swing_horizontal: True,
},
"unknown",
id="invalid_mode",
),
pytest.param(
{
ACAttributes.power: True,
ACAttributes.mode: 0,
ACAttributes.target_temperature: 22.0,
ACAttributes.indoor_temperature: 21.0,
ACAttributes.fan_speed: 103,
ACAttributes.swing_vertical: True,
ACAttributes.swing_horizontal: True,
},
"unknown",
id="protocol_mode_zero_while_powered_on",
),
pytest.param(
{
ACAttributes.power: False,
ACAttributes.mode: 1,
ACAttributes.target_temperature: 22.0,
ACAttributes.indoor_temperature: 21.0,
ACAttributes.fan_speed: 103,
ACAttributes.swing_vertical: True,
ACAttributes.swing_horizontal: True,
},
HVACMode.OFF,
id="power_off",
),
pytest.param(
{
ACAttributes.power: True,
ACAttributes.target_temperature: 22.0,
ACAttributes.indoor_temperature: 21.0,
ACAttributes.fan_speed: 103,
ACAttributes.swing_vertical: True,
ACAttributes.swing_horizontal: True,
},
"unknown",
id="missing_mode",
),
pytest.param(
{
ACAttributes.mode: 1,
ACAttributes.target_temperature: 22.0,
ACAttributes.indoor_temperature: 21.0,
ACAttributes.fan_speed: 103,
ACAttributes.swing_vertical: True,
ACAttributes.swing_horizontal: True,
},
"unknown",
id="missing_power",
),
],
)
async def test_ac_hvac_mode_branches(
hass: HomeAssistant,
mock_config_entry: Callable[[DummyDevice], MockConfigEntry],
attributes: dict[str, Any],
expected_state: str,
) -> None:
"""Test AC hvac_mode across power/mode edge cases.
Protocol mode 0 is reserved for the OFF entry in hvac_modes and is
never sent by the device while powered on; if the sub-protocol decoder
reports it anyway it must not be misread as an explicit OFF request.
"""
device = DummyDevice(DeviceType.AC, attributes=attributes)
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"]
assert (state := hass.states.get(entity_entry.entity_id))
assert state.state == expected_state
async def test_ac_fan_mode_invalid_type_returns_none(
hass: HomeAssistant,
mock_config_entry: Callable[[DummyDevice], MockConfigEntry],
) -> None:
"""Test AC fan_mode returns None for an unexpected attribute type."""
device = DummyDevice(
DeviceType.AC,
attributes={
ACAttributes.power: True,
ACAttributes.mode: 1,
ACAttributes.target_temperature: 22.0,
ACAttributes.indoor_temperature: 21.0,
ACAttributes.fan_speed: "auto",
ACAttributes.swing_vertical: True,
ACAttributes.swing_horizontal: True,
},
)
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"]
assert (state := hass.states.get(entity_entry.entity_id))
assert state.attributes.get(ATTR_FAN_MODE) is None
async def test_cf_min_max_temperature_from_device(
hass: HomeAssistant,
mock_config_entry: Callable[[DummyDevice], MockConfigEntry],
) -> None:
"""Test CF min/max temperature are read from the device attributes."""
device = DummyDevice(
DeviceType.CF,
attributes={
"power": True,
"mode": 2,
CFAttributes.min_temperature: 5,
CFAttributes.max_temperature: 55,
CFAttributes.current_temperature: 22,
},
)
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"]
entity = hass.data[CLIMATE_DOMAIN].get_entity(entity_entry.entity_id)
assert entity is not None
assert entity.min_temp == 5.0
assert entity.max_temp == 55.0
async def test_set_temperature_unsupported_hvac_mode_raises(
hass: HomeAssistant,
mock_config_entry: Callable[[DummyDevice], MockConfigEntry],
) -> None:
"""Test set_temperature with an hvac_mode unsupported by the device raises."""
device = DummyDevice(
DeviceType.CF,
attributes={
"power": True,
"mode": 2,
CFAttributes.min_temperature: 16,
CFAttributes.max_temperature: 30,
CFAttributes.current_temperature: 22,
},
)
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"]
device.calls.clear()
with pytest.raises(ServiceValidationError):
await hass.services.async_call(
CLIMATE_DOMAIN,
SERVICE_SET_TEMPERATURE,
{
ATTR_ENTITY_ID: entity_entry.entity_id,
ATTR_TEMPERATURE: 23.0,
"hvac_mode": HVACMode.DRY,
},
blocking=True,
)
assert device.calls == []
async def test_c3_temperature_fallback_and_turn_on(
hass: HomeAssistant,
mock_config_entry: Callable[[DummyDevice], MockConfigEntry],
) -> None:
"""Test C3 fallback temperatures and turn_on path for zone power."""
device = DummyDevice(
DeviceType.C3,
attributes={
C3Attributes.zone_temp_type: [True],
C3Attributes.temperature_min: [5, 5],
C3Attributes.temperature_max: [55, 55],
C3Attributes.mode: 1,
C3Attributes.zone1_power: True,
C3Attributes.target_temperature: [22],
C3Attributes.temp_tw_out: 21.5,
},
)
config_entry = mock_config_entry(device)
await setup_integration(hass, config_entry, device)
zone1 = entity_entries(hass, config_entry)[f"{TEST_DEVICE_ID}_climate_zone1"]
entity = hass.data[CLIMATE_DOMAIN].get_entity(zone1.entity_id)
assert entity is not None
assert entity.min_temp == 5.0
assert entity.max_temp == 55.0
await _assert_service_calls(
hass,
zone1.entity_id,
SERVICE_TURN_ON,
{},
[("set_attribute", C3Attributes.zone1_power, True)],
device,
)
async def test_c3_zero_temperature_limits_use_fallback(
hass: HomeAssistant,
mock_config_entry: Callable[[DummyDevice], MockConfigEntry],
) -> None:
"""Test C3 min/max fall back to defaults when the device reports [0.0, 0.0].
Some devices report [0.0, 0.0] for the temperature limits when in
water-mode combined with auto/cool, which must not be treated as a
valid (and therefore unusable) range.
"""
device = DummyDevice(
DeviceType.C3,
attributes={
C3Attributes.zone_temp_type: [True, False],
C3Attributes.temperature_min: [0.0, 0.0],
C3Attributes.temperature_max: [0.0, 0.0],
C3Attributes.mode: 1,
C3Attributes.zone1_power: True,
C3Attributes.target_temperature: [22, 23],
C3Attributes.temp_tw_out: 21.5,
},
)
config_entry = mock_config_entry(device)
await setup_integration(hass, config_entry, device)
zone1 = entity_entries(hass, config_entry)[f"{TEST_DEVICE_ID}_climate_zone1"]
entity = hass.data[CLIMATE_DOMAIN].get_entity(zone1.entity_id)
assert entity is not None
assert entity.min_temp == 5.0
assert entity.max_temp == 60.0
@pytest.mark.usefixtures("entity_registry_enabled_by_default")
async def test_c3_zero_temperature_limit_uses_fallback_per_zone(
hass: HomeAssistant,
mock_config_entry: Callable[[DummyDevice], MockConfigEntry],
) -> None:
"""Test C3 min/max fall back per zone when only one zone reports 0.0.
A zone stuck at 0.0 must fall back independently, even when the other
zone reports a valid, populated value.
"""
device = DummyDevice(
DeviceType.C3,
attributes={
C3Attributes.zone_temp_type: [True, False],
C3Attributes.temperature_min: [0.0, 10.0],
C3Attributes.temperature_max: [0.0, 45.0],
C3Attributes.mode: 1,
C3Attributes.zone1_power: True,
C3Attributes.target_temperature: [22, 23],
C3Attributes.temp_tw_out: 21.5,
},
)
config_entry = mock_config_entry(device)
await setup_integration(hass, config_entry, device)
zone1 = entity_entries(hass, config_entry)[f"{TEST_DEVICE_ID}_climate_zone1"]
zone2 = entity_entries(hass, config_entry)[f"{TEST_DEVICE_ID}_climate_zone2"]
entity1 = hass.data[CLIMATE_DOMAIN].get_entity(zone1.entity_id)
entity2 = hass.data[CLIMATE_DOMAIN].get_entity(zone2.entity_id)
assert entity1 is not None
assert entity1.min_temp == 5.0
assert entity1.max_temp == 60.0
assert entity2 is not None
assert entity2.min_temp == 10.0
assert entity2.max_temp == 45.0
async def test_c3_temperature_limit_list_too_short_uses_fallback(
hass: HomeAssistant,
mock_config_entry: Callable[[DummyDevice], MockConfigEntry],
) -> None:
"""Test C3 min/max fall back when the reported list has fewer than 2 entries."""
device = DummyDevice(
DeviceType.C3,
attributes={
C3Attributes.zone_temp_type: [True],
C3Attributes.temperature_min: [16],
C3Attributes.temperature_max: [30],
C3Attributes.mode: 1,
C3Attributes.zone1_power: True,
C3Attributes.target_temperature: [22],
C3Attributes.temp_tw_out: 21.5,
},
)
config_entry = mock_config_entry(device)
await setup_integration(hass, config_entry, device)
zone1 = entity_entries(hass, config_entry)[f"{TEST_DEVICE_ID}_climate_zone1"]
entity = hass.data[CLIMATE_DOMAIN].get_entity(zone1.entity_id)
assert entity is not None
assert entity.min_temp == 5.0
assert entity.max_temp == 60.0
@pytest.mark.parametrize(
("attributes", "expected_state"),
[
pytest.param(
{
C3Attributes.zone1_power: True,
C3Attributes.mode: 999,
C3Attributes.temp_tw_out: 21.5,
},
"unknown",
id="invalid_mode",
),
pytest.param(
{
C3Attributes.zone1_power: True,
C3Attributes.mode: 0,
C3Attributes.temp_tw_out: 21.5,
},
"unknown",
id="protocol_mode_zero_while_powered_on",
),
pytest.param(
{
C3Attributes.zone1_power: False,
C3Attributes.mode: 1,
C3Attributes.temp_tw_out: 21.5,
},
HVACMode.OFF,
id="power_off",
),
pytest.param(
{
C3Attributes.zone1_power: True,
C3Attributes.temp_tw_out: 21.5,
},
"unknown",
id="missing_mode",
),
pytest.param(
{
C3Attributes.zone_temp_type: [True],
C3Attributes.mode: 1,
C3Attributes.target_temperature: [22],
C3Attributes.temp_tw_out: 21.5,
},
"unknown",
id="missing_power",
),
],
)
async def test_c3_zone_hvac_mode_branches(
hass: HomeAssistant,
mock_config_entry: Callable[[DummyDevice], MockConfigEntry],
attributes: dict[str, Any],
expected_state: str,
) -> None:
"""Test C3 zone hvac_mode across power/mode edge cases."""
device = DummyDevice(DeviceType.C3, attributes=attributes)
config_entry = mock_config_entry(device)
await setup_integration(hass, config_entry, device)
zone1 = entity_entries(hass, config_entry)[f"{TEST_DEVICE_ID}_climate_zone1"]
assert (state := hass.states.get(zone1.entity_id))
assert state.state == expected_state
async def test_cf_set_hvac_mode_falls_back_to_min_temp(
hass: HomeAssistant,
mock_config_entry: Callable[[DummyDevice], MockConfigEntry],
) -> None:
"""Test CF set_hvac_mode falls back to min_temp when target_temperature is unset."""
device = DummyDevice(
DeviceType.CF,
attributes={
"power": True,
"mode": 2,
CFAttributes.min_temperature: 16,
CFAttributes.max_temperature: 30,
CFAttributes.current_temperature: 22,
},
)
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"]
await _assert_service_calls(
hass,
entity_entry.entity_id,
SERVICE_SET_HVAC_MODE,
{"hvac_mode": HVACMode.HEAT},
[
(
"set_target_temperature",
{"target_temperature": 16.0, "mode": 3},
)
],
device,
)
async def test_fb_set_hvac_off_calls_turn_off(
hass: HomeAssistant,
mock_config_entry: Callable[[DummyDevice], MockConfigEntry],
) -> None:
"""Test FB HVAC off delegates to turn_off."""
device = DummyDevice(
DeviceType.FB,
attributes={
FBAttributes.mode: "Comfort",
FBAttributes.power: True,
FBAttributes.current_temperature: 20,
},
)
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"]
await _assert_service_calls(
hass,
entity_entry.entity_id,
SERVICE_SET_HVAC_MODE,
{"hvac_mode": HVACMode.OFF},
[("set_attribute", FBAttributes.power, False)],
device,
)
async def test_fb_set_temperature_with_heat_mode_turns_on_when_off(
hass: HomeAssistant,
mock_config_entry: Callable[[DummyDevice], MockConfigEntry],
) -> None:
"""Test FB set_temperature turns the device on when off and hvac_mode is heat."""
device = DummyDevice(
DeviceType.FB,
attributes={
FBAttributes.mode: "Comfort",
FBAttributes.power: False,
FBAttributes.current_temperature: 20,
},
)
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"]
await _assert_service_calls(
hass,
entity_entry.entity_id,
SERVICE_SET_TEMPERATURE,
{ATTR_TEMPERATURE: 25.0, "hvac_mode": HVACMode.HEAT},
[
("set_attribute", FBAttributes.power, True),
(
"set_target_temperature",
{"target_temperature": 25.0, "mode": 1, "zone": None},
),
],
device,
)
async def test_cf_set_hvac_mode_off_calls_turn_off(
hass: HomeAssistant,
mock_config_entry: Callable[[DummyDevice], MockConfigEntry],
) -> None:
"""Test CF set_hvac_mode with OFF delegates to turn_off."""
device = DummyDevice(
DeviceType.CF,
attributes={
"power": True,
"mode": 2,
CFAttributes.min_temperature: 16,
CFAttributes.max_temperature: 30,
CFAttributes.current_temperature: 22,
},
)
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"]
await _assert_service_calls(
hass,
entity_entry.entity_id,
SERVICE_SET_HVAC_MODE,
{"hvac_mode": HVACMode.OFF},
[("set_attribute", CFAttributes.power, False)],
device,
)
async def test_cf_temperature_range_fallback_when_unset(
hass: HomeAssistant,
mock_config_entry: Callable[[DummyDevice], MockConfigEntry],
) -> None:
"""Test CF min/max temperature fall back to defaults when attributes are unset."""
device = DummyDevice(
DeviceType.CF,
attributes={"power": True, "mode": 2, CFAttributes.current_temperature: 22},
)
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"]
assert (state := hass.states.get(entity_entry.entity_id))
assert state.attributes[ATTR_MIN_TEMP] == 16.0
assert state.attributes[ATTR_MAX_TEMP] == 30.0
async def test_cc_fan_and_swing_invalid_types_return_none(
hass: HomeAssistant,
mock_config_entry: Callable[[DummyDevice], MockConfigEntry],
) -> None:
"""Test CC fan_mode/swing_mode return None for unexpected attribute types."""
device = DummyDevice(
DeviceType.CC,
attributes={
CCAttributes.power: True,
CCAttributes.mode: 5,
CCAttributes.fan_speed: 1,
CCAttributes.temperature_precision: 0.5,
CCAttributes.swing: "on",
},
)
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"]
assert (state := hass.states.get(entity_entry.entity_id))
assert state.attributes.get(ATTR_FAN_MODE) is None
assert state.attributes.get(ATTR_SWING_MODE) is None
@pytest.mark.usefixtures("entity_registry_enabled_by_default")
async def test_c3_zone2_service_calls_address_zone_two(
hass: HomeAssistant,
mock_config_entry: Callable[[DummyDevice], MockConfigEntry],
) -> None:
"""Test C3 zone2 service calls use zone index 1 and the zone2_power attribute."""
device = DummyDevice(
DeviceType.C3,
attributes={
C3Attributes.zone_temp_type: [True, False],
C3Attributes.temperature_min: [16, 17],
C3Attributes.temperature_max: [30, 29],
C3Attributes.mode: 1,
C3Attributes.zone1_power: True,
C3Attributes.zone2_power: True,
C3Attributes.target_temperature: [22, 23],
C3Attributes.temp_tw_out: 21.5,
},
)
config_entry = mock_config_entry(device)
await setup_integration(hass, config_entry, device)
zone2 = entity_entries(hass, config_entry)[f"{TEST_DEVICE_ID}_climate_zone2"]
await _assert_service_calls(
hass,
zone2.entity_id,
SERVICE_SET_TEMPERATURE,
{ATTR_TEMPERATURE: 24.0, "hvac_mode": HVACMode.COOL},
[
(
"set_target_temperature",
{"target_temperature": 24.0, "mode": 2, "zone": 1},
)
],
device,
)
await _assert_service_calls(
hass,
zone2.entity_id,
SERVICE_SET_HVAC_MODE,
{"hvac_mode": HVACMode.HEAT},
[("set_mode", 1, 3)],
device,
)
await _assert_service_calls(
hass,
zone2.entity_id,
SERVICE_TURN_OFF,
{},
[("set_attribute", C3Attributes.zone2_power, False)],
device,
)
async def test_c3_temperature_fallback_when_attribute_missing(
hass: HomeAssistant,
mock_config_entry: Callable[[DummyDevice], MockConfigEntry],
) -> None:
"""Test C3 min/max/target temperature fall back when attributes are missing."""
device = DummyDevice(
DeviceType.C3,
attributes={
C3Attributes.zone1_power: True,
C3Attributes.mode: 1,
C3Attributes.temp_tw_out: 21.5,
},
)
config_entry = mock_config_entry(device)
await setup_integration(hass, config_entry, device)
zone1 = entity_entries(hass, config_entry)[f"{TEST_DEVICE_ID}_climate_zone1"]
entity = hass.data[CLIMATE_DOMAIN].get_entity(zone1.entity_id)
assert entity is not None
assert entity.min_temp == 5.0
assert entity.max_temp == 60.0
assert entity.target_temperature is None
async def test_fb_invalid_attribute_types_return_none(
hass: HomeAssistant,
mock_config_entry: Callable[[DummyDevice], MockConfigEntry],
) -> None:
"""Test FB preset_mode/hvac_mode return None for unexpected attribute types."""
device = DummyDevice(
DeviceType.FB,
attributes={
FBAttributes.mode: 1,
FBAttributes.power: "on",
FBAttributes.current_temperature: 20,
},
)
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"]
assert (state := hass.states.get(entity_entry.entity_id))
assert state.attributes.get(ATTR_PRESET_MODE) is None
assert state.state == "unknown"
@pytest.mark.parametrize(
"device",
[
pytest.param(
DummyDevice(
DeviceType.AC,
attributes={
ACAttributes.power: True,
ACAttributes.mode: 1,
ACAttributes.target_temperature: 22.0,
ACAttributes.indoor_temperature: 21.0,
ACAttributes.comfort_mode: False,
ACAttributes.eco_mode: False,
ACAttributes.boost_mode: False,
ACAttributes.sleep_mode: False,
ACAttributes.frost_protect: False,
ACAttributes.fan_speed: 103,
ACAttributes.swing_vertical: True,
ACAttributes.swing_horizontal: True,
ACAttributes.indoor_humidity: 50,
},
),
id="ac",
),
pytest.param(
DummyDevice(
DeviceType.CC,
attributes={
CCAttributes.power: True,
CCAttributes.mode: 5,
CCAttributes.fan_speed: "High",
CCAttributes.temperature_precision: 0.5,
CCAttributes.swing: True,
},
),
id="cc",
),
pytest.param(
DummyDevice(
DeviceType.CF,
attributes={
"power": True,
"mode": 2,
CFAttributes.min_temperature: 16,
CFAttributes.max_temperature: 30,
CFAttributes.current_temperature: 22,
},
),
id="cf",
),
pytest.param(
DummyDevice(
DeviceType.C3,
attributes={
C3Attributes.zone_temp_type: [True, False],
C3Attributes.temperature_min: [16, 17],
C3Attributes.temperature_max: [30, 29],
C3Attributes.mode: 1,
C3Attributes.zone1_power: True,
C3Attributes.zone2_power: False,
C3Attributes.target_temperature: [22, 23],
C3Attributes.temp_tw_out: 21.5,
},
),
id="c3",
),
pytest.param(
DummyDevice(
DeviceType.FB,
attributes={
FBAttributes.mode: "Comfort",
FBAttributes.power: True,
FBAttributes.current_temperature: 20,
},
),
id="fb",
),
],
)
@pytest.mark.usefixtures("entity_registry_enabled_by_default")
async def test_climate_state_snapshot(
hass: HomeAssistant,
mock_config_entry: Callable[[DummyDevice], MockConfigEntry],
snapshot: SnapshotAssertion,
entity_registry: er.EntityRegistry,
device: DummyDevice,
) -> None:
"""Test async_setup_entry creates entities for each device type."""
config_entry = mock_config_entry(device)
with patch("homeassistant.components.midea._PLATFORMS", [Platform.CLIMATE]):
await setup_integration(hass, config_entry, device)
await snapshot_platform(hass, entity_registry, snapshot, config_entry.entry_id)