mirror of
https://github.com/home-assistant/core.git
synced 2026-05-14 13:01:46 +00:00
Compare commits
35 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 1a582cd289 | |||
| 4bc92dc0ac | |||
| 2567bb0883 | |||
| d4dea68b3a | |||
| f60864ce6f | |||
| 3675d7fc00 | |||
| c6e0bc76e0 | |||
| ad366b9122 | |||
| d83d3efe80 | |||
| 0ce5b8f1c7 | |||
| c7f116dc03 | |||
| 6fbc239a6b | |||
| fe00883d8a | |||
| 701096493d | |||
| c0e6dc8679 | |||
| fe07b05095 | |||
| 0843c5c5fc | |||
| 6dceaba20f | |||
| e8a3e54cc2 | |||
| f3350dd736 | |||
| 82378b39eb | |||
| e9b36fa841 | |||
| b03ab003ca | |||
| c6f8d9e307 | |||
| 07f1ce7fe2 | |||
| 2b65c8c992 | |||
| 7a7b0e294c | |||
| a9bcf42388 | |||
| 309afb3efb | |||
| 7e7590c8e2 | |||
| 49ab12c950 | |||
| 5d65d3e27b | |||
| 7eeea9060d | |||
| 4086d43a1b | |||
| 62dc48ddd3 |
@@ -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"
|
||||
|
||||
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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"
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user