Compare commits

...

35 Commits

Author SHA1 Message Date
abmantis 1a582cd289 Nits 2026-05-13 22:29:06 +01:00
abmantis 4bc92dc0ac Remove duplicated state handling 2026-05-13 22:12:58 +01:00
abmantis 2567bb0883 Use codes from lib 2026-05-13 22:07:54 +01:00
abmantis d4dea68b3a Merge branch 'dev' of github.com:home-assistant/core into lg_infrared_receiver 2026-05-13 18:50:56 +01:00
abmantis f60864ce6f Merge branch 'dev' of github.com:home-assistant/core into lg_infrared_receiver 2026-05-13 18:29:47 +01:00
abmantis 3675d7fc00 Improve config flow; add missing tests 2026-05-13 18:05:00 +01:00
abmantis c6e0bc76e0 Use static event list 2026-05-13 16:51:32 +01:00
abmantis ad366b9122 Always load LG infrared event platform 2026-05-13 15:03:03 +01:00
abmantis d83d3efe80 Remove custom decoder; improve subscription 2026-05-12 22:20:10 +01:00
abmantis 0ce5b8f1c7 Add receiver support to lg_infrared 2026-05-12 21:39:49 +01:00
abmantis c7f116dc03 Fix case 2026-05-12 15:36:51 +01:00
abmantis 6fbc239a6b Use common mocks 2026-05-12 15:29:08 +01:00
abmantis fe00883d8a Add tests 2026-05-11 19:23:41 +01:00
abmantis 701096493d Fix deprecated entity tests 2026-05-11 18:32:24 +01:00
Abílio Costa c0e6dc8679 Potential fix for pull request finding
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-05-11 18:31:03 +01:00
Abílio Costa fe07b05095 Update homeassistant/components/infrared/__init__.py
Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
2026-05-11 17:42:40 +01:00
Abílio Costa 0843c5c5fc Update homeassistant/components/infrared/__init__.py
Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
2026-05-11 17:42:21 +01:00
abmantis 6dceaba20f Update common IR fixtures to new naming 2026-05-11 17:41:34 +01:00
abmantis e8a3e54cc2 Decode raw timings in kitchen sink event 2026-05-11 17:11:17 +01:00
abmantis f3350dd736 Guard agasint no-emitter kitchen sink fan 2026-05-11 16:46:38 +01:00
copilot-swe-agent[bot] 82378b39eb Merge branch 'dev' into ir_receiver and resolve conflicts
- Resolve merge conflicts in tests/components/infrared/common.py:
  Add MockInfraredEmitterEntity and MockInfraredReceiverEntity classes
  alongside dev's MockInfraredEntity and fixture helpers
- Resolve merge conflicts in tests/components/infrared/test_init.py:
  Keep receiver tests from ir_receiver branch while incorporating
  dev's fixture pattern (init_infrared, mock_infrared_entity)
- Resolve merge conflicts in tests/components/lg_infrared/conftest.py:
  Use dev's approach (global fixtures from tests/components/conftest.py)
- Update tests/components/infrared/conftest.py to import from common.py
- Make emitter optional in kitchen_sink infrared fan subentry flow:
  Require at least one of emitter or receiver instead of requiring an emitter

