mirror of
https://github.com/home-assistant/core.git
synced 2025-04-26 10:17:51 +00:00
Add Xiaomi Universal IR Remote (Chuangmi IR) (#11891)
* First version of remote xiaomi-miio * added to coveragerc * fixed pylint error * misc fixes and input validation * address syssi's requests except device and async_service_handler * forgot to run linter * implemented async_service_handler * fixed delay == None, honor timeout given by user, pythonic compare of None * Added some whitespace for readability, added error message to turn_on and turn_off, fixed services.yaml examples * fixed pylint errors * readd pass for readability * fixed small stuff * Use RemoteDevice, Make send_command non-async * Ready code for next version of python-miio (Support for pronto hex codes) * cast command_optional to int, better input validation, fixed index out of bounds error. * revert code now in python-miio * ready for python-miio 0.3.5 * Removed unneccary return statements * require 0.3.5 * Rebase and update requirements_all.txt
This commit is contained in:
parent
a1d586c793
commit
49c7b422f2
@ -510,6 +510,7 @@ omit =
|
|||||||
homeassistant/components/remember_the_milk/__init__.py
|
homeassistant/components/remember_the_milk/__init__.py
|
||||||
homeassistant/components/remote/harmony.py
|
homeassistant/components/remote/harmony.py
|
||||||
homeassistant/components/remote/itach.py
|
homeassistant/components/remote/itach.py
|
||||||
|
homeassistant/components/remote/xiaomi_miio.py
|
||||||
homeassistant/components/scene/hunterdouglas_powerview.py
|
homeassistant/components/scene/hunterdouglas_powerview.py
|
||||||
homeassistant/components/scene/lifx_cloud.py
|
homeassistant/components/scene/lifx_cloud.py
|
||||||
homeassistant/components/sensor/airvisual.py
|
homeassistant/components/sensor/airvisual.py
|
||||||
|
@ -49,3 +49,16 @@ harmony_sync:
|
|||||||
entity_id:
|
entity_id:
|
||||||
description: Name(s) of entities to sync.
|
description: Name(s) of entities to sync.
|
||||||
example: 'remote.family_room'
|
example: 'remote.family_room'
|
||||||
|
|
||||||
|
xiaomi_miio_learn_command:
|
||||||
|
description: 'Learn an IR command, press "Call Service", point the remote at the IR device, and the learned command will be shown as a notification in Overview.'
|
||||||
|
fields:
|
||||||
|
entity_id:
|
||||||
|
description: 'Name of the entity to learn command from.'
|
||||||
|
example: 'remote.xiaomi_miio'
|
||||||
|
slot:
|
||||||
|
description: 'Define the slot used to save the IR command (Value from 1 to 1000000)'
|
||||||
|
example: '1'
|
||||||
|
timeout:
|
||||||
|
description: 'Define the timeout in seconds, before which the command must be learned.'
|
||||||
|
example: '30'
|
||||||
|
255
homeassistant/components/remote/xiaomi_miio.py
Executable file
255
homeassistant/components/remote/xiaomi_miio.py
Executable file
@ -0,0 +1,255 @@
|
|||||||
|
"""
|
||||||
|
Support for the Xiaomi IR Remote (Chuangmi IR).
|
||||||
|
|
||||||
|
For more details about this platform, please refer to the documentation
|
||||||
|
https://home-assistant.io/components/remote.xiaomi_miio/
|
||||||
|
"""
|
||||||
|
import asyncio
|
||||||
|
import logging
|
||||||
|
import time
|
||||||
|
|
||||||
|
from datetime import timedelta
|
||||||
|
|
||||||
|
import voluptuous as vol
|
||||||
|
|
||||||
|
from homeassistant.components.remote import (
|
||||||
|
PLATFORM_SCHEMA, DOMAIN, ATTR_NUM_REPEATS, ATTR_DELAY_SECS,
|
||||||
|
DEFAULT_DELAY_SECS, RemoteDevice)
|
||||||
|
from homeassistant.const import (
|
||||||
|
CONF_NAME, CONF_HOST, CONF_TOKEN, CONF_TIMEOUT,
|
||||||
|
ATTR_ENTITY_ID, ATTR_HIDDEN, CONF_COMMAND)
|
||||||
|
import homeassistant.helpers.config_validation as cv
|
||||||
|
from homeassistant.util.dt import utcnow
|
||||||
|
|
||||||
|
REQUIREMENTS = ['python-miio==0.3.5']
|
||||||
|
|
||||||
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
SERVICE_LEARN = 'xiaomi_miio_learn_command'
|
||||||
|
PLATFORM = 'xiaomi_miio'
|
||||||
|
|
||||||
|
CONF_SLOT = 'slot'
|
||||||
|
CONF_COMMANDS = 'commands'
|
||||||
|
|
||||||
|
DEFAULT_TIMEOUT = 10
|
||||||
|
DEFAULT_SLOT = 1
|
||||||
|
|
||||||
|
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={}):
|
||||||
|
vol.Schema({cv.slug: COMMAND_SCHEMA}),
|
||||||
|
}, extra=vol.ALLOW_EXTRA)
|
||||||
|
|
||||||
|
|
||||||
|
@asyncio.coroutine
|
||||||
|
def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||||
|
"""Set up the Xiaomi IR Remote (Chuangmi IR) platform."""
|
||||||
|
from miio import ChuangmiIr, DeviceException
|
||||||
|
|
||||||
|
host = config.get(CONF_HOST)
|
||||||
|
token = config.get(CONF_TOKEN)
|
||||||
|
|
||||||
|
# Create handler
|
||||||
|
_LOGGER.info("Initializing with host %s (token %s...)", host, token[:5])
|
||||||
|
device = ChuangmiIr(host, token)
|
||||||
|
|
||||||
|
# Check that we can communicate with device.
|
||||||
|
try:
|
||||||
|
device.info()
|
||||||
|
except DeviceException as ex:
|
||||||
|
_LOGGER.error("Token not accepted by device : %s", ex)
|
||||||
|
return
|
||||||
|
|
||||||
|
if PLATFORM not in hass.data:
|
||||||
|
hass.data[PLATFORM] = {}
|
||||||
|
|
||||||
|
friendly_name = config.get(CONF_NAME, "xiaomi_miio_" +
|
||||||
|
host.replace('.', '_'))
|
||||||
|
slot = config.get(CONF_SLOT)
|
||||||
|
timeout = config.get(CONF_TIMEOUT)
|
||||||
|
|
||||||
|
hidden = config.get(ATTR_HIDDEN)
|
||||||
|
|
||||||
|
xiaomi_miio_remote = XiaomiMiioRemote(
|
||||||
|
friendly_name, device, slot, timeout,
|
||||||
|
hidden, config.get(CONF_COMMANDS))
|
||||||
|
|
||||||
|
hass.data[PLATFORM][host] = xiaomi_miio_remote
|
||||||
|
|
||||||
|
async_add_devices([xiaomi_miio_remote])
|
||||||
|
|
||||||
|
@asyncio.coroutine
|
||||||
|
def async_service_handler(service):
|
||||||
|
"""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
|
||||||
|
for remote in hass.data[PLATFORM].values():
|
||||||
|
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)
|
||||||
|
|
||||||
|
yield from hass.async_add_job(device.learn, slot)
|
||||||
|
|
||||||
|
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):
|
||||||
|
message = yield from hass.async_add_job(
|
||||||
|
device.read, slot)
|
||||||
|
_LOGGER.debug("Message recieved from device: '%s'", message)
|
||||||
|
|
||||||
|
if 'code' in message and message['code']:
|
||||||
|
log_msg = "Received command is: {}".format(message['code'])
|
||||||
|
_LOGGER.info(log_msg)
|
||||||
|
hass.components.persistent_notification.async_create(
|
||||||
|
log_msg, title='Xiaomi Miio Remote')
|
||||||
|
return
|
||||||
|
|
||||||
|
if ('error' in message and
|
||||||
|
message['error']['message'] == "learn timeout"):
|
||||||
|
yield from hass.async_add_job(device.learn, slot)
|
||||||
|
|
||||||
|
yield from asyncio.sleep(1, loop=hass.loop)
|
||||||
|
|
||||||
|
_LOGGER.error("Timeout. No infrared command captured")
|
||||||
|
hass.components.persistent_notification.async_create(
|
||||||
|
"Timeout. No infrared command captured",
|
||||||
|
title='Xiaomi Miio Remote')
|
||||||
|
|
||||||
|
hass.services.async_register(DOMAIN, SERVICE_LEARN, async_service_handler,
|
||||||
|
schema=LEARN_COMMAND_SCHEMA)
|
||||||
|
|
||||||
|
|
||||||
|
class XiaomiMiioRemote(RemoteDevice):
|
||||||
|
"""Representation of a Xiaomi Miio Remote device."""
|
||||||
|
|
||||||
|
def __init__(self, friendly_name, device,
|
||||||
|
slot, timeout, hidden, commands):
|
||||||
|
"""Initialize the remote."""
|
||||||
|
self._name = friendly_name
|
||||||
|
self._device = device
|
||||||
|
self._is_hidden = hidden
|
||||||
|
self._slot = slot
|
||||||
|
self._timeout = timeout
|
||||||
|
self._state = False
|
||||||
|
self._commands = commands
|
||||||
|
|
||||||
|
@property
|
||||||
|
def name(self):
|
||||||
|
"""Return the name of the remote."""
|
||||||
|
return self._name
|
||||||
|
|
||||||
|
@property
|
||||||
|
def device(self):
|
||||||
|
"""Return the remote object."""
|
||||||
|
return self._device
|
||||||
|
|
||||||
|
@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."""
|
||||||
|
from miio import DeviceException
|
||||||
|
try:
|
||||||
|
self.device.info()
|
||||||
|
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:
|
||||||
|
return {'hidden': 'true'}
|
||||||
|
else:
|
||||||
|
return
|
||||||
|
|
||||||
|
# pylint: disable=R0201
|
||||||
|
@asyncio.coroutine
|
||||||
|
def async_turn_on(self, **kwargs):
|
||||||
|
"""Turn the device on."""
|
||||||
|
_LOGGER.error("Device does not support turn_on, " +
|
||||||
|
"please use 'remote.send_command' to send commands.")
|
||||||
|
|
||||||
|
@asyncio.coroutine
|
||||||
|
def async_turn_off(self, **kwargs):
|
||||||
|
"""Turn the device off."""
|
||||||
|
_LOGGER.error("Device does not support turn_off, " +
|
||||||
|
"please use 'remote.send_command' to send commands.")
|
||||||
|
|
||||||
|
# pylint: enable=R0201
|
||||||
|
def _send_command(self, payload):
|
||||||
|
"""Send a command."""
|
||||||
|
from miio import DeviceException
|
||||||
|
|
||||||
|
_LOGGER.debug("Sending payload: '%s'", payload)
|
||||||
|
try:
|
||||||
|
self.device.play(payload)
|
||||||
|
except DeviceException as ex:
|
||||||
|
_LOGGER.error(
|
||||||
|
"Transmit of IR command failed, %s, exception: %s",
|
||||||
|
payload, ex)
|
||||||
|
|
||||||
|
def send_command(self, command, **kwargs):
|
||||||
|
"""Wrapper for _send_command."""
|
||||||
|
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)
|
@ -918,6 +918,7 @@ python-juicenet==0.0.5
|
|||||||
|
|
||||||
# homeassistant.components.fan.xiaomi_miio
|
# homeassistant.components.fan.xiaomi_miio
|
||||||
# homeassistant.components.light.xiaomi_miio
|
# homeassistant.components.light.xiaomi_miio
|
||||||
|
# homeassistant.components.remote.xiaomi_miio
|
||||||
# homeassistant.components.switch.xiaomi_miio
|
# homeassistant.components.switch.xiaomi_miio
|
||||||
# homeassistant.components.vacuum.xiaomi_miio
|
# homeassistant.components.vacuum.xiaomi_miio
|
||||||
python-miio==0.3.5
|
python-miio==0.3.5
|
||||||
|
Loading…
x
Reference in New Issue
Block a user