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

Add basic support for Xiaomi Mi Air Purifier 3/3H #585

Closed
wants to merge 7 commits into from

Conversation

petrkotek
Copy link
Contributor

@petrkotek petrkotek commented Dec 1, 2019

Add support for basic operations for new Xiaomi Air Purifier 3/3H (zhimi-airpurifier-mb3 and zhimi-airpurifier-ma4 (#577)).

In order to support that, also implement MiotDevice class with basic support for MIoT protocol which the AirPurifierMiot extends to retrieve & set properties.

Mapping is based on #543 (comment) - thanks, it helped a lot!

Note

  • I have not added any tests yet. I'd appreciate first round of a review before I do so.
  • I have not tried the template/generative approach. Thought it would be better just to get it working. I can attempt to do that separately later.

Disclaimer: this is my first contribution to this project & I don't have Python experience.

ToDo:

  • extract Filter Type detection to a separate class
  • add test coverage

Manually tested operations:

  • info (see output below)
  • status (see output below)
  • off
  • on
  • set_buzzer <true|false>
  • set_child_lock <true|false>
  • set_fan_level <1-3>
  • set_favorite_level <0-14>
  • set_led <true|false>
  • set_led_brightness <off|dim|bright>
  • set_mode <auto|fan|favorite|silent>

Example output of info:

$ python miio/cli.py airpurifiermiot --ip $AIR_IP --token $TOKEN info
Model: zhimi.airpurifier.mb3
Hardware version: esp32
Firmware version: 2.0.5
Network: {'localIp': '192.168.0.22', 'mask': '255.255.255.0', 'gw': '192.168.0.1'}
AP: {'ssid': 'coolwifi', 'bssid': '20:E5:2A:3C:BC:82', 'rssi': -59, 'primary': 8}

Example output of status:

$ python miio/cli.py airpurifiermiot --ip $AIR_IP --token $TOKEN status
Power: on
AQI: 3 μg/m³
Average AQI: 6 μg/m³
Humidity: 58 %
Temperature: 25.799999 °C
Mode: OperationMode.Favorite
LED: True
LED brightness: LedBrightness.Dim
Buzzer: False
Child lock: False
Favorite level: 14
Filter life remaining: 98 %
Filter hours used: 59
Use time: 212700 s
Purify volume: 8606 m³
Motor speed: 2148 rpm
Filter RFID product id: 0:0:31:31
Filter RFID tag: 80:66:6a:da:38:bd:4
Filter type: FilterType.Regular

Closes #577

@coveralls
Copy link

coveralls commented Dec 1, 2019

Coverage Status

Coverage increased (+0.4%) to 73.044% when pulling 09f27a5 on petrkotek:air-purifier-3h-support into 1a52117 on rytilahti:master.

@petrkotek petrkotek marked this pull request as ready for review December 1, 2019 13:52
@petrkotek
Copy link
Contributor Author

@rytilahti I'm thinking about extracting the send() functionality to extra class and then using composition rather than inheritance. This would make testing easier. We could trial this for this device and potentially then adopt for other new miot devices.

What do you think?

@rytilahti
Copy link
Owner

@petrkotek I agree that it makes sense to separate the protocol handling out from the device class, so feel free to experiment :-) The pieces are quite convoluted, see also #437 which is somewhat related.

Petr Kotek added 3 commits December 7, 2019 20:45
@petrkotek petrkotek force-pushed the air-purifier-3h-support branch from aefdd96 to 9507181 Compare December 7, 2019 09:59
@c-soft
Copy link

c-soft commented Jan 2, 2020

Gentlemen, I see this is not yet merged in - can I help somehow? I just got the hardware and I can't wait to get it into my Home Assistant.

@ivanxx
Copy link

ivanxx commented Jan 5, 2020

Hi there, I just git cloned this and checked out PR585 to try things locally with my Air Purifier 3, and despite seeing the miot related files, when I run cli.py, airpurifiermiot is not listed, nor it is accepted as a command. Anything wrong with this version cli.py?

@bluemanos
Copy link

Same with me - I have Air Purifier 3 (CN) zhimi-airpurifier-ma4 and your code @petrkotek stoped working.

Error: No such command "airpurifiermiot".

image

@petrkotek
Copy link
Contributor Author

I was on holidays; I'll have a look into this tonight tonight as well as will try to progress with #592 (1st step for getting this merged).

Thanks for understanding!

@petrkotek
Copy link
Contributor Author

Same with me - I have Air Purifier 3 (CN) zhimi-airpurifier-ma4 and your code @petrkotek stoped working.

Error: No such command "airpurifiermiot".

image

Are you sure you're on the right branch?

(python-miio) bash-3.2$ git branch
* air-purifier-3h-support
  air-purifier-3h-support-wip
  command-sender
  master
(python-miio) bash-3.2$ python miio/cli.py | grep miot
  airpurifiermiot

On a separate note, i did notice there was an issue in button_pressed causing an error if no button was pressed since device's start up, so I removed it (at least for now).

… out if no button was pressed since purifier started up
@ivanxx
Copy link

ivanxx commented Jan 7, 2020

Are you sure you're on the right branch?

(python-miio) bash-3.2$ git branch
* air-purifier-3h-support
  air-purifier-3h-support-wip
  command-sender
  master
(python-miio) bash-3.2$ python miio/cli.py | grep miot
  airpurifiermiot

I think so...

ivan@zuncho:~/src$ git clone https://github.com/petrkotek/python-miio.git
Clonando en 'python-miio'...
remote: Enumerating objects: 96, done.
remote: Counting objects: 100% (96/96), done.
remote: Compressing objects: 100% (56/56), done.
remote: Total 2634 (delta 62), reused 58 (delta 40), pack-reused 2538
Recibiendo objetos: 100% (2634/2634), 945.66 KiB | 2.39 MiB/s, listo.
Resolviendo deltas: 100% (1863/1863), listo.
ivan@zuncho:~/src$ cd python-miio/
ivan@zuncho:~/src/python-miio$ git status
En la rama master
Tu rama está actualizada con 'origin/master'.
ivan@zuncho:~/src/python-miio$ git checkout air-purifier-3h-support
Rama 'air-purifier-3h-support' configurada para hacer seguimiento a la rama remota 'air-purifier-3h-support' de 'origin'.
Cambiado a nueva rama 'air-purifier-3h-support'

...but running miio/cli-py:

ivan@zuncho:~/src/python-miio$ python3 miio/cli.py 
Usage: cli.py [OPTIONS] COMMAND [ARGS]...

Options:
  -d, --debug
  -o, --output [default|json|json_pretty]
  --help                          Show this message and exit.

Commands:
  airconditioningcompanion
  airconditioningcompanionv3
  airdehumidifier
  airfresh
  airfresht2017
  airhumidifier
  airhumidifierca1
  airhumidifiercb1
  airhumidifiermjjsq
  airpurifier
  airqualitymonitor
  alarmclock
  aqaracamera
  ceil
  chuangmicamera
  chuangmiir
  chuangmiplug
  cooker
  device
  fan
  fanp5
  fansa1
  fanv2
  fanza1
  fanza3
  fanza4
  philipsbulb
  philipseyecare
  philipsmoonlight
  philipsrwread
  philipswhitebulb
  plug
  plugv1
  plugv3
  powerstrip
  pwznrelay
  toiletlid
  vacuum
  viomivacuum
  waterpurifier
  wifirepeater
  wifispeaker
  yeelight
ivan@zuncho:~/src/python-miio$ git branch
* air-purifier-3h-support
  master

...airpurifiermiot still missing... maybe something is not commited to the repo?

@bluemanos
Copy link

Yep, I'm on the correct branch

image

(sorry for screenshoots, but copy&past my ZSH console gave not readable format to past it here)

@petrkotek
Copy link
Contributor Author

@ivanxx, @bluemanos Thank you both! I'm able to replicate it now on freshly cloned directory.

Doesn't seem to be due to uncommitted files - most importantly, the miio/airpurifier_miot.py is there. Let me look into it a bit deeper.

(Sorry, I'm somewhat a Python newbie, so might be related to that :) )

@petrkotek
Copy link
Contributor Author

@ivanxx, @bluemanos Looks like it might be because previously executed python setup.py install.

This sequence works fine for me:

git clone git@github.com:petrkotek/python-miio.git
cd python-miio
git checkout air-purifier-3h-support
python setup.py install
python miio/cli.py

@curtis86
Copy link

curtis86 commented Jan 8, 2020

@ivanxx, @bluemanos Looks like it might be because previously executed python setup.py install.

This sequence works fine for me:

git clone git@github.com:petrkotek/python-miio.git
cd python-miio
git checkout air-purifier-3h-support
python setup.py install
python miio/cli.py

That's working for me too. I just had to change
git clone git@github.com:petrkotek/python-miio.git
to
git clone https://github.com/petrkotek/python-miio.git

@cisco-devnet
Copy link

cisco-devnet commented Jan 8, 2020

Hello,

First - grats that you did so much in this area and many thanks.

I had the same issue with missing airpurifiermiot option. It was on container with python 3.7.6 (I'm not sure if this is important - but may be). Container was based on Debian GNU/Linux 9 (stretch), but python 3.7 was from testing installed. After this I installed second container based on ubuntu (Ubuntu 18.04.3 LTS), on which I installed python3 (version 3.6.9) - and here I have this airpurifiermiot option.

I'm new here, and I don't have any mi home app installed. I have couple of 2s purifier working like a charm on stub network (they even don't have any DNS and any Gateway - they only take an ip/subnet from dhcp). I used node js with miio for them to obtain token, update wifi network ssid/psk and after this I was able to manage them using node miio or python-miio using the token they sent on first step on their open wifi network.
I thought that here for purifier 3h I can do similar thing. But I don't know yet how to use python-miio to change wifi information, so purifier will connect to my network.
What I observed for now with this 3h purifier.
I can connect to its open wifi network and see the token using node js milo discover. I don't know how to do the same using python-miio. For my understanding in case of python-miio there is unfortunately for me an assumption, that purifier is already connected to correct WiFi and that we have already a token. For this we have to have mi home app (do we?). This is not my case.
Anyway when I'm connected to its open network, I can use your python-miio fork to send for example status command to obtain information. (to be honest I only tested status for now). I can do this - but I noticed, that the value of aqi is wrong. I have four purifiers next to each other to compare the values of aqi (to make sure, there is the same value). I have one pro and two 2s and all of them shown value of 001. The 3h also show value 001, but it returns value of 20! (it was not connected to any network yet)
I received something this:

/python-miio# python3 miio/cli.py -d airpurifiermiot --ip 192.168.4.1 --token d426829f54159729f891faeb6048684e status
INFO:main:Debug mode active
DEBUG:miio.protocol:Unable to decrypt, returning raw bytes: b''
DEBUG:miio.command_sender:Got a response: Container:
data = Container:
data = b'' (total 0)
value = b'' (total 0)
offset1 = 32
offset2 = 32
length = 0
header = Container:
data = b'!1\x00 \x00\x00\x00\x00\x07\x98A\x83\x00\x00\x00\xff' (total 16)
value = Container:
length = 32
unknown = 0
device_id = b'\x07\x98A\x83' (total 4)
ts = 1970-01-01 00:04:15
offset1 = 0
offset2 = 16
length = 16
checksum = b'\xd4&\x82\x9fT\x19W)\xf8\x91\xfa\xeb`HhN' (total 16)
DEBUG:miio.command_sender:Discovered 08894124 with ts: 1970-01-01 00:04:15, token: b'd426829f54159729f891faeb6048684e'
DEBUG:miio.command_sender:192.168.4.1:54321 >>: {'id': 1, 'method': 'get_properties', 'params': [{'did': 'power', 'siid': 2, 'piid': 2}, {'did': 'fan_level', 'siid': 2, 'piid': 4}, {'did': 'mode', 'siid': 2, 'piid': 5}, {'did': 'humidity', 'siid': 3, 'piid': 7}, {'did': 'temperature', 'siid': 3, 'piid': 8}, {'did': 'aqi', 'siid': 3, 'piid': 6}, {'did': 'filter_life_remaining', 'siid': 4, 'piid': 3}, {'did': 'filter_hours_used', 'siid': 4, 'piid': 5}, {'did': 'buzzer', 'siid': 5, 'piid': 1}, {'did': 'led_brightness', 'siid': 6, 'piid': 1}, {'did': 'led', 'siid': 6, 'piid': 6}, {'did': 'child_lock', 'siid': 7, 'piid': 1}, {'did': 'favorite_level', 'siid': 10, 'piid': 10}, {'did': 'set_favorite_rpm', 'siid': 10, 'piid': 7}, {'did': 'motor_speed', 'siid': 10, 'piid': 8}]}
DEBUG:miio.command_sender:192.168.4.1:54321 (ts: 1970-01-01 00:04:15, id: 1) << {'id': 1, 'result': [{'did': 'power', 'siid': 2, 'piid': 2, 'code': 0, 'value': True}, {'did': 'fan_level', 'siid': 2, 'piid': 4, 'code': 0, 'value': 1}, {'did': 'mode', 'siid': 2, 'piid': 5, 'code': 0, 'value': 0}, {'did': 'humidity', 'siid': 3, 'piid': 7, 'code': 0, 'value': 61}, {'did': 'temperature', 'siid': 3, 'piid': 8, 'code': 0, 'value': 18.0}, {'did': 'aqi', 'siid': 3, 'piid': 6, 'code': 0, 'value': 20}, {'did': 'filter_life_remaining', 'siid': 4, 'piid': 3, 'code': 0, 'value': 99}, {'did': 'filter_hours_used', 'siid': 4, 'piid': 5, 'code': 0, 'value': 3}, {'did': 'buzzer', 'siid': 5, 'piid': 1, 'code': 0, 'value': True}, {'did': 'led_brightness', 'siid': 6, 'piid': 1, 'code': 0, 'value': 0}, {'did': 'led', 'siid': 6, 'piid': 6, 'code': 0, 'value': True}, {'did': 'child_lock', 'siid': 7, 'piid': 1, 'code': 0, 'value': False}, {'did': 'favorite_level', 'siid': 10, 'piid': 10, 'code': 0, 'value': 14}, {'did': 'set_favorite_rpm', 'siid': 10, 'piid': 7, 'code': 0, 'value': 2150}, {'did': 'motor_speed', 'siid': 10, 'piid': 8, 'code': 0, 'value': 389}]}
DEBUG:miio.command_sender:192.168.4.1:54321 >>: {'id': 2, 'method': 'get_properties', 'params': [{'did': 'use_time', 'siid': 12, 'piid': 1}, {'did': 'purify_volume', 'siid': 13, 'piid': 1}, {'did': 'average_aqi', 'siid': 13, 'piid': 2}, {'did': 'filter_rfid_tag', 'siid': 14, 'piid': 1}, {'did': 'filter_rfid_product_id', 'siid': 14, 'piid': 3}, {'did': 'app_extra', 'siid': 15, 'piid': 1}]}
DEBUG:miio.command_sender:192.168.4.1:54321 (ts: 1970-01-01 00:04:16, id: 2) << {'id': 2, 'result': [{'did': 'use_time', 'siid': 12, 'piid': 1, 'code': 0, 'value': 10800}, {'did': 'purify_volume', 'siid': 13, 'piid': 1, 'code': 0, 'value': 138}, {'did': 'average_aqi', 'siid': 13, 'piid': 2, 'code': 0, 'value': 2}, {'did': 'filter_rfid_tag', 'siid': 14, 'piid': 1, 'code': 0, 'value': '81:66:5f:81:76:60:4'}, {'did': 'filter_rfid_product_id', 'siid': 14, 'piid': 3, 'code': 0, 'value': '0:0:31:31'}, {'did': 'app_extra', 'siid': 15, 'piid': 1, 'code': 0, 'value': 0}]}
Power: on
AQI: 20 μg/m³
Average AQI: 2 μg/m³
Humidity: 61 %
Temperature: 18.0 °C
Fan Level: 1
Mode: OperationMode.Auto
LED: True
LED brightness: LedBrightness.Bright
Buzzer: True
Child lock: False
Favorite level: 14
Filter life remaining: 99 %
Filter hours used: 3
Use time: 10800 s
Purify volume: 138 m³
Motor speed: 389 rpm
Filter RFID product id: 0:0:31:31
Filter RFID tag: 81:66:5f:81:76:60:4
Filter type: FilterType.Regular
/python-miio#

After this I used node js milo to send information about WiFi network which purifier should be connected to. It connects properly, takes an IP address properly, and probably it changes the token. I can ping it, but I cannot discover it using node js milo nor send direct command using python-miio.

I'm wondering about this message of token set to ffffffffffffffffffffffffffffffff :/ is

/python-miio# python3 miio/cli.py -d airpurifiermiot --ip 192.168.144.24 --token d426829f54159729f891faeb6048684e status
INFO:main:Debug mode active
DEBUG:miio.protocol:Unable to decrypt, returning raw bytes: b''
DEBUG:miio.command_sender:Got a response: Container:
data = Container:
data = b'' (total 0)
value = b'' (total 0)
offset1 = 32
offset2 = 32
length = 0
header = Container:
data = b'!1\x00 \x00\x00\x00\x00\x07\x98A\x83\x00\x00\x01\xc6' (total 16)
value = Container:
length = 32
unknown = 0
device_id = b'\x07\x98A\x83' (total 4)
ts = 1970-01-01 00:07:34
offset1 = 0
offset2 = 16
length = 16
checksum = b'\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff' (total 16)
DEBUG:miio.command_sender:Discovered 08894124 with ts: 1970-01-01 00:07:34, token: b'ffffffffffffffffffffffffffffffff'
DEBUG:miio.command_sender:192.168.144.24:54321 >>: {'id': 1, 'method': 'get_properties', 'params': [{'did': 'power', 'siid': 2, 'piid': 2}, {'did': 'fan_level', 'siid': 2, 'piid': 4}, {'did': 'mode', 'siid': 2, 'piid': 5}, {'did': 'humidity', 'siid': 3, 'piid': 7}, {'did': 'temperature', 'siid': 3, 'piid': 8}, {'did': 'aqi', 'siid': 3, 'piid': 6}, {'did': 'filter_life_remaining', 'siid': 4, 'piid': 3}, {'did': 'filter_hours_used', 'siid': 4, 'piid': 5}, {'did': 'buzzer', 'siid': 5, 'piid': 1}, {'did': 'led_brightness', 'siid': 6, 'piid': 1}, {'did': 'led', 'siid': 6, 'piid': 6}, {'did': 'child_lock', 'siid': 7, 'piid': 1}, {'did': 'favorite_level', 'siid': 10, 'piid': 10}, {'did': 'set_favorite_rpm', 'siid': 10, 'piid': 7}, {'did': 'motor_speed', 'siid': 10, 'piid': 8}]}
DEBUG:miio.command_sender:Retrying with incremented id, retries left: 3
DEBUG:miio.protocol:Unable to decrypt, returning raw bytes: b''
DEBUG:miio.command_sender:Got a response: Container:
data = Container:
data = b'' (total 0)
value = b'' (total 0)
offset1 = 32
offset2 = 32
length = 0
header = Container:
data = b'!1\x00 \x00\x00\x00\x00\x07\x98A\x83\x00\x00\x01\xcb' (total 16)
value = Container:
length = 32
unknown = 0
device_id = b'\x07\x98A\x83' (total 4)
ts = 1970-01-01 00:07:39
offset1 = 0
offset2 = 16
length = 16
checksum = b'\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff' (total 16)
DEBUG:miio.command_sender:Discovered 08894124 with ts: 1970-01-01 00:07:39, token: b'ffffffffffffffffffffffffffffffff'
DEBUG:miio.command_sender:192.168.144.24:54321 >>: {'id': 102, 'method': 'get_properties', 'params': [{'did': 'power', 'siid': 2, 'piid': 2}, {'did': 'fan_level', 'siid': 2, 'piid': 4}, {'did': 'mode', 'siid': 2, 'piid': 5}, {'did': 'humidity', 'siid': 3, 'piid': 7}, {'did': 'temperature', 'siid': 3, 'piid': 8}, {'did': 'aqi', 'siid': 3, 'piid': 6}, {'did': 'filter_life_remaining', 'siid': 4, 'piid': 3}, {'did': 'filter_hours_used', 'siid': 4, 'piid': 5}, {'did': 'buzzer', 'siid': 5, 'piid': 1}, {'did': 'led_brightness', 'siid': 6, 'piid': 1}, {'did': 'led', 'siid': 6, 'piid': 6}, {'did': 'child_lock', 'siid': 7, 'piid': 1}, {'did': 'favorite_level', 'siid': 10, 'piid': 10}, {'did': 'set_favorite_rpm', 'siid': 10, 'piid': 7}, {'did': 'motor_speed', 'siid': 10, 'piid': 8}]}
DEBUG:miio.command_sender:Retrying with incremented id, retries left: 2
DEBUG:miio.protocol:Unable to decrypt, returning raw bytes: b''
DEBUG:miio.command_sender:Got a response: Container:
data = Container:
data = b'' (total 0)
value = b'' (total 0)
offset1 = 32
offset2 = 32
length = 0
header = Container:
data = b'!1\x00 \x00\x00\x00\x00\x07\x98A\x83\x00\x00\x01\xd0' (total 16)
value = Container:
length = 32
unknown = 0
device_id = b'\x07\x98A\x83' (total 4)
ts = 1970-01-01 00:07:44
offset1 = 0
offset2 = 16
length = 16
checksum = b'\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff' (total 16)
DEBUG:miio.command_sender:Discovered 08894124 with ts: 1970-01-01 00:07:44, token: b'ffffffffffffffffffffffffffffffff'
DEBUG:miio.command_sender:192.168.144.24:54321 >>: {'id': 203, 'method': 'get_properties', 'params': [{'did': 'power', 'siid': 2, 'piid': 2}, {'did': 'fan_level', 'siid': 2, 'piid': 4}, {'did': 'mode', 'siid': 2, 'piid': 5}, {'did': 'humidity', 'siid': 3, 'piid': 7}, {'did': 'temperature', 'siid': 3, 'piid': 8}, {'did': 'aqi', 'siid': 3, 'piid': 6}, {'did': 'filter_life_remaining', 'siid': 4, 'piid': 3}, {'did': 'filter_hours_used', 'siid': 4, 'piid': 5}, {'did': 'buzzer', 'siid': 5, 'piid': 1}, {'did': 'led_brightness', 'siid': 6, 'piid': 1}, {'did': 'led', 'siid': 6, 'piid': 6}, {'did': 'child_lock', 'siid': 7, 'piid': 1}, {'did': 'favorite_level', 'siid': 10, 'piid': 10}, {'did': 'set_favorite_rpm', 'siid': 10, 'piid': 7}, {'did': 'motor_speed', 'siid': 10, 'piid': 8}]}
DEBUG:miio.command_sender:Retrying with incremented id, retries left: 1
DEBUG:miio.protocol:Unable to decrypt, returning raw bytes: b''
DEBUG:miio.command_sender:Got a response: Container:
data = Container:
data = b'' (total 0)
value = b'' (total 0)
offset1 = 32
offset2 = 32
length = 0
header = Container:
data = b'!1\x00 \x00\x00\x00\x00\x07\x98A\x83\x00\x00\x01\xd6' (total 16)
value = Container:
length = 32
unknown = 0
device_id = b'\x07\x98A\x83' (total 4)
ts = 1970-01-01 00:07:50
offset1 = 0
offset2 = 16
length = 16
checksum = b'\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff' (total 16)
DEBUG:miio.command_sender:Discovered 08894124 with ts: 1970-01-01 00:07:50, token: b'ffffffffffffffffffffffffffffffff'
DEBUG:miio.command_sender:192.168.144.24:54321 >>: {'id': 304, 'method': 'get_properties', 'params': [{'did': 'power', 'siid': 2, 'piid': 2}, {'did': 'fan_level', 'siid': 2, 'piid': 4}, {'did': 'mode', 'siid': 2, 'piid': 5}, {'did': 'humidity', 'siid': 3, 'piid': 7}, {'did': 'temperature', 'siid': 3, 'piid': 8}, {'did': 'aqi', 'siid': 3, 'piid': 6}, {'did': 'filter_life_remaining', 'siid': 4, 'piid': 3}, {'did': 'filter_hours_used', 'siid': 4, 'piid': 5}, {'did': 'buzzer', 'siid': 5, 'piid': 1}, {'did': 'led_brightness', 'siid': 6, 'piid': 1}, {'did': 'led', 'siid': 6, 'piid': 6}, {'did': 'child_lock', 'siid': 7, 'piid': 1}, {'did': 'favorite_level', 'siid': 10, 'piid': 10}, {'did': 'set_favorite_rpm', 'siid': 10, 'piid': 7}, {'did': 'motor_speed', 'siid': 10, 'piid': 8}]}
ERROR:miio.command_sender:Got error when receiving: timed out
DEBUG:miio.click_common:Exception: No response from the device
Traceback (most recent call last):
File "/usr/local/lib/python3.6/dist-packages/python_miio-0.4.7-py3.6.egg/miio/command_sender.py", line 172, in send
data, addr = s.recvfrom(1024)
socket.timeout: timed out

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
File "/usr/local/lib/python3.6/dist-packages/python_miio-0.4.7-py3.6.egg/miio/command_sender.py", line 172, in send
data, addr = s.recvfrom(1024)
socket.timeout: timed out

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
File "/usr/local/lib/python3.6/dist-packages/python_miio-0.4.7-py3.6.egg/miio/command_sender.py", line 172, in send
data, addr = s.recvfrom(1024)
socket.timeout: timed out

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
File "/usr/local/lib/python3.6/dist-packages/python_miio-0.4.7-py3.6.egg/miio/command_sender.py", line 172, in send
data, addr = s.recvfrom(1024)
socket.timeout: timed out

The above exception was the direct cause of the following exception:

Traceback (most recent call last):
File "/usr/local/lib/python3.6/dist-packages/python_miio-0.4.7-py3.6.egg/miio/click_common.py", line 59, in call
return self.main(*args, **kwargs)
File "/usr/local/lib/python3.6/dist-packages/Click-7.0-py3.6.egg/click/core.py", line 717, in main
rv = self.invoke(ctx)
File "/usr/local/lib/python3.6/dist-packages/Click-7.0-py3.6.egg/click/core.py", line 1137, in invoke
return _process_result(sub_ctx.command.invoke(sub_ctx))
File "/usr/local/lib/python3.6/dist-packages/Click-7.0-py3.6.egg/click/core.py", line 1137, in invoke
return _process_result(sub_ctx.command.invoke(sub_ctx))
File "/usr/local/lib/python3.6/dist-packages/Click-7.0-py3.6.egg/click/core.py", line 956, in invoke
return ctx.invoke(self.callback, **ctx.params)
File "/usr/local/lib/python3.6/dist-packages/Click-7.0-py3.6.egg/click/core.py", line 555, in invoke
return callback(*args, **kwargs)
File "/usr/local/lib/python3.6/dist-packages/python_miio-0.4.7-py3.6.egg/miio/click_common.py", line 280, in wrap
kwargs["result"] = func(*args, **kwargs)
File "/usr/local/lib/python3.6/dist-packages/Click-7.0-py3.6.egg/click/decorators.py", line 64, in new_func
return ctx.invoke(f, obj, *args, **kwargs)
File "/usr/local/lib/python3.6/dist-packages/Click-7.0-py3.6.egg/click/core.py", line 555, in invoke
return callback(*args, **kwargs)
File "/usr/local/lib/python3.6/dist-packages/python_miio-0.4.7-py3.6.egg/miio/click_common.py", line 245, in command_callback
return miio_command.call(miio_device, *args, **kwargs)
File "/usr/local/lib/python3.6/dist-packages/python_miio-0.4.7-py3.6.egg/miio/click_common.py", line 193, in call
return method(*args, **kwargs)
File "/usr/local/lib/python3.6/dist-packages/python_miio-0.4.7-py3.6.egg/miio/airpurifier_miot.py", line 297, in status
{prop["did"]: prop["value"] for prop in self.miot_client.get_properties()}
File "/usr/local/lib/python3.6/dist-packages/python_miio-0.4.7-py3.6.egg/miio/miot_client.py", line 24, in get_properties
values.extend(self.command_sender.send("get_properties", _props[:15]))
File "/usr/local/lib/python3.6/dist-packages/python_miio-0.4.7-py3.6.egg/miio/command_sender.py", line 210, in send
return self.send(command, parameters, retry_count - 1)
File "/usr/local/lib/python3.6/dist-packages/python_miio-0.4.7-py3.6.egg/miio/command_sender.py", line 210, in send
return self.send(command, parameters, retry_count - 1)
File "/usr/local/lib/python3.6/dist-packages/python_miio-0.4.7-py3.6.egg/miio/command_sender.py", line 210, in send
return self.send(command, parameters, retry_count - 1)
File "/usr/local/lib/python3.6/dist-packages/python_miio-0.4.7-py3.6.egg/miio/command_sender.py", line 213, in send
raise DeviceException("No response from the device") from ex
miio.exceptions.DeviceException: No response from the device
Error: No response from the device
/python-miio#

Model info taken from node js milo command run when container was connected to purifiers open WiFi network (after reset of purifier) shown such name of my device:

Model info: zhimi.airpurifier.mb3

Device id is changing (didn't noticed yet if every reset of device or in other way. tokens are changing every reset of device for sure.

What I observed also. When I'm pinging devices - I have responses from 2s or pro in around 1-2ms.
When I'm pinging the 3h device - the delay is around tens or even couple hundreds of ms.

:# ping 192.168.144.24
PING 192.168.144.24 (192.168.144.24) 56(84) bytes of data.
64 bytes from 192.168.144.24: icmp_seq=1 ttl=255 time=116 ms
64 bytes from 192.168.144.24: icmp_seq=2 ttl=255 time=240 ms
64 bytes from 192.168.144.24: icmp_seq=3 ttl=255 time=363 ms
^C
--- 192.168.144.24 ping statistics ---
4 packets transmitted, 3 received, 25% packet loss, time 3003ms
rtt min/avg/max/mdev = 116.331/239.969/363.414/100.872 ms
:
# ping 192.168.144.22
PING 192.168.144.22 (192.168.144.22) 56(84) bytes of data.
64 bytes from 192.168.144.22: icmp_seq=1 ttl=255 time=23.0 ms
64 bytes from 192.168.144.22: icmp_seq=2 ttl=255 time=1.52 ms
64 bytes from 192.168.144.22: icmp_seq=3 ttl=255 time=4.38 ms
64 bytes from 192.168.144.22: icmp_seq=4 ttl=255 time=1.52 ms
^C
--- 192.168.144.22 ping statistics ---
4 packets transmitted, 4 received, 0% packet loss, time 3004ms
rtt min/avg/max/mdev = 1.527/7.612/23.014/8.968 ms
:# ping 192.168.144.23
PING 192.168.144.23 (192.168.144.23) 56(84) bytes of data.
64 bytes from 192.168.144.23: icmp_seq=1 ttl=255 time=1.99 ms
64 bytes from 192.168.144.23: icmp_seq=2 ttl=255 time=12.2 ms
^C
--- 192.168.144.23 ping statistics ---
2 packets transmitted, 2 received, 0% packet loss, time 1001ms
rtt min/avg/max/mdev = 1.998/7.113/12.229/5.116 ms
:
# ping 192.168.144.20
PING 192.168.144.20 (192.168.144.20) 56(84) bytes of data.
64 bytes from 192.168.144.20: icmp_seq=1 ttl=255 time=1.31 ms
64 bytes from 192.168.144.20: icmp_seq=2 ttl=255 time=2.93 ms
64 bytes from 192.168.144.20: icmp_seq=3 ttl=255 time=1.35 ms
^C
--- 192.168.144.20 ping statistics ---
3 packets transmitted, 3 received, 0% packet loss, time 2002ms
rtt min/avg/max/mdev = 1.314/1.866/2.930/0.753 ms
:~# ping 192.168.144.24
PING 192.168.144.24 (192.168.144.24) 56(84) bytes of data.
64 bytes from 192.168.144.24: icmp_seq=1 ttl=255 time=66.6 ms
64 bytes from 192.168.144.24: icmp_seq=2 ttl=255 time=187 ms
64 bytes from 192.168.144.24: icmp_seq=3 ttl=255 time=311 ms
64 bytes from 192.168.144.24: icmp_seq=4 ttl=255 time=122 ms
^C
--- 192.168.144.24 ping statistics ---
4 packets transmitted, 4 received, 0% packet loss, time 3004ms
rtt min/avg/max/mdev = 66.622/171.971/311.179/91.076 ms

devices 20,21,22,23 are 2s and pro, while 24 is 3h.

isn't it wired?

@curtis86
Copy link

curtis86 commented Jan 9, 2020

Hello,

....
devices 20,21,22,23 are 2s and pro, while 24 is 3h.

isn't it wired?

@cisco-devnet - sorry not related to your original issue, but you should probably redact your token from your message above.

@cisco-devnet
Copy link

Hi @curtis86 what do you mean by redact?

Before pasting here outputs I changed device ID, token and ip addresses just in case, although I noticed, that tokens are different for sure every reset of purifier (as I checked on console logs it looks like the same is with device id).

@ivanxx
Copy link

ivanxx commented Jan 9, 2020

@ivanxx, @bluemanos Looks like it might be because previously executed python setup.py install.
This sequence works fine for me:

git clone git@github.com:petrkotek/python-miio.git
cd python-miio
git checkout air-purifier-3h-support
python setup.py install
python miio/cli.py

That's working for me too. I just had to change
git clone git@github.com:petrkotek/python-miio.git
to
git clone https://github.com/petrkotek/python-miio.git

@petrkotek @bluemanos @curtis86 if I go through the setup.py install I get a miioclient executable installed in /usr/local/bin that actually works!! (but I'm still unable to use the python3 miio/cli.py directly, it's still missing the airpurifiermiot command when I run it). All in all, it's good to see it finally working. Readings are consistent with the on-screen data (temp is rounded on the screen) and on/off, set_mode, set_led_brightness, set buzzer... all working. Great job!! Many many thanks @petrkotek

@skvalex
Copy link

skvalex commented Jan 9, 2020

Does it work with no internet connection? Seems like some Xiaomi devices work locally only if they able to connect to Chinese servers. They become uncontrollable without the internet.

@cisco-devnet
Copy link

For sure purifiers 2s and pro can be managed without Internet connection (never connected to Internet).

@bluemanos
Copy link

@petrkotek just want to confirm that the python setup.py install made all magic and now the command is working :)

@lucaam
Copy link

lucaam commented Jan 15, 2020

I don't have any purifier but I am able to get "airpurifiermiot" after executing "python3 setup.py install". @petrkotek I am interested in adding basic support for new mijia vacuum 1c STYTJ01ZHM (dreame.vacuum.mc1808), can you help me somehow understanding how to do it?

@pawelkw
Copy link

pawelkw commented Jan 18, 2020

#585 (comment)

Can confirm that I also see the 20 AQI issue. Couldn't pinpoint when it happens but it does happen. The value on the 3h screen is different.

@cisco-devnet
Copy link

cisco-devnet commented Jan 20, 2020

#585 (comment)

Can confirm that I also see the 20 AQI issue. Couldn't pinpoint when it happens but it does happen. The value on the 3h screen is different.

I noticed that this aqi value 20 (returner via local API) changed after some time (didn't check yet if the same period or random time), but this delay in changing is around 10 maybe even more minutes. I have many purifiers (s2/pro/3h) and only 3h has such delay in changing aqi value returned via this API. In my case all purifiers are cut from the internet at all.
So for example s2, pro and 3h in the same room has the same value on their displays (t0 time). Next I open the window, so polluted air can blow into the room, and display values on devices are changing (increasing). For s2 and pro every API call return proper aqi value (according to what is at the moment on the display) in every t0+n, but not for 3h. 3h returns the same value which was at time t0 during some time (a few minutes). after some time (I didn't have enough time to make real experiment and measure this time) 3h starts to return (via API) increased aqi value, but usually not the same as it has on display (unless air pollution state is constant - not changing - for this time of delay). Its really annoying, and it makes the value of aqi useless for automation. On the other side, the temperature and humidity values are returned rather correctly (also didn't focused on them as much as on aqi values).

@pawelkw is your device connected to Internet? are you using in parallel mi home app?

@pawelkw
Copy link

pawelkw commented Jan 20, 2020

#585 (comment)
Can confirm that I also see the 20 AQI issue. Couldn't pinpoint when it happens but it does happen. The value on the 3h screen is different.

I noticed that this aqi value 20 (returner via local API) changed after some time (didn't check yet if the same period or random time), but this delay in changing is around 10 maybe even more minutes. I have many purifiers (s2/pro/3h) and only 3h has such delay in changing aqi value returned via this API. In my case all purifiers are cut from the internet at all.
So for example s2, pro and 3h in the same room has the same value on their displays (t0 time). Next I open the window, so polluted air can blow into the room, and display values on devices are changing (increasing). For s2 and pro every API call return proper aqi value (according to what is at the moment on the display) in every t0+n, but not for 3h. 3h returns the same value which was at time t0 during some time (a few minutes). after some time (I didn't have enough time to make real experiment and measure this time) 3h starts to return (via API) increased aqi value, but usually not the same as it has on display (unless air pollution state is constant - not changing - for this time of delay). Its really annoying, and it makes the value of aqi useless for automation. On the other side, the temperature and humidity values are returned rather correctly (also didn't focused on them as much as on aqi values).

@pawelkw is your device connected to Internet? are you using in parallel mi home app?

Yes and yes.

@foxel
Copy link
Contributor

foxel commented Feb 4, 2020

Hi.
What's the plan for this PR?
I've checked the code working and eager to see this live and included in HA.
Do you need help with this?

@lucidyan
Copy link

lucidyan commented Feb 12, 2020

@petrkotek Hello!
I have a relatively rare model of Mi Air Purifier Pro H aka zhimi.airpurifier.va1, which has a similar API as 3* models. So, I run in your branch:

from miio import AirPurifierMiot
air_purifier_device = AirPurifierMiot(ip, token)
for prop in air_purifier_device.get_properties():
    print(prop)

...and got:

{'did': 'power', 'siid': 2, 'piid': 2, 'code': 0, 'value': True}
{'did': 'fan_level', 'siid': 2, 'piid': 4, 'code': 0, 'value': 0}
{'did': 'mode', 'siid': 2, 'piid': 5, 'code': 0, 'value': 0}
{'did': 'humidity', 'siid': 3, 'piid': 7, 'code': 0, 'value': 43}
{'did': 'temperature', 'siid': 3, 'piid': 8, 'code': 0, 'value': 24.8}
{'did': 'aqi', 'siid': 3, 'piid': 6, 'code': 0, 'value': 1}
{'did': 'filter_life_remaining', 'siid': 4, 'piid': 3, 'code': 0, 'value': 73}
{'did': 'filter_hours_used', 'siid': 4, 'piid': 5, 'code': 0, 'value': 2109}
{'did': 'buzzer', 'siid': 5, 'piid': 1, 'code': -4001}
{'did': 'led_brightness', 'siid': 6, 'piid': 1, 'code': 0, 'value': 0}
{'did': 'led', 'siid': 6, 'piid': 6, 'code': 0, 'value': True}
{'did': 'child_lock', 'siid': 7, 'piid': 1, 'code': 0, 'value': False}
{'did': 'favorite_level', 'siid': 10, 'piid': 10, 'code': 0, 'value': 9}
{'did': 'set_favorite_rpm', 'siid': 10, 'piid': 7, 'code': 0, 'value': 1530}
{'did': 'motor_speed', 'siid': 10, 'piid': 8, 'code': 0, 'value': 447}
{'did': 'use_time', 'siid': 12, 'piid': 1, 'code': 0, 'value': 7640100}
{'did': 'purify_volume', 'siid': 13, 'piid': 1, 'code': 0, 'value': 331818}
{'did': 'average_aqi', 'siid': 13, 'piid': 2, 'code': 0, 'value': 1}
{'did': 'filter_rfid_tag', 'siid': 14, 'piid': 1, 'code': 0, 'value': '00:00:00:00:00:00:0'} # MAC Changed
{'did': 'filter_rfid_product_id', 'siid': 14, 'piid': 3, 'code': 0, 'value': '0:0:30:38'}
{'did': 'app_extra', 'siid': 15, 'piid': 1, 'code': 0, 'value': 0}

As you can see, I've got an error on parsing this list in the method below:

def status(self) -> AirPurifierMiotStatus:
    """Retrieve properties."""

    return AirPurifierMiotStatus(
        {prop["did"]: prop["value"] for prop in self.miot_client.get_properties()}
    )

But API (http://miot-spec.org/miot-spec-v2/instance?type=urn:miot-spec-v2:device:air-purifier:0000A007:zhimi-va1:2) says that siid and piid are correct.


EDIT:

So I figure out, how make it work.

You can get level of alarm volume (from 0 to 100) with the next command:

air_purifier_device.send(
    "get_properties",[{"did": 'buzzer', 'siid': 5, 'piid': 2}]
)

Also you can set level of it (even app can't):

air_purifier_device.send(
    "set_properties",[{"did": 'buzzer', 'siid': 5, 'piid': 2, "value": 10}]
)

Notification sound toggle in the app becomes selected when you set values >=50.

But I still don't know why other command doesn't work. Maybe something with the bool type?

@tuxuser
Copy link

tuxuser commented Feb 14, 2020

@petrkotek Could you need any assistance in getting this branch in a mergeable state? I would like to help :)

@petrkotek
Copy link
Contributor Author

Hi all! Sorry, I've been a bit busy 😞

I can see that @foxel opened a new (draft) PR - home-assistant/core#31729, so I think it's better if he takes over.

@petrkotek petrkotek closed this Feb 15, 2020
@rytilahti
Copy link
Owner

Hi @petrkotek – we are all volunteers here, so no worries! To my understanding that PR is just about integrating this PR, so I think we should re-open this and figure out what changes are still necessary prior to merging (if any).

Do you mind taking a look what you think is still missing, and rebasing this to the current master? We (@tuxuser and @foxel are seemingly volunteering to help) can help you out from there :-)

@rytilahti
Copy link
Owner

Closing in favor of now-merged #634. Thanks @petrkotek for making this happen :-)

@rytilahti rytilahti closed this Mar 15, 2020
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Support for Xiaomi Air Purifier 3 (zhimi.airpurifier.ma4)