Skip to content
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

Xiaomi MiIO Switch: Support for different device types #9836

Merged
2 changes: 1 addition & 1 deletion homeassistant/components/light/xiaomi_miio.py
Expand Up @@ -28,7 +28,7 @@
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
})

REQUIREMENTS = ['python-mirobo==0.2.0']
REQUIREMENTS = ['python-miio==0.3.0']

# The light does not accept cct values < 1
CCT_MIN = 1
Expand Down
158 changes: 141 additions & 17 deletions homeassistant/components/switch/xiaomi_miio.py
Expand Up @@ -18,14 +18,14 @@
_LOGGER = logging.getLogger(__name__)

DEFAULT_NAME = 'Xiaomi Miio Switch'
PLATFORM = 'xiaomi_miio'

PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_HOST): cv.string,
vol.Required(CONF_TOKEN): vol.All(cv.string, vol.Length(min=32, max=32)),
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
})

REQUIREMENTS = ['python-mirobo==0.2.0']
REQUIREMENTS = ['python-miio==0.3.0']

ATTR_POWER = 'power'
ATTR_TEMPERATURE = 'temperature'
Expand All @@ -38,31 +38,59 @@
@asyncio.coroutine
def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
"""Set up the switch from config."""
from mirobo import Plug, DeviceException
from miio import Device, DeviceException

host = config.get(CONF_HOST)
name = config.get(CONF_NAME)
token = config.get(CONF_TOKEN)

_LOGGER.info("Initializing with host %s (token %s...)", host, token[:5])

devices = []
try:
plug = Plug(host, token)
plug = Device(host, token)
device_info = plug.info()
_LOGGER.info("%s %s %s initialized",
device_info.raw['model'],
device_info.raw['fw_ver'],
device_info.raw['hw_ver'])

xiaomi_plug_switch = XiaomiPlugSwitch(name, plug, device_info)
device_info.model,
device_info.firmware_version,
device_info.hardware_version)

if device_info.model in ['chuangmi.plug.v1']:
from miio import PlugV1
plug = PlugV1(host, token)

# The device has two switchable channels (mains and a USB port).
# A switch device per channel will be created.
for channel_usb in [True, False]:
device = ChuangMiPlugV1Switch(
name, plug, device_info, channel_usb)
devices.append(device)

elif device_info.model in ['qmi.powerstrip.v1',
'zimi.powerstrip.v2']:
from miio import Strip
plug = Strip(host, token)
device = XiaomiPowerStripSwitch(name, plug, device_info)
devices.append(device)
elif device_info.model in ['chuangmi.plug.m1',
'chuangmi.plug.v2']:
from miio import Plug
plug = Plug(host, token)
device = XiaomiPlugGenericSwitch(name, plug, device_info)
devices.append(device)
else:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just a side-note, I think we need to start looking into getting these devices auto-detected (through mDNS), then we can create devices automatically based on that information.

Furthermore we could add a configurator for the UI to allow user input the tokens without touching the configuration files.

_LOGGER.error(
'Unsupported device found! Please create an issue at '
'https://github.com/rytilahti/python-miio/issues '
'and provide the following data: %s', device_info.model)
except DeviceException:
raise PlatformNotReady

async_add_devices([xiaomi_plug_switch], update_before_add=True)
async_add_devices(devices, update_before_add=True)


class XiaomiPlugSwitch(SwitchDevice):
"""Representation of a Xiaomi Plug."""
class XiaomiPlugGenericSwitch(SwitchDevice):
"""Representation of a Xiaomi Plug Generic."""

def __init__(self, name, plug, device_info):
"""Initialize the plug switch."""
Expand All @@ -74,8 +102,7 @@ def __init__(self, name, plug, device_info):
self._state = None
self._state_attrs = {
ATTR_TEMPERATURE: None,
ATTR_LOAD_POWER: None,
ATTR_MODEL: self._device_info.raw['model'],
ATTR_MODEL: self._device_info.model,
}
self._skip_update = False

Expand Down Expand Up @@ -112,7 +139,7 @@ def is_on(self):
@asyncio.coroutine
def _try_command(self, mask_error, func, *args, **kwargs):
"""Call a plug command handling error messages."""
from mirobo import DeviceException
from miio import DeviceException
try:
result = yield from self.hass.async_add_job(
partial(func, *args, **kwargs))
Expand Down Expand Up @@ -147,7 +174,43 @@ def async_turn_off(self, **kwargs):
@asyncio.coroutine
def async_update(self):
"""Fetch state from the device."""
from mirobo import DeviceException
from miio import DeviceException