Co-authored-by: abmantis <974569+abmantis@users.noreply.github.com>
2026-05-11 14:58:15 +00:00
abmantis e9b36fa841 Address review feedback 2026-05-08 18:06:05 +01:00
abmantis b03ab003ca Fix deprecated_class to work with inheritance 2026-05-08 17:47:23 +01:00
abmantis c6f8d9e307 Merge branch 'dev' of github.com:home-assistant/core into ir_receiver 2026-05-08 15:37:27 +01:00
Abílio Costa 07f1ce7fe2 Merge branch 'dev' into ir_receiver 2026-05-05 10:13:43 +01:00
abmantis 2b65c8c992 Fix subscription; update test 2026-05-04 22:46:21 +01:00
abmantis 7a7b0e294c Update kitchen_sink 2026-05-04 22:24:29 +01:00
abmantis a9bcf42388 Lazy init __signal_callbacks 2026-04-30 20:13:02 +01:00
abmantis 309afb3efb RestoreEntity + tests 2026-04-30 19:58:10 +01:00
abmantis 7e7590c8e2 Address Copilot feedback
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-30 19:29:28 +01:00
abmantis 49ab12c950 Update broadlink 2026-04-30 19:20:44 +01:00
abmantis 5d65d3e27b Merge branch 'dev' of github.com:home-assistant/core into ir_receiver 2026-04-30 19:18:14 +01:00
abmantis 7eeea9060d Update integrations 2026-04-30 19:07:35 +01:00
abmantis 4086d43a1b Minor improvements; update kitchen_sink 2026-04-30 17:59:27 +01:00
abmantis 62dc48ddd3 Add infrared receiver entity 2026-04-25 00:30:05 +01:00
13 changed files with 716 additions and 81 deletions
@@ -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.EVENT, Platform.MEDIA_PLAYER]
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
@@ -116,7 +116,9 @@ async def async_setup_entry(
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up LG IR buttons from config entry."""
infrared_entity_id = entry.data[CONF_INFRARED_ENTITY_ID]
if not (infrared_entity_id := entry.data.get(CONF_INFRARED_ENTITY_ID)):
return
device_type = entry.data[CONF_DEVICE_TYPE]
if device_type == LGDeviceType.TV:
async_add_entities(
@@ -7,6 +7,7 @@ import voluptuous as vol
from homeassistant.components.infrared import (
DOMAIN as INFRARED_DOMAIN,
async_get_emitters,
async_get_receivers,
)
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
from homeassistant.helpers import entity_registry as er
@@ -18,7 +19,13 @@ from homeassistant.helpers.selector import (
SelectSelectorMode,
)
from .const import CONF_DEVICE_TYPE, CONF_INFRARED_ENTITY_ID, DOMAIN, LGDeviceType
from .const import (
CONF_DEVICE_TYPE,
CONF_INFRARED_ENTITY_ID,
CONF_INFRARED_RECEIVER_ENTITY_ID,
DOMAIN,
LGDeviceType,
)
DEVICE_TYPE_NAMES: dict[LGDeviceType, str] = {
LGDeviceType.TV: "TV",
@@ -35,44 +42,60 @@ class LgIrConfigFlow(ConfigFlow, domain=DOMAIN):
) -> ConfigFlowResult:
"""Handle the initial step."""
emitter_entity_ids = async_get_emitters(self.hass)
if not emitter_entity_ids:
return self.async_abort(reason="no_emitters")
receiver_entity_ids = async_get_receivers(self.hass)
if not emitter_entity_ids and not receiver_entity_ids:
return self.async_abort(reason="no_emitters_receivers")
errors: dict[str, str] = {}
if user_input is not None:
entity_id = user_input[CONF_INFRARED_ENTITY_ID]
device_type = user_input[CONF_DEVICE_TYPE]
if entity_id := user_input.get(CONF_INFRARED_ENTITY_ID) or user_input.get(
CONF_INFRARED_RECEIVER_ENTITY_ID
):
device_type = user_input[CONF_DEVICE_TYPE]
await self.async_set_unique_id(f"lg_ir_{device_type}_{entity_id}")
self._abort_if_unique_id_configured()
await self.async_set_unique_id(f"lg_ir_{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[LGDeviceType(device_type)]
title = f"LG {device_type_name} via {entity_name}"
# 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[LGDeviceType(device_type)]
title = f"LG {device_type_name} via {entity_name}"
return self.async_create_entry(title=title, data=user_input)
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(
{
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.Required(CONF_INFRARED_ENTITY_ID): EntitySelector(
EntitySelectorConfig(
domain=INFRARED_DOMAIN,
include_entities=emitter_entity_ids,
)
),
}
),
data_schema=vol.Schema(schema_dict),
errors=errors,
)
@@ -4,6 +4,7 @@ from enum import StrEnum
DOMAIN = "lg_infrared"
CONF_INFRARED_ENTITY_ID = "infrared_entity_id"
CONF_INFRARED_RECEIVER_ENTITY_ID = "infrared_receiver_entity_id"
CONF_DEVICE_TYPE = "device_type"
+17 -19
View File
@@ -36,27 +36,9 @@ class LgIrEntity(Entity):
"""Subscribe to infrared entity state changes."""
await super().async_added_to_hass()
@callback
def _async_ir_state_changed(event: Event[EventStateChangedData]) -> None:
"""Handle infrared entity state changes."""
new_state = event.data["new_state"]
ir_available = (
new_state is not None and new_state.state != STATE_UNAVAILABLE
)
if ir_available != self.available:
_LOGGER.info(
"Infrared entity %s used by %s is %s",
self._infrared_entity_id,
self.entity_id,
"available" if ir_available else "unavailable",
)
self._attr_available = ir_available
self.async_write_ha_state()
self.async_on_remove(
async_track_state_change_event(
self.hass, [self._infrared_entity_id], _async_ir_state_changed
self.hass, [self._infrared_entity_id], self._async_ir_state_changed
)
)
@@ -74,3 +56,19 @@ class LgIrEntity(Entity):
code.to_command(),
context=self._context,
)
@callback
def _async_ir_state_changed(self, event: Event[EventStateChangedData]) -> None:
"""Handle infrared entity state changes."""
new_state = event.data["new_state"]
ir_available = new_state is not None and new_state.state != STATE_UNAVAILABLE
if ir_available != self.available:
_LOGGER.info(
"Infrared entity %s used by %s is %s",
self._infrared_entity_id,
self.entity_id,
"available" if ir_available else "unavailable",
)
self._attr_available = ir_available
self.async_write_ha_state()
@@ -0,0 +1,179 @@
"""Event platform for LG IR integration."""
import logging
from typing import override
from infrared_protocols.codes.lg.tv import LG_ADDRESS, LGTVCode
from infrared_protocols.commands.nec import NECCommand
from homeassistant.components.event import EventEntity
from homeassistant.components.infrared import (
InfraredReceivedSignal,
async_subscribe_receiver,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import (
CALLBACK_TYPE,
Event,
EventStateChangedData,
HomeAssistant,
callback,
)
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from .const import CONF_DEVICE_TYPE, CONF_INFRARED_RECEIVER_ENTITY_ID, LGDeviceType
from .entity import LgIrEntity
_LOGGER = logging.getLogger(__name__)
PARALLEL_UPDATES = 0
_COMMAND_CODE_TO_EVENT_TYPE: dict[LGTVCode, str] = {
LGTVCode.ASPECT: "aspect",
LGTVCode.BACK: "back",
LGTVCode.BLUE: "blue",
LGTVCode.CHANNEL_DOWN: "channel_down",
LGTVCode.CHANNEL_UP: "channel_up",
LGTVCode.EXIT: "exit",
LGTVCode.EZ_ADJUST: "ez_adjust",
LGTVCode.FAST_FORWARD: "fast_forward",
LGTVCode.GREEN: "green",
LGTVCode.GUIDE: "guide",
LGTVCode.HDMI_1: "hdmi_1",
LGTVCode.HDMI_2: "hdmi_2",
LGTVCode.HDMI_3: "hdmi_3",
LGTVCode.HDMI_4: "hdmi_4",
LGTVCode.HOME: "home",
LGTVCode.INFO: "info",
LGTVCode.INPUT: "input",
LGTVCode.IN_START: "in_start",
LGTVCode.LIST: "list",
LGTVCode.MENU: "menu",
LGTVCode.MUTE: "mute",
LGTVCode.NAV_DOWN: "nav_down",
LGTVCode.NAV_LEFT: "nav_left",
LGTVCode.NAV_RIGHT: "nav_right",
LGTVCode.NAV_UP: "nav_up",
LGTVCode.NUM_0: "num_0",
LGTVCode.NUM_1: "num_1",
LGTVCode.NUM_2: "num_2",
LGTVCode.NUM_3: "num_3",
LGTVCode.NUM_4: "num_4",
LGTVCode.NUM_5: "num_5",
LGTVCode.NUM_6: "num_6",
LGTVCode.NUM_7: "num_7",
LGTVCode.NUM_8: "num_8",
LGTVCode.NUM_9: "num_9",
LGTVCode.OK: "ok",
LGTVCode.PAUSE: "pause",
LGTVCode.PLAY: "play",
LGTVCode.POWER: "power",
LGTVCode.POWER_OFF: "power_off",
LGTVCode.POWER_ON: "power_on",
LGTVCode.RED: "red",
LGTVCode.REWIND: "rewind",
LGTVCode.SAP: "sap",
LGTVCode.SETTINGS: "settings",
LGTVCode.STOP: "stop",
LGTVCode.SUBTITLE: "subtitle",
LGTVCode.TEXT: "text",
LGTVCode.VOLUME_DOWN: "volume_down",
LGTVCode.VOLUME_UP: "volume_up",
LGTVCode.YELLOW: "yellow",
}
_EVENT_TYPE_UNKNOWN = "unknown"
_EVENT_TYPES: list[str] = [*_COMMAND_CODE_TO_EVENT_TYPE.values(), _EVENT_TYPE_UNKNOWN]
async def async_setup_entry(
hass: HomeAssistant,
entry: ConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up LG IR event entity from config entry."""
if entry.data[CONF_DEVICE_TYPE] != LGDeviceType.TV:
return
if not (receiver_entity_id := entry.data.get(CONF_INFRARED_RECEIVER_ENTITY_ID)):
return
async_add_entities([LgIrReceivedCommandEvent(entry, receiver_entity_id)])
class LgIrReceivedCommandEvent(LgIrEntity, EventEntity):
"""Event entity that fires when an LG TV IR command is received."""
_attr_translation_key = "received_command"
_attr_event_types = _EVENT_TYPES
def __init__(
self,
entry: ConfigEntry,
receiver_entity_id: str,
) -> None:
"""Initialize the event entity."""
super().__init__(entry, receiver_entity_id, unique_id_suffix="received_command")
self._remove_signal_subscription: CALLBACK_TYPE | None = None
async def async_added_to_hass(self) -> None:
"""Subscribe to the IR receiver when added to hass."""
await super().async_added_to_hass()
self._async_update_receiver_subscription()
self.async_on_remove(self._async_unsubscribe_receiver)
@callback
def _handle_signal(self, signal: InfraredReceivedSignal) -> None:
"""Handle a received IR signal."""
nec_command = NECCommand.from_raw_timings(signal.timings)
if nec_command is None:
return
if nec_command.address != LG_ADDRESS:
return
try:
command_code = LGTVCode(nec_command.command)
except ValueError:
# Ensure that a future change to the LGTVCode enum doesn't break this and
# shows as unknown
event_type = _EVENT_TYPE_UNKNOWN
else:
event_type = _COMMAND_CODE_TO_EVENT_TYPE.get(
command_code, _EVENT_TYPE_UNKNOWN
)
_LOGGER.debug(
"Received LG TV IR command: %s (0x%02X)", event_type, nec_command.command
)
self._trigger_event(event_type)
self.async_write_ha_state()
@callback
def _async_unsubscribe_receiver(self) -> None:
"""Unsubscribe from the current IR receiver."""
if self._remove_signal_subscription is None:
return
self._remove_signal_subscription()
self._remove_signal_subscription = None
@callback
def _async_update_receiver_subscription(self) -> None:
"""Update the IR receiver subscription when availability changes."""
if not self.available:
self._async_unsubscribe_receiver()
elif self._remove_signal_subscription is None:
_LOGGER.debug(
"Subscribing to infrared receiver entity %s for %s",
self._infrared_entity_id,
self.entity_id,
)
self._remove_signal_subscription = async_subscribe_receiver(
self.hass, self._infrared_entity_id, self._handle_signal
)
@override
@callback
def _async_ir_state_changed(self, event: Event[EventStateChangedData]) -> None:
"""Handle infrared entity state changes."""
super()._async_ir_state_changed(event)
self._async_update_receiver_subscription()
@@ -24,7 +24,9 @@ async def async_setup_entry(
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up LG IR media player from config entry."""
infrared_entity_id = entry.data[CONF_INFRARED_ENTITY_ID]
if not (infrared_entity_id := entry.data.get(CONF_INFRARED_ENTITY_ID)):
return
device_type = entry.data[CONF_DEVICE_TYPE]
if device_type == LGDeviceType.TV:
async_add_entities([LgIrTvMediaPlayer(entry, infrared_entity_id)])
@@ -1,20 +1,25 @@
{
"config": {
"abort": {
"already_configured": "This LG device has already been configured with this transmitter.",
"no_emitters": "No infrared transmitter entities found. Please set up an infrared device first."
"already_configured": "This LG device has already been configured with this infrared entity.",
"no_emitters_receivers": "No infrared emitter or receiver entities found. Please set up an infrared device first."
},
"error": {
"missing_infrared_entity": "Select an infrared emitter or receiver."
},
"step": {
"user": {
"data": {
"device_type": "Device type",
"infrared_entity_id": "Infrared transmitter"
"infrared_entity_id": "Infrared emitter",
"infrared_receiver_entity_id": "Infrared receiver"
},
"data_description": {
"device_type": "The type of LG device to control.",
"infrared_entity_id": "The infrared transmitter entity to use for sending commands."
"infrared_entity_id": "The infrared emitter entity to use for sending commands.",
"infrared_receiver_entity_id": "The infrared receiver entity to use for receiving signals."
},
"description": "Select the device type and the infrared transmitter entity to use for controlling your LG device.",
"description": "Select the device type and at least one infrared emitter or receiver to use with your LG device.",
"title": "Set up LG IR Remote"
}
}
@@ -105,6 +110,69 @@
"up": {
"name": "Up"
}
},
"event": {
"received_command": {
"name": "Received command",
"state_attributes": {
"event_type": {
"state": {
"aspect": "Aspect",
"back": "Back",
"blue": "Blue",
"channel_down": "Channel down",
"channel_up": "Channel up",
"exit": "Exit",
"ez_adjust": "EZ adjust",
"fast_forward": "Fast forward",
"green": "Green",
"guide": "Guide",
"hdmi_1": "HDMI 1",
"hdmi_2": "HDMI 2",
"hdmi_3": "HDMI 3",
"hdmi_4": "HDMI 4",
"home": "Home",
"in_start": "In start",
"info": "Info",
"input": "Input",
"list": "List",
"menu": "Menu",
"mute": "Mute",
"nav_down": "Navigate down",
"nav_left": "Navigate left",
"nav_right": "Navigate right",
"nav_up": "Navigate up",
"num_0": "Number 0",
"num_1": "Number 1",
"num_2": "Number 2",
"num_3": "Number 3",
"num_4": "Number 4",
"num_5": "Number 5",
"num_6": "Number 6",
"num_7": "Number 7",
"num_8": "Number 8",
"num_9": "Number 9",
"ok": "OK",
"pause": "Pause",
"play": "Play",
"power": "Power",
"power_off": "Power off",
"power_on": "Power on",
"red": "Red",
"rewind": "Rewind",
"sap": "SAP",
"settings": "Settings",
"stop": "Stop",
"subtitle": "Subtitle",
"text": "Text",
"unknown": "Unknown",
"volume_down": "Volume down",
"volume_up": "Volume up",
"yellow": "Yellow"
}
}
}
}
}
},
"selector": {
+13 -4
View File
@@ -9,6 +9,7 @@ from homeassistant.components.lg_infrared import PLATFORMS
from homeassistant.components.lg_infrared.const import (
CONF_DEVICE_TYPE,
CONF_INFRARED_ENTITY_ID,
CONF_INFRARED_RECEIVER_ENTITY_ID,
DOMAIN,
LGDeviceType,
)
@@ -16,8 +17,14 @@ from homeassistant.const import Platform
from homeassistant.core import HomeAssistant
from tests.common import MockConfigEntry
from tests.components.infrared import EMITTER_ENTITY_ID as MOCK_INFRARED_ENTITY_ID
from tests.components.infrared.common import MockInfraredEmitterEntity
from tests.components.infrared import (
EMITTER_ENTITY_ID as MOCK_INFRARED_EMITTER_ENTITY_ID,
RECEIVER_ENTITY_ID as MOCK_INFRARED_RECEIVER_ENTITY_ID,
)
from tests.components.infrared.common import (
MockInfraredEmitterEntity,
MockInfraredReceiverEntity,
)
@pytest.fixture
@@ -29,9 +36,10 @@ def mock_config_entry() -> MockConfigEntry:
title="LG TV via Test IR emitter",
data={
CONF_DEVICE_TYPE: LGDeviceType.TV,
CONF_INFRARED_ENTITY_ID: MOCK_INFRARED_ENTITY_ID,
CONF_INFRARED_ENTITY_ID: MOCK_INFRARED_EMITTER_ENTITY_ID,
CONF_INFRARED_RECEIVER_ENTITY_ID: MOCK_INFRARED_RECEIVER_ENTITY_ID,
},
unique_id=f"lg_ir_tv_{MOCK_INFRARED_ENTITY_ID}",
unique_id=f"lg_ir_tv_{MOCK_INFRARED_EMITTER_ENTITY_ID}",
)
@@ -61,6 +69,7 @@ async def init_integration(
hass: HomeAssistant,
mock_config_entry: MockConfigEntry,
mock_infrared_emitter_entity: MockInfraredEmitterEntity,
mock_infrared_receiver_entity: MockInfraredReceiverEntity,
mock_lg_tv_code_to_command: None,
platforms: list[Platform],
) -> MockConfigEntry:
@@ -0,0 +1,161 @@
# serializer version: 1
# name: test_entities[event.lg_tv_received_command-entry]
EntityRegistryEntrySnapshot({
'aliases': list([
None,
]),
'area_id': None,
'capabilities': dict({
'event_types': list([
'aspect',
'back',
'blue',
'channel_down',
'channel_up',
'exit',
'ez_adjust',
'fast_forward',
'green',
'guide',
'hdmi_1',
'hdmi_2',
'hdmi_3',
'hdmi_4',
'home',
'info',
'input',
'in_start',
'list',
'menu',
'mute',
'nav_down',
'nav_left',
'nav_right',
'nav_up',
'num_0',
'num_1',
'num_2',
'num_3',
'num_4',
'num_5',
'num_6',
'num_7',
'num_8',
'num_9',
'ok',
'pause',
'play',
'power',
'power_off',
'power_on',
'red',
'rewind',
'sap',
'settings',
'stop',
'subtitle',
'text',
'volume_down',
'volume_up',
'yellow',
'unknown',
]),
}),
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'event',
'entity_category': None,
'entity_id': 'event.lg_tv_received_command',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'object_id_base': 'Received command',
'options': dict({
}),
'original_device_class': None,
'original_icon': None,
'original_name': 'Received command',
'platform': 'lg_infrared',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'received_command',
'unique_id': '01JTEST0000000000000000000_received_command',
'unit_of_measurement': None,
})
# ---
# name: test_entities[event.lg_tv_received_command-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'event_type': None,
'event_types': list([
'aspect',
'back',
'blue',
'channel_down',
'channel_up',
'exit',
'ez_adjust',
'fast_forward',
'green',
'guide',
'hdmi_1',
'hdmi_2',
'hdmi_3',
'hdmi_4',
'home',
'info',
'input',
'in_start',
'list',
'menu',
'mute',
'nav_down',
'nav_left',
'nav_right',
'nav_up',
'num_0',
'num_1',
'num_2',
'num_3',
'num_4',
'num_5',
'num_6',
'num_7',
'num_8',
'num_9',
'ok',
'pause',
'play',
'power',
'power_off',
'power_on',
'red',
'rewind',
'sap',
'settings',
'stop',
'subtitle',
'text',
'volume_down',
'volume_up',
'yellow',
'unknown',
]),
'friendly_name': 'LG TV Received command',
}),
'context': <ANY>,
'entity_id': 'event.lg_tv_received_command',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'unknown',
})
# ---
@@ -5,6 +5,7 @@ import pytest
from homeassistant.components.lg_infrared.const import (
CONF_DEVICE_TYPE,
CONF_INFRARED_ENTITY_ID,
CONF_INFRARED_RECEIVER_ENTITY_ID,
DOMAIN,
LGDeviceType,
)
@@ -16,12 +17,41 @@ from homeassistant.helpers import entity_registry as er
from tests.common import MockConfigEntry
from tests.components.infrared import (
EMITTER_ENTITY_ID as mock_infrared_emitter_entity_id,
RECEIVER_ENTITY_ID as mock_infrared_receiver_entity_id,
)
@pytest.mark.usefixtures("mock_infrared_emitter_entity")
@pytest.mark.parametrize(
("config", "expected_title", "unique_id_entity_id"),
[
(
{CONF_INFRARED_ENTITY_ID: mock_infrared_emitter_entity_id},
"LG TV via Test IR emitter",
mock_infrared_emitter_entity_id,
),
(
{
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",
mock_infrared_emitter_entity_id,
),
(
{CONF_INFRARED_RECEIVER_ENTITY_ID: mock_infrared_receiver_entity_id},
"LG TV via Test IR receiver",
mock_infrared_receiver_entity_id,
),
],
)
@pytest.mark.usefixtures(
"mock_infrared_emitter_entity", "mock_infrared_receiver_entity"
)
async def test_user_flow_success(
hass: HomeAssistant,
config: dict[str, str],
expected_title: str,
unique_id_entity_id: str,
) -> None:
"""Test successful user config flow."""
result = await hass.config_entries.flow.async_init(
@@ -33,19 +63,31 @@ async def test_user_flow_success(
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_DEVICE_TYPE: LGDeviceType.TV, **config},
)
assert result["type"] is FlowResultType.CREATE_ENTRY
assert result["title"] == "LG TV via Test IR emitter"
assert result["data"] == {
CONF_DEVICE_TYPE: LGDeviceType.TV,
CONF_INFRARED_ENTITY_ID: mock_infrared_emitter_entity_id,
}
assert result["result"].unique_id == f"lg_ir_tv_{mock_infrared_emitter_entity_id}"
assert result["title"] == expected_title
assert result["data"] == {CONF_DEVICE_TYPE: LGDeviceType.TV, **config}
assert result["result"].unique_id == f"lg_ir_tv_{unique_id_entity_id}"
@pytest.mark.usefixtures("mock_infrared_emitter_entity")
async def test_user_flow_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}
)
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
user_input={CONF_DEVICE_TYPE: LGDeviceType.TV},
)
assert result["type"] is FlowResultType.FORM
assert result["errors"] == {"base": "missing_infrared_entity"}
@pytest.mark.usefixtures("mock_infrared_emitter_entity")
@@ -74,14 +116,14 @@ async def test_user_flow_already_configured(
@pytest.mark.usefixtures("init_infrared")
async def test_user_flow_no_emitters(hass: HomeAssistant) -> None:
"""Test user flow aborts when no infrared emitters exist."""
async def test_user_flow_no_emitters_receivers(hass: HomeAssistant) -> None:
"""Test user flow aborts when no infrared emitters or receivers exist."""
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": SOURCE_USER}
)
assert result["type"] is FlowResultType.ABORT
assert result["reason"] == "no_emitters"
assert result["reason"] == "no_emitters_receivers"
@pytest.mark.usefixtures("mock_infrared_emitter_entity")
+148
View File
@@ -0,0 +1,148 @@
"""Tests for the LG Infrared event platform."""
from freezegun.api import FrozenDateTimeFactory
from infrared_protocols.codes.lg.tv import LGTVCode
from infrared_protocols.commands.nec import NECCommand
import pytest
from syrupy.assertion import SnapshotAssertion
from homeassistant.components.event import ATTR_EVENT_TYPE
from homeassistant.components.infrared import InfraredReceivedSignal
from homeassistant.const import STATE_UNAVAILABLE, STATE_UNKNOWN, Platform
from homeassistant.core import HomeAssistant
from homeassistant.helpers import entity_registry as er
from homeassistant.util import dt as dt_util
from tests.common import MockConfigEntry, snapshot_platform
from tests.components.infrared import RECEIVER_ENTITY_ID
from tests.components.infrared.common import MockInfraredReceiverEntity
EVENT_ENTITY_ID = "event.lg_tv_received_command"
_LG_TV_NEC_ADDRESS = 0xFB04
@pytest.fixture
def platforms() -> list[Platform]:
"""Return platforms to set up."""
return [Platform.EVENT]
@pytest.mark.usefixtures("init_integration")
async def test_entities(
hass: HomeAssistant,
snapshot: SnapshotAssertion,
entity_registry: er.EntityRegistry,
mock_config_entry: MockConfigEntry,
) -> None:
"""Test the event entity is created with the expected attributes."""
await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id)
@pytest.mark.parametrize(
("command_byte", "expected_event_type"),
[
(LGTVCode.POWER_ON, "power_on"),
(LGTVCode.POWER_OFF, "power_off"),
(LGTVCode.VOLUME_UP, "volume_up"),
(LGTVCode.MUTE, "mute"),
(0xFE, "unknown"),
],
)
@pytest.mark.usefixtures("init_integration")
async def test_event_fires(
hass: HomeAssistant,
mock_infrared_receiver_entity: MockInfraredReceiverEntity,
freezer: FrozenDateTimeFactory,
command_byte: int,
expected_event_type: str,
) -> None:
"""Test the event entity fires the expected event type."""
now = dt_util.parse_datetime("2026-05-12 12:00:00+00:00")
assert now is not None
freezer.move_to(now)
command = NECCommand(address=_LG_TV_NEC_ADDRESS, command=command_byte)
mock_infrared_receiver_entity._handle_received_signal(
InfraredReceivedSignal(timings=command.get_raw_timings())
)
await hass.async_block_till_done()
state = hass.states.get(EVENT_ENTITY_ID)
assert state is not None
assert state.state == now.isoformat(timespec="milliseconds")
assert state.attributes[ATTR_EVENT_TYPE] == expected_event_type
@pytest.mark.usefixtures("init_integration")
async def test_event_ignores_other_nec_addresses(
hass: HomeAssistant,
mock_infrared_receiver_entity: MockInfraredReceiverEntity,
) -> None:
"""Test the event entity ignores NEC signals from other addresses."""
command = NECCommand(address=0x1234, command=LGTVCode.POWER_ON)
mock_infrared_receiver_entity._handle_received_signal(
InfraredReceivedSignal(timings=command.get_raw_timings())
)
await hass.async_block_till_done()
state = hass.states.get(EVENT_ENTITY_ID)
assert state is not None
assert state.state == STATE_UNKNOWN
assert state.attributes.get(ATTR_EVENT_TYPE) is None
@pytest.mark.usefixtures("init_integration")
async def test_event_ignores_non_nec_signals(
hass: HomeAssistant,
mock_infrared_receiver_entity: MockInfraredReceiverEntity,
) -> None:
"""Test the event entity ignores signals that cannot be decoded as NEC."""
mock_infrared_receiver_entity._handle_received_signal(
InfraredReceivedSignal(timings=[1, 2, 3, 4])
)
await hass.async_block_till_done()
state = hass.states.get(EVENT_ENTITY_ID)
assert state is not None
assert state.state == STATE_UNKNOWN
assert state.attributes.get(ATTR_EVENT_TYPE) is None
@pytest.mark.usefixtures("init_integration")
async def test_event_resubscribes_after_receiver_unavailable(
hass: HomeAssistant,
mock_infrared_receiver_entity: MockInfraredReceiverEntity,
freezer: FrozenDateTimeFactory,
) -> None:
"""Test the event entity resubscribes when the receiver becomes available again."""
state = hass.states.get(EVENT_ENTITY_ID)
assert state is not None
assert state.state != STATE_UNAVAILABLE
hass.states.async_set(RECEIVER_ENTITY_ID, STATE_UNAVAILABLE)
await hass.async_block_till_done()
state = hass.states.get(EVENT_ENTITY_ID)
assert state is not None
assert state.state == STATE_UNAVAILABLE
hass.states.async_set(RECEIVER_ENTITY_ID, STATE_UNKNOWN)
await hass.async_block_till_done()
state = hass.states.get(EVENT_ENTITY_ID)
assert state is not None
assert state.state != STATE_UNAVAILABLE
now = dt_util.parse_datetime("2026-05-12 12:00:00+00:00")
assert now is not None
freezer.move_to(now)
command = NECCommand(address=_LG_TV_NEC_ADDRESS, command=LGTVCode.POWER_ON)
mock_infrared_receiver_entity._handle_received_signal(
InfraredReceivedSignal(timings=command.get_raw_timings())
)
await hass.async_block_till_done()
state = hass.states.get(EVENT_ENTITY_ID)
assert state is not None
assert state.state == now.isoformat(timespec="milliseconds")
assert state.attributes[ATTR_EVENT_TYPE] == "power_on"
+5 -3
View File
@@ -3,7 +3,9 @@
from homeassistant.const import STATE_UNAVAILABLE
from homeassistant.core import HomeAssistant
from tests.components.infrared import EMITTER_ENTITY_ID as MOCK_INFRARED_ENTITY_ID
from tests.components.infrared import (
EMITTER_ENTITY_ID as MOCK_INFRARED_EMITTER_ENTITY_ID,
)
async def check_availability_follows_ir_entity(
@@ -17,7 +19,7 @@ async def check_availability_follows_ir_entity(
assert state.state != STATE_UNAVAILABLE
# Make IR entity unavailable
hass.states.async_set(MOCK_INFRARED_ENTITY_ID, STATE_UNAVAILABLE)
hass.states.async_set(MOCK_INFRARED_EMITTER_ENTITY_ID, STATE_UNAVAILABLE)
await hass.async_block_till_done()
state = hass.states.get(entity_id)
@@ -25,7 +27,7 @@ async def check_availability_follows_ir_entity(
assert state.state == STATE_UNAVAILABLE
# Restore IR entity
hass.states.async_set(MOCK_INFRARED_ENTITY_ID, "2026-01-01T00:00:00.000")
hass.states.async_set(MOCK_INFRARED_EMITTER_ENTITY_ID, "2026-01-01T00:00:00.000")
await hass.async_block_till_done()
state = hass.states.get(entity_id)