mirror of
https://github.com/home-assistant/core.git
synced 2026-05-11 14:09:44 +00:00
Compare commits
2 Commits
dev
...
infrared-d
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f39620a0fa | ||
|
|
9f2eb56a7e |
@@ -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 0–255 (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 0–255, 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 0–255, 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)
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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"]
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
)
|
||||
|
||||
@@ -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"
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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
2
requirements.txt
generated
@@ -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
2
requirements_all.txt
generated
@@ -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
|
||||
|
||||
2
requirements_test_all.txt
generated
2
requirements_test_all.txt
generated
@@ -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
|
||||
|
||||
@@ -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 0–255"):
|
||||
|
||||
@@ -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."""
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user