From 46ae12ca54b04311621bbe764707f1f42d093083 Mon Sep 17 00:00:00 2001 From: Emanuele <55278049+elax46@users.noreply.github.com> Date: Sun, 30 Aug 2026 20:43:11 +0200 Subject: [PATCH] Added AC support for Samsung Infrared (#173692) Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- .../components/samsung_infrared/__init__.py | 2 +- .../components/samsung_infrared/button.py | 4 +- .../components/samsung_infrared/climate.py | 241 +++++++++++ .../samsung_infrared/config_flow.py | 17 +- .../components/samsung_infrared/const.py | 1 + .../components/samsung_infrared/entity.py | 9 +- .../samsung_infrared/media_player.py | 4 +- .../components/samsung_infrared/strings.json | 1 + .../samsung_infrared/test_climate.py | 380 ++++++++++++++++++ 9 files changed, 646 insertions(+), 13 deletions(-) create mode 100644 homeassistant/components/samsung_infrared/climate.py create mode 100644 tests/components/samsung_infrared/test_climate.py diff --git a/homeassistant/components/samsung_infrared/__init__.py b/homeassistant/components/samsung_infrared/__init__.py index 0d46c49a6876..fb37358dd756 100644 --- a/homeassistant/components/samsung_infrared/__init__.py +++ b/homeassistant/components/samsung_infrared/__init__.py @@ -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: diff --git a/homeassistant/components/samsung_infrared/button.py b/homeassistant/components/samsung_infrared/button.py index e1fb78c8d635..e25f4ebaa164 100644 --- a/homeassistant/components/samsung_infrared/button.py +++ b/homeassistant/components/samsung_infrared/button.py @@ -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 diff --git a/homeassistant/components/samsung_infrared/climate.py b/homeassistant/components/samsung_infrared/climate.py new file mode 100644 index 000000000000..a973438d376d --- /dev/null +++ b/homeassistant/components/samsung_infrared/climate.py @@ -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) diff --git a/homeassistant/components/samsung_infrared/config_flow.py b/homeassistant/components/samsung_infrared/config_flow.py index 7a5a223afbe2..471354b05de7 100644 --- a/homeassistant/components/samsung_infrared/config_flow.py +++ b/homeassistant/components/samsung_infrared/config_flow.py @@ -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) diff --git a/homeassistant/components/samsung_infrared/const.py b/homeassistant/components/samsung_infrared/const.py index 94bf29378748..9079e0eae4ab 100644 --- a/homeassistant/components/samsung_infrared/const.py +++ b/homeassistant/components/samsung_infrared/const.py @@ -11,3 +11,4 @@ class SamsungDeviceType(StrEnum): """Samsung device types.""" TV = "tv" + AC = "ac" diff --git a/homeassistant/components/samsung_infrared/entity.py b/homeassistant/components/samsung_infrared/entity.py index 92eb814416d9..fa602898ea22 100644 --- a/homeassistant/components/samsung_infrared/entity.py +++ b/homeassistant/components/samsung_infrared/entity.py @@ -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", ) diff --git a/homeassistant/components/samsung_infrared/media_player.py b/homeassistant/components/samsung_infrared/media_player.py index 27a6aa108dc7..feb7a3eb1d9e 100644 --- a/homeassistant/components/samsung_infrared/media_player.py +++ b/homeassistant/components/samsung_infrared/media_player.py @@ -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 diff --git a/homeassistant/components/samsung_infrared/strings.json b/homeassistant/components/samsung_infrared/strings.json index 464876d41220..fed0e56c16ba 100644 --- a/homeassistant/components/samsung_infrared/strings.json +++ b/homeassistant/components/samsung_infrared/strings.json @@ -148,6 +148,7 @@ "selector": { "device_type": { "options": { + "ac": "Air Conditioner", "tv": "TV" } } diff --git a/tests/components/samsung_infrared/test_climate.py b/tests/components/samsung_infrared/test_climate.py new file mode 100644 index 000000000000..e5a2a997a27c --- /dev/null +++ b/tests/components/samsung_infrared/test_climate.py @@ -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