python-miio
python-miio copied to clipboard
Xiaomi Air Purifier 3C (zhimi.airpurifier.mb4) can handle one udp packet at once while offline
While investigating unreliable behavior of my Air Purifier 3C I've tried to determine if my network is at fault or something else. Eventually I've found out that nodejs client miio is much more reliable at discovering my device. The only difference was that node client sent single packet and python-miio
optimistically sends 3 (attached is wireshark log down below).
I've confirmed that this is an issue for by editing miioprotocol.py to only send a single packet for handshake.
This improved situation a lot, however device still becomes unavailable from time to time due to the fact that now that single packet must arrive.
Issue is also present when trying to simultaneously trying to set mode to Favorite
and setting RPM
. Each call sends its own discovery packet and that happens to not work 4 out of 5 times. If one waits for the previous call to finish before doing next all is good.
Version information:
- OS:
Docker/HomeAssistant 2021.10.6
- python-miio:
0.5.8
Device information:
- Model:
zhimi.airpurifier.mb4
- Hardware version:
esp32
- Firmware version:
2.0.8
To Reproduce Steps to reproduce the behavior:
- Disable internet access to your device (dns: reject, else: drop)
- Execute
miiocli -d device --ip 192.168.64.143 --token <redacted> info
- Device does not respond to any of handshake packets (almost always)
Expected behavior
def discover(...)
should wait for each udp packet to timeout independently.
Perhaps calls should be queued for execution ?
Wire shark log
out-hades.zip
192.168.64.143
- Air Purifier 3C
192.168.64.224
- python-miio
192.168.64.225
- nodejs miio
Could you try adding a very short sleep after each try to see if that helps? Could the issue be similar as described in https://github.com/python-kasa/python-kasa/issues/229 (and its linked issue), i.e., related to wifi powersave missing those discovery/handshake packets?
1 second works for the initial discovery, however simultaneous calls still still result in a problem.
I think that the packets are not missed (wifi power saving etc.), since single packet has a higher chance of working than 3. Probably less then perfect firmware quality on that ESP32 :)
EDIT: With 1 sec it still manages to become unavailable from time to time.
After 2 days of use here is a visualization of the effect that change has made.
Remaining issue is due to OpenWrt configuration probably, as I get:
Mon Oct 25 20:31:57 2021 daemon.info hostapd: wlan1-2: STA <mac addr> IEEE 802.11: deauthenticated due to inactivity (timer DEAUTH/REMOVE)
For other unfortunate souls who might have this issue:
https://openwrt.org/faq/deauthenticated_due_to_inactivity
Thanks for the elaborate tests! I'm wondering what'd be the best way to handle this, maybe make the default number of packets device implementation specific?
On the second issue, would you mind adding a note about the deauthenticated issue to the troubleshooting page in the docs?
I'm happy to help :)
Firstly I would opt for changing behavior of def discover
to be more like def send
in that instead of sending n
packets lets wait for a response for the first packet (however no longer than around 1 second) and only retry when no reply is received. If it's going to work it will probably work on first or second try. On second try this translates to around ~1s.
Secondly, regarding the allowed packet count. If not for the fact that there is already a ton of code in here I would probably go for queuing of api calls. This would make it reasonably easy to predefine number of calls allowed to be executed simultaneously on per device basis.
I'm no python expert, but since explicit queuing would probably to problematic I would instead opt for resource locking.
So for example when a call is made, ultimately it will have to reach an instance of MiIOProtocol
and execute def send
, I'm thinking that if on api level method set_whatever
would have to obtain exclusive access to this instance (or some other entity), otherwise wait till it's available this will effectively act as a queue but the change may be confined to a smaller area of code.
See this sample code: https://www.bogotobogo.com/python/Multithread/python_multithreading_Synchronization_Lock_Objects_Acquire_Release.php
PS: That issue with OpenWrt is not totally sorted out on my part (I gave my self a little break) I'm worried that there may be something else wrong. I'm planning on resuming debugging sessions on weekend with different access point (different chipset and software) and see if the issue remains.
This looks to be better suited: https://docs.python.org/3/library/asyncio-sync.html
This would allow to even confine the change to send method only.
I've done some experimenting: https://github.com/gfduszynski/python-miio/commit/d996e9c668c9076e593c1c8cb0971631f5718837
This now works when executing simultaneous calls, like set mode "Favorite" + Speed.
It's would probably be better to use asyncio
instead of threading
library but this is just a proof of concept.
Regarding the OpenWrt issue, I've connected the device to the Ubiquiti AP for now and we will see it it behaves differently.
Device seems to be working better with Ubiquity AP. Dropouts occur about every hour or two for minute or two; it's acceptable for this kind of device.
@rytilahti Hello there, I haven't heard from you recently. Are you ok ?
Maybe my issue is related. My Smartmi Fan 2 can be detected but then immediately becomes unavailable in Home Assistant. Then it sometimes pop backs up randomly allowing for a command or two before going offline again.
It works perfectly reliably in the Mi app and through Homebridge with this plugin so it does seem to be an issue with the python Xiaomi lib. Not sure what to do here.