Xiaomi Humidifier support

I think you’d better delete a ‘#’ in line 163 :slight_smile:

I would love to use it. But when I am trying to test it, I am getting CheckSum error:

File “/usr/lib/python3.6/site-packages/construct/core.py”, line 2272, in _parse
raise ChecksumError(“wrong checksum, read %r, computed %r” % (hexlify(hash1), hexlify(hash2)))
construct.core.ChecksumError: wrong checksum, read b’ffffffffffffffffffffffffffffffff’, computed b’86f9dfc783dd2b635f17750e28fc898c’

Am I doing something wrong?

And maybe I should note that I am also using Xiaomi Air Purifier 2 bulitin integration.

Thanks, fixed now.

Sorry, no idea… Looks to me like this error is unrelated to the custom platform code.

I recommend you use python3.5.
If you use HASSBIAN, it works well.

Thanks @bezkod for the initial work. I didn’t know your custom_component and did the same like you as proof of concept:

If the component works fine I will merge the code into fan/xiaomi_miio soon in order to support the Xiaomi Air Humidifier officially in future.

1 Like

@bezkod Our code differs in one feature: Is the LED of the Air Humidifier controllable? Did you remove set_led_on and set_led_off on purpose? Could someone (@af950833, @itchaboy) test all exposed services of the component? Thanks in advance!

Another request: Could someone provide the output of following mirobo (cli, part of python-miio) calls for the air humidifier:

# ip and token needs to be adapted

bin/mirobo --ip 192.168.1.71 --token 3d5f7ae53b51aa312e464b150b37453b -d info

bin/mirobo --ip 192.168.1.71 --token 3d5f7ae53b51aa312e464b150b37453b get_prop '["power", "mode", "temp_dec", "humidity", "buzzer","led_b", "led"]'

how do I go about adding a custom component? Would I add the following folder to my config folder? custom_components/fan/custom_component.py

