Compare commits

...

6 Commits

Author SHA1 Message Date
abmantis
8560b4f020 Simplify dict access; rename number entities 2026-03-24 22:17:51 +00:00
abmantis
1b68ba1d1d Remove uneeded assignment 2026-03-24 18:42:36 +00:00
abmantis
8834f8f38f Fix test 2026-03-24 18:26:32 +00:00
abmantis
0a929b4fe9 Merge branch 'dev' of github.com:home-assistant/core into lg_infrared_buttons 2026-03-24 18:25:50 +00:00
abmantis
7d43fba039 Add button platform to LG Infrared 2026-03-24 18:07:11 +00:00
abmantis
f08444b271 Move common code to entity class in LG Infrared 2026-03-24 17:54:19 +00:00
8 changed files with 1783 additions and 24 deletions

View File

@@ -6,7 +6,7 @@ from homeassistant.config_entries import ConfigEntry
from homeassistant.const import Platform
from homeassistant.core import HomeAssistant
PLATFORMS = [Platform.MEDIA_PLAYER]
PLATFORMS = [Platform.BUTTON, Platform.MEDIA_PLAYER]
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:

View File

@@ -0,0 +1,147 @@
"""Button platform for LG IR integration."""
from __future__ import annotations
from dataclasses import dataclass
from infrared_protocols.codes.lg.tv import LGTVCode
from homeassistant.components.button import ButtonEntity, ButtonEntityDescription
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from .const import CONF_DEVICE_TYPE, CONF_INFRARED_ENTITY_ID, LGDeviceType
from .entity import LgIrEntity
PARALLEL_UPDATES = 1
@dataclass(frozen=True, kw_only=True)
class LgIrButtonEntityDescription(ButtonEntityDescription):
"""Describes LG IR button entity."""
command_code: LGTVCode
TV_BUTTON_DESCRIPTIONS: tuple[LgIrButtonEntityDescription, ...] = (
LgIrButtonEntityDescription(
key="power_on", translation_key="power_on", command_code=LGTVCode.POWER_ON
),
LgIrButtonEntityDescription(
key="power_off", translation_key="power_off", command_code=LGTVCode.POWER_OFF
),
LgIrButtonEntityDescription(
key="hdmi_1", translation_key="hdmi_1", command_code=LGTVCode.HDMI_1
),
LgIrButtonEntityDescription(
key="hdmi_2", translation_key="hdmi_2", command_code=LGTVCode.HDMI_2
),
LgIrButtonEntityDescription(
key="hdmi_3", translation_key="hdmi_3", command_code=LGTVCode.HDMI_3
),
LgIrButtonEntityDescription(
key="hdmi_4", translation_key="hdmi_4", command_code=LGTVCode.HDMI_4
),
LgIrButtonEntityDescription(
key="exit", translation_key="exit", command_code=LGTVCode.EXIT
),
LgIrButtonEntityDescription(
key="info", translation_key="info", command_code=LGTVCode.INFO
),
LgIrButtonEntityDescription(
key="guide", translation_key="guide", command_code=LGTVCode.GUIDE
),
LgIrButtonEntityDescription(
key="up", translation_key="up", command_code=LGTVCode.NAV_UP
),
LgIrButtonEntityDescription(
key="down", translation_key="down", command_code=LGTVCode.NAV_DOWN
),
LgIrButtonEntityDescription(
key="left", translation_key="left", command_code=LGTVCode.NAV_LEFT
),
LgIrButtonEntityDescription(
key="right", translation_key="right", command_code=LGTVCode.NAV_RIGHT
),
LgIrButtonEntityDescription(
key="ok", translation_key="ok", command_code=LGTVCode.OK
),
LgIrButtonEntityDescription(
key="back", translation_key="back", command_code=LGTVCode.BACK
),
LgIrButtonEntityDescription(
key="home", translation_key="home", command_code=LGTVCode.HOME
),
LgIrButtonEntityDescription(
key="menu", translation_key="menu", command_code=LGTVCode.MENU
),
LgIrButtonEntityDescription(
key="input", translation_key="input", command_code=LGTVCode.INPUT
),
LgIrButtonEntityDescription(
key="num_0", translation_key="num_0", command_code=LGTVCode.NUM_0
),
LgIrButtonEntityDescription(
key="num_1", translation_key="num_1", command_code=LGTVCode.NUM_1
),
LgIrButtonEntityDescription(
key="num_2", translation_key="num_2", command_code=LGTVCode.NUM_2
),
LgIrButtonEntityDescription(
key="num_3", translation_key="num_3", command_code=LGTVCode.NUM_3
),
LgIrButtonEntityDescription(
key="num_4", translation_key="num_4", command_code=LGTVCode.NUM_4
),
LgIrButtonEntityDescription(
key="num_5", translation_key="num_5", command_code=LGTVCode.NUM_5
),
LgIrButtonEntityDescription(
key="num_6", translation_key="num_6", command_code=LGTVCode.NUM_6
),
LgIrButtonEntityDescription(
key="num_7", translation_key="num_7", command_code=LGTVCode.NUM_7
),
LgIrButtonEntityDescription(
key="num_8", translation_key="num_8", command_code=LGTVCode.NUM_8
),
LgIrButtonEntityDescription(
key="num_9", translation_key="num_9", command_code=LGTVCode.NUM_9
),
)
async def async_setup_entry(
hass: HomeAssistant,
entry: ConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up LG IR buttons from config entry."""
infrared_entity_id = entry.data[CONF_INFRARED_ENTITY_ID]
device_type = entry.data[CONF_DEVICE_TYPE]
if device_type == LGDeviceType.TV:
async_add_entities(
LgIrButton(entry, infrared_entity_id, description)
for description in TV_BUTTON_DESCRIPTIONS
)
class LgIrButton(LgIrEntity, ButtonEntity):
"""LG IR button entity."""
entity_description: LgIrButtonEntityDescription
def __init__(
self,
entry: ConfigEntry,
infrared_entity_id: str,
description: LgIrButtonEntityDescription,
) -> None:
"""Initialize LG IR button."""
super().__init__(entry, infrared_entity_id, unique_id_suffix=description.key)
self.entity_description = description
async def async_press(self) -> None:
"""Press the button."""
await self._send_command(self.entity_description.command_code)

View File

@@ -19,6 +19,94 @@
}
}
},
"entity": {
"button": {
"back": {
"name": "Back"
},
"down": {
"name": "Down"
},
"exit": {
"name": "Exit"
},
"guide": {
"name": "Guide"
},
"hdmi_1": {
"name": "HDMI 1"
},
"hdmi_2": {
"name": "HDMI 2"
},
"hdmi_3": {
"name": "HDMI 3"
},
"hdmi_4": {
"name": "HDMI 4"
},
"home": {
"name": "Home"
},
"info": {
"name": "Info"
},
"input": {
"name": "Input"
},
"left": {
"name": "Left"
},
"menu": {
"name": "Menu"
},
"num_0": {
"name": "Number 0"
},
"num_1": {
"name": "Number 1"
},
"num_2": {
"name": "Number 2"
},
"num_3": {
"name": "Number 3"
},
"num_4": {
"name": "Number 4"
},
"num_5": {
"name": "Number 5"
},
"num_6": {
"name": "Number 6"
},
"num_7": {
"name": "Number 7"
},
"num_8": {
"name": "Number 8"
},
"num_9": {
"name": "Number 9"
},
"ok": {
"name": "OK"
},
"power_off": {
"name": "Power off"
},
"power_on": {
"name": "Power on"
},
"right": {
"name": "Right"
},
"up": {
"name": "Up"
}
}
},
"selector": {
"device_type": {
"options": {

View File

@@ -13,6 +13,7 @@ from homeassistant.components.infrared import (
DOMAIN as INFRARED_DOMAIN,
InfraredEntity,
)
from homeassistant.components.lg_infrared import PLATFORMS
from homeassistant.components.lg_infrared.const import (
CONF_DEVICE_TYPE,
CONF_INFRARED_ENTITY_ID,
@@ -68,7 +69,7 @@ def mock_infrared_entity() -> MockInfraredEntity:
@pytest.fixture
def platforms() -> list[Platform]:
"""Return platforms to set up."""
return [Platform.MEDIA_PLAYER]
return PLATFORMS
@pytest.fixture

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,107 @@
"""Tests for the LG Infrared button platform."""
from __future__ import annotations
from infrared_protocols.codes.lg.tv import LGTVCode
import pytest
from syrupy.assertion import SnapshotAssertion
from homeassistant.components.button import DOMAIN as BUTTON_DOMAIN, SERVICE_PRESS
from homeassistant.const import ATTR_ENTITY_ID, Platform
from homeassistant.core import HomeAssistant
from homeassistant.helpers import device_registry as dr, entity_registry as er
from .conftest import MockInfraredEntity
from .utils import check_availability_follows_ir_entity
from tests.common import MockConfigEntry, snapshot_platform
@pytest.fixture
def platforms() -> list[Platform]:
"""Return platforms to set up."""
return [Platform.BUTTON]
@pytest.mark.usefixtures("init_integration")
async def test_entities(
hass: HomeAssistant,
snapshot: SnapshotAssertion,
entity_registry: er.EntityRegistry,
device_registry: dr.DeviceRegistry,
mock_config_entry: MockConfigEntry,
) -> None:
"""Test all button entities are created with correct attributes."""
await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id)
# Verify all entities belong to the same device
device_entry = device_registry.async_get_device(
identifiers={("lg_infrared", mock_config_entry.entry_id)}
)
assert device_entry
entity_entries = er.async_entries_for_config_entry(
entity_registry, mock_config_entry.entry_id
)
for entity_entry in entity_entries:
assert entity_entry.device_id == device_entry.id
@pytest.mark.parametrize(
("entity_id", "expected_code"),
[
("button.lg_tv_power_on", LGTVCode.POWER_ON),
("button.lg_tv_power_off", LGTVCode.POWER_OFF),
("button.lg_tv_hdmi_1", LGTVCode.HDMI_1),
("button.lg_tv_hdmi_2", LGTVCode.HDMI_2),
("button.lg_tv_hdmi_3", LGTVCode.HDMI_3),
("button.lg_tv_hdmi_4", LGTVCode.HDMI_4),
("button.lg_tv_exit", LGTVCode.EXIT),
("button.lg_tv_info", LGTVCode.INFO),
("button.lg_tv_guide", LGTVCode.GUIDE),
("button.lg_tv_up", LGTVCode.NAV_UP),
("button.lg_tv_down", LGTVCode.NAV_DOWN),
("button.lg_tv_left", LGTVCode.NAV_LEFT),
("button.lg_tv_right", LGTVCode.NAV_RIGHT),
("button.lg_tv_ok", LGTVCode.OK),
("button.lg_tv_back", LGTVCode.BACK),
("button.lg_tv_home", LGTVCode.HOME),
("button.lg_tv_menu", LGTVCode.MENU),
("button.lg_tv_input", LGTVCode.INPUT),
("button.lg_tv_number_0", LGTVCode.NUM_0),
("button.lg_tv_number_1", LGTVCode.NUM_1),
("button.lg_tv_number_2", LGTVCode.NUM_2),
("button.lg_tv_number_3", LGTVCode.NUM_3),
("button.lg_tv_number_4", LGTVCode.NUM_4),
("button.lg_tv_number_5", LGTVCode.NUM_5),
("button.lg_tv_number_6", LGTVCode.NUM_6),
("button.lg_tv_number_7", LGTVCode.NUM_7),
("button.lg_tv_number_8", LGTVCode.NUM_8),
("button.lg_tv_number_9", LGTVCode.NUM_9),
],
)
@pytest.mark.usefixtures("init_integration")
async def test_button_press_sends_correct_code(
hass: HomeAssistant,
mock_infrared_entity: MockInfraredEntity,
entity_id: str,
expected_code: LGTVCode,
) -> None:
"""Test pressing a button sends the correct IR code."""
await hass.services.async_call(
BUTTON_DOMAIN,
SERVICE_PRESS,
{ATTR_ENTITY_ID: entity_id},
blocking=True,
)
assert len(mock_infrared_entity.send_command_calls) == 1
assert mock_infrared_entity.send_command_calls[0] == expected_code
@pytest.mark.usefixtures("init_integration")
async def test_button_availability_follows_ir_entity(
hass: HomeAssistant,
) -> None:
"""Test button becomes unavailable when IR entity is unavailable."""
entity_id = "button.lg_tv_power_on"
await check_availability_follows_ir_entity(hass, entity_id)

View File

@@ -19,11 +19,12 @@ from homeassistant.components.media_player import (
SERVICE_VOLUME_MUTE,
SERVICE_VOLUME_UP,
)
from homeassistant.const import ATTR_ENTITY_ID, STATE_UNAVAILABLE, Platform
from homeassistant.const import ATTR_ENTITY_ID, Platform
from homeassistant.core import HomeAssistant
from homeassistant.helpers import device_registry as dr, entity_registry as er
from .conftest import MOCK_INFRARED_ENTITY_ID, MockInfraredEntity
from .conftest import MockInfraredEntity
from .utils import check_availability_follows_ir_entity
from tests.common import MockConfigEntry, snapshot_platform
@@ -99,23 +100,4 @@ async def test_media_player_availability_follows_ir_entity(
hass: HomeAssistant,
) -> None:
"""Test media player becomes unavailable when IR entity is unavailable."""
# Initially available
state = hass.states.get(MEDIA_PLAYER_ENTITY_ID)
assert state is not None
assert state.state != STATE_UNAVAILABLE
# Make IR entity unavailable
hass.states.async_set(MOCK_INFRARED_ENTITY_ID, STATE_UNAVAILABLE)
await hass.async_block_till_done()
state = hass.states.get(MEDIA_PLAYER_ENTITY_ID)
assert state is not None
assert state.state == STATE_UNAVAILABLE
# Restore IR entity
hass.states.async_set(MOCK_INFRARED_ENTITY_ID, "2026-01-01T00:00:00.000")
await hass.async_block_till_done()
state = hass.states.get(MEDIA_PLAYER_ENTITY_ID)
assert state is not None
assert state.state != STATE_UNAVAILABLE
await check_availability_follows_ir_entity(hass, MEDIA_PLAYER_ENTITY_ID)

View File

@@ -0,0 +1,33 @@
"""Tests for the LG Infrared integration."""
from homeassistant.const import STATE_UNAVAILABLE
from homeassistant.core import HomeAssistant
from .conftest import MOCK_INFRARED_ENTITY_ID
async def check_availability_follows_ir_entity(
hass: HomeAssistant,
entity_id: str,
) -> None:
"""Check that entity becomes unavailable when IR entity is unavailable."""
# Initially available
state = hass.states.get(entity_id)
assert state is not None
assert state.state != STATE_UNAVAILABLE
# Make IR entity unavailable
hass.states.async_set(MOCK_INFRARED_ENTITY_ID, STATE_UNAVAILABLE)
await hass.async_block_till_done()
state = hass.states.get(entity_id)
assert state is not None
assert state.state == STATE_UNAVAILABLE
# Restore IR entity
hass.states.async_set(MOCK_INFRARED_ENTITY_ID, "2026-01-01T00:00:00.000")
await hass.async_block_till_done()
state = hass.states.get(entity_id)
assert state is not None
assert state.state != STATE_UNAVAILABLE