Home Assistant Unofficial Reference 2024.12.1
remote.py
Go to the documentation of this file.
1 """Support for the Xiaomi IR Remote (Chuangmi IR)."""
2 
3 from __future__ import annotations
4 
5 import asyncio
6 from datetime import timedelta
7 import logging
8 import time
9 from typing import Any
10 
11 from miio import ChuangmiIr, DeviceException
12 import voluptuous as vol
13 
14 from homeassistant.components import persistent_notification
16  ATTR_DELAY_SECS,
17  ATTR_NUM_REPEATS,
18  DEFAULT_DELAY_SECS,
19  PLATFORM_SCHEMA as REMOTE_PLATFORM_SCHEMA,
20  RemoteEntity,
21 )
22 from homeassistant.const import (
23  CONF_COMMAND,
24  CONF_HOST,
25  CONF_NAME,
26  CONF_TIMEOUT,
27  CONF_TOKEN,
28 )
29 from homeassistant.core import HomeAssistant
30 from homeassistant.exceptions import PlatformNotReady
31 from homeassistant.helpers import config_validation as cv, entity_platform
32 from homeassistant.helpers.entity_platform import AddEntitiesCallback
33 from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
34 from homeassistant.util.dt import utcnow
35 
36 from .const import SERVICE_LEARN, SERVICE_SET_REMOTE_LED_OFF, SERVICE_SET_REMOTE_LED_ON
37 
38 _LOGGER = logging.getLogger(__name__)
39 
40 DATA_KEY = "remote.xiaomi_miio"
41 
42 CONF_SLOT = "slot"
43 CONF_COMMANDS = "commands"
44 
45 DEFAULT_TIMEOUT = 10
46 DEFAULT_SLOT = 1
47 
48 COMMAND_SCHEMA = vol.Schema(
49  {vol.Required(CONF_COMMAND): vol.All(cv.ensure_list, [cv.string])}
50 )
51 
52 PLATFORM_SCHEMA = REMOTE_PLATFORM_SCHEMA.extend(
53  {
54  vol.Optional(CONF_NAME): cv.string,
55  vol.Required(CONF_HOST): cv.string,
56  vol.Optional(CONF_TIMEOUT, default=DEFAULT_TIMEOUT): cv.positive_int,
57  vol.Optional(CONF_SLOT, default=DEFAULT_SLOT): vol.All(
58  int, vol.Range(min=1, max=1000000)
59  ),
60  vol.Required(CONF_TOKEN): vol.All(str, vol.Length(min=32, max=32)),
61  vol.Optional(CONF_COMMANDS, default={}): cv.schema_with_slug_keys(
62  COMMAND_SCHEMA
63  ),
64  },
65  extra=vol.ALLOW_EXTRA,
66 )
67 
68 
70  hass: HomeAssistant,
71  config: ConfigType,
72  async_add_entities: AddEntitiesCallback,
73  discovery_info: DiscoveryInfoType | None = None,
74 ) -> None:
75  """Set up the Xiaomi IR Remote (Chuangmi IR) platform."""
76  host = config[CONF_HOST]
77  token = config[CONF_TOKEN]
78 
79  # Create handler
80  _LOGGER.debug("Initializing with host %s (token %s...)", host, token[:5])
81 
82  # The Chuang Mi IR Remote Controller wants to be re-discovered every
83  # 5 minutes. As long as polling is disabled the device should be
84  # re-discovered (lazy_discover=False) in front of every command.
85  device = ChuangmiIr(host, token, lazy_discover=False)
86 
87  # Check that we can communicate with device.
88  try:
89  device_info = await hass.async_add_executor_job(device.info)
90  model = device_info.model
91  unique_id = f"{model}-{device_info.mac_address}"
92  _LOGGER.debug(
93  "%s %s %s detected",
94  model,
95  device_info.firmware_version,
96  device_info.hardware_version,
97  )
98  except DeviceException as ex:
99  _LOGGER.error("Device unavailable or token incorrect: %s", ex)
100  raise PlatformNotReady from ex
101 
102  if DATA_KEY not in hass.data:
103  hass.data[DATA_KEY] = {}
104 
105  friendly_name = config.get(CONF_NAME, f"xiaomi_miio_{host.replace('.', '_')}")
106  slot = config.get(CONF_SLOT)
107  timeout = config.get(CONF_TIMEOUT)
108 
109  xiaomi_miio_remote = XiaomiMiioRemote(
110  friendly_name, device, unique_id, slot, timeout, config.get(CONF_COMMANDS)
111  )
112 
113  hass.data[DATA_KEY][host] = xiaomi_miio_remote
114 
115  async_add_entities([xiaomi_miio_remote])
116 
117  async def async_service_led_off_handler(entity, service):
118  """Handle set_led_off command."""
119  await hass.async_add_executor_job(entity.device.set_indicator_led, False)
120 
121  async def async_service_led_on_handler(entity, service):
122  """Handle set_led_on command."""
123  await hass.async_add_executor_job(entity.device.set_indicator_led, True)
124 
125  async def async_service_learn_handler(entity, service):
126  """Handle a learn command."""
127  device = entity.device
128 
129  slot = service.data.get(CONF_SLOT, entity.slot)
130 
131  await hass.async_add_executor_job(device.learn, slot)
132 
133  timeout = service.data.get(CONF_TIMEOUT, entity.timeout)
134 
135  _LOGGER.info("Press the key you want Home Assistant to learn")
136  start_time = utcnow()
137  while (utcnow() - start_time) < timedelta(seconds=timeout):
138  message = await hass.async_add_executor_job(device.read, slot)
139  _LOGGER.debug("Message received from device: '%s'", message)
140 
141  if code := message.get("code"):
142  log_msg = f"Received command is: {code}"
143  _LOGGER.info(log_msg)
144  persistent_notification.async_create(
145  hass, log_msg, title="Xiaomi Miio Remote"
146  )
147  return
148 
149  if "error" in message and message["error"]["message"] == "learn timeout":
150  await hass.async_add_executor_job(device.learn, slot)
151 
152  await asyncio.sleep(1)
153 
154  _LOGGER.error("Timeout. No infrared command captured")
155  persistent_notification.async_create(
156  hass, "Timeout. No infrared command captured", title="Xiaomi Miio Remote"
157  )
158 
159  platform = entity_platform.async_get_current_platform()
160 
161  platform.async_register_entity_service(
162  SERVICE_LEARN,
163  {
164  vol.Optional(CONF_TIMEOUT, default=10): cv.positive_int,
165  vol.Optional(CONF_SLOT, default=1): vol.All(
166  int, vol.Range(min=1, max=1000000)
167  ),
168  },
169  async_service_learn_handler,
170  )
171  platform.async_register_entity_service(
172  SERVICE_SET_REMOTE_LED_ON,
173  None,
174  async_service_led_on_handler,
175  )
176  platform.async_register_entity_service(
177  SERVICE_SET_REMOTE_LED_OFF,
178  None,
179  async_service_led_off_handler,
180  )
181 
182 
184  """Representation of a Xiaomi Miio Remote device."""
185 
186  _attr_should_poll = False
187 
188  def __init__(self, friendly_name, device, unique_id, slot, timeout, commands):
189  """Initialize the remote."""
190  self._name_name = friendly_name
191  self._device_device = device
192  self._unique_id_unique_id = unique_id
193  self._slot_slot = slot
194  self._timeout_timeout = timeout
195  self._state_state = False
196  self._commands_commands = commands
197 
198  @property
199  def unique_id(self):
200  """Return an unique ID."""
201  return self._unique_id_unique_id
202 
203  @property
204  def name(self):
205  """Return the name of the remote."""
206  return self._name_name
207 
208  @property
209  def device(self):
210  """Return the remote object."""
211  return self._device_device
212 
213  @property
214  def slot(self):
215  """Return the slot to save learned command."""
216  return self._slot_slot
217 
218  @property
219  def timeout(self):
220  """Return the timeout for learning command."""
221  return self._timeout_timeout
222 
223  @property
224  def is_on(self):
225  """Return False if device is unreachable, else True."""
226  try:
227  self.devicedevice.info()
228  except DeviceException:
229  return False
230  return True
231 
232  async def async_turn_on(self, **kwargs: Any) -> None:
233  """Turn the device on."""
234  _LOGGER.error(
235  "Device does not support turn_on, "
236  "please use 'remote.send_command' to send commands"
237  )
238 
239  async def async_turn_off(self, **kwargs: Any) -> None:
240  """Turn the device off."""
241  _LOGGER.error(
242  "Device does not support turn_off, "
243  "please use 'remote.send_command' to send commands"
244  )
245 
246  def _send_command(self, payload):
247  """Send a command."""
248  _LOGGER.debug("Sending payload: '%s'", payload)
249  try:
250  self.devicedevice.play(payload)
251  except DeviceException as ex:
252  _LOGGER.error(
253  "Transmit of IR command failed, %s, exception: %s", payload, ex
254  )
255 
256  def send_command(self, command, **kwargs):
257  """Send a command."""
258  num_repeats = kwargs.get(ATTR_NUM_REPEATS)
259 
260  delay = kwargs.get(ATTR_DELAY_SECS, DEFAULT_DELAY_SECS)
261 
262  for _ in range(num_repeats):
263  for payload in command:
264  if payload in self._commands_commands:
265  for local_payload in self._commands_commands[payload][CONF_COMMAND]:
266  self._send_command_send_command(local_payload)
267  else:
268  self._send_command_send_command(payload)
269  time.sleep(delay)
def __init__(self, friendly_name, device, unique_id, slot, timeout, commands)
Definition: remote.py:188
None async_setup_platform(HomeAssistant hass, ConfigType config, AddEntitiesCallback async_add_entities, DiscoveryInfoType|None discovery_info=None)
Definition: remote.py:74