(homeassistant) homeassistant@Tommy:/home/pi $ mirobo --ip 192.168.0.25 --token 689c4056fe28ebb3a2e8c2fe350e51ba -d info
INFO:miio.vacuum_cli:Debug mode active
DEBUG:miio.vacuum_cli:Read stored sequence ids: {‘seq’: 2, ‘manual_seq’: 0}
DEBUG:miio.vacuum_cli:Connecting to 192.168.0.25 with token 689c4056fe28ebb3a2e8c2fe350e51ba
DEBUG:miio.protocol:Unable to decrypt, returning raw bytes: b’’
DEBUG:miio.device:Got a response: Container:
data = Container:
length = 0
value = (total 0)
offset2 = 32
offset1 = 32
data = (total 0)
header = Container:
length = 16
value = Container:
length = 32
unknown = 0
device_id = 047c3fd9 (total 8)
ts = 1970-01-07 15:54:21
offset2 = 16
offset1 = 0
data = !1\x00 \x00\x00\x00\x00\x04|?\xd9\x00\x08\xc8\xad (total 16)
checksum = h\x9c@V\xfe(\xeb\xb3\xa2\xe8\xc2\xfe5\x0eQ\xba (total 16)
DEBUG:miio.device:Discovered b’047c3fd9’ with ts: 1970-01-07 15:54:21, token: b’689c4056fe28ebb3a2e8c2fe350e51ba’
DEBUG:miio.device:192.168.0.25:54321 >>: {‘method’: ‘miIO.info’, ‘params’: [], ‘id’: 3}
DEBUG:miio.device:192.168.0.25:54321 (ts: 1970-01-07 15:54:21, id: 3) << {‘id’: 3, ‘result’: {‘fw_ver’: ‘1.2.9_5033’, ‘token’: ‘689c4056fe28ebb3a2e8c2fe350e51ba’, ‘otu_stat’: [101, 74, 5343, 0, 5327, 407], ‘mmfree’: 228248, ‘netif’: {‘gw’: ‘192.168.0.1’, ‘localIp’: ‘192.168.0.25’, ‘mask’: ‘255.255.255.0’}, ‘ott_stat’: [0, 0, 0, 0], ‘model’: ‘zhimi.humidifier.v1’, ‘cfg_time’: 0, ‘life’: 575661, ‘ap’: {‘rssi’: -35, ‘ssid’: ‘Tommy’, ‘bssid’: ‘AC:9E:17:A1:DF:F8’}, ‘wifi_fw_ver’: ‘SD878x-14.76.36.p84-702.1.0-WM’, ‘hw_ver’: ‘MW300’, ‘ot’: ‘otu’, ‘mac’: ‘78:11:DC:8E:00:E7’}}
zhimi.humidifier.v1 v1.2.9_5033 (78:11:DC:8E:00:E7) @ 192.168.0.25 - token: 689c4056fe28ebb3a2e8c2fe350e51ba
DEBUG:miio.vacuum_cli:Full response: {‘ap’: {‘bssid’: ‘AC:9E:17:A1:DF:F8’, ‘rssi’: -35, ‘ssid’: ‘Tommy’},
‘cfg_time’: 0,
‘fw_ver’: ‘1.2.9_5033’,
‘hw_ver’: ‘MW300’,
‘life’: 575661,
‘mac’: ‘78:11:DC:8E:00:E7’,
‘mmfree’: 228248,
‘model’: ‘zhimi.humidifier.v1’,
‘netif’: {‘gw’: ‘192.168.0.1’,
‘localIp’: ‘192.168.0.25’,
‘mask’: ‘255.255.255.0’},
‘ot’: ‘otu’,
‘ott_stat’: [0, 0, 0, 0],
‘otu_stat’: [101, 74, 5343, 0, 5327, 407],
‘token’: ‘689c4056fe28ebb3a2e8c2fe350e51ba’,
‘wifi_fw_ver’: ‘SD878x-14.76.36.p84-702.1.0-WM’}
DEBUG:miio.vacuum_cli:Writing {‘seq’: 3, ‘manual_seq’: 0} to /tmp/python-mirobo.seq
(homeassistant) homeassistant@Tommy:/home/pi $
(homeassistant) homeassistant@Tommy:/home/pi $ mirobo --ip 192.168.0.25 --token 689c4056fe28ebb3a2e8c2fe350e51ba raw_command get_prop ‘[“power”, “mode”, “temp_dec”, “humidity”, “buzzer”,“led_b”, “led”]’
Sending cmd get_prop with params [‘power’, ‘mode’, ‘temp_dec’, ‘humidity’, ‘buzzer’, ‘led_b’, ‘led’]
[‘off’, ‘high’, 294, 33, ‘on’, 0, None]
(homeassistant) homeassistant@Tommy:/home/pi $

“”"
Support for Xiaomi Mi Air Humidifier

“”"
import asyncio
from functools import partial
import logging
import os

import voluptuous as vol

from homeassistant.helpers.entity import ToggleEntity
from homeassistant.components.fan import (FanEntity, PLATFORM_SCHEMA,
SUPPORT_SET_SPEED, DOMAIN)
from homeassistant.config import load_yaml_config_file
from homeassistant.const import (CONF_NAME, CONF_HOST, CONF_TOKEN,
ATTR_ENTITY_ID, )
from homeassistant.exceptions import PlatformNotReady
import homeassistant.helpers.config_validation as cv

_LOGGER = logging.getLogger(name)

DEFAULT_NAME = ‘Xiaomi Air Humidifier’
PLATFORM = ‘xiaomi_airhumidifier’

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.3’]

ATTR_TEMPERATURE = ‘temperature’
ATTR_HUMIDITY = ‘humidity’
ATTR_MODE = ‘mode’

SUCCESS = [‘ok’]

pylint: disable=unused-argument

@asyncio.coroutine
def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
“”“Set up the air humidifier from config.”""
from miio import AirHumidifier, DeviceException
if PLATFORM not in hass.data:
hass.data[PLATFORM] = {}

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])

try:
    air_humidifier = AirHumidifier(host, token)

    xiaomi_air_humidifier = XiaomiAirHumidifier(name, air_humidifier)
    hass.data[PLATFORM][host] = xiaomi_air_humidifier
