2019-02-13 20:21:14 +00:00
|
|
|
"""Support for the Xiaomi IR Remote (Chuangmi IR)."""
|
2018-02-06 18:47:24 +00:00
|
|
|
import asyncio
|
2019-11-08 17:32:44 +00:00
|
|
|
from datetime import timedelta
|
2018-02-06 18:47:24 +00:00
|
|
|
import logging
|
|
|
|
import time
|
|
|
|
|
2019-11-08 17:32:44 +00:00
|
|
|
from miio import ChuangmiIr, DeviceException # pylint: disable=import-error
|
2018-02-06 18:47:24 +00:00
|
|
|
import voluptuous as vol
|
|
|
|
|
|
|
|
from homeassistant.components.remote import (
|
2019-07-31 19:25:30 +00:00
|
|
|
ATTR_DELAY_SECS,
|
2019-11-08 17:32:44 +00:00
|
|
|
ATTR_NUM_REPEATS,
|
2019-07-31 19:25:30 +00:00
|
|
|
DEFAULT_DELAY_SECS,
|
2019-11-08 17:32:44 +00:00
|
|
|
DOMAIN,
|
|
|
|
PLATFORM_SCHEMA,
|
2019-07-31 19:25:30 +00:00
|
|
|
RemoteDevice,
|
|
|
|
)
|
2018-02-06 18:47:24 +00:00
|
|
|
from homeassistant.const import (
|
2019-07-31 19:25:30 +00:00
|
|
|
ATTR_ENTITY_ID,
|
|
|
|
ATTR_HIDDEN,
|
|
|
|
CONF_COMMAND,
|
2019-11-08 17:32:44 +00:00
|
|
|
CONF_HOST,
|
|
|
|
CONF_NAME,
|
|
|
|
CONF_TIMEOUT,
|
|
|
|
CONF_TOKEN,
|
2019-07-31 19:25:30 +00:00
|
|
|
)
|
2018-03-16 20:15:23 +00:00
|
|
|
from homeassistant.exceptions import PlatformNotReady
|
2018-02-06 18:47:24 +00:00
|
|
|
import homeassistant.helpers.config_validation as cv
|
|
|
|
from homeassistant.util.dt import utcnow
|
|
|
|
|
|
|
|
_LOGGER = logging.getLogger(__name__)
|
|
|
|
|
2019-07-31 19:25:30 +00:00
|
|
|
SERVICE_LEARN = "xiaomi_miio_learn_command"
|
|
|
|
DATA_KEY = "remote.xiaomi_miio"
|
2018-02-06 18:47:24 +00:00
|
|
|
|
2019-07-31 19:25:30 +00:00
|
|
|
CONF_SLOT = "slot"
|
|
|
|
CONF_COMMANDS = "commands"
|
2018-02-06 18:47:24 +00:00
|
|
|
|
|
|
|
DEFAULT_TIMEOUT = 10
|
|
|
|
DEFAULT_SLOT = 1
|
|
|
|
|
2019-07-31 19:25:30 +00:00
|
|
|
LEARN_COMMAND_SCHEMA = vol.Schema(
|
|
|
|
{
|
|
|
|
vol.Required(ATTR_ENTITY_ID): vol.All(str),
|
|
|
|
vol.Optional(CONF_TIMEOUT, default=10): vol.All(int, vol.Range(min=0)),
|
|
|
|
vol.Optional(CONF_SLOT, default=1): vol.All(int, vol.Range(min=1, max=1000000)),
|
|
|
|
}
|
|
|
|
)
|
|
|
|
|
|
|
|
COMMAND_SCHEMA = vol.Schema(
|
|
|
|
{vol.Required(CONF_COMMAND): vol.All(cv.ensure_list, [cv.string])}
|
|
|
|
)
|
|
|
|
|
|
|
|
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
|
|
|
|
{
|
|
|
|
vol.Optional(CONF_NAME): cv.string,
|
|
|
|
vol.Required(CONF_HOST): cv.string,
|
|
|
|
vol.Optional(CONF_TIMEOUT, default=DEFAULT_TIMEOUT): vol.All(
|
|
|
|
int, vol.Range(min=0)
|
|
|
|
),
|
|
|
|
vol.Optional(CONF_SLOT, default=DEFAULT_SLOT): vol.All(
|
|
|
|
int, vol.Range(min=1, max=1000000)
|
|
|
|
),
|
|
|
|
vol.Optional(ATTR_HIDDEN, default=True): cv.boolean,
|
|
|
|
vol.Required(CONF_TOKEN): vol.All(str, vol.Length(min=32, max=32)),
|
|
|
|
vol.Optional(CONF_COMMANDS, default={}): cv.schema_with_slug_keys(
|
|
|
|
COMMAND_SCHEMA
|
|
|
|
),
|
|
|
|
},
|
|
|
|
extra=vol.ALLOW_EXTRA,
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
async def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
|
2018-02-06 18:47:24 +00:00
|
|
|
"""Set up the Xiaomi IR Remote (Chuangmi IR) platform."""
|
2019-10-29 00:45:22 +00:00
|
|
|
host = config[CONF_HOST]
|
|
|
|
token = config[CONF_TOKEN]
|
2018-02-06 18:47:24 +00:00
|
|
|
|
|
|
|
# Create handler
|
|
|
|
_LOGGER.info("Initializing with host %s (token %s...)", host, token[:5])
|
2018-03-05 06:06:00 +00:00
|
|
|
|
|
|
|
# The Chuang Mi IR Remote Controller wants to be re-discovered every
|
|
|
|
# 5 minutes. As long as polling is disabled the device should be
|
|
|
|
# re-discovered (lazy_discover=False) in front of every command.
|
|
|
|
device = ChuangmiIr(host, token, lazy_discover=False)
|
2018-02-06 18:47:24 +00:00
|
|
|
|
|
|
|
# Check that we can communicate with device.
|
|
|
|
try:
|
2019-10-29 00:44:26 +00:00
|
|
|
device_info = await hass.async_add_executor_job(device.info)
|
2018-03-16 20:15:23 +00:00
|
|
|
model = device_info.model
|
2019-09-03 19:15:31 +00:00
|
|
|
unique_id = f"{model}-{device_info.mac_address}"
|
2019-07-31 19:25:30 +00:00
|
|
|
_LOGGER.info(
|
|
|
|
"%s %s %s detected",
|
|
|
|
model,
|
|
|
|
device_info.firmware_version,
|
|
|
|
device_info.hardware_version,
|
|
|
|
)
|
2018-02-06 18:47:24 +00:00
|
|
|
except DeviceException as ex:
|
2018-03-16 20:15:23 +00:00
|
|
|
_LOGGER.error("Device unavailable or token incorrect: %s", ex)
|
|
|
|
raise PlatformNotReady
|
2018-02-06 18:47:24 +00:00
|
|
|
|
2018-03-05 06:06:00 +00:00
|
|
|
if DATA_KEY not in hass.data:
|
|
|
|
hass.data[DATA_KEY] = {}
|
2018-02-06 18:47:24 +00:00
|
|
|
|
2019-07-31 19:25:30 +00:00
|
|
|
friendly_name = config.get(CONF_NAME, "xiaomi_miio_" + host.replace(".", "_"))
|
2018-02-06 18:47:24 +00:00
|
|
|
slot = config.get(CONF_SLOT)
|
|
|
|
timeout = config.get(CONF_TIMEOUT)
|
|
|
|
|
|
|
|
hidden = config.get(ATTR_HIDDEN)
|
|
|
|
|
2019-07-31 19:25:30 +00:00
|
|
|
xiaomi_miio_remote = XiaomiMiioRemote(
|
|
|
|
friendly_name,
|
|
|
|
device,
|
|
|
|
unique_id,
|
|
|
|
slot,
|
|
|
|
timeout,
|
|
|
|
hidden,
|
|
|
|
config.get(CONF_COMMANDS),
|
|
|
|
)
|
2018-02-06 18:47:24 +00:00
|
|
|
|
2018-03-05 06:06:00 +00:00
|
|
|
hass.data[DATA_KEY][host] = xiaomi_miio_remote
|
2018-02-06 18:47:24 +00:00
|
|
|
|
2018-08-24 14:37:30 +00:00
|
|
|
async_add_entities([xiaomi_miio_remote])
|
2018-02-06 18:47:24 +00:00
|
|
|
|
2018-10-01 06:56:50 +00:00
|
|
|
async def async_service_handler(service):
|
2018-02-06 18:47:24 +00:00
|
|
|
"""Handle a learn command."""
|
|
|
|
if service.service != SERVICE_LEARN:
|
|
|
|
_LOGGER.error("We should not handle service: %s", service.service)
|
|
|
|
return
|
|
|
|
|
|
|
|
entity_id = service.data.get(ATTR_ENTITY_ID)
|
|
|
|
entity = None
|
2018-03-05 06:06:00 +00:00
|
|
|
for remote in hass.data[DATA_KEY].values():
|
2018-02-06 18:47:24 +00:00
|
|
|
if remote.entity_id == entity_id:
|
|
|
|
entity = remote
|
|
|
|
|
|
|
|
if not entity:
|
|
|
|
_LOGGER.error("entity_id: '%s' not found", entity_id)
|
|
|
|
return
|
|
|
|
|
|
|
|
device = entity.device
|
|
|
|
|
|
|
|
slot = service.data.get(CONF_SLOT, entity.slot)
|
|
|
|
|
2018-11-07 08:03:35 +00:00
|
|
|
await hass.async_add_executor_job(device.learn, slot)
|
2018-02-06 18:47:24 +00:00
|
|
|
|
|
|
|
timeout = service.data.get(CONF_TIMEOUT, entity.timeout)
|
|
|
|
|
|
|
|
_LOGGER.info("Press the key you want Home Assistant to learn")
|
|
|
|
start_time = utcnow()
|
|
|
|
while (utcnow() - start_time) < timedelta(seconds=timeout):
|
2019-07-31 19:25:30 +00:00
|
|
|
message = await hass.async_add_executor_job(device.read, slot)
|
2018-04-04 21:30:02 +00:00
|
|
|
_LOGGER.debug("Message received from device: '%s'", message)
|
2018-02-06 18:47:24 +00:00
|
|
|
|
2019-07-31 19:25:30 +00:00
|
|
|
if "code" in message and message["code"]:
|
|
|
|
log_msg = "Received command is: {}".format(message["code"])
|
2018-02-06 18:47:24 +00:00
|
|
|
_LOGGER.info(log_msg)
|
|
|
|
hass.components.persistent_notification.async_create(
|
2019-07-31 19:25:30 +00:00
|
|
|
log_msg, title="Xiaomi Miio Remote"
|
|
|
|
)
|
2018-02-06 18:47:24 +00:00
|
|
|
return
|
|
|
|
|
2019-07-31 19:25:30 +00:00
|
|
|
if "error" in message and message["error"]["message"] == "learn timeout":
|
2018-11-07 08:03:35 +00:00
|
|
|
await hass.async_add_executor_job(device.learn, slot)
|
2018-02-06 18:47:24 +00:00
|
|
|
|
2019-05-23 04:09:59 +00:00
|
|
|
await asyncio.sleep(1)
|
2018-02-06 18:47:24 +00:00
|
|
|
|
|
|
|
_LOGGER.error("Timeout. No infrared command captured")
|
|
|
|
hass.components.persistent_notification.async_create(
|
2019-07-31 19:25:30 +00:00
|
|
|
"Timeout. No infrared command captured", title="Xiaomi Miio Remote"
|
|
|
|
)
|
2018-02-06 18:47:24 +00:00
|
|
|
|
2019-07-31 19:25:30 +00:00
|
|
|
hass.services.async_register(
|
|
|
|
DOMAIN, SERVICE_LEARN, async_service_handler, schema=LEARN_COMMAND_SCHEMA
|
|
|
|
)
|
2018-02-06 18:47:24 +00:00
|
|
|
|
|
|
|
|
|
|
|
class XiaomiMiioRemote(RemoteDevice):
|
|
|
|
"""Representation of a Xiaomi Miio Remote device."""
|
|
|
|
|
2019-07-31 19:25:30 +00:00
|
|
|
def __init__(
|
|
|
|
self, friendly_name, device, unique_id, slot, timeout, hidden, commands
|
|
|
|
):
|
2018-02-06 18:47:24 +00:00
|
|
|
"""Initialize the remote."""
|
|
|
|
self._name = friendly_name
|
|
|
|
self._device = device
|
2018-03-16 20:15:23 +00:00
|
|
|
self._unique_id = unique_id
|
2018-02-06 18:47:24 +00:00
|
|
|
self._is_hidden = hidden
|
|
|
|
self._slot = slot
|
|
|
|
self._timeout = timeout
|
|
|
|
self._state = False
|
|
|
|
self._commands = commands
|
|
|
|
|
2018-03-16 20:15:23 +00:00
|
|
|
@property
|
|
|
|
def unique_id(self):
|
|
|
|
"""Return an unique ID."""
|
|
|
|
return self._unique_id
|
|
|
|
|
2018-02-06 18:47:24 +00:00
|
|
|
@property
|
|
|
|
def name(self):
|
|
|
|
"""Return the name of the remote."""
|
|
|
|
return self._name
|
|
|
|
|
2018-08-26 19:25:39 +00:00
|
|
|
@property
|
|
|
|
def device(self):
|
|
|
|
"""Return the remote object."""
|
|
|
|
return self._device
|
|
|
|
|
2018-02-06 18:47:24 +00:00
|
|
|
@property
|
|
|
|
def hidden(self):
|
|
|
|
"""Return if we should hide entity."""
|
|
|
|
return self._is_hidden
|
|
|
|
|
|
|
|
@property
|
|
|
|
def slot(self):
|
|
|
|
"""Return the slot to save learned command."""
|
|
|
|
return self._slot
|
|
|
|
|
|
|
|
@property
|
|
|
|
def timeout(self):
|
|
|
|
"""Return the timeout for learning command."""
|
|
|
|
return self._timeout
|
|
|
|
|
|
|
|
@property
|
|
|
|
def is_on(self):
|
|
|
|
"""Return False if device is unreachable, else True."""
|
|
|
|
try:
|
2018-08-26 19:25:39 +00:00
|
|
|
self.device.info()
|
2018-02-06 18:47:24 +00:00
|
|
|
return True
|
|
|
|
except DeviceException:
|
|
|
|
return False
|
|
|
|
|
|
|
|
@property
|
|
|
|
def should_poll(self):
|
|
|
|
"""We should not be polled for device up state."""
|
|
|
|
return False
|
|
|
|
|
|
|
|
@property
|
|
|
|
def device_state_attributes(self):
|
|
|
|
"""Hide remote by default."""
|
|
|
|
if self._is_hidden:
|
2019-07-31 19:25:30 +00:00
|
|
|
return {"hidden": "true"}
|
2018-02-11 17:20:28 +00:00
|
|
|
return
|
2018-02-06 18:47:24 +00:00
|
|
|
|
2018-10-01 06:56:50 +00:00
|
|
|
async def async_turn_on(self, **kwargs):
|
2018-02-06 18:47:24 +00:00
|
|
|
"""Turn the device on."""
|
2019-07-31 19:25:30 +00:00
|
|
|
_LOGGER.error(
|
|
|
|
"Device does not support turn_on, "
|
|
|
|
"please use 'remote.send_command' to send commands."
|
|
|
|
)
|
2018-02-06 18:47:24 +00:00
|
|
|
|
2018-10-01 06:56:50 +00:00
|
|
|
async def async_turn_off(self, **kwargs):
|
2018-02-06 18:47:24 +00:00
|
|
|
"""Turn the device off."""
|
2019-07-31 19:25:30 +00:00
|
|
|
_LOGGER.error(
|
|
|
|
"Device does not support turn_off, "
|
|
|
|
"please use 'remote.send_command' to send commands."
|
|
|
|
)
|
2018-02-06 18:47:24 +00:00
|
|
|
|
|
|
|
def _send_command(self, payload):
|
|
|
|
"""Send a command."""
|
|
|
|
_LOGGER.debug("Sending payload: '%s'", payload)
|
|
|
|
try:
|
2018-08-26 19:25:39 +00:00
|
|
|
self.device.play(payload)
|
2018-02-06 18:47:24 +00:00
|
|
|
except DeviceException as ex:
|
|
|
|
_LOGGER.error(
|
2019-07-31 19:25:30 +00:00
|
|
|
"Transmit of IR command failed, %s, exception: %s", payload, ex
|
|
|
|
)
|
2018-02-06 18:47:24 +00:00
|
|
|
|
|
|
|
def send_command(self, command, **kwargs):
|
2018-08-24 08:28:43 +00:00
|
|
|
"""Send a command."""
|
2018-02-06 18:47:24 +00:00
|
|
|
num_repeats = kwargs.get(ATTR_NUM_REPEATS)
|
|
|
|
|
|
|
|
delay = kwargs.get(ATTR_DELAY_SECS, DEFAULT_DELAY_SECS)
|
|
|
|
|
|
|
|
for _ in range(num_repeats):
|
|
|
|
for payload in command:
|
|
|
|
if payload in self._commands:
|
|
|
|
for local_payload in self._commands[payload][CONF_COMMAND]:
|
|
|
|
self._send_command(local_payload)
|
|
|
|
else:
|
|
|
|
self._send_command(payload)
|
|
|
|
time.sleep(delay)
|