# On state change the device doesn't provide the new state immediately.
if self._skip_update:
self._skip_update = False
return

try:
state = yield from self.hass.async_add_job(self._plug.status)
_LOGGER.debug("Got new state: %s", state)

self._state = state.is_on
self._state_attrs.update({
ATTR_TEMPERATURE: state.temperature
})

except DeviceException as ex:
_LOGGER.error("Got exception while fetching the state: %s", ex)


class XiaomiPowerStripSwitch(XiaomiPlugGenericSwitch, SwitchDevice):
"""Representation of a Xiaomi Power Strip."""

def __init__(self, name, plug, device_info):
"""Initialize the plug switch."""
XiaomiPlugGenericSwitch.__init__(self, name, plug, device_info)

self._state_attrs = {
ATTR_TEMPERATURE: None,
ATTR_LOAD_POWER: None,
ATTR_MODEL: self._device_info.model,
}

@asyncio.coroutine
def async_update(self):
"""Fetch state from the device."""
from miio import DeviceException

# On state change the device doesn't provide the new state immediately.
if self._skip_update:
Expand All @@ -161,8 +224,69 @@ def async_update(self):
self._state = state.is_on
self._state_attrs.update({
ATTR_TEMPERATURE: state.temperature,
ATTR_LOAD_POWER: state.load_power,
ATTR_LOAD_POWER: state.load_power
})

except DeviceException as ex:
_LOGGER.error("Got exception while fetching the state: %s", ex)


class ChuangMiPlugV1Switch(XiaomiPlugGenericSwitch, SwitchDevice):
"""Representation of a Chuang Mi Plug V1."""

def __init__(self, name, plug, device_info, channel_usb):
"""Initialize the plug switch."""
name = name + ' USB' if channel_usb else name

XiaomiPlugGenericSwitch.__init__(self, name, plug, device_info)
self._channel_usb = channel_usb

@asyncio.coroutine
def async_turn_on(self, **kwargs):
"""Turn a channel on."""
if self._channel_usb:
result = yield from self._try_command(
"Turning the plug on failed.", self._plug.usb_on)
else:
result = yield from self._try_command(
"Turning the plug on failed.", self._plug.on)

if result:
self._state = True
self._skip_update = True

@asyncio.coroutine
def async_turn_off(self, **kwargs):
"""Turn a channel off."""
if self._channel_usb:
result = yield from self._try_command(
"Turning the plug on failed.", self._plug.usb_off)
else:
result = yield from self._try_command(
"Turning the plug on failed.", self._plug.off)

if result:
self._state = False
self._skip_update = True

@asyncio.coroutine
def async_update(self):
"""Fetch state from the device."""
from miio import DeviceException

# On state change the device doesn't provide the new state immediately.
if self._skip_update:
self._skip_update = False
return
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A question here, would it be that bad if the device provides an incorrect state for one run? I think it would make more sense and make the device adhere to how other devices (afaik) do it. So instead of trying to trick the system, just believing what comes out from the device.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, it's bad. If you turn on the switch at the user interface it will flip back immediately. 30 seconds later the correct state will be received. The user experience suffers without the workaround.


try:
state = yield from self.hass.async_add_job(self._plug.status)
_LOGGER.debug("Got new state: %s", state)

if self._channel_usb:
self._state = state.usb_power
else:
self._state = state.is_on

except DeviceException as ex:
_LOGGER.error("Got exception while fetching the state: %s", ex)
2 changes: 1 addition & 1 deletion homeassistant/components/vacuum/xiaomi_miio.py
Expand Up @@ -21,7 +21,7 @@
ATTR_ENTITY_ID, CONF_HOST, CONF_NAME, CONF_TOKEN, STATE_OFF, STATE_ON)
import homeassistant.helpers.config_validation as cv

REQUIREMENTS = ['python-mirobo==0.2.0']
REQUIREMENTS = ['python-miio==0.3.0']

_LOGGER = logging.getLogger(__name__)

Expand Down
2 changes: 1 addition & 1 deletion requirements_all.txt
Expand Up @@ -775,7 +775,7 @@ python-juicenet==0.0.5
# homeassistant.components.light.xiaomi_miio
# homeassistant.components.switch.xiaomi_miio
# homeassistant.components.vacuum.xiaomi_miio
python-mirobo==0.2.0
python-miio==0.3.0

# homeassistant.components.media_player.mpd
python-mpd2==0.5.5
Expand Down