Compare commits

...

2 Commits

Author SHA1 Message Date
Paulus Schoutsen
f39620a0fa Migrate braodlink to no timings API 2026-04-19 19:43:55 -04:00
Paulus Schoutsen
9f2eb56a7e Adopt infrared-protocols 2.0.0 (Flipper loader + signed list[int] timings)
infrared-protocols 2.0.0 drops the Timing dataclass in favor of a flat
list[int] using the signed-alternating convention (positive mark,
negative space), and moves bundled code sets to Flipper `.ir` files
loaded lazily through `get_codes(...)`.

lg_infrared migrates off the per-device `LGTVCode` / `make_command`
helpers. Buttons and the media player now reference commands by name
(e.g. "POWER", "HDMI_1"), resolved through a module-level
`LG_TV_CODES = get_codes("lg/tv")` CommandCollection whose backing
`.ir` file is parsed lazily in the executor on first use.

The esphome, smlight, and kitchen_sink consumers no longer flatten
pairs at the call site — esphome and kitchen_sink pass the signed
list straight through, smlight takes `abs()` per element since its
protocol wants unsigned magnitudes.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-19 19:37:44 -04:00
16 changed files with 147 additions and 177 deletions

View File

@@ -6,7 +6,6 @@ from typing import TYPE_CHECKING
from broadlink.exceptions import BroadlinkException
from broadlink.remote import pulses_to_data as _bl_pulses_to_data
import infrared_protocols
from homeassistant.components.infrared import InfraredCommand, InfraredEntity
from homeassistant.config_entries import ConfigEntry
@@ -35,9 +34,9 @@ class BroadlinkIRCommand(InfraredCommand):
IR code databases like SmartIR) and want to use it with the new
infrared platform.
Protocol-aware commands (infrared_protocols.NECCommand, LgTVCommand,
etc.) manage repeats *inside* get_raw_timings() and should use the
default repeat=0. Only BroadlinkIRCommand should set hardware repeat.
Protocol-aware commands (infrared_protocols.NECCommand, etc.) manage
repeats *inside* get_raw_timings() and should use the default
repeat=0. Only BroadlinkIRCommand should set hardware repeat.
Example: Migrating IR code database base64 codes to the infrared platform:
@@ -50,11 +49,10 @@ class BroadlinkIRCommand(InfraredCommand):
packet_data = base64.b64decode(b64_code)
repeat_count = packet_data[IR_PACKET_REPEAT_INDEX]
# Parse Broadlink packet to microsecond timings
# Parse Broadlink packet to alternating µs pulses, then convert to
# the signed-alternating convention (positive mark, negative space).
pulses = data_to_pulses(packet_data)
timings = list(zip(pulses[::2], pulses[1::2]))
if len(pulses) % 2:
timings.append((pulses[-1], 0))
timings = [p if i % 2 == 0 else -p for i, p in enumerate(pulses)]
# Create command
cmd = BroadlinkIRCommand(timings, repeat_count=repeat_count)
@@ -67,13 +65,13 @@ class BroadlinkIRCommand(InfraredCommand):
def __init__(
self,
timings: list[tuple[int, int]],
timings: list[int],
repeat_count: int = 0,
) -> None:
"""Initialize with timing pairs and optional repeat count.
"""Initialize with signed-alternating timings and optional repeat count.
Args:
timings: List of (mark_us, space_us) pairs in microseconds.
timings: Signed-alternating µs pulses (positive mark, negative space).
repeat_count: Broadlink hardware repeat count (0 = send once).
Must be 0255 (the hardware repeat byte is a single unsigned byte).
@@ -83,23 +81,21 @@ class BroadlinkIRCommand(InfraredCommand):
if not 0 <= repeat_count <= 255:
raise ValueError(f"repeat_count must be 0255, got {repeat_count}")
super().__init__(modulation=self.MODULATION, repeat_count=repeat_count)
self._timings = [
infrared_protocols.Timing(high_us=high, low_us=low) for high, low in timings
]
self._timings = list(timings)
def get_raw_timings(self) -> list[infrared_protocols.Timing]:
"""Return timing pairs for transmission."""
def get_raw_timings(self) -> list[int]:
"""Return signed-alternating timings for transmission."""
return self._timings
def timings_to_broadlink_packet(
timings: list[tuple[int, int]],
timings: list[int],
repeat: int = 0,
) -> bytes:
"""Convert raw timing pairs (high_us, low_us) to a Broadlink IR packet.
"""Convert signed-alternating timings to a Broadlink IR packet.
Args:
timings: List of (mark_us, space_us) pairs in microseconds.
timings: Signed-alternating µs pulses (positive mark, negative space).
repeat: Number of extra repeats (0 = send once).
Returns:
@@ -109,12 +105,9 @@ def timings_to_broadlink_packet(
if not 0 <= repeat <= 255:
raise ValueError(f"repeat must be 0255, got {repeat}")
# Flatten (mark, space) pairs into a pulse list, omitting any zero-length spaces
pulses: list[int] = []
for high_us, low_us in timings:
pulses.append(high_us)
if low_us:
pulses.append(low_us)
# Broadlink hardware wants unsigned alternating pulses; drop the sign
# used by the signed-alternating convention.
pulses = [abs(t) for t in timings]
# Use broadlink library's encoder (tick=32.84 µs)
packet = bytearray(_bl_pulses_to_data(pulses))
@@ -160,10 +153,6 @@ class BroadlinkInfraredEntity(BroadlinkEntity, InfraredEntity):
Using isinstance check ensures protocol-level repeats (already in
timing data) don't get conflated with hardware repeats.
"""
timings = [
(timing.high_us, timing.low_us) for timing in command.get_raw_timings()
]
# Only BroadlinkIRCommand uses Broadlink hardware repeat. Protocol-aware
# commands (NECCommand, etc.) encode repeats inside get_raw_timings()
# and must use hardware repeat=0 to avoid double-repeating.
@@ -172,7 +161,7 @@ class BroadlinkInfraredEntity(BroadlinkEntity, InfraredEntity):
else:
repeat = 0
packet = timings_to_broadlink_packet(timings, repeat=repeat)
packet = timings_to_broadlink_packet(command.get_raw_timings(), repeat=repeat)
try:
await self._device.async_request(self._device.api.send_data, packet)

View File

@@ -35,11 +35,7 @@ class EsphomeInfraredEntity(EsphomeEntity[InfraredInfo, EntityState], InfraredEn
@convert_api_error_ha_error
async def async_send_command(self, command: InfraredCommand) -> None:
"""Send an IR command."""
timings = [
interval
for timing in command.get_raw_timings()
for interval in (timing.high_us, -timing.low_us)
]
timings = command.get_raw_timings()
_LOGGER.debug("Sending command: %s", timings)
self._client.infrared_rf_transmit_raw_timings(

View File

@@ -5,5 +5,5 @@
"documentation": "https://www.home-assistant.io/integrations/infrared",
"integration_type": "entity",
"quality_scale": "internal",
"requirements": ["infrared-protocols==1.3.0"]
"requirements": ["infrared-protocols==2.0.0"]
}

View File

@@ -55,11 +55,7 @@ class DemoInfrared(InfraredEntity):
async def async_send_command(self, command: infrared_protocols.Command) -> None:
"""Send an IR command."""
timings = [
interval
for timing in command.get_raw_timings()
for interval in (timing.high_us, -timing.low_us)
]
timings = command.get_raw_timings()
persistent_notification.async_create(
self.hass, str(timings), title="Infrared Command"
)

View File

@@ -4,8 +4,6 @@ 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
@@ -21,93 +19,89 @@ PARALLEL_UPDATES = 1
class LgIrButtonEntityDescription(ButtonEntityDescription):
"""Describes LG IR button entity."""
command_code: LGTVCode
command_code: str
TV_BUTTON_DESCRIPTIONS: tuple[LgIrButtonEntityDescription, ...] = (
LgIrButtonEntityDescription(
key="power_on", translation_key="power_on", command_code=LGTVCode.POWER_ON
key="power_on", translation_key="power_on", command_code="POWER_ON"
),
LgIrButtonEntityDescription(
key="power_off", translation_key="power_off", command_code=LGTVCode.POWER_OFF
key="power_off", translation_key="power_off", command_code="POWER_OFF"
),
LgIrButtonEntityDescription(
key="hdmi_1", translation_key="hdmi_1", command_code=LGTVCode.HDMI_1
key="hdmi_1", translation_key="hdmi_1", command_code="HDMI_1"
),
LgIrButtonEntityDescription(
key="hdmi_2", translation_key="hdmi_2", command_code=LGTVCode.HDMI_2
key="hdmi_2", translation_key="hdmi_2", command_code="HDMI_2"
),
LgIrButtonEntityDescription(
key="hdmi_3", translation_key="hdmi_3", command_code=LGTVCode.HDMI_3
key="hdmi_3", translation_key="hdmi_3", command_code="HDMI_3"
),
LgIrButtonEntityDescription(
key="hdmi_4", translation_key="hdmi_4", command_code=LGTVCode.HDMI_4
key="hdmi_4", translation_key="hdmi_4", command_code="HDMI_4"
),
LgIrButtonEntityDescription(
key="exit", translation_key="exit", command_code=LGTVCode.EXIT
key="exit", translation_key="exit", command_code="EXIT"
),
LgIrButtonEntityDescription(
key="info", translation_key="info", command_code=LGTVCode.INFO
key="info", translation_key="info", command_code="INFO"
),
LgIrButtonEntityDescription(
key="guide", translation_key="guide", command_code=LGTVCode.GUIDE
key="guide", translation_key="guide", command_code="GUIDE"
),
LgIrButtonEntityDescription(key="up", translation_key="up", command_code="NAV_UP"),
LgIrButtonEntityDescription(
key="down", translation_key="down", command_code="NAV_DOWN"
),
LgIrButtonEntityDescription(
key="up", translation_key="up", command_code=LGTVCode.NAV_UP
key="left", translation_key="left", command_code="NAV_LEFT"
),
LgIrButtonEntityDescription(
key="down", translation_key="down", command_code=LGTVCode.NAV_DOWN
key="right", translation_key="right", command_code="NAV_RIGHT"
),
LgIrButtonEntityDescription(key="ok", translation_key="ok", command_code="OK"),
LgIrButtonEntityDescription(
key="back", translation_key="back", command_code="BACK"
),
LgIrButtonEntityDescription(
key="left", translation_key="left", command_code=LGTVCode.NAV_LEFT
key="home", translation_key="home", command_code="HOME"
),
LgIrButtonEntityDescription(
key="right", translation_key="right", command_code=LGTVCode.NAV_RIGHT
key="menu", translation_key="menu", command_code="MENU"
),
LgIrButtonEntityDescription(
key="ok", translation_key="ok", command_code=LGTVCode.OK
key="input", translation_key="input", command_code="INPUT"
),
LgIrButtonEntityDescription(
key="back", translation_key="back", command_code=LGTVCode.BACK
key="num_0", translation_key="num_0", command_code="NUM_0"
),
LgIrButtonEntityDescription(
key="home", translation_key="home", command_code=LGTVCode.HOME
key="num_1", translation_key="num_1", command_code="NUM_1"
),
LgIrButtonEntityDescription(
key="menu", translation_key="menu", command_code=LGTVCode.MENU
key="num_2", translation_key="num_2", command_code="NUM_2"
),
LgIrButtonEntityDescription(
key="input", translation_key="input", command_code=LGTVCode.INPUT
key="num_3", translation_key="num_3", command_code="NUM_3"
),
LgIrButtonEntityDescription(
key="num_0", translation_key="num_0", command_code=LGTVCode.NUM_0
key="num_4", translation_key="num_4", command_code="NUM_4"
),
LgIrButtonEntityDescription(
key="num_1", translation_key="num_1", command_code=LGTVCode.NUM_1
key="num_5", translation_key="num_5", command_code="NUM_5"
),
LgIrButtonEntityDescription(
key="num_2", translation_key="num_2", command_code=LGTVCode.NUM_2
key="num_6", translation_key="num_6", command_code="NUM_6"
),
LgIrButtonEntityDescription(
key="num_3", translation_key="num_3", command_code=LGTVCode.NUM_3
key="num_7", translation_key="num_7", command_code="NUM_7"
),
LgIrButtonEntityDescription(
key="num_4", translation_key="num_4", command_code=LGTVCode.NUM_4
key="num_8", translation_key="num_8", command_code="NUM_8"
),
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
key="num_9", translation_key="num_9", command_code="NUM_9"
),
)

View File

@@ -2,7 +2,7 @@
import logging
from infrared_protocols.codes.lg.tv import LGTVCode, make_command as make_lg_tv_command
from infrared_protocols import get_codes
from homeassistant.components.infrared import async_send_command
from homeassistant.config_entries import ConfigEntry
@@ -16,6 +16,8 @@ from .const import DOMAIN
_LOGGER = logging.getLogger(__name__)
LG_TV_CODES = get_codes("lg/tv")
class LgIrEntity(Entity):
"""LG IR base entity."""
@@ -66,11 +68,12 @@ class LgIrEntity(Entity):
ir_state is not None and ir_state.state != STATE_UNAVAILABLE
)
async def _send_command(self, code: LGTVCode) -> None:
async def _send_command(self, code_name: str) -> None:
"""Send an IR command using the LG protocol."""
command = await LG_TV_CODES.load_command(code_name)
await async_send_command(
self.hass,
self._infrared_entity_id,
make_lg_tv_command(code),
command,
context=self._context,
)

View File

@@ -2,8 +2,6 @@
from __future__ import annotations
from infrared_protocols.codes.lg.tv import LGTVCode
from homeassistant.components.media_player import (
MediaPlayerDeviceClass,
MediaPlayerEntity,
@@ -57,40 +55,40 @@ class LgIrTvMediaPlayer(LgIrEntity, MediaPlayerEntity):
async def async_turn_on(self) -> None:
"""Turn on the TV."""
await self._send_command(LGTVCode.POWER)
await self._send_command("POWER")
async def async_turn_off(self) -> None:
"""Turn off the TV."""
await self._send_command(LGTVCode.POWER)
await self._send_command("POWER")
async def async_volume_up(self) -> None:
"""Send volume up command."""
await self._send_command(LGTVCode.VOLUME_UP)
await self._send_command("VOLUME_UP")
async def async_volume_down(self) -> None:
"""Send volume down command."""
await self._send_command(LGTVCode.VOLUME_DOWN)
await self._send_command("VOLUME_DOWN")
async def async_mute_volume(self, mute: bool) -> None:
"""Send mute command."""
await self._send_command(LGTVCode.MUTE)
await self._send_command("MUTE")
async def async_media_next_track(self) -> None:
"""Send channel up command."""
await self._send_command(LGTVCode.CHANNEL_UP)
await self._send_command("CHANNEL_UP")
async def async_media_previous_track(self) -> None:
"""Send channel down command."""
await self._send_command(LGTVCode.CHANNEL_DOWN)
await self._send_command("CHANNEL_DOWN")
async def async_media_play(self) -> None:
"""Send play command."""
await self._send_command(LGTVCode.PLAY)
await self._send_command("PLAY")
async def async_media_pause(self) -> None:
"""Send pause command."""
await self._send_command(LGTVCode.PAUSE)
await self._send_command("PAUSE")
async def async_media_stop(self) -> None:
"""Send stop command."""
await self._send_command(LGTVCode.STOP)
await self._send_command("STOP")

View File

@@ -41,11 +41,9 @@ class SmInfraredEntity(SmEntity, InfraredEntity):
async def async_send_command(self, command: InfraredCommand) -> None:
"""Send an IR command."""
timings = [
interval
for timing in command.get_raw_timings()
for interval in (timing.high_us, timing.low_us)
]
# smlight expects unsigned alternating mark/space µs, while the library
# emits signed values (negative = space). Drop the sign on the wire.
timings = [abs(t) for t in command.get_raw_timings()]
freq = command.modulation

2
requirements.txt generated
View File

@@ -30,7 +30,7 @@ home-assistant-bluetooth==2.0.0
home-assistant-intents==2026.3.24
httpx==0.28.1
ifaddr==0.2.0
infrared-protocols==1.3.0
infrared-protocols==2.0.0
Jinja2==3.1.6
lru-dict==1.4.1
mutagen==1.47.0

2
requirements_all.txt generated
View File

@@ -1334,7 +1334,7 @@ influxdb-client==1.50.0
influxdb==5.3.1
# homeassistant.components.infrared
infrared-protocols==1.3.0
infrared-protocols==2.0.0
# homeassistant.components.inkbird
inkbird-ble==1.1.1

View File

@@ -1186,7 +1186,7 @@ influxdb-client==1.50.0
influxdb==5.3.1
# homeassistant.components.infrared
infrared-protocols==1.3.0
infrared-protocols==2.0.0
# homeassistant.components.inkbird
inkbird-ble==1.1.1

View File

@@ -22,7 +22,7 @@ NEC_ZERO_SPACE_US = 562
def test_packet_header() -> None:
"""Test IR type byte, repeat, and length fields."""
timings = [(NEC_HEADER_MARK_US, NEC_HEADER_SPACE_US)]
timings = [NEC_HEADER_MARK_US, -NEC_HEADER_SPACE_US]
packet = timings_to_broadlink_packet(timings, repeat=0)
assert packet[0] == IR_PACKET_TYPE
@@ -34,7 +34,7 @@ def test_packet_header() -> None:
def test_packet_ends_with_silence() -> None:
"""Test packet structure is well-formed."""
timings = [(NEC_BIT_MARK_US, NEC_ZERO_SPACE_US)]
timings = [NEC_BIT_MARK_US, -NEC_ZERO_SPACE_US]
packet = timings_to_broadlink_packet(timings)
assert packet[0] == IR_PACKET_TYPE
assert len(packet) >= IR_PACKET_PAYLOAD_OFFSET # header + minimum payload
@@ -42,14 +42,14 @@ def test_packet_ends_with_silence() -> None:
def test_packet_repeat_count() -> None:
"""Test repeat count is set."""
timings = [(NEC_BIT_MARK_US, NEC_ZERO_SPACE_US)]
timings = [NEC_BIT_MARK_US, -NEC_ZERO_SPACE_US]
packet = timings_to_broadlink_packet(timings, repeat=2)
assert packet[IR_PACKET_REPEAT_INDEX] == 2
def test_packet_repeat_out_of_range() -> None:
"""Test that out-of-range repeat raises ValueError."""
timings = [(NEC_BIT_MARK_US, NEC_ZERO_SPACE_US)]
timings = [NEC_BIT_MARK_US, -NEC_ZERO_SPACE_US]
with pytest.raises(ValueError, match="repeat must be 0"):
timings_to_broadlink_packet(timings, repeat=-1)
with pytest.raises(ValueError, match="repeat must be 0"):
@@ -58,7 +58,7 @@ def test_packet_repeat_out_of_range() -> None:
def test_packet_nec_header_encoding() -> None:
"""Test that a NEC header encodes correctly."""
timings = [(NEC_HEADER_MARK_US, NEC_HEADER_SPACE_US)]
timings = [NEC_HEADER_MARK_US, -NEC_HEADER_SPACE_US]
packet = timings_to_broadlink_packet(timings)
# Skip packet header to get encoded payload
@@ -75,7 +75,7 @@ def test_packet_nec_header_encoding() -> None:
def test_packet_known_nec_command() -> None:
"""Encode a full NEC power command and verify it's well-formed."""
nec_timings: list[tuple[int, int]] = [(NEC_HEADER_MARK_US, NEC_HEADER_SPACE_US)]
nec_timings: list[int] = [NEC_HEADER_MARK_US, -NEC_HEADER_SPACE_US]
for bit in (
0,
0,
@@ -110,11 +110,9 @@ def test_packet_known_nec_command() -> None:
1,
1, # ~command
):
if bit:
nec_timings.append((NEC_BIT_MARK_US, NEC_ONE_SPACE_US))
else:
nec_timings.append((NEC_BIT_MARK_US, NEC_ZERO_SPACE_US))
nec_timings.append((NEC_BIT_MARK_US, 0)) # stop bit
nec_timings.append(NEC_BIT_MARK_US)
nec_timings.append(-NEC_ONE_SPACE_US if bit else -NEC_ZERO_SPACE_US)
nec_timings.append(NEC_BIT_MARK_US) # stop bit
packet = timings_to_broadlink_packet(nec_timings)
@@ -124,21 +122,17 @@ def test_packet_known_nec_command() -> None:
def test_broadlink_ir_command_basic() -> None:
"""Test BroadlinkIRCommand initialization and interface."""
timings = [(500, 500), (500, 1000), (NEC_BIT_MARK_US, NEC_ZERO_SPACE_US)]
timings = [500, -500, 500, -1000, NEC_BIT_MARK_US, -NEC_ZERO_SPACE_US]
cmd = BroadlinkIRCommand(timings, repeat_count=3)
assert cmd.repeat_count == 3
raw_timings = cmd.get_raw_timings()
assert len(raw_timings) == 3
assert raw_timings[0].high_us == 500
assert raw_timings[0].low_us == 500
assert raw_timings[2].high_us == NEC_BIT_MARK_US
assert raw_timings[2].low_us == NEC_ZERO_SPACE_US
assert raw_timings == timings
def test_broadlink_ir_command_default_repeat() -> None:
"""Test BroadlinkIRCommand defaults to repeat=0."""
timings = [(500, 500)]
timings = [500, -500]
cmd = BroadlinkIRCommand(timings)
assert cmd.repeat_count == 0
@@ -146,7 +140,7 @@ def test_broadlink_ir_command_default_repeat() -> None:
def test_broadlink_ir_command_invalid_repeat() -> None:
"""Test that BroadlinkIRCommand raises ValueError for out-of-range repeat_count."""
timings = [(500, 500)]
timings = [500, -500]
# Test negative repeat count
with pytest.raises(ValueError, match="repeat_count must be 0255"):

View File

@@ -3,7 +3,8 @@
from __future__ import annotations
from collections.abc import Generator
from unittest.mock import patch
from typing import Any
from unittest.mock import AsyncMock, patch
from infrared_protocols import Command as InfraredCommand
import pytest
@@ -38,7 +39,7 @@ class MockInfraredEntity(InfraredEntity):
def __init__(self, unique_id: str) -> None:
"""Initialize mock entity."""
self._attr_unique_id = unique_id
self.send_command_calls: list[InfraredCommand] = []
self.send_command_calls: list[Any] = []
async def async_send_command(self, command: InfraredCommand) -> None:
"""Mock send command."""
@@ -73,15 +74,18 @@ def platforms() -> list[Platform]:
@pytest.fixture
def mock_make_lg_tv_command() -> Generator[None]:
"""Patch make_command to return the LGTVCode directly.
def mock_lg_tv_load_command() -> Generator[None]:
"""Replace LG_TV_CODES with a stub whose load_command returns the name.
This allows tests to assert on the high-level code enum value
rather than the raw NEC timings.
This lets tests assert on the high-level command name rather than the
resolved NEC command object.
"""
stub_codes = AsyncMock()
stub_codes.load_command.side_effect = lambda name: name
with patch(
"homeassistant.components.lg_infrared.entity.make_lg_tv_command",
side_effect=lambda code, **kwargs: code,
"homeassistant.components.lg_infrared.entity.LG_TV_CODES",
stub_codes,
):
yield
@@ -91,7 +95,7 @@ async def init_integration(
hass: HomeAssistant,
mock_config_entry: MockConfigEntry,
mock_infrared_entity: MockInfraredEntity,
mock_make_lg_tv_command: None,
mock_lg_tv_load_command: None,
platforms: list[Platform],
) -> MockConfigEntry:
"""Set up the LG Infrared integration for testing."""

View File

@@ -2,7 +2,6 @@
from __future__ import annotations
from infrared_protocols.codes.lg.tv import LGTVCode
import pytest
from syrupy.assertion import SnapshotAssertion
@@ -49,34 +48,34 @@ async def test_entities(
@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),
("button.lg_tv_power_on", "POWER_ON"),
("button.lg_tv_power_off", "POWER_OFF"),
("button.lg_tv_hdmi_1", "HDMI_1"),
("button.lg_tv_hdmi_2", "HDMI_2"),
("button.lg_tv_hdmi_3", "HDMI_3"),
("button.lg_tv_hdmi_4", "HDMI_4"),
("button.lg_tv_exit", "EXIT"),
("button.lg_tv_info", "INFO"),
("button.lg_tv_guide", "GUIDE"),
("button.lg_tv_up", "NAV_UP"),
("button.lg_tv_down", "NAV_DOWN"),
("button.lg_tv_left", "NAV_LEFT"),
("button.lg_tv_right", "NAV_RIGHT"),
("button.lg_tv_ok", "OK"),
("button.lg_tv_back", "BACK"),
("button.lg_tv_home", "HOME"),
("button.lg_tv_menu", "MENU"),
("button.lg_tv_input", "INPUT"),
("button.lg_tv_number_0", "NUM_0"),
("button.lg_tv_number_1", "NUM_1"),
("button.lg_tv_number_2", "NUM_2"),
("button.lg_tv_number_3", "NUM_3"),
("button.lg_tv_number_4", "NUM_4"),
("button.lg_tv_number_5", "NUM_5"),
("button.lg_tv_number_6", "NUM_6"),
("button.lg_tv_number_7", "NUM_7"),
("button.lg_tv_number_8", "NUM_8"),
("button.lg_tv_number_9", "NUM_9"),
],
)
@pytest.mark.usefixtures("init_integration")
@@ -84,7 +83,7 @@ async def test_button_press_sends_correct_code(
hass: HomeAssistant,
mock_infrared_entity: MockInfraredEntity,
entity_id: str,
expected_code: LGTVCode,
expected_code: str,
) -> None:
"""Test pressing a button sends the correct IR code."""
await hass.services.async_call(

View File

@@ -2,7 +2,6 @@
from __future__ import annotations
from infrared_protocols.codes.lg.tv import LGTVCode
import pytest
from syrupy.assertion import SnapshotAssertion
@@ -63,16 +62,16 @@ async def test_entities(
@pytest.mark.parametrize(
("service", "service_data", "expected_code"),
[
(SERVICE_TURN_ON, {}, LGTVCode.POWER),
(SERVICE_TURN_OFF, {}, LGTVCode.POWER),
(SERVICE_VOLUME_UP, {}, LGTVCode.VOLUME_UP),
(SERVICE_VOLUME_DOWN, {}, LGTVCode.VOLUME_DOWN),
(SERVICE_VOLUME_MUTE, {"is_volume_muted": True}, LGTVCode.MUTE),
(SERVICE_MEDIA_NEXT_TRACK, {}, LGTVCode.CHANNEL_UP),
(SERVICE_MEDIA_PREVIOUS_TRACK, {}, LGTVCode.CHANNEL_DOWN),
(SERVICE_MEDIA_PLAY, {}, LGTVCode.PLAY),
(SERVICE_MEDIA_PAUSE, {}, LGTVCode.PAUSE),
(SERVICE_MEDIA_STOP, {}, LGTVCode.STOP),
(SERVICE_TURN_ON, {}, "POWER"),
(SERVICE_TURN_OFF, {}, "POWER"),
(SERVICE_VOLUME_UP, {}, "VOLUME_UP"),
(SERVICE_VOLUME_DOWN, {}, "VOLUME_DOWN"),
(SERVICE_VOLUME_MUTE, {"is_volume_muted": True}, "MUTE"),
(SERVICE_MEDIA_NEXT_TRACK, {}, "CHANNEL_UP"),
(SERVICE_MEDIA_PREVIOUS_TRACK, {}, "CHANNEL_DOWN"),
(SERVICE_MEDIA_PLAY, {}, "PLAY"),
(SERVICE_MEDIA_PAUSE, {}, "PAUSE"),
(SERVICE_MEDIA_STOP, {}, "STOP"),
],
)
@pytest.mark.usefixtures("init_integration")
@@ -81,7 +80,7 @@ async def test_media_player_action_sends_correct_code(
mock_infrared_entity: MockInfraredEntity,
service: str,
service_data: dict[str, bool],
expected_code: LGTVCode,
expected_code: str,
) -> None:
"""Test each media player action sends the correct IR code."""
await hass.services.async_call(

View File

@@ -2,7 +2,7 @@
from unittest.mock import MagicMock
from infrared_protocols import Command, Timing
from infrared_protocols import Command
from pysmlight.exceptions import SmlightError
from pysmlight.models import IRPayload
import pytest
@@ -24,9 +24,9 @@ class MockCommand(Command):
"""Initialize with fixed 38kHz modulation."""
super().__init__(modulation=38000)
def get_raw_timings(self) -> list[Timing]:
"""Return some fake timings."""
return [Timing(high_us=9000, low_us=4500), Timing(high_us=560, low_us=1690)]
def get_raw_timings(self) -> list[int]:
"""Return some fake timings (signed: +mark / -space µs)."""
return [9000, -4500, 560, -1690]
@pytest.fixture