except DeviceException:
    raise PlatformNotReady

async_add_devices([xiaomi_air_humidifier], update_before_add=True)

class XiaomiAirHumidifier(FanEntity):
“”“Representation of a Xiaomi Air Humidifier.”""

def __init__(self, name, air_humidifier):
    """Initialize the air humidifier."""
    self._name = name

    self._air_humidifier = air_humidifier
    self._state = None
    self._state_attrs = {
        ATTR_TEMPERATURE: None,
        ATTR_HUMIDITY: None,
        ATTR_MODE: None,
    }

@property
def supported_features(self):
    """Flag supported features."""
    return SUPPORT_SET_SPEED

@property
def should_poll(self):
    """Poll the fan."""
    return True

@property
def name(self):
    """Return the name of the device if any."""
    return self._name

@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 fan is on."""
    return self._state

@asyncio.coroutine
def _try_command(self, mask_error, func, *args, **kwargs):
    """Call a air humidifier 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 air humidifier: %s", result)

        return result == SUCCESS
    except DeviceException as exc:
        _LOGGER.error(mask_error, exc)
        return False

@asyncio.coroutine
def async_turn_on(self: ToggleEntity, speed: str=None, **kwargs) -> None:
    """Turn the fan on."""
    if speed:
        # If operation mode was set the device must not be turned on.
        yield from self.async_set_speed(speed)
        return

    yield from self._try_command(
        "Turning the air humidifier on failed.", self._air_humidifier.on)

@asyncio.coroutine
def async_turn_off(self: ToggleEntity, **kwargs) -> None:
    """Turn the fan off."""
    yield from self._try_command(
        "Turning the air humidifier off failed.", self._air_humidifier.off)

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

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

        self._state = state.is_on
        self._state_attrs = {
            ATTR_TEMPERATURE: state.temperature,
            ATTR_HUMIDITY: state.humidity,
            ATTR_MODE: state.mode.value
        }

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

@property
def speed_list(self: ToggleEntity) -> list:
    """Get the list of available speeds."""
    from miio.airhumidifier import OperationMode
    return [mode.name for mode in OperationMode]

@property
def speed(self):
    """Return the current speed."""
    if self._state:
        from miio.airhumidifier import OperationMode

        return OperationMode(self._state_attrs[ATTR_MODE]).name

    return None

@asyncio.coroutine
def async_set_speed(self: ToggleEntity, speed: str) -> None:
    """Set the speed of the fan."""
    _LOGGER.debug("Setting the operation mode to: " + speed)
    from miio.airhumidifier import OperationMode

    yield from self._try_command(
        "Setting operation mode of the air humidifier failed.",
        self._air_humidifier.set_mode, OperationMode[speed])

from HA 0.61.0 the servicess are not working, so I removed all service in the code and using upper code as custom_components.
Also I wanna let you know that there is no LED Brigtness in Air Purifier Pro. I am also using it after removing the service"LED Brightness" in HA 0.61.1

It looks like there is not attribute “led_b” or “led” for the Air Humidifier. Could you execute this two commands for testing:

mirobo --ip 192.168.0.25 --token 689c4056fe28ebb3a2e8c2fe350e51ba raw_command get_prop '["power", "mode", "temp_dec", "humidity", "buzzer","led"]'
mirobo --ip 192.168.0.25 --token 689c4056fe28ebb3a2e8c2fe350e51ba raw_command get_prop '["power", "mode", "temp_dec", "humidity", "buzzer","led_b"]'

One of the outputs will contain a “null” value.

Just copy the file

https://raw.githubusercontent.com/syssi/xiaomi_airhumidifier/master/custom_components/fan/xiaomi_airhumidifier.py

to custom_components/fan/xiaomi_airhumidifier.py and add the configuration to your

# configurations.yaml

fan:
  - platform: xiaomi_airhumidifier
    name: Xiaomi Air Humidifier
    host: 192.168.130.71
    token: b7c4a758c251955d2c24b1d9e41ce47d

