mirror of
https://github.com/home-assistant/core.git
synced 2026-08-24 10:13:52 -05:00
Add LG AC support to lg_infrared integration (#174142)
This commit is contained in:
@@ -6,7 +6,7 @@ from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
PLATFORMS = [Platform.BUTTON, Platform.EVENT, Platform.MEDIA_PLAYER]
|
||||
PLATFORMS = [Platform.BUTTON, Platform.CLIMATE, Platform.EVENT, Platform.MEDIA_PLAYER]
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@@ -0,0 +1,243 @@
|
||||
"""Climate platform for LG IR integration — LG AC."""
|
||||
|
||||
from typing import Any, override
|
||||
|
||||
from infrared_protocols.commands.lg_ac import (
|
||||
MAX_TEMP,
|
||||
MIN_TEMP,
|
||||
LgAcCommand,
|
||||
LgAcFanSpeed,
|
||||
LgAcMode,
|
||||
)
|
||||
|
||||
from homeassistant.components.climate import (
|
||||
ATTR_FAN_MODE,
|
||||
ATTR_HVAC_MODE,
|
||||
FAN_AUTO,
|
||||
FAN_HIGH,
|
||||
FAN_LOW,
|
||||
FAN_MEDIUM,
|
||||
ClimateEntity,
|
||||
ClimateEntityFeature,
|
||||
HVACMode,
|
||||
)
|
||||
from homeassistant.components.infrared import (
|
||||
InfraredEmitterConsumerEntity,
|
||||
InfraredReceivedSignal,
|
||||
InfraredReceiverConsumerEntity,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import (
|
||||
ATTR_TEMPERATURE,
|
||||
STATE_UNAVAILABLE,
|
||||
STATE_UNKNOWN,
|
||||
UnitOfTemperature,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
from homeassistant.helpers.restore_state import RestoreEntity
|
||||
|
||||
from .const import (
|
||||
CONF_DEVICE_TYPE,
|
||||
CONF_HVAC_MODES,
|
||||
CONF_INFRARED_ENTITY_ID,
|
||||
CONF_INFRARED_RECEIVER_ENTITY_ID,
|
||||
LGDeviceType,
|
||||
)
|
||||
from .entity import LgIrEntity
|
||||
|
||||
PARALLEL_UPDATES = 1
|
||||
|
||||
FAN_QUIET = "quiet"
|
||||
FAN_MEDIUM_LOW = "medium_low"
|
||||
FAN_MEDIUM_HIGH = "medium_high"
|
||||
|
||||
_HA_FAN_TO_LIB: dict[str, LgAcFanSpeed] = {
|
||||
FAN_AUTO: LgAcFanSpeed.AUTO,
|
||||
FAN_QUIET: LgAcFanSpeed.QUIET,
|
||||
FAN_LOW: LgAcFanSpeed.LOW,
|
||||
FAN_MEDIUM_LOW: LgAcFanSpeed.MEDIUM_LOW,
|
||||
FAN_MEDIUM: LgAcFanSpeed.MEDIUM,
|
||||
FAN_MEDIUM_HIGH: LgAcFanSpeed.MEDIUM_HIGH,
|
||||
FAN_HIGH: LgAcFanSpeed.HIGH,
|
||||
}
|
||||
_LIB_FAN_TO_HA: dict[LgAcFanSpeed, str] = {v: k for k, v in _HA_FAN_TO_LIB.items()}
|
||||
|
||||
_HA_MODE_TO_LIB: dict[HVACMode, LgAcMode] = {
|
||||
HVACMode.OFF: LgAcMode.OFF,
|
||||
HVACMode.COOL: LgAcMode.COOL,
|
||||
HVACMode.HEAT: LgAcMode.HEAT,
|
||||
HVACMode.DRY: LgAcMode.DRY,
|
||||
HVACMode.FAN_ONLY: LgAcMode.FAN_ONLY,
|
||||
}
|
||||
_LIB_MODE_TO_HA: dict[LgAcMode, HVACMode] = {v: k for k, v in _HA_MODE_TO_LIB.items()}
|
||||
|
||||
# Only these modes carry a temperature in the LG AC protocol frame.
|
||||
_TEMPERATURE_MODES = (LgAcMode.COOL, LgAcMode.HEAT)
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
entry: ConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up LG AC climate entity from config entry."""
|
||||
if entry.data[CONF_DEVICE_TYPE] != LGDeviceType.AC:
|
||||
return
|
||||
|
||||
emitter_entity_id = entry.data[CONF_INFRARED_ENTITY_ID]
|
||||
if receiver_entity_id := entry.data.get(CONF_INFRARED_RECEIVER_ENTITY_ID):
|
||||
async_add_entities(
|
||||
[LgAcClimateWithReceiver(entry, emitter_entity_id, receiver_entity_id)]
|
||||
)
|
||||
else:
|
||||
async_add_entities([LgAcClimateEntity(entry, emitter_entity_id)])
|
||||
|
||||
|
||||
class LgAcClimateEntity(
|
||||
LgIrEntity, InfraredEmitterConsumerEntity, ClimateEntity, RestoreEntity
|
||||
):
|
||||
"""LG AC climate entity controlled via infrared emitter."""
|
||||
|
||||
_attr_name = None
|
||||
_attr_temperature_unit = UnitOfTemperature.CELSIUS
|
||||
_attr_target_temperature_step = 1.0
|
||||
_attr_min_temp = float(MIN_TEMP)
|
||||
_attr_max_temp = float(MAX_TEMP)
|
||||
_attr_should_poll = False
|
||||
_attr_assumed_state = True
|
||||
_attr_translation_key = "lg_ac"
|
||||
_attr_fan_modes = [
|
||||
FAN_AUTO,
|
||||
FAN_QUIET,
|
||||
FAN_LOW,
|
||||
FAN_MEDIUM_LOW,
|
||||
FAN_MEDIUM,
|
||||
FAN_MEDIUM_HIGH,
|
||||
FAN_HIGH,
|
||||
]
|
||||
|
||||
def __init__(self, entry: ConfigEntry, emitter_entity_id: str) -> None:
|
||||
"""Initialize LG AC climate entity."""
|
||||
super().__init__(entry, unique_id_suffix="climate", device_name="LG AC")
|
||||
self._infrared_emitter_entity_id = emitter_entity_id
|
||||
|
||||
configured_modes = entry.data.get(
|
||||
CONF_HVAC_MODES, [HVACMode.COOL, HVACMode.DRY]
|
||||
)
|
||||
self._attr_hvac_modes = [HVACMode.OFF] + [HVACMode(m) for m in configured_modes]
|
||||
self._attr_hvac_mode = HVACMode.OFF
|
||||
self._attr_target_temperature = float(MIN_TEMP)
|
||||
self._attr_fan_mode = FAN_AUTO
|
||||
|
||||
self._attr_supported_features = ClimateEntityFeature.FAN_MODE
|
||||
# Without a temperature-carrying mode no target temperature can ever be sent.
|
||||
if any(
|
||||
_HA_MODE_TO_LIB[mode] in _TEMPERATURE_MODES
|
||||
for mode in self._attr_hvac_modes
|
||||
):
|
||||
self._attr_supported_features |= ClimateEntityFeature.TARGET_TEMPERATURE
|
||||
|
||||
@override
|
||||
async def async_added_to_hass(self) -> None:
|
||||
"""Restore the assumed state, as infrared cannot read it back from the AC."""
|
||||
await super().async_added_to_hass()
|
||||
|
||||
last_state = await self.async_get_last_state()
|
||||
if last_state is None or last_state.state in (
|
||||
STATE_UNAVAILABLE,
|
||||
STATE_UNKNOWN,
|
||||
):
|
||||
return
|
||||
|
||||
if last_state.state in self._attr_hvac_modes:
|
||||
self._attr_hvac_mode = HVACMode(last_state.state)
|
||||
if (fan_mode := last_state.attributes.get(ATTR_FAN_MODE)) in _HA_FAN_TO_LIB:
|
||||
self._attr_fan_mode = fan_mode
|
||||
if (temperature := last_state.attributes.get(ATTR_TEMPERATURE)) is not None:
|
||||
self._attr_target_temperature = float(temperature)
|
||||
|
||||
@override
|
||||
async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None:
|
||||
"""Set HVAC mode."""
|
||||
temp = int(self._attr_target_temperature or MIN_TEMP)
|
||||
await self._send_command(
|
||||
self._build_command(
|
||||
_HA_MODE_TO_LIB[hvac_mode], temp, self._attr_fan_mode or FAN_AUTO
|
||||
)
|
||||
)
|
||||
self._attr_hvac_mode = hvac_mode
|
||||
self.async_write_ha_state()
|
||||
|
||||
@override
|
||||
async def async_set_temperature(self, **kwargs: Any) -> None:
|
||||
"""Set the target temperature, switching the HVAC mode when one is given."""
|
||||
temp = int(kwargs[ATTR_TEMPERATURE])
|
||||
hvac_mode: HVACMode | None = kwargs.get(ATTR_HVAC_MODE)
|
||||
if hvac_mode is not None:
|
||||
self._valid_mode_or_raise("hvac", hvac_mode, self.hvac_modes)
|
||||
|
||||
lib_mode = _HA_MODE_TO_LIB.get(
|
||||
hvac_mode or self._attr_hvac_mode or HVACMode.OFF, LgAcMode.OFF
|
||||
)
|
||||
if hvac_mode is not None or lib_mode in _TEMPERATURE_MODES:
|
||||
await self._send_command(
|
||||
self._build_command(lib_mode, temp, self._attr_fan_mode or FAN_AUTO)
|
||||
)
|
||||
if hvac_mode is not None:
|
||||
self._attr_hvac_mode = hvac_mode
|
||||
|
||||
self._attr_target_temperature = float(temp)
|
||||
self.async_write_ha_state()
|
||||
|
||||
@override
|
||||
async def async_set_fan_mode(self, fan_mode: str) -> None:
|
||||
"""Set fan mode."""
|
||||
lib_mode = _HA_MODE_TO_LIB.get(
|
||||
self._attr_hvac_mode or HVACMode.OFF, LgAcMode.OFF
|
||||
)
|
||||
if lib_mode is not LgAcMode.OFF:
|
||||
temp = int(self._attr_target_temperature or MIN_TEMP)
|
||||
await self._send_command(self._build_command(lib_mode, temp, fan_mode))
|
||||
self._attr_fan_mode = fan_mode
|
||||
self.async_write_ha_state()
|
||||
|
||||
def _build_command(self, mode: LgAcMode, temp: int, fan_mode: str) -> LgAcCommand:
|
||||
"""Build a command from a mode, a temperature and a fan mode.
|
||||
|
||||
The library drops the temperature for the modes whose frames cannot carry one,
|
||||
so it can be passed unconditionally.
|
||||
"""
|
||||
return LgAcCommand(mode=mode, temperature=temp, fan=_HA_FAN_TO_LIB[fan_mode])
|
||||
|
||||
|
||||
class LgAcClimateWithReceiver(LgAcClimateEntity, InfraredReceiverConsumerEntity):
|
||||
"""LG AC climate entity that also tracks a configured infrared receiver."""
|
||||
|
||||
def __init__(
|
||||
self, entry: ConfigEntry, emitter_entity_id: str, receiver_entity_id: str
|
||||
) -> None:
|
||||
"""Initialize LG AC climate entity with a receiver."""
|
||||
super().__init__(entry, emitter_entity_id)
|
||||
self._infrared_receiver_entity_id = receiver_entity_id
|
||||
|
||||
@override
|
||||
@callback
|
||||
def _handle_signal(self, signal: InfraredReceivedSignal) -> None:
|
||||
"""Update state from a physical remote signal."""
|
||||
command = LgAcCommand.from_raw_timings(signal.timings)
|
||||
if command is None:
|
||||
return
|
||||
|
||||
hvac_mode = _LIB_MODE_TO_HA[command.mode]
|
||||
if hvac_mode not in self._attr_hvac_modes:
|
||||
return
|
||||
self._attr_hvac_mode = hvac_mode
|
||||
|
||||
# Power-off frames omit fan and temperature, so preserve the last known values.
|
||||
if command.fan is not None:
|
||||
self._attr_fan_mode = _LIB_FAN_TO_HA[command.fan]
|
||||
if command.temperature is not None:
|
||||
self._attr_target_temperature = float(command.temperature)
|
||||
|
||||
self.async_write_ha_state()
|
||||
@@ -4,12 +4,14 @@ from typing import TYPE_CHECKING, Any, override
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.climate import HVACMode
|
||||
from homeassistant.components.infrared import (
|
||||
DOMAIN as INFRARED_DOMAIN,
|
||||
async_get_emitters,
|
||||
async_get_receivers,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers import entity_registry as er
|
||||
from homeassistant.helpers.selector import (
|
||||
EntitySelector,
|
||||
@@ -21,6 +23,7 @@ from homeassistant.helpers.selector import (
|
||||
|
||||
from .const import (
|
||||
CONF_DEVICE_TYPE,
|
||||
CONF_HVAC_MODES,
|
||||
CONF_INFRARED_ENTITY_ID,
|
||||
CONF_INFRARED_RECEIVER_ENTITY_ID,
|
||||
DOMAIN,
|
||||
@@ -29,89 +32,140 @@ from .const import (
|
||||
|
||||
DEVICE_TYPE_NAMES: dict[LGDeviceType, str] = {
|
||||
LGDeviceType.TV: "TV",
|
||||
LGDeviceType.AC: "AC",
|
||||
}
|
||||
|
||||
_HVAC_MODE_OPTIONS = [
|
||||
HVACMode.COOL,
|
||||
HVACMode.HEAT,
|
||||
HVACMode.DRY,
|
||||
HVACMode.FAN_ONLY,
|
||||
]
|
||||
_DEFAULT_HVAC_MODES = [HVACMode.COOL, HVACMode.DRY]
|
||||
|
||||
|
||||
@callback
|
||||
def _infrared_entity_schema(
|
||||
hass: HomeAssistant, *, emitter_required: bool
|
||||
) -> vol.Schema:
|
||||
"""Return the emitter/receiver selection schema shared by every device type."""
|
||||
emitter_marker = vol.Required if emitter_required else vol.Optional
|
||||
return vol.Schema(
|
||||
{
|
||||
emitter_marker(CONF_INFRARED_ENTITY_ID): EntitySelector(
|
||||
EntitySelectorConfig(
|
||||
domain=INFRARED_DOMAIN,
|
||||
include_entities=async_get_emitters(hass),
|
||||
)
|
||||
),
|
||||
vol.Optional(CONF_INFRARED_RECEIVER_ENTITY_ID): EntitySelector(
|
||||
EntitySelectorConfig(
|
||||
domain=INFRARED_DOMAIN,
|
||||
include_entities=async_get_receivers(hass),
|
||||
)
|
||||
),
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
class LgIrConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
"""Handle config flow for LG IR."""
|
||||
|
||||
VERSION = 2
|
||||
|
||||
def _entity_name(self, entity_id: str) -> str:
|
||||
ent_reg = er.async_get(self.hass)
|
||||
entry = ent_reg.async_get(entity_id)
|
||||
return entry.name or entry.original_name or entity_id if entry else entity_id
|
||||
|
||||
async def _async_create_device_entry(
|
||||
self, device_type: LGDeviceType, user_input: dict[str, Any]
|
||||
) -> ConfigFlowResult:
|
||||
"""Abort on a duplicate IR entity and create the entry for the device."""
|
||||
emitter_id = user_input.get(CONF_INFRARED_ENTITY_ID)
|
||||
receiver_id = user_input.get(CONF_INFRARED_RECEIVER_ENTITY_ID)
|
||||
if emitter_id:
|
||||
self._async_abort_entries_match(
|
||||
{CONF_DEVICE_TYPE: device_type, CONF_INFRARED_ENTITY_ID: emitter_id}
|
||||
)
|
||||
if receiver_id:
|
||||
self._async_abort_entries_match(
|
||||
{
|
||||
CONF_DEVICE_TYPE: device_type,
|
||||
CONF_INFRARED_RECEIVER_ENTITY_ID: receiver_id,
|
||||
}
|
||||
)
|
||||
|
||||
title_entity_id = emitter_id or receiver_id
|
||||
if TYPE_CHECKING:
|
||||
assert title_entity_id is not None
|
||||
return self.async_create_entry(
|
||||
title=f"LG {DEVICE_TYPE_NAMES[device_type]} via "
|
||||
f"{self._entity_name(title_entity_id)}",
|
||||
data={CONF_DEVICE_TYPE: device_type, **user_input},
|
||||
)
|
||||
|
||||
@override
|
||||
async def async_step_user(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
"""Handle the initial step."""
|
||||
"""Handle device type selection."""
|
||||
emitter_entity_ids = async_get_emitters(self.hass)
|
||||
receiver_entity_ids = async_get_receivers(self.hass)
|
||||
if not emitter_entity_ids and not receiver_entity_ids:
|
||||
if not emitter_entity_ids and not async_get_receivers(self.hass):
|
||||
return self.async_abort(reason="no_infrared_entities")
|
||||
|
||||
menu_options = [LGDeviceType.TV.value]
|
||||
# The AC step requires an emitter, so offering it without one dead-ends.
|
||||
if emitter_entity_ids:
|
||||
menu_options.append(LGDeviceType.AC.value)
|
||||
|
||||
return self.async_show_menu(step_id="user", menu_options=menu_options)
|
||||
|
||||
async def async_step_tv(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
"""Handle TV entity selection."""
|
||||
errors: dict[str, str] = {}
|
||||
|
||||
if user_input is not None:
|
||||
emitter_id = user_input.get(CONF_INFRARED_ENTITY_ID)
|
||||
receiver_id = user_input.get(CONF_INFRARED_RECEIVER_ENTITY_ID)
|
||||
if emitter_id or receiver_id:
|
||||
device_type = user_input[CONF_DEVICE_TYPE]
|
||||
|
||||
if emitter_id:
|
||||
self._async_abort_entries_match(
|
||||
{
|
||||
CONF_DEVICE_TYPE: device_type,
|
||||
CONF_INFRARED_ENTITY_ID: emitter_id,
|
||||
}
|
||||
)
|
||||
if receiver_id:
|
||||
self._async_abort_entries_match(
|
||||
{
|
||||
CONF_DEVICE_TYPE: device_type,
|
||||
CONF_INFRARED_RECEIVER_ENTITY_ID: receiver_id,
|
||||
}
|
||||
)
|
||||
|
||||
# Get entity name for the title
|
||||
title_entity_id = emitter_id or receiver_id
|
||||
if TYPE_CHECKING:
|
||||
assert title_entity_id is not None
|
||||
ent_reg = er.async_get(self.hass)
|
||||
entry = ent_reg.async_get(title_entity_id)
|
||||
title_entity_name = (
|
||||
entry.name or entry.original_name or title_entity_id
|
||||
if entry
|
||||
else title_entity_id
|
||||
if user_input.get(CONF_INFRARED_ENTITY_ID) or user_input.get(
|
||||
CONF_INFRARED_RECEIVER_ENTITY_ID
|
||||
):
|
||||
return await self._async_create_device_entry(
|
||||
LGDeviceType.TV, user_input
|
||||
)
|
||||
device_type_name = DEVICE_TYPE_NAMES[LGDeviceType(device_type)]
|
||||
title = f"LG {device_type_name} via {title_entity_name}"
|
||||
|
||||
return self.async_create_entry(title=title, data=user_input)
|
||||
|
||||
errors["base"] = "missing_infrared_entity"
|
||||
|
||||
schema_dict: dict[vol.Marker, Any] = {
|
||||
vol.Required(CONF_DEVICE_TYPE): SelectSelector(
|
||||
SelectSelectorConfig(
|
||||
options=[device_type.value for device_type in LGDeviceType],
|
||||
translation_key=CONF_DEVICE_TYPE,
|
||||
mode=SelectSelectorMode.DROPDOWN,
|
||||
)
|
||||
),
|
||||
vol.Optional(CONF_INFRARED_ENTITY_ID): EntitySelector(
|
||||
EntitySelectorConfig(
|
||||
domain=INFRARED_DOMAIN,
|
||||
include_entities=emitter_entity_ids,
|
||||
)
|
||||
),
|
||||
vol.Optional(CONF_INFRARED_RECEIVER_ENTITY_ID): EntitySelector(
|
||||
EntitySelectorConfig(
|
||||
domain=INFRARED_DOMAIN,
|
||||
include_entities=receiver_entity_ids,
|
||||
)
|
||||
),
|
||||
}
|
||||
|
||||
return self.async_show_form(
|
||||
step_id="user",
|
||||
data_schema=vol.Schema(schema_dict),
|
||||
step_id="tv",
|
||||
data_schema=_infrared_entity_schema(self.hass, emitter_required=False),
|
||||
errors=errors,
|
||||
)
|
||||
|
||||
async def async_step_ac(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
"""Handle AC entity and mode selection."""
|
||||
if user_input is not None:
|
||||
return await self._async_create_device_entry(LGDeviceType.AC, user_input)
|
||||
|
||||
return self.async_show_form(
|
||||
step_id="ac",
|
||||
data_schema=_infrared_entity_schema(
|
||||
self.hass, emitter_required=True
|
||||
).extend(
|
||||
{
|
||||
vol.Required(CONF_HVAC_MODES, default=_DEFAULT_HVAC_MODES): vol.All(
|
||||
SelectSelector(
|
||||
SelectSelectorConfig(
|
||||
options=[mode.value for mode in _HVAC_MODE_OPTIONS],
|
||||
translation_key=CONF_HVAC_MODES,
|
||||
mode=SelectSelectorMode.LIST,
|
||||
multiple=True,
|
||||
)
|
||||
),
|
||||
vol.Length(min=1, msg="no_hvac_modes"),
|
||||
)
|
||||
}
|
||||
),
|
||||
)
|
||||
|
||||
@@ -6,9 +6,11 @@ DOMAIN = "lg_infrared"
|
||||
CONF_INFRARED_ENTITY_ID = "infrared_entity_id"
|
||||
CONF_INFRARED_RECEIVER_ENTITY_ID = "infrared_receiver_entity_id"
|
||||
CONF_DEVICE_TYPE = "device_type"
|
||||
CONF_HVAC_MODES = "hvac_modes"
|
||||
|
||||
|
||||
class LGDeviceType(StrEnum):
|
||||
"""LG device types."""
|
||||
|
||||
TV = "tv"
|
||||
AC = "ac"
|
||||
|
||||
@@ -12,9 +12,16 @@ class LgIrEntity(Entity):
|
||||
|
||||
_attr_has_entity_name = True
|
||||
|
||||
def __init__(self, entry: ConfigEntry, unique_id_suffix: str) -> None:
|
||||
def __init__(
|
||||
self,
|
||||
entry: ConfigEntry,
|
||||
unique_id_suffix: str,
|
||||
device_name: str = "LG TV",
|
||||
) -> None:
|
||||
"""Initialize LG IR entity."""
|
||||
self._attr_unique_id = f"{entry.entry_id}_{unique_id_suffix}"
|
||||
self._attr_device_info = DeviceInfo(
|
||||
identifiers={(DOMAIN, entry.entry_id)}, name="LG TV", manufacturer="LG"
|
||||
identifiers={(DOMAIN, entry.entry_id)},
|
||||
name=device_name,
|
||||
manufacturer="LG",
|
||||
)
|
||||
|
||||
@@ -5,21 +5,42 @@
|
||||
"no_infrared_entities": "[%key:common::config_flow::abort::no_infrared_entities%]"
|
||||
},
|
||||
"error": {
|
||||
"missing_infrared_entity": "Select an infrared emitter or receiver."
|
||||
"missing_infrared_entity": "Select an infrared emitter or receiver.",
|
||||
"no_hvac_modes": "Select at least one supported mode."
|
||||
},
|
||||
"step": {
|
||||
"user": {
|
||||
"ac": {
|
||||
"data": {
|
||||
"hvac_modes": "Supported modes",
|
||||
"infrared_entity_id": "[%key:common::config_flow::data::infrared_entity_id%]",
|
||||
"infrared_receiver_entity_id": "[%key:common::config_flow::data::infrared_receiver_entity_id%]"
|
||||
},
|
||||
"data_description": {
|
||||
"hvac_modes": "Select the operating modes your AC unit supports. Heat is not available on all models.",
|
||||
"infrared_entity_id": "[%key:common::config_flow::data_description::infrared_entity_id%]",
|
||||
"infrared_receiver_entity_id": "Optional — allows the integration to update state when the physical remote is used."
|
||||
},
|
||||
"description": "Select an infrared emitter and the modes your LG AC supports.",
|
||||
"title": "Set up LG AC"
|
||||
},
|
||||
"tv": {
|
||||
"data": {
|
||||
"device_type": "[%key:common::generic::device_type%]",
|
||||
"infrared_entity_id": "[%key:common::config_flow::data::infrared_entity_id%]",
|
||||
"infrared_receiver_entity_id": "[%key:common::config_flow::data::infrared_receiver_entity_id%]"
|
||||
},
|
||||
"data_description": {
|
||||
"device_type": "The type of LG device to control.",
|
||||
"infrared_entity_id": "[%key:common::config_flow::data_description::infrared_entity_id%]",
|
||||
"infrared_receiver_entity_id": "The infrared receiver entity to use for receiving signals."
|
||||
},
|
||||
"description": "Select the device type and at least one infrared emitter or receiver to use with your LG device.",
|
||||
"description": "Select at least one infrared emitter or receiver to use with your LG TV.",
|
||||
"title": "Set up LG TV"
|
||||
},
|
||||
"user": {
|
||||
"description": "Select the type of LG device to set up.",
|
||||
"menu_options": {
|
||||
"ac": "Air conditioner",
|
||||
"tv": "[%key:common::generic::tv%]"
|
||||
},
|
||||
"title": "Set up LG IR Remote"
|
||||
}
|
||||
}
|
||||
@@ -114,6 +135,19 @@
|
||||
"name": "[%key:common::entity::button::up::name%]"
|
||||
}
|
||||
},
|
||||
"climate": {
|
||||
"lg_ac": {
|
||||
"state_attributes": {
|
||||
"fan_mode": {
|
||||
"state": {
|
||||
"medium_high": "Medium high",
|
||||
"medium_low": "Medium low",
|
||||
"quiet": "Quiet"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"event": {
|
||||
"received_command": {
|
||||
"name": "Received command",
|
||||
@@ -179,9 +213,12 @@
|
||||
}
|
||||
},
|
||||
"selector": {
|
||||
"device_type": {
|
||||
"hvac_modes": {
|
||||
"options": {
|
||||
"tv": "[%key:common::generic::tv%]"
|
||||
"cool": "Cool",
|
||||
"dry": "Dry",
|
||||
"fan_only": "Fan only",
|
||||
"heat": "Heat"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,13 +1,17 @@
|
||||
"""Common fixtures for the LG Infrared tests."""
|
||||
|
||||
from collections.abc import Generator
|
||||
from typing import Any
|
||||
from unittest.mock import patch
|
||||
|
||||
import pytest
|
||||
|
||||
from homeassistant.components.climate import HVACMode
|
||||
from homeassistant.components.lg_infrared import PLATFORMS
|
||||
from homeassistant.components.lg_infrared.config_flow import DEVICE_TYPE_NAMES
|
||||
from homeassistant.components.lg_infrared.const import (
|
||||
CONF_DEVICE_TYPE,
|
||||
CONF_HVAC_MODES,
|
||||
CONF_INFRARED_ENTITY_ID,
|
||||
CONF_INFRARED_RECEIVER_ENTITY_ID,
|
||||
DOMAIN,
|
||||
@@ -26,19 +30,59 @@ from tests.components.infrared.common import (
|
||||
MockInfraredReceiverEntity,
|
||||
)
|
||||
|
||||
ENTRY_ID = "01JTEST0000000000000000000"
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_config_entry() -> MockConfigEntry:
|
||||
"""Return a mock config entry."""
|
||||
def device_type() -> LGDeviceType:
|
||||
"""Return the device type of the config entry."""
|
||||
return LGDeviceType.TV
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def hvac_modes() -> list[HVACMode]:
|
||||
"""Return the HVAC modes configured on an AC config entry."""
|
||||
return [HVACMode.COOL, HVACMode.DRY]
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def has_receiver() -> bool:
|
||||
"""Return whether the config entry has an infrared receiver configured."""
|
||||
return True
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def extra_entry_data(
|
||||
device_type: LGDeviceType, hvac_modes: list[HVACMode]
|
||||
) -> dict[str, Any]:
|
||||
"""Return the device type specific config entry data."""
|
||||
return {
|
||||
LGDeviceType.TV: {},
|
||||
LGDeviceType.AC: {CONF_HVAC_MODES: hvac_modes},
|
||||
}[device_type]
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_config_entry(
|
||||
device_type: LGDeviceType,
|
||||
extra_entry_data: dict[str, Any],
|
||||
has_receiver: bool,
|
||||
) -> MockConfigEntry:
|
||||
"""Return a mock config entry for the configured device type."""
|
||||
data: dict[str, Any] = {
|
||||
CONF_DEVICE_TYPE: device_type,
|
||||
CONF_INFRARED_ENTITY_ID: MOCK_INFRARED_EMITTER_ENTITY_ID,
|
||||
**extra_entry_data,
|
||||
}
|
||||
if has_receiver:
|
||||
data[CONF_INFRARED_RECEIVER_ENTITY_ID] = MOCK_INFRARED_RECEIVER_ENTITY_ID
|
||||
|
||||
device_name = DEVICE_TYPE_NAMES[device_type]
|
||||
return MockConfigEntry(
|
||||
domain=DOMAIN,
|
||||
entry_id="01JTEST0000000000000000000",
|
||||
title="LG TV via Test IR emitter",
|
||||
data={
|
||||
CONF_DEVICE_TYPE: LGDeviceType.TV,
|
||||
CONF_INFRARED_ENTITY_ID: MOCK_INFRARED_EMITTER_ENTITY_ID,
|
||||
CONF_INFRARED_RECEIVER_ENTITY_ID: MOCK_INFRARED_RECEIVER_ENTITY_ID,
|
||||
},
|
||||
entry_id=ENTRY_ID,
|
||||
title=f"LG {device_name} via Test IR emitter",
|
||||
data=data,
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,91 @@
|
||||
# serializer version: 1
|
||||
# name: test_entities[climate.lg_ac-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': list([
|
||||
None,
|
||||
]),
|
||||
'area_id': None,
|
||||
'capabilities': dict({
|
||||
<ClimateEntityCapabilityAttribute.FAN_MODES: 'fan_modes'>: list([
|
||||
'auto',
|
||||
'quiet',
|
||||
'low',
|
||||
'medium_low',
|
||||
'medium',
|
||||
'medium_high',
|
||||
'high',
|
||||
]),
|
||||
<ClimateEntityCapabilityAttribute.HVAC_MODES: 'hvac_modes'>: list([
|
||||
<HVACMode.OFF: 'off'>,
|
||||
<HVACMode.COOL: 'cool'>,
|
||||
<HVACMode.DRY: 'dry'>,
|
||||
]),
|
||||
<ClimateEntityCapabilityAttribute.MAX_TEMP: 'max_temp'>: 30.0,
|
||||
<ClimateEntityCapabilityAttribute.MIN_TEMP: 'min_temp'>: 16.0,
|
||||
<ClimateEntityCapabilityAttribute.TARGET_TEMP_STEP: 'target_temp_step'>: 1.0,
|
||||
}),
|
||||
'config_entry_id': <ANY>,
|
||||
'config_subentry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'climate',
|
||||
'entity_category': None,
|
||||
'entity_id': 'climate.lg_ac',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'object_id_base': None,
|
||||
'options': dict({
|
||||
}),
|
||||
'original_device_class': None,
|
||||
'original_icon': None,
|
||||
'original_name': None,
|
||||
'platform': 'lg_infrared',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
'supported_features': <ClimateEntityFeature: 9>,
|
||||
'translation_key': 'lg_ac',
|
||||
'unique_id': '01JTEST0000000000000000000_climate',
|
||||
'unit_of_measurement': None,
|
||||
})
|
||||
# ---
|
||||
# name: test_entities[climate.lg_ac-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
<EntityStateAttribute.ASSUMED_STATE: 'assumed_state'>: True,
|
||||
<ClimateEntityStateAttribute.CURRENT_TEMPERATURE: 'current_temperature'>: None,
|
||||
<ClimateEntityStateAttribute.FAN_MODE: 'fan_mode'>: 'auto',
|
||||
<ClimateEntityCapabilityAttribute.FAN_MODES: 'fan_modes'>: list([
|
||||
'auto',
|
||||
'quiet',
|
||||
'low',
|
||||
'medium_low',
|
||||
'medium',
|
||||
'medium_high',
|
||||
'high',
|
||||
]),
|
||||
<EntityStateAttribute.FRIENDLY_NAME: 'friendly_name'>: 'LG AC',
|
||||
<ClimateEntityCapabilityAttribute.HVAC_MODES: 'hvac_modes'>: list([
|
||||
<HVACMode.OFF: 'off'>,
|
||||
<HVACMode.COOL: 'cool'>,
|
||||
<HVACMode.DRY: 'dry'>,
|
||||
]),
|
||||
<ClimateEntityCapabilityAttribute.MAX_TEMP: 'max_temp'>: 30.0,
|
||||
<ClimateEntityCapabilityAttribute.MIN_TEMP: 'min_temp'>: 16.0,
|
||||
<EntityStateAttribute.SUPPORTED_FEATURES: 'supported_features'>: <ClimateEntityFeature: 9>,
|
||||
<ClimateEntityCapabilityAttribute.TARGET_TEMP_STEP: 'target_temp_step'>: 1.0,
|
||||
<ClimateEntityStateAttribute.TEMPERATURE: 'temperature'>: 16.0,
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'climate.lg_ac',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': 'off',
|
||||
})
|
||||
# ---
|
||||
@@ -0,0 +1,565 @@
|
||||
"""Tests for the LG Infrared climate platform."""
|
||||
|
||||
from typing import Any
|
||||
from unittest.mock import patch
|
||||
|
||||
from infrared_protocols.commands.lg_ac import (
|
||||
MIN_TEMP,
|
||||
LgAcCommand,
|
||||
LgAcFanSpeed,
|
||||
LgAcMode,
|
||||
)
|
||||
import pytest
|
||||
from syrupy.assertion import SnapshotAssertion
|
||||
|
||||
from homeassistant.components.climate import (
|
||||
DOMAIN as CLIMATE_DOMAIN,
|
||||
FAN_AUTO,
|
||||
FAN_HIGH,
|
||||
FAN_LOW,
|
||||
FAN_MEDIUM,
|
||||
SERVICE_SET_FAN_MODE,
|
||||
SERVICE_SET_HVAC_MODE,
|
||||
SERVICE_SET_TEMPERATURE,
|
||||
ClimateEntityFeature,
|
||||
HVACMode,
|
||||
)
|
||||
from homeassistant.components.infrared import InfraredReceivedSignal
|
||||
from homeassistant.components.lg_infrared.climate import (
|
||||
FAN_MEDIUM_HIGH,
|
||||
FAN_MEDIUM_LOW,
|
||||
FAN_QUIET,
|
||||
)
|
||||
from homeassistant.components.lg_infrared.const import LGDeviceType
|
||||
from homeassistant.const import (
|
||||
ATTR_ENTITY_ID,
|
||||
ATTR_TEMPERATURE,
|
||||
STATE_UNAVAILABLE,
|
||||
Platform,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant, State
|
||||
from homeassistant.exceptions import ServiceValidationError
|
||||
from homeassistant.helpers import entity_registry as er
|
||||
|
||||
from tests.common import MockConfigEntry, mock_restore_cache, snapshot_platform
|
||||
from tests.components.common import assert_availability_follows_source_entity
|
||||
from tests.components.infrared import EMITTER_ENTITY_ID
|
||||
from tests.components.infrared.common import (
|
||||
MockInfraredEmitterEntity,
|
||||
MockInfraredReceiverEntity,
|
||||
)
|
||||
|
||||
_CLIMATE_ENTITY_ID = "climate.lg_ac"
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def platforms() -> list[Platform]:
|
||||
"""Return platforms to set up."""
|
||||
return [Platform.CLIMATE]
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def device_type() -> LGDeviceType:
|
||||
"""Return the device type of the config entry."""
|
||||
return LGDeviceType.AC
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def has_receiver() -> bool:
|
||||
"""Return whether the config entry has an infrared receiver configured."""
|
||||
return False
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("init_integration")
|
||||
async def test_entities(
|
||||
hass: HomeAssistant,
|
||||
snapshot: SnapshotAssertion,
|
||||
entity_registry: er.EntityRegistry,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
) -> None:
|
||||
"""Test entity state and registry snapshot."""
|
||||
await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id)
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("init_integration")
|
||||
async def test_availability_follows_emitter(
|
||||
hass: HomeAssistant,
|
||||
mock_infrared_emitter_entity: MockInfraredEmitterEntity,
|
||||
) -> None:
|
||||
"""Test climate entity availability follows the infrared emitter."""
|
||||
await assert_availability_follows_source_entity(
|
||||
hass, _CLIMATE_ENTITY_ID, EMITTER_ENTITY_ID
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("init_integration")
|
||||
async def test_set_hvac_mode_off(
|
||||
hass: HomeAssistant,
|
||||
mock_infrared_emitter_entity: MockInfraredEmitterEntity,
|
||||
) -> None:
|
||||
"""Test setting HVAC mode to off sends power-off timings."""
|
||||
await hass.services.async_call(
|
||||
CLIMATE_DOMAIN,
|
||||
SERVICE_SET_HVAC_MODE,
|
||||
{ATTR_ENTITY_ID: _CLIMATE_ENTITY_ID, "hvac_mode": HVACMode.OFF},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
assert len(mock_infrared_emitter_entity.send_command_calls) == 1
|
||||
timings = mock_infrared_emitter_entity.send_command_calls[0].get_raw_timings()
|
||||
assert timings == LgAcCommand(mode=LgAcMode.OFF).get_raw_timings()
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("init_integration")
|
||||
@pytest.mark.parametrize(
|
||||
("hvac_mode", "temp", "fan", "expected_cmd"),
|
||||
[
|
||||
pytest.param(
|
||||
HVACMode.COOL,
|
||||
24,
|
||||
FAN_AUTO,
|
||||
LgAcCommand(mode=LgAcMode.COOL, temperature=24, fan=LgAcFanSpeed.AUTO),
|
||||
id="cool_24_auto",
|
||||
),
|
||||
pytest.param(
|
||||
HVACMode.COOL,
|
||||
18,
|
||||
FAN_LOW,
|
||||
LgAcCommand(mode=LgAcMode.COOL, temperature=18, fan=LgAcFanSpeed.LOW),
|
||||
id="cool_18_low",
|
||||
),
|
||||
pytest.param(
|
||||
HVACMode.COOL,
|
||||
30,
|
||||
FAN_HIGH,
|
||||
LgAcCommand(mode=LgAcMode.COOL, temperature=30, fan=LgAcFanSpeed.HIGH),
|
||||
id="cool_30_high",
|
||||
),
|
||||
pytest.param(
|
||||
HVACMode.DRY,
|
||||
24,
|
||||
FAN_MEDIUM,
|
||||
LgAcCommand(mode=LgAcMode.DRY, fan=LgAcFanSpeed.MEDIUM),
|
||||
id="dry_medium",
|
||||
),
|
||||
],
|
||||
)
|
||||
async def test_set_hvac_mode_encodes_correctly(
|
||||
hass: HomeAssistant,
|
||||
mock_infrared_emitter_entity: MockInfraredEmitterEntity,
|
||||
hvac_mode: HVACMode,
|
||||
temp: int,
|
||||
fan: str,
|
||||
expected_cmd: LgAcCommand,
|
||||
) -> None:
|
||||
"""Test that set_hvac_mode sends correctly encoded timings."""
|
||||
await hass.services.async_call(
|
||||
CLIMATE_DOMAIN,
|
||||
SERVICE_SET_TEMPERATURE,
|
||||
{ATTR_ENTITY_ID: _CLIMATE_ENTITY_ID, ATTR_TEMPERATURE: temp},
|
||||
blocking=True,
|
||||
)
|
||||
await hass.services.async_call(
|
||||
CLIMATE_DOMAIN,
|
||||
SERVICE_SET_FAN_MODE,
|
||||
{ATTR_ENTITY_ID: _CLIMATE_ENTITY_ID, "fan_mode": fan},
|
||||
blocking=True,
|
||||
)
|
||||
mock_infrared_emitter_entity.send_command_calls.clear()
|
||||
|
||||
await hass.services.async_call(
|
||||
CLIMATE_DOMAIN,
|
||||
SERVICE_SET_HVAC_MODE,
|
||||
{ATTR_ENTITY_ID: _CLIMATE_ENTITY_ID, "hvac_mode": hvac_mode},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
assert len(mock_infrared_emitter_entity.send_command_calls) == 1
|
||||
timings = mock_infrared_emitter_entity.send_command_calls[0].get_raw_timings()
|
||||
assert timings == expected_cmd.get_raw_timings()
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"hvac_modes",
|
||||
[[HVACMode.COOL, HVACMode.HEAT, HVACMode.DRY, HVACMode.FAN_ONLY]],
|
||||
)
|
||||
@pytest.mark.usefixtures("init_integration")
|
||||
@pytest.mark.parametrize(
|
||||
("hvac_mode", "expected_cmd"),
|
||||
[
|
||||
pytest.param(
|
||||
HVACMode.HEAT,
|
||||
LgAcCommand(
|
||||
mode=LgAcMode.HEAT, temperature=MIN_TEMP, fan=LgAcFanSpeed.AUTO
|
||||
),
|
||||
id="heat",
|
||||
),
|
||||
pytest.param(
|
||||
HVACMode.FAN_ONLY,
|
||||
LgAcCommand(mode=LgAcMode.FAN_ONLY, fan=LgAcFanSpeed.AUTO),
|
||||
id="fan_only",
|
||||
),
|
||||
],
|
||||
)
|
||||
async def test_set_hvac_mode_heat_and_fan_only(
|
||||
hass: HomeAssistant,
|
||||
mock_infrared_emitter_entity: MockInfraredEmitterEntity,
|
||||
hvac_mode: HVACMode,
|
||||
expected_cmd: LgAcCommand,
|
||||
) -> None:
|
||||
"""Test heat and fan-only modes encode from the entity defaults."""
|
||||
await hass.services.async_call(
|
||||
CLIMATE_DOMAIN,
|
||||
SERVICE_SET_HVAC_MODE,
|
||||
{ATTR_ENTITY_ID: _CLIMATE_ENTITY_ID, "hvac_mode": hvac_mode},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
assert len(mock_infrared_emitter_entity.send_command_calls) == 1
|
||||
timings = mock_infrared_emitter_entity.send_command_calls[0].get_raw_timings()
|
||||
assert timings == expected_cmd.get_raw_timings()
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("init_integration")
|
||||
async def test_set_temperature_sends_command_when_active(
|
||||
hass: HomeAssistant,
|
||||
mock_infrared_emitter_entity: MockInfraredEmitterEntity,
|
||||
) -> None:
|
||||
"""Test set_temperature sends IR when AC is on."""
|
||||
await hass.services.async_call(
|
||||
CLIMATE_DOMAIN,
|
||||
SERVICE_SET_HVAC_MODE,
|
||||
{ATTR_ENTITY_ID: _CLIMATE_ENTITY_ID, "hvac_mode": HVACMode.COOL},
|
||||
blocking=True,
|
||||
)
|
||||
mock_infrared_emitter_entity.send_command_calls.clear()
|
||||
|
||||
await hass.services.async_call(
|
||||
CLIMATE_DOMAIN,
|
||||
SERVICE_SET_TEMPERATURE,
|
||||
{ATTR_ENTITY_ID: _CLIMATE_ENTITY_ID, ATTR_TEMPERATURE: 26},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
assert len(mock_infrared_emitter_entity.send_command_calls) == 1
|
||||
timings = mock_infrared_emitter_entity.send_command_calls[0].get_raw_timings()
|
||||
assert (
|
||||
timings
|
||||
== LgAcCommand(
|
||||
mode=LgAcMode.COOL, temperature=26, fan=LgAcFanSpeed.AUTO
|
||||
).get_raw_timings()
|
||||
)
|
||||
|
||||
state = hass.states.get(_CLIMATE_ENTITY_ID)
|
||||
assert state is not None
|
||||
assert float(state.attributes["temperature"]) == 26.0
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("init_integration")
|
||||
async def test_set_temperature_no_command_when_off(
|
||||
hass: HomeAssistant,
|
||||
mock_infrared_emitter_entity: MockInfraredEmitterEntity,
|
||||
) -> None:
|
||||
"""Test set_temperature updates state but sends no IR when AC is off."""
|
||||
await hass.services.async_call(
|
||||
CLIMATE_DOMAIN,
|
||||
SERVICE_SET_TEMPERATURE,
|
||||
{ATTR_ENTITY_ID: _CLIMATE_ENTITY_ID, ATTR_TEMPERATURE: 22},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
assert len(mock_infrared_emitter_entity.send_command_calls) == 0
|
||||
|
||||
state = hass.states.get(_CLIMATE_ENTITY_ID)
|
||||
assert state is not None
|
||||
assert float(state.attributes["temperature"]) == 22.0
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("init_integration")
|
||||
async def test_set_temperature_no_command_in_dry_mode(
|
||||
hass: HomeAssistant,
|
||||
mock_infrared_emitter_entity: MockInfraredEmitterEntity,
|
||||
) -> None:
|
||||
"""Test set_temperature sends no IR in dry mode (protocol-fixed temp)."""
|
||||
await hass.services.async_call(
|
||||
CLIMATE_DOMAIN,
|
||||
SERVICE_SET_HVAC_MODE,
|
||||
{ATTR_ENTITY_ID: _CLIMATE_ENTITY_ID, "hvac_mode": HVACMode.DRY},
|
||||
blocking=True,
|
||||
)
|
||||
mock_infrared_emitter_entity.send_command_calls.clear()
|
||||
|
||||
await hass.services.async_call(
|
||||
CLIMATE_DOMAIN,
|
||||
SERVICE_SET_TEMPERATURE,
|
||||
{ATTR_ENTITY_ID: _CLIMATE_ENTITY_ID, ATTR_TEMPERATURE: 25},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
assert len(mock_infrared_emitter_entity.send_command_calls) == 0
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("init_integration")
|
||||
async def test_set_fan_mode_sends_command_when_active(
|
||||
hass: HomeAssistant,
|
||||
mock_infrared_emitter_entity: MockInfraredEmitterEntity,
|
||||
) -> None:
|
||||
"""Test set_fan_mode sends IR when AC is on."""
|
||||
await hass.services.async_call(
|
||||
CLIMATE_DOMAIN,
|
||||
SERVICE_SET_HVAC_MODE,
|
||||
{ATTR_ENTITY_ID: _CLIMATE_ENTITY_ID, "hvac_mode": HVACMode.COOL},
|
||||
blocking=True,
|
||||
)
|
||||
mock_infrared_emitter_entity.send_command_calls.clear()
|
||||
|
||||
await hass.services.async_call(
|
||||
CLIMATE_DOMAIN,
|
||||
SERVICE_SET_FAN_MODE,
|
||||
{ATTR_ENTITY_ID: _CLIMATE_ENTITY_ID, "fan_mode": FAN_HIGH},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
assert len(mock_infrared_emitter_entity.send_command_calls) == 1
|
||||
timings = mock_infrared_emitter_entity.send_command_calls[0].get_raw_timings()
|
||||
assert (
|
||||
timings
|
||||
== LgAcCommand(
|
||||
mode=LgAcMode.COOL, temperature=MIN_TEMP, fan=LgAcFanSpeed.HIGH
|
||||
).get_raw_timings()
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("has_receiver", [True])
|
||||
@pytest.mark.usefixtures("init_integration")
|
||||
@pytest.mark.parametrize(
|
||||
("lib_fan", "expected_fan_mode"),
|
||||
[
|
||||
pytest.param(LgAcFanSpeed.QUIET, FAN_QUIET, id="quiet"),
|
||||
pytest.param(LgAcFanSpeed.LOW, FAN_LOW, id="low"),
|
||||
pytest.param(LgAcFanSpeed.MEDIUM_LOW, FAN_MEDIUM_LOW, id="medium_low"),
|
||||
pytest.param(LgAcFanSpeed.MEDIUM, FAN_MEDIUM, id="medium"),
|
||||
pytest.param(LgAcFanSpeed.MEDIUM_HIGH, FAN_MEDIUM_HIGH, id="medium_high"),
|
||||
pytest.param(LgAcFanSpeed.HIGH, FAN_HIGH, id="high"),
|
||||
pytest.param(LgAcFanSpeed.AUTO, FAN_AUTO, id="auto"),
|
||||
],
|
||||
)
|
||||
async def test_receiver_updates_state_on_cool_signal(
|
||||
hass: HomeAssistant,
|
||||
mock_infrared_receiver_entity: MockInfraredReceiverEntity,
|
||||
lib_fan: LgAcFanSpeed,
|
||||
expected_fan_mode: str,
|
||||
) -> None:
|
||||
"""Test that a received cool signal updates mode, temperature and every fan speed."""
|
||||
timings = LgAcCommand(
|
||||
mode=LgAcMode.COOL, temperature=24, fan=lib_fan
|
||||
).get_raw_timings()
|
||||
|
||||
signal = InfraredReceivedSignal(timings=timings)
|
||||
mock_infrared_receiver_entity._handle_received_signal(signal)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
state = hass.states.get(_CLIMATE_ENTITY_ID)
|
||||
assert state is not None
|
||||
assert state.state == HVACMode.COOL
|
||||
assert state.attributes["fan_mode"] == expected_fan_mode
|
||||
assert float(state.attributes["temperature"]) == 24.0
|
||||
|
||||
|
||||
@pytest.mark.parametrize("has_receiver", [True])
|
||||
@pytest.mark.usefixtures("init_integration")
|
||||
async def test_receiver_updates_state_on_off_signal(
|
||||
hass: HomeAssistant,
|
||||
mock_infrared_receiver_entity: MockInfraredReceiverEntity,
|
||||
) -> None:
|
||||
"""Test a received off signal sets mode to off, keeping temperature and fan.
|
||||
|
||||
Power-off is a fixed code carrying neither, so they must survive it.
|
||||
"""
|
||||
mock_infrared_receiver_entity._handle_received_signal(
|
||||
InfraredReceivedSignal(
|
||||
timings=LgAcCommand(
|
||||
mode=LgAcMode.COOL, temperature=24, fan=LgAcFanSpeed.MEDIUM
|
||||
).get_raw_timings()
|
||||
)
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
mock_infrared_receiver_entity._handle_received_signal(
|
||||
InfraredReceivedSignal(timings=LgAcCommand(mode=LgAcMode.OFF).get_raw_timings())
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
state = hass.states.get(_CLIMATE_ENTITY_ID)
|
||||
assert state is not None
|
||||
assert state.state == HVACMode.OFF
|
||||
assert state.attributes["fan_mode"] == FAN_MEDIUM
|
||||
assert float(state.attributes["temperature"]) == 24.0
|
||||
|
||||
|
||||
@pytest.mark.parametrize("has_receiver", [True])
|
||||
@pytest.mark.usefixtures("init_integration")
|
||||
async def test_receiver_ignores_unconfigured_hvac_mode(
|
||||
hass: HomeAssistant,
|
||||
mock_infrared_receiver_entity: MockInfraredReceiverEntity,
|
||||
) -> None:
|
||||
"""Test a signal for a mode the user did not configure does not change state."""
|
||||
mock_infrared_receiver_entity._handle_received_signal(
|
||||
InfraredReceivedSignal(
|
||||
timings=LgAcCommand(
|
||||
mode=LgAcMode.HEAT, temperature=24, fan=LgAcFanSpeed.HIGH
|
||||
).get_raw_timings()
|
||||
)
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
state = hass.states.get(_CLIMATE_ENTITY_ID)
|
||||
assert state is not None
|
||||
assert state.state == HVACMode.OFF
|
||||
assert state.attributes["fan_mode"] == FAN_AUTO
|
||||
|
||||
|
||||
@pytest.mark.parametrize("has_receiver", [True])
|
||||
@pytest.mark.usefixtures("init_integration")
|
||||
async def test_receiver_ignores_non_lg_ac_signal(
|
||||
hass: HomeAssistant,
|
||||
mock_infrared_receiver_entity: MockInfraredReceiverEntity,
|
||||
) -> None:
|
||||
"""Test that an unrecognised IR signal does not change state."""
|
||||
mock_infrared_receiver_entity._handle_received_signal(
|
||||
InfraredReceivedSignal(timings=[500, -500, 300, -300])
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
state = hass.states.get(_CLIMATE_ENTITY_ID)
|
||||
assert state is not None
|
||||
assert state.state == HVACMode.OFF
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("hvac_modes", "expected_features"),
|
||||
[
|
||||
pytest.param(
|
||||
[HVACMode.COOL, HVACMode.DRY],
|
||||
ClimateEntityFeature.TARGET_TEMPERATURE | ClimateEntityFeature.FAN_MODE,
|
||||
id="with_temperature_mode",
|
||||
),
|
||||
pytest.param(
|
||||
[HVACMode.DRY, HVACMode.FAN_ONLY],
|
||||
ClimateEntityFeature.FAN_MODE,
|
||||
id="without_temperature_mode",
|
||||
),
|
||||
],
|
||||
)
|
||||
@pytest.mark.usefixtures("init_integration")
|
||||
async def test_target_temperature_feature_follows_configured_modes(
|
||||
hass: HomeAssistant,
|
||||
expected_features: ClimateEntityFeature,
|
||||
) -> None:
|
||||
"""Test target temperature is only offered when a mode can carry one."""
|
||||
state = hass.states.get(_CLIMATE_ENTITY_ID)
|
||||
assert state is not None
|
||||
assert state.attributes["supported_features"] == expected_features
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("restored_state", "restored_attributes", "expected"),
|
||||
[
|
||||
pytest.param(
|
||||
HVACMode.COOL,
|
||||
{"fan_mode": FAN_HIGH, "temperature": 29.0},
|
||||
(HVACMode.COOL, FAN_HIGH, 29.0),
|
||||
id="full_state",
|
||||
),
|
||||
pytest.param(
|
||||
STATE_UNAVAILABLE,
|
||||
{},
|
||||
(HVACMode.OFF, FAN_AUTO, float(MIN_TEMP)),
|
||||
id="unavailable_falls_back_to_defaults",
|
||||
),
|
||||
pytest.param(
|
||||
HVACMode.HEAT,
|
||||
{"fan_mode": FAN_HIGH, "temperature": 29.0},
|
||||
(HVACMode.OFF, FAN_HIGH, 29.0),
|
||||
id="mode_no_longer_configured_is_ignored",
|
||||
),
|
||||
],
|
||||
)
|
||||
async def test_state_restored_on_restart(
|
||||
hass: HomeAssistant,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
mock_infrared_emitter_entity: MockInfraredEmitterEntity,
|
||||
platforms: list[Platform],
|
||||
restored_state: str,
|
||||
restored_attributes: dict[str, Any],
|
||||
expected: tuple[HVACMode, str, float],
|
||||
) -> None:
|
||||
"""Test the assumed state is restored, since infrared cannot read it back."""
|
||||
mock_restore_cache(
|
||||
hass, [State(_CLIMATE_ENTITY_ID, restored_state, restored_attributes)]
|
||||
)
|
||||
mock_config_entry.add_to_hass(hass)
|
||||
|
||||
with patch("homeassistant.components.lg_infrared.PLATFORMS", platforms):
|
||||
await hass.config_entries.async_setup(mock_config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
state = hass.states.get(_CLIMATE_ENTITY_ID)
|
||||
assert state is not None
|
||||
expected_mode, expected_fan, expected_temp = expected
|
||||
assert state.state == expected_mode
|
||||
assert state.attributes["fan_mode"] == expected_fan
|
||||
assert state.attributes["temperature"] == expected_temp
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("init_integration")
|
||||
async def test_set_temperature_with_hvac_mode(
|
||||
hass: HomeAssistant,
|
||||
mock_infrared_emitter_entity: MockInfraredEmitterEntity,
|
||||
) -> None:
|
||||
"""Test set_temperature switches mode when one is given, even while off."""
|
||||
await hass.services.async_call(
|
||||
CLIMATE_DOMAIN,
|
||||
SERVICE_SET_TEMPERATURE,
|
||||
{
|
||||
ATTR_ENTITY_ID: _CLIMATE_ENTITY_ID,
|
||||
ATTR_TEMPERATURE: 24,
|
||||
"hvac_mode": HVACMode.COOL,
|
||||
},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
assert len(mock_infrared_emitter_entity.send_command_calls) == 1
|
||||
timings = mock_infrared_emitter_entity.send_command_calls[0].get_raw_timings()
|
||||
assert (
|
||||
timings
|
||||
== LgAcCommand(
|
||||
mode=LgAcMode.COOL, temperature=24, fan=LgAcFanSpeed.AUTO
|
||||
).get_raw_timings()
|
||||
)
|
||||
|
||||
state = hass.states.get(_CLIMATE_ENTITY_ID)
|
||||
assert state is not None
|
||||
assert state.state == HVACMode.COOL
|
||||
assert state.attributes["temperature"] == 24.0
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("init_integration")
|
||||
async def test_set_temperature_with_unsupported_hvac_mode(
|
||||
hass: HomeAssistant,
|
||||
mock_infrared_emitter_entity: MockInfraredEmitterEntity,
|
||||
) -> None:
|
||||
"""Test a mode the unit does not support is rejected instead of sent."""
|
||||
with pytest.raises(ServiceValidationError):
|
||||
await hass.services.async_call(
|
||||
CLIMATE_DOMAIN,
|
||||
SERVICE_SET_TEMPERATURE,
|
||||
{
|
||||
ATTR_ENTITY_ID: _CLIMATE_ENTITY_ID,
|
||||
ATTR_TEMPERATURE: 24,
|
||||
"hvac_mode": HVACMode.HEAT,
|
||||
},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
assert len(mock_infrared_emitter_entity.send_command_calls) == 0
|
||||
@@ -2,16 +2,18 @@
|
||||
|
||||
import pytest
|
||||
|
||||
from homeassistant.components.climate import HVACMode
|
||||
from homeassistant.components.lg_infrared.const import (
|
||||
CONF_DEVICE_TYPE,
|
||||
CONF_HVAC_MODES,
|
||||
CONF_INFRARED_ENTITY_ID,
|
||||
CONF_INFRARED_RECEIVER_ENTITY_ID,
|
||||
DOMAIN,
|
||||
LGDeviceType,
|
||||
)
|
||||
from homeassistant.config_entries import SOURCE_USER
|
||||
from homeassistant.config_entries import SOURCE_USER, ConfigFlowResult
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.data_entry_flow import FlowResultType
|
||||
from homeassistant.data_entry_flow import FlowResultType, InvalidData
|
||||
from homeassistant.helpers import entity_registry as er
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
@@ -21,65 +23,80 @@ from tests.components.infrared import (
|
||||
)
|
||||
|
||||
|
||||
async def _async_start_flow(
|
||||
hass: HomeAssistant, device_type: LGDeviceType
|
||||
) -> ConfigFlowResult:
|
||||
"""Start the user flow and pick a device type from the menu."""
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": SOURCE_USER}
|
||||
)
|
||||
|
||||
assert result["type"] is FlowResultType.MENU
|
||||
assert result["step_id"] == "user"
|
||||
|
||||
return await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"], user_input={"next_step_id": device_type.value}
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("config", "expected_title"),
|
||||
("tv_config", "expected_title"),
|
||||
[
|
||||
(
|
||||
pytest.param(
|
||||
{CONF_INFRARED_ENTITY_ID: mock_infrared_emitter_entity_id},
|
||||
"LG TV via Test IR emitter",
|
||||
id="emitter_only",
|
||||
),
|
||||
(
|
||||
pytest.param(
|
||||
{
|
||||
CONF_INFRARED_ENTITY_ID: mock_infrared_emitter_entity_id,
|
||||
CONF_INFRARED_RECEIVER_ENTITY_ID: mock_infrared_receiver_entity_id,
|
||||
},
|
||||
"LG TV via Test IR emitter",
|
||||
id="emitter_and_receiver",
|
||||
),
|
||||
(
|
||||
pytest.param(
|
||||
{CONF_INFRARED_RECEIVER_ENTITY_ID: mock_infrared_receiver_entity_id},
|
||||
"LG TV via Test IR receiver",
|
||||
id="receiver_only",
|
||||
),
|
||||
],
|
||||
)
|
||||
@pytest.mark.usefixtures(
|
||||
"mock_infrared_emitter_entity", "mock_infrared_receiver_entity"
|
||||
)
|
||||
async def test_user_flow_success(
|
||||
async def test_user_flow_tv_success(
|
||||
hass: HomeAssistant,
|
||||
config: dict[str, str],
|
||||
tv_config: dict[str, str],
|
||||
expected_title: str,
|
||||
) -> None:
|
||||
"""Test successful user config flow."""
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": SOURCE_USER}
|
||||
)
|
||||
"""Test successful TV config flow."""
|
||||
result = await _async_start_flow(hass, LGDeviceType.TV)
|
||||
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["step_id"] == "user"
|
||||
assert result["step_id"] == "tv"
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
user_input={CONF_DEVICE_TYPE: LGDeviceType.TV, **config},
|
||||
result["flow_id"], user_input=tv_config
|
||||
)
|
||||
|
||||
assert result["type"] is FlowResultType.CREATE_ENTRY
|
||||
assert result["title"] == expected_title
|
||||
assert result["data"] == {CONF_DEVICE_TYPE: LGDeviceType.TV, **config}
|
||||
assert result["data"] == {CONF_DEVICE_TYPE: LGDeviceType.TV, **tv_config}
|
||||
assert result["result"].unique_id is None
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("mock_infrared_emitter_entity")
|
||||
async def test_user_flow_requires_emitter_or_receiver(
|
||||
async def test_user_flow_tv_requires_emitter_or_receiver(
|
||||
hass: HomeAssistant,
|
||||
) -> None:
|
||||
"""Test user flow requires an infrared emitter or receiver."""
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": SOURCE_USER}
|
||||
)
|
||||
"""Test TV flow shows error when neither emitter nor receiver is selected."""
|
||||
result = await _async_start_flow(hass, LGDeviceType.TV)
|
||||
|
||||
assert result["step_id"] == "tv"
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
user_input={CONF_DEVICE_TYPE: LGDeviceType.TV},
|
||||
result["flow_id"], user_input={}
|
||||
)
|
||||
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
@@ -114,18 +131,12 @@ async def test_user_flow_already_configured(
|
||||
mock_config_entry: MockConfigEntry,
|
||||
user_input: dict[str, str],
|
||||
) -> None:
|
||||
"""Test user flow aborts when entry is already configured."""
|
||||
"""Test TV flow aborts when entry is already configured."""
|
||||
mock_config_entry.add_to_hass(hass)
|
||||
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": SOURCE_USER}
|
||||
)
|
||||
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
|
||||
result = await _async_start_flow(hass, LGDeviceType.TV)
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
user_input={CONF_DEVICE_TYPE: LGDeviceType.TV, **user_input},
|
||||
result["flow_id"], user_input=user_input
|
||||
)
|
||||
|
||||
assert result["type"] is FlowResultType.ABORT
|
||||
@@ -134,7 +145,7 @@ async def test_user_flow_already_configured(
|
||||
|
||||
@pytest.mark.usefixtures("init_infrared")
|
||||
async def test_user_flow_no_emitters_receivers(hass: HomeAssistant) -> None:
|
||||
"""Test user flow aborts when no infrared emitters or receivers exist."""
|
||||
"""Test flow aborts when no infrared emitters or receivers exist."""
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": SOURCE_USER}
|
||||
)
|
||||
@@ -143,34 +154,131 @@ async def test_user_flow_no_emitters_receivers(hass: HomeAssistant) -> None:
|
||||
assert result["reason"] == "no_infrared_entities"
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("mock_infrared_receiver_entity")
|
||||
async def test_user_flow_menu_hides_ac_without_emitter(hass: HomeAssistant) -> None:
|
||||
"""Test the AC option is hidden when only a receiver is available."""
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": SOURCE_USER}
|
||||
)
|
||||
|
||||
assert result["type"] is FlowResultType.MENU
|
||||
assert result["menu_options"] == [LGDeviceType.TV.value]
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("mock_infrared_emitter_entity")
|
||||
@pytest.mark.parametrize(
|
||||
("entity_name", "expected_title"),
|
||||
[
|
||||
(None, "LG TV via Test IR emitter"),
|
||||
("AC IR emitter", "LG TV via AC IR emitter"),
|
||||
pytest.param(None, "LG TV via Test IR emitter", id="original_name"),
|
||||
pytest.param("AC IR emitter", "LG TV via AC IR emitter", id="custom_name"),
|
||||
],
|
||||
)
|
||||
async def test_user_flow_title_from_entity_name(
|
||||
async def test_user_flow_tv_title_from_entity_name(
|
||||
hass: HomeAssistant,
|
||||
entity_registry: er.EntityRegistry,
|
||||
entity_name: str | None,
|
||||
expected_title: str,
|
||||
) -> None:
|
||||
"""Test config entry title uses the entity name."""
|
||||
"""Test TV config entry title uses the entity name."""
|
||||
entity_registry.async_update_entity(
|
||||
mock_infrared_emitter_entity_id, name=entity_name
|
||||
)
|
||||
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": SOURCE_USER}
|
||||
)
|
||||
result = await _async_start_flow(hass, LGDeviceType.TV)
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
user_input={
|
||||
CONF_DEVICE_TYPE: LGDeviceType.TV,
|
||||
CONF_INFRARED_ENTITY_ID: mock_infrared_emitter_entity_id,
|
||||
},
|
||||
user_input={CONF_INFRARED_ENTITY_ID: mock_infrared_emitter_entity_id},
|
||||
)
|
||||
assert result["type"] is FlowResultType.CREATE_ENTRY
|
||||
assert result["title"] == expected_title
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("mock_infrared_emitter_entity")
|
||||
async def test_user_flow_ac_success(hass: HomeAssistant) -> None:
|
||||
"""Test successful AC config flow with default modes (cool + dry)."""
|
||||
result = await _async_start_flow(hass, LGDeviceType.AC)
|
||||
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["step_id"] == "ac"
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
user_input={
|
||||
CONF_INFRARED_ENTITY_ID: mock_infrared_emitter_entity_id,
|
||||
CONF_HVAC_MODES: [HVACMode.COOL, HVACMode.DRY],
|
||||
},
|
||||
)
|
||||
|
||||
assert result["type"] is FlowResultType.CREATE_ENTRY
|
||||
assert result["title"] == "LG AC via Test IR emitter"
|
||||
assert result["data"] == {
|
||||
CONF_DEVICE_TYPE: LGDeviceType.AC,
|
||||
CONF_INFRARED_ENTITY_ID: mock_infrared_emitter_entity_id,
|
||||
CONF_HVAC_MODES: [HVACMode.COOL, HVACMode.DRY],
|
||||
}
|
||||
assert result["result"].unique_id is None
|
||||
|
||||
|
||||
@pytest.mark.usefixtures(
|
||||
"mock_infrared_emitter_entity", "mock_infrared_receiver_entity"
|
||||
)
|
||||
async def test_user_flow_ac_with_heat_and_receiver(hass: HomeAssistant) -> None:
|
||||
"""Test AC flow with heat mode and optional receiver."""
|
||||
result = await _async_start_flow(hass, LGDeviceType.AC)
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
user_input={
|
||||
CONF_INFRARED_ENTITY_ID: mock_infrared_emitter_entity_id,
|
||||
CONF_INFRARED_RECEIVER_ENTITY_ID: mock_infrared_receiver_entity_id,
|
||||
CONF_HVAC_MODES: [HVACMode.COOL, HVACMode.HEAT, HVACMode.DRY],
|
||||
},
|
||||
)
|
||||
|
||||
assert result["type"] is FlowResultType.CREATE_ENTRY
|
||||
assert result["data"][CONF_HVAC_MODES] == [
|
||||
HVACMode.COOL,
|
||||
HVACMode.HEAT,
|
||||
HVACMode.DRY,
|
||||
]
|
||||
assert result["data"][CONF_INFRARED_RECEIVER_ENTITY_ID] == (
|
||||
mock_infrared_receiver_entity_id
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("mock_infrared_emitter_entity")
|
||||
async def test_user_flow_ac_requires_hvac_mode(hass: HomeAssistant) -> None:
|
||||
"""Test AC flow rejects an empty list of supported modes."""
|
||||
result = await _async_start_flow(hass, LGDeviceType.AC)
|
||||
|
||||
with pytest.raises(InvalidData) as err:
|
||||
await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
user_input={
|
||||
CONF_INFRARED_ENTITY_ID: mock_infrared_emitter_entity_id,
|
||||
CONF_HVAC_MODES: [],
|
||||
},
|
||||
)
|
||||
|
||||
assert err.value.schema_errors == {CONF_HVAC_MODES: "no_hvac_modes"}
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("mock_infrared_emitter_entity")
|
||||
@pytest.mark.parametrize("device_type", [LGDeviceType.AC])
|
||||
@pytest.mark.parametrize("has_receiver", [False])
|
||||
async def test_user_flow_ac_already_configured(
|
||||
hass: HomeAssistant, mock_config_entry: MockConfigEntry
|
||||
) -> None:
|
||||
"""Test AC flow aborts when entry is already configured."""
|
||||
mock_config_entry.add_to_hass(hass)
|
||||
|
||||
result = await _async_start_flow(hass, LGDeviceType.AC)
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
user_input={
|
||||
CONF_INFRARED_ENTITY_ID: mock_infrared_emitter_entity_id,
|
||||
CONF_HVAC_MODES: [HVACMode.COOL, HVACMode.DRY],
|
||||
},
|
||||
)
|
||||
|
||||
assert result["type"] is FlowResultType.ABORT
|
||||
assert result["reason"] == "already_configured"
|
||||
|
||||
Reference in New Issue
Block a user