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
Changes from all commits
1a94133
deb7b3c
5179a35
7aa608b
b9dbd3c
1498c2c
79b03f0
eb2aff1
794e20b
96b191c
533a814
61c77fb
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 |
---|---|---|
|
@@ -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' | ||
|
@@ -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: | ||
_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.""" | ||
|
@@ -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 | ||
|
||
|
@@ -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)) | ||
|
@@ -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: | ||
|
@@ -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 | ||
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. 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. 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. 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) |
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.
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.