infrared.py 4.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134
  1. """
  2. Implementation of Tuya infrared control devices
  3. """
  4. import asyncio
  5. import json
  6. import logging
  7. from typing import override
  8. from homeassistant.components.infrared import InfraredCommand, InfraredEntity
  9. from tinytuya.Contrib.IRRemoteControlDevice import IRRemoteControlDevice as IR
  10. from .device import TuyaLocalDevice
  11. from .entity import TuyaLocalEntity
  12. from .helpers.config import async_tuya_setup_platform
  13. from .helpers.device_config import TuyaEntityConfig
  14. _LOGGER = logging.getLogger(__name__)
  15. async def async_setup_entry(hass, entry, async_add_entities):
  16. """Set up the Tuya Local infrared control platform."""
  17. config = {**entry.data, **entry.options}
  18. await async_tuya_setup_platform(
  19. hass,
  20. async_add_entities,
  21. config,
  22. "infrared",
  23. TuyaLocalInfrared,
  24. )
  25. class TuyaLocalInfrared(TuyaLocalEntity, InfraredEntity):
  26. """Representation of a Tuya Local infrared control device."""
  27. def __init__(self, device: TuyaLocalDevice, config: TuyaEntityConfig):
  28. """Initialize the infrared control device."""
  29. super().__init__()
  30. dps_map = self._init_begin(device, config)
  31. self._send_dp = dps_map.pop("send", None)
  32. self._command_dp = dps_map.pop("control", None)
  33. self._type_dp = dps_map.pop("code_type", None)
  34. self._init_end(dps_map)
  35. async def async_send_command(self, command: InfraredCommand) -> None:
  36. """Handle sending an infrared command."""
  37. if isinstance(command, TuyaRemoteCommand):
  38. # If it's already a TuyaRemoteCommand, skip the round-trip conversion
  39. tuya_command = command.code
  40. _LOGGER.info("%s sending command: %s", self._config.config_id, tuya_command)
  41. await self._ir_send(tuya_command)
  42. return
  43. timings = command.get_raw_timings()
  44. split = {}
  45. raw = []
  46. i = 0
  47. for timing in timings:
  48. utiming = abs(timing)
  49. if utiming > 50000:
  50. split[i] = utiming - 5000
  51. raw.append(5000)
  52. else:
  53. raw.append(utiming)
  54. i += 1
  55. # HA format leaves off the last low timing, but Tuya needs it
  56. if len(raw) % 2 == 1:
  57. raw.append(5000)
  58. start = 0
  59. for s, t in split.items():
  60. tuya_command = IR.pulses_to_base64(raw[start:s])
  61. _LOGGER.info("%s sending command: %s", self._config.config_id, tuya_command)
  62. start = s
  63. await self._ir_send(tuya_command)
  64. await asyncio.sleep(t / 1000000.0)
  65. if start < len(raw):
  66. tuya_command = IR.pulses_to_base64(raw[start:])
  67. _LOGGER.info("%s sending command: %s", self._config.config_id, tuya_command)
  68. await self._ir_send(tuya_command)
  69. async def _ir_send(self, tuya_command: str):
  70. """Send the infrared command to the device."""
  71. if self._send_dp:
  72. if self._command_dp:
  73. await self._device.async_set_properties(
  74. self._package_multi_dp_send(tuya_command)
  75. )
  76. else:
  77. await self._send_dp.async_set_value(
  78. self._device,
  79. self._package_single_dp_send(tuya_command),
  80. )
  81. def _package_single_dp_send(self, command: str) -> str:
  82. """Package the command for a single DP (usually dp id 201) send."""
  83. json_command = {
  84. "control": "send_ir",
  85. "type": 0,
  86. "head": "",
  87. "key1": "1" + command,
  88. }
  89. return json.dumps(json_command)
  90. def _package_multi_dp_send(self, command: str) -> dict:
  91. """Package the command for a multi DP send"""
  92. return {
  93. f"{self._command_dp.id}": "send_ir",
  94. f"{self._type_dp.id}": 0,
  95. f"{self._send_dp.id}": command,
  96. }
  97. # Used to send legacy remote learned commands via infrared entities.
  98. # This allows the learned commands to be sent via infrared entities
  99. # provided by other integrations.
  100. class TuyaRemoteCommand(InfraredCommand):
  101. """Representation of a command in Tuya uncompressed format."""
  102. def __init__(self, *, code: str):
  103. super().__init__(modulation=38000, repeat_count=0)
  104. self.code = code
  105. @override
  106. def get_raw_timings(self) -> list[int]:
  107. """Get the raw timings for the command."""
  108. # The code is expected to be an uncompressed Tuya IR code
  109. try:
  110. pulses = IR.base64_to_pulses(self.code)
  111. even = True
  112. return [p * -1 if (even := not even) else p for p in pulses]
  113. except ValueError as err:
  114. raise ValueError(f"Invalid code format: {repr(self.code)}") from err