297 lines
9.2 KiB
Python
297 lines
9.2 KiB
Python
"""
|
|
Support for Xiaomi Smart WiFi Socket and Smart Power Strip.
|
|
|
|
For more details about this platform, please refer to the documentation
|
|
https://home-assistant.io/components/switch.xiaomi_miio/
|
|
"""
|
|
import asyncio
|
|
from functools import partial
|
|
import logging
|
|
|
|
import voluptuous as vol
|
|
|
|
import homeassistant.helpers.config_validation as cv
|
|
from homeassistant.components.switch import (SwitchDevice, PLATFORM_SCHEMA, )
|
|
from homeassistant.const import (CONF_NAME, CONF_HOST, CONF_TOKEN, )
|
|
from homeassistant.exceptions import PlatformNotReady
|
|
|
|
_LOGGER = logging.getLogger(__name__)
|
|
|
|
DEFAULT_NAME = 'Xiaomi Miio Switch'
|
|
|
|
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-miio==0.3.7']
|
|
|
|
ATTR_POWER = 'power'
|
|
ATTR_TEMPERATURE = 'temperature'
|
|
ATTR_LOAD_POWER = 'load_power'
|
|
ATTR_MODEL = 'model'
|
|
SUCCESS = ['ok']
|
|
|
|
|
|
# pylint: disable=unused-argument
|
|
@asyncio.coroutine
|
|
def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
|
"""Set up the switch from config."""
|
|
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 = Device(host, token)
|
|
device_info = plug.info()
|
|
_LOGGER.info("%s %s %s initialized",
|
|
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 PowerStrip
|
|
plug = PowerStrip(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:
|
|
_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(devices, update_before_add=True)
|
|
|
|
|
|
class XiaomiPlugGenericSwitch(SwitchDevice):
|
|
"""Representation of a Xiaomi Plug Generic."""
|
|
|
|
def __init__(self, name, plug, device_info):
|
|
"""Initialize the plug switch."""
|
|
self._name = name
|
|
self._icon = 'mdi:power-socket'
|
|
self._device_info = device_info
|
|
|
|
self._plug = plug
|
|
self._state = None
|
|
self._state_attrs = {
|
|
ATTR_TEMPERATURE: None,
|
|
ATTR_MODEL: self._device_info.model,
|
|
}
|
|
self._skip_update = False
|
|
|
|
@property
|
|
def should_poll(self):
|
|
"""Poll the plug."""
|
|
return True
|
|
|
|
@property
|
|
def name(self):
|
|
"""Return the name of the device if any."""
|
|
return self._name
|
|
|
|
@property
|
|
def icon(self):
|
|
"""Return the icon to use for device if any."""
|
|
return self._icon
|
|
|
|
@property
|
|
def available(self):
|
|
"""Return true when state is known."""
|
|
return self._state is not None
|
|
|
|
@property
|
|
def device_state_attributes(self):
|
|
"""Return the state attributes of the device."""
|
|
return self._state_attrs
|
|
|
|
@property
|
|
def is_on(self):
|
|
"""Return true if switch is on."""
|
|
return self._state
|
|
|
|
@asyncio.coroutine
|
|
def _try_command(self, mask_error, func, *args, **kwargs):
|
|
"""Call a plug command handling error messages."""
|
|
from miio import DeviceException
|
|
try:
|
|
result = yield from self.hass.async_add_job(
|
|
partial(func, *args, **kwargs))
|
|
|
|
_LOGGER.debug("Response received from plug: %s", result)
|
|
|
|
return result == SUCCESS
|
|
except DeviceException as exc:
|
|
_LOGGER.error(mask_error, exc)
|
|
return False
|
|
|
|
@asyncio.coroutine
|
|
def async_turn_on(self, **kwargs):
|
|
"""Turn the plug on."""
|
|
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 the plug off."""
|
|
result = yield from self._try_command(
|
|
"Turning the plug off 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
|
|
|
|
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:
|
|
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,
|
|
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
|
|
|
|
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
|
|
|
|
self._state_attrs.update({
|
|
ATTR_TEMPERATURE: state.temperature
|
|
})
|
|
|
|
except DeviceException as ex:
|
|
_LOGGER.error("Got exception while fetching the state: %s", ex)
|