mirror of
https://github.com/home-assistant/core.git
synced 2026-09-26 01:11:51 -04:00
Added AC support for Samsung Infrared (#173692)
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
co-authored by
Copilot Autofix powered by AI
parent
21743843e7
commit
46ae12ca54
@@ -4,7 +4,7 @@ from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
PLATFORMS = [Platform.BUTTON, Platform.MEDIA_PLAYER]
|
||||
PLATFORMS = [Platform.BUTTON, Platform.CLIMATE, Platform.MEDIA_PLAYER]
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
|
||||
@@ -168,7 +168,9 @@ class SamsungIrButton(SamsungIrEntity, InfraredEmitterConsumerEntity, ButtonEnti
|
||||
description: SamsungIrButtonEntityDescription,
|
||||
) -> None:
|
||||
"""Initialize Samsung IR button."""
|
||||
super().__init__(entry, unique_id_suffix=description.key)
|
||||
super().__init__(
|
||||
entry, unique_id_suffix=description.key, device_name="Samsung TV"
|
||||
)
|
||||
self._infrared_emitter_entity_id = infrared_emitter_entity_id
|
||||
self.entity_description = description
|
||||
|
||||
|
||||
@@ -0,0 +1,241 @@
|
||||
"""Climate platform for Samsung IR integration."""
|
||||
|
||||
from dataclasses import dataclass
|
||||
from typing import Any, override
|
||||
|
||||
from infrared_protocols.commands.samsung_ac import (
|
||||
SamsungAC0292Command,
|
||||
SamsungAC0292HvacMode,
|
||||
SamsungACFanMode,
|
||||
SamsungACSwingMode,
|
||||
)
|
||||
|
||||
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
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import (
|
||||
ATTR_TEMPERATURE,
|
||||
STATE_UNAVAILABLE,
|
||||
STATE_UNKNOWN,
|
||||
UnitOfTemperature,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
from homeassistant.helpers.restore_state import ExtraStoredData, RestoreEntity
|
||||
|
||||
from .const import CONF_DEVICE_TYPE, CONF_INFRARED_EMITTER_ENTITY_ID, SamsungDeviceType
|
||||
from .entity import SamsungIrEntity
|
||||
|
||||
PARALLEL_UPDATES = 1
|
||||
|
||||
|
||||
HA_TO_LIB_HVAC = {
|
||||
HVACMode.OFF: SamsungAC0292HvacMode.OFF,
|
||||
HVACMode.COOL: SamsungAC0292HvacMode.COOL,
|
||||
HVACMode.HEAT: SamsungAC0292HvacMode.HEAT,
|
||||
HVACMode.DRY: SamsungAC0292HvacMode.DRY,
|
||||
HVACMode.FAN_ONLY: SamsungAC0292HvacMode.FAN_ONLY,
|
||||
HVACMode.AUTO: SamsungAC0292HvacMode.AUTO,
|
||||
}
|
||||
|
||||
|
||||
HA_TO_LIB_FAN = {
|
||||
FAN_AUTO: SamsungACFanMode.AUTO,
|
||||
FAN_LOW: SamsungACFanMode.LOW,
|
||||
FAN_MEDIUM: SamsungACFanMode.MEDIUM,
|
||||
FAN_HIGH: SamsungACFanMode.HIGH,
|
||||
}
|
||||
|
||||
|
||||
@dataclass
|
||||
class _SamsungAcExtraStoredData(ExtraStoredData):
|
||||
"""Extra data restored alongside the entity's visible state.
|
||||
|
||||
Holds the last non-OFF HVAC mode, which isn't part of the visible state (the
|
||||
entity may currently be OFF) but is needed by turn_on to know which mode to
|
||||
resume, so it can't be recovered from last_state.state alone when that state
|
||||
is OFF.
|
||||
"""
|
||||
|
||||
last_on_hvac_mode: str
|
||||
|
||||
@override
|
||||
def as_dict(self) -> dict[str, Any]:
|
||||
"""Return a dict representation for storage."""
|
||||
return {"last_on_hvac_mode": self.last_on_hvac_mode}
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, restored: dict[str, Any]) -> _SamsungAcExtraStoredData | None:
|
||||
"""Build from a stored dict, or None if it doesn't look valid."""
|
||||
last_on_hvac_mode = restored.get("last_on_hvac_mode")
|
||||
if not isinstance(last_on_hvac_mode, str):
|
||||
return None
|
||||
return cls(last_on_hvac_mode=last_on_hvac_mode)
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
entry: ConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up Samsung IR climate from a config entry."""
|
||||
infrared_emitter_entity_id = entry.data[CONF_INFRARED_EMITTER_ENTITY_ID]
|
||||
device_type = entry.data[CONF_DEVICE_TYPE]
|
||||
|
||||
if device_type == SamsungDeviceType.AC:
|
||||
async_add_entities(
|
||||
[SamsungIrClimate(entry, infrared_emitter_entity_id, device_type)]
|
||||
)
|
||||
|
||||
|
||||
class SamsungIrClimate(
|
||||
SamsungIrEntity, InfraredEmitterConsumerEntity, ClimateEntity, RestoreEntity
|
||||
):
|
||||
"""Samsung IR climate entity."""
|
||||
|
||||
_attr_name = None
|
||||
_attr_assumed_state = True
|
||||
_attr_temperature_unit = UnitOfTemperature.CELSIUS
|
||||
_attr_fan_modes = [FAN_AUTO, FAN_LOW, FAN_MEDIUM, FAN_HIGH]
|
||||
_attr_hvac_mode = HVACMode.OFF
|
||||
_attr_target_temperature = 24.0
|
||||
_attr_min_temp = 16.0
|
||||
_attr_max_temp = 30.0
|
||||
_attr_target_temperature_step = 1.0
|
||||
_attr_fan_mode = FAN_AUTO
|
||||
_attr_hvac_modes = [
|
||||
HVACMode.OFF,
|
||||
HVACMode.AUTO,
|
||||
HVACMode.COOL,
|
||||
HVACMode.HEAT,
|
||||
HVACMode.DRY,
|
||||
HVACMode.FAN_ONLY,
|
||||
]
|
||||
_attr_supported_features = (
|
||||
ClimateEntityFeature.TARGET_TEMPERATURE
|
||||
| ClimateEntityFeature.FAN_MODE
|
||||
| ClimateEntityFeature.TURN_ON
|
||||
| ClimateEntityFeature.TURN_OFF
|
||||
)
|
||||
|
||||
def __init__(
|
||||
self, entry: ConfigEntry, infrared_emitter_entity_id: str, device_type: str
|
||||
) -> None:
|
||||
"""Initialize the climate entity."""
|
||||
super().__init__(entry, unique_id_suffix="climate", device_name="Samsung AC")
|
||||
self._infrared_emitter_entity_id = infrared_emitter_entity_id
|
||||
self._device_type = device_type
|
||||
|
||||
self._last_on_hvac_mode = HVACMode.COOL
|
||||
|
||||
@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_TO_LIB_FAN:
|
||||
self._attr_fan_mode = fan_mode
|
||||
if (temperature := last_state.attributes.get(ATTR_TEMPERATURE)) is not None:
|
||||
self._attr_target_temperature = float(temperature)
|
||||
|
||||
if self._attr_hvac_mode != HVACMode.OFF:
|
||||
self._last_on_hvac_mode = self._attr_hvac_mode
|
||||
elif (last_extra_data := await self.async_get_last_extra_data()) is not None:
|
||||
restored = _SamsungAcExtraStoredData.from_dict(last_extra_data.as_dict())
|
||||
if restored is not None and restored.last_on_hvac_mode in (
|
||||
mode.value for mode in self._attr_hvac_modes if mode != HVACMode.OFF
|
||||
):
|
||||
self._last_on_hvac_mode = HVACMode(restored.last_on_hvac_mode)
|
||||
|
||||
@property
|
||||
@override
|
||||
def extra_restore_state_data(self) -> ExtraStoredData:
|
||||
"""Return extra data to be restored alongside the entity's state."""
|
||||
return _SamsungAcExtraStoredData(
|
||||
last_on_hvac_mode=self._last_on_hvac_mode.value
|
||||
)
|
||||
|
||||
async def _async_send_command(self) -> None:
|
||||
"""Generate the logical state and delegate transmission to the infrared platform."""
|
||||
hvac_mode = HA_TO_LIB_HVAC.get(self._attr_hvac_mode, SamsungAC0292HvacMode.OFF)
|
||||
|
||||
if hvac_mode is SamsungAC0292HvacMode.OFF:
|
||||
command = SamsungAC0292Command(hvac_mode=hvac_mode)
|
||||
else:
|
||||
fan_mode = HA_TO_LIB_FAN.get(self._attr_fan_mode, SamsungACFanMode.AUTO)
|
||||
command = SamsungAC0292Command(
|
||||
hvac_mode=hvac_mode,
|
||||
target_temperature=int(self._attr_target_temperature),
|
||||
fan_mode=fan_mode,
|
||||
swing_mode=SamsungACSwingMode.OFF,
|
||||
)
|
||||
|
||||
await self._send_command(command)
|
||||
|
||||
@override
|
||||
async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None:
|
||||
"""Set HVAC mode."""
|
||||
self._attr_hvac_mode = hvac_mode
|
||||
if hvac_mode != HVACMode.OFF:
|
||||
self._last_on_hvac_mode = hvac_mode
|
||||
# The unit always transmits a fixed fan value in auto mode, regardless of
|
||||
# what was previously selected; keep the reported state consistent with
|
||||
# what's actually being sent.
|
||||
if hvac_mode == HVACMode.AUTO:
|
||||
self._attr_fan_mode = FAN_AUTO
|
||||
|
||||
await self._async_send_command()
|
||||
self.async_write_ha_state()
|
||||
|
||||
@override
|
||||
async def async_set_fan_mode(self, fan_mode: str) -> None:
|
||||
"""Set fan mode."""
|
||||
self._attr_fan_mode = fan_mode
|
||||
await self._async_send_command()
|
||||
self.async_write_ha_state()
|
||||
|
||||
@override
|
||||
async def async_set_temperature(self, **kwargs: Any) -> None:
|
||||
"""Set temperature."""
|
||||
if (hvac_mode := kwargs.get(ATTR_HVAC_MODE)) is not None:
|
||||
self._attr_hvac_mode = hvac_mode
|
||||
if hvac_mode != HVACMode.OFF:
|
||||
self._last_on_hvac_mode = hvac_mode
|
||||
if hvac_mode == HVACMode.AUTO:
|
||||
self._attr_fan_mode = FAN_AUTO
|
||||
|
||||
if (temperature := kwargs.get(ATTR_TEMPERATURE)) is not None:
|
||||
self._attr_target_temperature = round(temperature)
|
||||
|
||||
if ATTR_HVAC_MODE in kwargs or ATTR_TEMPERATURE in kwargs:
|
||||
await self._async_send_command()
|
||||
self.async_write_ha_state()
|
||||
|
||||
@override
|
||||
async def async_turn_on(self) -> None:
|
||||
"""Turn the entity on."""
|
||||
await self.async_set_hvac_mode(self._last_on_hvac_mode)
|
||||
|
||||
@override
|
||||
async def async_turn_off(self) -> None:
|
||||
"""Turn the entity off."""
|
||||
await self.async_set_hvac_mode(HVACMode.OFF)
|
||||
@@ -9,7 +9,7 @@ from homeassistant.components.infrared import (
|
||||
async_get_emitters,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
|
||||
from homeassistant.helpers import entity_registry as er
|
||||
from homeassistant.helpers import entity_registry as er, translation
|
||||
from homeassistant.helpers.selector import (
|
||||
EntitySelector,
|
||||
EntitySelectorConfig,
|
||||
@@ -25,10 +25,6 @@ from .const import (
|
||||
SamsungDeviceType,
|
||||
)
|
||||
|
||||
DEVICE_TYPE_NAMES: dict[SamsungDeviceType, str] = {
|
||||
SamsungDeviceType.TV: "TV",
|
||||
}
|
||||
|
||||
|
||||
class SamsungIrConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
"""Handle config flow for Samsung IR."""
|
||||
@@ -52,14 +48,19 @@ class SamsungIrConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
f"samsung_infrared_{device_type}_{entity_id}"
|
||||
)
|
||||
self._abort_if_unique_id_configured()
|
||||
|
||||
# Get entity name for the title
|
||||
ent_reg = er.async_get(self.hass)
|
||||
entry = ent_reg.async_get(entity_id)
|
||||
entity_name = (
|
||||
entry.name or entry.original_name or entity_id if entry else entity_id
|
||||
)
|
||||
device_type_name = DEVICE_TYPE_NAMES[SamsungDeviceType(device_type)]
|
||||
device_type_key = SamsungDeviceType(device_type).value
|
||||
translations = await translation.async_get_translations(
|
||||
self.hass, self.hass.config.language, "selector", {DOMAIN}
|
||||
)
|
||||
device_type_name = translations.get(
|
||||
f"component.{DOMAIN}.selector.device_type.options.{device_type_key}",
|
||||
device_type_key,
|
||||
)
|
||||
title = f"Samsung {device_type_name} via {entity_name}"
|
||||
|
||||
return self.async_create_entry(title=title, data=user_input)
|
||||
|
||||
@@ -11,3 +11,4 @@ class SamsungDeviceType(StrEnum):
|
||||
"""Samsung device types."""
|
||||
|
||||
TV = "tv"
|
||||
AC = "ac"
|
||||
|
||||
@@ -12,11 +12,16 @@ class SamsungIrEntity(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 = "Samsung Device",
|
||||
) -> None:
|
||||
"""Initialize Samsung IR entity."""
|
||||
self._attr_unique_id = f"{entry.entry_id}_{unique_id_suffix}"
|
||||
self._attr_device_info = DeviceInfo(
|
||||
identifiers={(DOMAIN, entry.entry_id)},
|
||||
name="Samsung TV",
|
||||
name=device_name,
|
||||
manufacturer="Samsung",
|
||||
)
|
||||
|
||||
@@ -74,7 +74,9 @@ class SamsungIrTvMediaPlayer(
|
||||
|
||||
def __init__(self, entry: ConfigEntry, infrared_emitter_entity_id: str) -> None:
|
||||
"""Initialize Samsung IR media player."""
|
||||
super().__init__(entry, unique_id_suffix="media_player")
|
||||
super().__init__(
|
||||
entry, unique_id_suffix="media_player", device_name="Samsung TV"
|
||||
)
|
||||
self._infrared_emitter_entity_id = infrared_emitter_entity_id
|
||||
|
||||
@override
|
||||
|
||||
@@ -148,6 +148,7 @@
|
||||
"selector": {
|
||||
"device_type": {
|
||||
"options": {
|
||||
"ac": "Air Conditioner",
|
||||
"tv": "TV"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,380 @@
|
||||
"""Tests for the Samsung Infrared climate platform."""
|
||||
|
||||
from unittest.mock import AsyncMock, patch
|
||||
|
||||
from infrared_protocols.commands.samsung_ac import (
|
||||
SamsungAC0292Command,
|
||||
SamsungAC0292HvacMode,
|
||||
SamsungACFanMode,
|
||||
)
|
||||
|
||||
from homeassistant.components.climate import (
|
||||
ATTR_FAN_MODE,
|
||||
ATTR_HVAC_MODE,
|
||||
FAN_AUTO,
|
||||
FAN_HIGH,
|
||||
SERVICE_SET_FAN_MODE,
|
||||
SERVICE_SET_HVAC_MODE,
|
||||
SERVICE_SET_TEMPERATURE,
|
||||
HVACMode,
|
||||
)
|
||||
from homeassistant.components.samsung_infrared.const import DOMAIN
|
||||
from homeassistant.const import ATTR_ENTITY_ID, ATTR_TEMPERATURE, STATE_ON
|
||||
from homeassistant.core import HomeAssistant, State
|
||||
|
||||
from tests.common import (
|
||||
MockConfigEntry,
|
||||
mock_restore_cache,
|
||||
mock_restore_cache_with_extra_data,
|
||||
)
|
||||
|
||||
|
||||
async def test_samsung_infrared_climate_services(hass: HomeAssistant) -> None:
|
||||
"""Test climate services send the correct IR commands."""
|
||||
remote_entity_id = "remote.living_room_ir"
|
||||
hass.states.async_set(remote_entity_id, STATE_ON)
|
||||
|
||||
entry = MockConfigEntry(
|
||||
domain=DOMAIN,
|
||||
data={
|
||||
"infrared_emitter_entity_id": remote_entity_id,
|
||||
"device_type": "ac",
|
||||
},
|
||||
unique_id="samsung_ir_ac_test",
|
||||
)
|
||||
entry.add_to_hass(hass)
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.samsung_infrared.climate.SamsungIrClimate._send_command",
|
||||
new_callable=AsyncMock,
|
||||
) as mock_send_command:
|
||||
assert await hass.config_entries.async_setup(entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
entity_id = "climate.samsung_ac"
|
||||
|
||||
state = hass.states.get(entity_id)
|
||||
assert state is not None
|
||||
assert state.state != "unavailable"
|
||||
|
||||
await hass.services.async_call(
|
||||
"climate",
|
||||
SERVICE_SET_HVAC_MODE,
|
||||
{ATTR_ENTITY_ID: entity_id, ATTR_HVAC_MODE: HVACMode.COOL},
|
||||
blocking=True,
|
||||
)
|
||||
mock_send_command.assert_called_once()
|
||||
|
||||
sent_command = mock_send_command.call_args[0][0]
|
||||
assert isinstance(sent_command, SamsungAC0292Command)
|
||||
assert sent_command.hvac_mode == SamsungAC0292HvacMode.COOL
|
||||
|
||||
mock_send_command.reset_mock()
|
||||
|
||||
await hass.services.async_call(
|
||||
"climate",
|
||||
SERVICE_SET_TEMPERATURE,
|
||||
{ATTR_ENTITY_ID: entity_id, ATTR_TEMPERATURE: 26},
|
||||
blocking=True,
|
||||
)
|
||||
mock_send_command.assert_called_once()
|
||||
sent_command = mock_send_command.call_args[0][0]
|
||||
assert sent_command.target_temperature == 26
|
||||
|
||||
mock_send_command.reset_mock()
|
||||
|
||||
await hass.services.async_call(
|
||||
"climate",
|
||||
SERVICE_SET_FAN_MODE,
|
||||
{ATTR_ENTITY_ID: entity_id, ATTR_FAN_MODE: FAN_HIGH},
|
||||
blocking=True,
|
||||
)
|
||||
mock_send_command.assert_called_once()
|
||||
sent_command = mock_send_command.call_args[0][0]
|
||||
assert sent_command.fan_mode == SamsungACFanMode.HIGH
|
||||
|
||||
|
||||
async def test_samsung_infrared_climate_turn_off_sends_bare_off_command(
|
||||
hass: HomeAssistant,
|
||||
) -> None:
|
||||
"""Test that turning off sends OFF with no temperature, fan, or swing fields.
|
||||
|
||||
SamsungAC0292Command raises if hvac_mode is OFF and any of those fields are not
|
||||
None, so this also guards against a regression that would break every turn_off.
|
||||
"""
|
||||
remote_entity_id = "remote.living_room_ir"
|
||||
hass.states.async_set(remote_entity_id, STATE_ON)
|
||||
|
||||
entry = MockConfigEntry(
|
||||
domain=DOMAIN,
|
||||
data={
|
||||
"infrared_emitter_entity_id": remote_entity_id,
|
||||
"device_type": "ac",
|
||||
},
|
||||
unique_id="samsung_ir_ac_test",
|
||||
)
|
||||
entry.add_to_hass(hass)
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.samsung_infrared.climate.SamsungIrClimate._send_command",
|
||||
new_callable=AsyncMock,
|
||||
) as mock_send_command:
|
||||
assert await hass.config_entries.async_setup(entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
entity_id = "climate.samsung_ac"
|
||||
|
||||
await hass.services.async_call(
|
||||
"climate",
|
||||
SERVICE_SET_HVAC_MODE,
|
||||
{ATTR_ENTITY_ID: entity_id, ATTR_HVAC_MODE: HVACMode.COOL},
|
||||
blocking=True,
|
||||
)
|
||||
mock_send_command.reset_mock()
|
||||
|
||||
await hass.services.async_call(
|
||||
"climate",
|
||||
SERVICE_SET_HVAC_MODE,
|
||||
{ATTR_ENTITY_ID: entity_id, ATTR_HVAC_MODE: HVACMode.OFF},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
mock_send_command.assert_called_once()
|
||||
sent_command = mock_send_command.call_args[0][0]
|
||||
assert isinstance(sent_command, SamsungAC0292Command)
|
||||
assert sent_command.hvac_mode == SamsungAC0292HvacMode.OFF
|
||||
assert sent_command.target_temperature is None
|
||||
assert sent_command.fan_mode is None
|
||||
assert sent_command.swing_mode is None
|
||||
|
||||
|
||||
async def test_samsung_infrared_climate_set_temperature_with_hvac_mode(
|
||||
hass: HomeAssistant,
|
||||
) -> None:
|
||||
"""Test that set_temperature applies an included HVAC mode atomically."""
|
||||
remote_entity_id = "remote.living_room_ir"
|
||||
hass.states.async_set(remote_entity_id, STATE_ON)
|
||||
|
||||
entry = MockConfigEntry(
|
||||
domain=DOMAIN,
|
||||
data={
|
||||
"infrared_emitter_entity_id": remote_entity_id,
|
||||
"device_type": "ac",
|
||||
},
|
||||
unique_id="samsung_ir_ac_test",
|
||||
)
|
||||
entry.add_to_hass(hass)
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.samsung_infrared.climate.SamsungIrClimate._send_command",
|
||||
new_callable=AsyncMock,
|
||||
) as mock_send_command:
|
||||
assert await hass.config_entries.async_setup(entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
entity_id = "climate.samsung_ac"
|
||||
|
||||
state = hass.states.get(entity_id)
|
||||
assert state is not None
|
||||
assert state.state == HVACMode.OFF
|
||||
|
||||
await hass.services.async_call(
|
||||
"climate",
|
||||
SERVICE_SET_TEMPERATURE,
|
||||
{
|
||||
ATTR_ENTITY_ID: entity_id,
|
||||
ATTR_HVAC_MODE: HVACMode.COOL,
|
||||
ATTR_TEMPERATURE: 26,
|
||||
},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
mock_send_command.assert_called_once()
|
||||
sent_command = mock_send_command.call_args[0][0]
|
||||
assert sent_command.hvac_mode == SamsungAC0292HvacMode.COOL
|
||||
assert sent_command.target_temperature == 26
|
||||
|
||||
state = hass.states.get(entity_id)
|
||||
assert state is not None
|
||||
assert state.state == HVACMode.COOL
|
||||
assert state.attributes[ATTR_TEMPERATURE] == 26
|
||||
|
||||
mock_send_command.reset_mock()
|
||||
|
||||
await hass.services.async_call(
|
||||
"climate",
|
||||
SERVICE_SET_TEMPERATURE,
|
||||
{ATTR_ENTITY_ID: entity_id, ATTR_TEMPERATURE: 22.5},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
mock_send_command.assert_called_once()
|
||||
sent_command = mock_send_command.call_args[0][0]
|
||||
assert sent_command.target_temperature == 22
|
||||
|
||||
state = hass.states.get(entity_id)
|
||||
assert state is not None
|
||||
assert state.attributes[ATTR_TEMPERATURE] == 22
|
||||
|
||||
|
||||
async def test_samsung_infrared_climate_set_hvac_mode_auto_normalizes_fan_mode(
|
||||
hass: HomeAssistant,
|
||||
) -> None:
|
||||
"""Test that switching to AUTO resets the reported fan mode to FAN_AUTO.
|
||||
|
||||
SamsungAC0292Command always transmits a fixed fan value in auto mode and
|
||||
reports fan_mode=None, so the assumed state must not keep showing a previously
|
||||
selected fan speed (e.g. "high") that isn't actually being sent anymore.
|
||||
"""
|
||||
remote_entity_id = "remote.living_room_ir"
|
||||
hass.states.async_set(remote_entity_id, STATE_ON)
|
||||
|
||||
entry = MockConfigEntry(
|
||||
domain=DOMAIN,
|
||||
data={
|
||||
"infrared_emitter_entity_id": remote_entity_id,
|
||||
"device_type": "ac",
|
||||
},
|
||||
unique_id="samsung_ir_ac_test",
|
||||
)
|
||||
entry.add_to_hass(hass)
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.samsung_infrared.climate.SamsungIrClimate._send_command",
|
||||
new_callable=AsyncMock,
|
||||
):
|
||||
assert await hass.config_entries.async_setup(entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
entity_id = "climate.samsung_ac"
|
||||
|
||||
await hass.services.async_call(
|
||||
"climate",
|
||||
SERVICE_SET_HVAC_MODE,
|
||||
{ATTR_ENTITY_ID: entity_id, ATTR_HVAC_MODE: HVACMode.COOL},
|
||||
blocking=True,
|
||||
)
|
||||
await hass.services.async_call(
|
||||
"climate",
|
||||
SERVICE_SET_FAN_MODE,
|
||||
{ATTR_ENTITY_ID: entity_id, ATTR_FAN_MODE: FAN_HIGH},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
state = hass.states.get(entity_id)
|
||||
assert state is not None
|
||||
assert state.attributes[ATTR_FAN_MODE] == FAN_HIGH
|
||||
|
||||
await hass.services.async_call(
|
||||
"climate",
|
||||
SERVICE_SET_HVAC_MODE,
|
||||
{ATTR_ENTITY_ID: entity_id, ATTR_HVAC_MODE: HVACMode.AUTO},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
state = hass.states.get(entity_id)
|
||||
assert state is not None
|
||||
assert state.attributes[ATTR_FAN_MODE] == FAN_AUTO
|
||||
|
||||
|
||||
async def test_samsung_infrared_climate_restores_state_after_restart(
|
||||
hass: HomeAssistant,
|
||||
) -> None:
|
||||
"""Test that hvac_mode, temperature, and fan_mode survive a restart."""
|
||||
remote_entity_id = "remote.living_room_ir"
|
||||
hass.states.async_set(remote_entity_id, STATE_ON)
|
||||
|
||||
entry = MockConfigEntry(
|
||||
domain=DOMAIN,
|
||||
data={
|
||||
"infrared_emitter_entity_id": remote_entity_id,
|
||||
"device_type": "ac",
|
||||
},
|
||||
unique_id="samsung_ir_ac_test",
|
||||
)
|
||||
entry.add_to_hass(hass)
|
||||
|
||||
mock_restore_cache(
|
||||
hass,
|
||||
[
|
||||
State(
|
||||
"climate.samsung_ac",
|
||||
HVACMode.HEAT,
|
||||
{ATTR_TEMPERATURE: 27, ATTR_FAN_MODE: FAN_HIGH},
|
||||
)
|
||||
],
|
||||
)
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.samsung_infrared.climate.SamsungIrClimate._send_command",
|
||||
new_callable=AsyncMock,
|
||||
):
|
||||
assert await hass.config_entries.async_setup(entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
state = hass.states.get("climate.samsung_ac")
|
||||
assert state is not None
|
||||
assert state.state == HVACMode.HEAT
|
||||
assert state.attributes[ATTR_TEMPERATURE] == 27
|
||||
assert state.attributes[ATTR_FAN_MODE] == FAN_HIGH
|
||||
|
||||
|
||||
async def test_samsung_infrared_climate_turn_on_after_restart_resumes_last_mode(
|
||||
hass: HomeAssistant,
|
||||
) -> None:
|
||||
"""Test that turn_on after a restart resumes the last non-OFF mode, not COOL.
|
||||
|
||||
Regression test: without restoring _last_on_hvac_mode, an AC that was last
|
||||
HEAT and got turned OFF, then restarted, would resume in COOL on turn_on
|
||||
instead of HEAT.
|
||||
"""
|
||||
remote_entity_id = "remote.living_room_ir"
|
||||
hass.states.async_set(remote_entity_id, STATE_ON)
|
||||
|
||||
entry = MockConfigEntry(
|
||||
domain=DOMAIN,
|
||||
data={
|
||||
"infrared_emitter_entity_id": remote_entity_id,
|
||||
"device_type": "ac",
|
||||
},
|
||||
unique_id="samsung_ir_ac_test",
|
||||
)
|
||||
entry.add_to_hass(hass)
|
||||
|
||||
# The entity's last visible state was OFF, but it had been heating before that.
|
||||
mock_restore_cache_with_extra_data(
|
||||
hass,
|
||||
[
|
||||
(
|
||||
State("climate.samsung_ac", HVACMode.OFF, {}),
|
||||
{"last_on_hvac_mode": HVACMode.HEAT.value},
|
||||
)
|
||||
],
|
||||
)
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.samsung_infrared.climate.SamsungIrClimate._send_command",
|
||||
new_callable=AsyncMock,
|
||||
) as mock_send_command:
|
||||
assert await hass.config_entries.async_setup(entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
entity_id = "climate.samsung_ac"
|
||||
|
||||
state = hass.states.get(entity_id)
|
||||
assert state is not None
|
||||
assert state.state == HVACMode.OFF
|
||||
|
||||
await hass.services.async_call(
|
||||
"climate",
|
||||
"turn_on",
|
||||
{ATTR_ENTITY_ID: entity_id},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
state = hass.states.get(entity_id)
|
||||
assert state is not None
|
||||
assert state.state == HVACMode.HEAT
|
||||
|
||||
sent_command = mock_send_command.call_args[0][0]
|
||||
assert sent_command.hvac_mode == SamsungAC0292HvacMode.HEAT
|
||||
Reference in New Issue
Block a user