pi@Tommy:~ $ sudo su -s /bin/bash homeassistant
homeassistant@Tommy:/home/pi $ source /srv/homeassistant/bin/activate
(homeassistant) homeassistant@Tommy:/home/pi $ mirobo --ip 192.168.0.25 --token 689c4056fe28ebb3a2e8c2fe350e51ba raw_command get_prop ‘[“power”, “mode”, “temp_dec”, “humidity”, “buzzer”,“led”]’
Sending cmd get_prop with params [‘power’, ‘mode’, ‘temp_dec’, ‘humidity’, ‘buzzer’, ‘led’]
[‘off’, ‘high’, 268, 37, ‘on’, None]
(homeassistant) homeassistant@Tommy:/home/pi $ mirobo --ip 192.168.0.25 --token 689c4056fe28ebb3a2e8c2fe350e51ba raw_command get_prop ‘[“power”, “mode”, “temp_dec”, “humidity”, “buzzer”,“led_b”]’
Sending cmd get_prop with params [‘power’, ‘mode’, ‘temp_dec’, ‘humidity’, ‘buzzer’, ‘led_b’]
[‘off’, ‘high’, 268, 37, ‘on’, 0]
(homeassistant) homeassistant@Tommy:/home/pi $

In the Xiaomi App’s Air Humidifier, there is no LED on/off but there is the LED brightness.
In the LED brightness, 2 is off, 1 is Dim light and 0 is Bright light.
I used it as a sevice in HA 0.60.1 and it worked well.

Perfect! I updated the component.

I always thank you :slight_smile:

Your screenshots are showing some missing features:

  1. Target humidity
  2. Sounds (Volume?)
  3. Parental controls

We can extend the component if you provide some further testing. :slight_smile:

I miscommented for LED level.
2 is off, 1 is Dim light and 0 is Bright light.(These are corrent)

What test do I have to do for extend component?
Please let me know it and I will follow it up~

Please provide the feedback of:

# target humidity?
mirobo --ip 192.168.0.25 --token 689c4056fe28ebb3a2e8c2fe350e51ba raw_command get_prop '["limit_hum"]'
# parental controls?
mirobo --ip 192.168.0.25 --token 689c4056fe28ebb3a2e8c2fe350e51ba raw_command get_prop '["child_lock"]'
# sounds?
mirobo --ip 192.168.0.25 --token 689c4056fe28ebb3a2e8c2fe350e51ba raw_command get_prop '["volume"]'

pi@Tommy:~ $ sudo su -s /bin/bash homeassistant
homeassistant@Tommy:/home/pi $ source /srv/homeassistant/bin/activate
(homeassistant) homeassistant@Tommy:/home/pi $ # target humidity?
mirobo --ip 192.168.0.25 --token 689c4056fe28ebb3a2e8c2fe350e51ba raw_command get_prop ‘[“limit_hum”]’

parental controls?

mirobo --ip 192.168.0.25 --token 689c4056fe28ebb3a2e8c2fe350e51ba raw_command get_prop ‘[“child_lock”]’

sounds?

(homeassistant) homeassistant@Tommy:/home/pi $ mirobo --ip 192.168.0.25 --token 689c4056fe28ebb3a2e8c2fe350e51ba raw_command get_prop ‘[“limit_hum”]’
Sending cmd get_prop with params [‘limit_hum’]
[40]
(homeassistant) homeassistant@Tommy:/home/pi $ # parental controls?
(homeassistant) homeassistant@Tommy:/home/pi $ mirobo --ip 192.168.0.25 --token 689c4056fe28ebb3a2e8c2fe350e51ba raw_command get_prop ‘[“child_lock”]’
Sending cmd get_prop with params [‘child_lock’]
[‘off’]
(homeassistant) homeassistant@Tommy:/home/pi $ # sounds?
(homeassistant) homeassistant@Tommy:/home/pi $ mirobo --ip 192.168.0.25 --token 689c4056fe28ebb3a2e8c2fe350e51ba raw_command get_prop ‘[“volume”]’
Sending cmd get_prop with params [‘volume’]
[None]
(homeassistant) homeassistant@Tommy:/home/pi $

if you need more, pls let me konw it.