-
-
Notifications
You must be signed in to change notification settings - Fork 30.3k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
python-miio remote. #11891
python-miio remote. #11891
Changes from all commits
17e3975
520bdcd
1561b93
e7385bd
cc1598a
5d7b91c
f4fd9b9
e1d26f6
e153425
1fb4d00
141cf4c
0800f89
0371fdd
7ecb8c6
45eec73
cbf49b0
2827425
5907d6b
aa5fbc2
f7dfe02
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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), | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This should be |
||
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] = {} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Maybe you should use a key specific for the remote xiaomi_miio platform? Other xiaomi_miio platforms might want to store things in hass.data in the future. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I am pretty sure that this part is correct, if you look at the other python-miio platforms they do the same thing: Create a dict if one is not on the PLATFORM key, and save the devices in the new dict using their host (ip) as key. The host should be unique but there may be some edge cases where it is not. See these lines in the vacuum python-miio: |
||
|
||
friendly_name = config.get(CONF_NAME, "xiaomi_miio_" + | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Use new style string formatting, not string concatenation. |
||
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: | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. When would this happen? |
||
_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(): | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You could use a generator expression and entity = next((
remote for remote in hass.data[PLATFORM].values()
if remote.entity_id == entity_id), None) |
||
if remote.entity_id == entity_id: | ||
entity = remote | ||
|
||
if not entity: | ||
_LOGGER.error("entity_id: '%s' not found", entity_id) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Please remove quotes around 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 | ||
|
||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Please add a should_poll method. Just to make clear it's a polling entity or not. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. done |
||
@property | ||
def name(self): | ||
"""Return the name of the remote.""" | ||
return self._name | ||
|
||
@property | ||
def device(self): | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If the property isn't needed just make a regular instance attribute of it instead. |
||
"""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): | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Same as above. |
||
"""Return the slot to save learned command.""" | ||
return self._slot | ||
|
||
@property | ||
def timeout(self): | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Same as above. |
||
"""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'} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is already reported in the base entity class. |
||
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, " + | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Remove There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Usually we let not implemented methods raise that error if called. I'd just remove these methods from the class here. |
||
"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, " + | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Same as above. |
||
"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) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If the xiaomi_miio library supports asyncio it was better when this method was async, since we want to sleep here. We usually frown upon sync sleep in worker threads which this now does. On the other hand async sleep doesn't cost anything. I'm not sure why @pvizeli wanted this to be done sync? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
These two constants aren't used anymore, too.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
They are now, forgot to use them in the correct place.