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

NotImplementedError when trying to get status from Xiaomi Air Purifier 4 Lite (zhimi.airp.rma3) #1892

Open
technyon opened this issue Jan 25, 2024 · 13 comments

Comments

@technyon
Copy link

Describe the bug
I want to control my Xiaomi Air Purifier 4 Lite, it is a model for chinese mainland. It took some work to get it to work outside of china with the smartphone app, and I can connect using miiocli now:

miiocli device --ip 192.168.8.232 --token <redacted> info
Running command info
Model: zhimi.airp.rma3
Hardware version: esp32
Firmware version: 2.2.6_0022
Supported using: GenericMiot
Command: miiocli genericmiot --ip 192.168.8.232 --token <redacted>
Supported by genericmiot: True

If I try however to run the status command, I get a NotImplementedError in device.py:

miiocli device --ip 192.168.8.232 --token <redacted> status                    
Running command status
ERROR    Exception:                                                                      click_common.py:56
         Traceback (most recent call last):                                                                
           File "/usr/local/lib/python3.11/dist-packages/miio/click_common.py", line 54,                   
         in __call__                                                                                       
             return self.main(*args, **kwargs)                                                             
                    ^^^^^^^^^^^^^^^^^^^^^^^^^^                                                             
           File "/usr/local/lib/python3.11/dist-packages/click/core.py", line 1078, in                     
         main                                                                                              
             rv = self.invoke(ctx)                                                                         
                  ^^^^^^^^^^^^^^^^                                                                         
           File "/usr/local/lib/python3.11/dist-packages/click/core.py", line 1688, in                     
         invoke                                                                                            
             return _process_result(sub_ctx.command.invoke(sub_ctx))                                       
                                    ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^                                        
           File "/usr/local/lib/python3.11/dist-packages/click/core.py", line 1688, in                     
         invoke                                                                                            
             return _process_result(sub_ctx.command.invoke(sub_ctx))                                       
                                    ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^                                        
           File "/usr/local/lib/python3.11/dist-packages/click/core.py", line 1434, in                     
         invoke                                                                                            
             return ctx.invoke(self.callback, **ctx.params)                                                
                    ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^                                                
           File "/usr/local/lib/python3.11/dist-packages/click/core.py", line 783, in                      
         invoke                                                                                            
             return __callback(*args, **kwargs)                                                            
                    ^^^^^^^^^^^^^^^^^^^^^^^^^^^                                                            
           File "/usr/local/lib/python3.11/dist-packages/miio/click_common.py", line                       
         305, in wrap                                                                                      
             result = kwargs["result"] = func(*args, **kwargs)                                             
                                         ^^^^^^^^^^^^^^^^^^^^^                                             
           File "/usr/local/lib/python3.11/dist-packages/click/decorators.py", line 92,                    
         in new_func                                                                                       
             return ctx.invoke(f, obj, *args, **kwargs)                                                    
                    ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^                                                    
           File "/usr/local/lib/python3.11/dist-packages/click/core.py", line 783, in                      
         invoke                                                                                            
             return __callback(*args, **kwargs)                                                            
                    ^^^^^^^^^^^^^^^^^^^^^^^^^^^                                                            
           File "/usr/local/lib/python3.11/dist-packages/miio/click_common.py", line                       
         270, in command_callback                                                                          
             return miio_command.call(miio_device, *args, **kwargs)                                        
                    ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^                                        
           File "/usr/local/lib/python3.11/dist-packages/miio/click_common.py", line                       
         218, in call                                                                                      
             return method(*args, **kwargs)                                                                
                    ^^^^^^^^^^^^^^^^^^^^^^^                                                                
           File "/usr/local/lib/python3.11/dist-packages/miio/click_common.py", line                       
         185, in _wrap                                                                                     
             return func(self, *args, **kwargs)                                                            
                    ^^^^^^^^^^^^^^^^^^^^^^^^^^^                                                            
           File "/usr/local/lib/python3.11/dist-packages/miio/device.py", line 259, in                     
         status                                                                                            
             raise NotImplementedError()                                                                   
         NotImplementedError 

Version information (please complete the following information):

  • OS: Ubuntu 23.04
  • python-miio: [Use miiocli --version or pip show python-miio]
    I've installed the latest version directly from github:
# miiocli --version
miiocli, version 0.6.0.dev0

Device information:
If the issue is specific to a device [Use miiocli device --ip <ip address> --token <token> info]:

  • Model: Xiaomi Air Purifier 4 Lite (zhimi.airp.rma3)
  • Hardware version: ?
  • Firmware version: 2.2.6_0022

To Reproduce
Steps to reproduce the behavior:

  1. Run the miiocli status command

Expected behavior
The output of the status command is printed.

Console output
See above.

@technyon technyon added the bug label Jan 25, 2024
@technyon
Copy link
Author

P.S.:

If I run any other "device" command (descriptors, sensors, settings), I get this message:

WARNING 'Device' does not specify any descriptors, please considering creating a PR.

@technyon
Copy link
Author

technyon commented Jan 25, 2024

I've tried adding it to airpurifier_miot.py:

"zhimi.airp.rma3": _MAPPING_RMB1, # airpurifier 4 lite

The result is the same unfortunately.

@rytilahti
Copy link
Owner

rytilahti commented Jan 25, 2024

Did you try the command shown in the info? Try miiocli genericmiot --ip 192.168.8.232 --token <redacted> status :-)

The warning about missing descriptors is unrelated, it's more aimed for other commands than miiocli device.

For the airpurifier_miot.py, the command is miiocli airpurifiermiot.

@technyon
Copy link
Author

I've made some progress. Yes running the suggested command works, I have to say this is very confusing as a user. You run a command which outputs the correct command to run.

I was also able to control the device using "miiocli airpurifiermiot", status gives an empty output though. Calling status on genericmiot only gives me read only properties like temperature and humidity, nothing to control the device. I can control the device though by specifying siid and piid manually like this:

miiocli genericmiot --ip 192.168.8.232 --token set_property_by 2 1 1 bool

Thanks so far, next I'll try to access the API directly using a python script. Would you still classify this as a bug, since the original NotImplementedError is still valid? Otherwise I'd close this issue.

@rytilahti
Copy link
Owner

rytilahti commented Jan 25, 2024

Ohh, I didn't realize genericmiot status is not showing the settable items, that's a bug...

You can try miiocli genericmiot settings (and miiocli genericmiot set for changing values), miiocli genericmiot actions (and miiocli genericmiot call for executing an action) for the time being. For example, on your device:

# set the screen brightness to brightest (0 = off, 1 = bright, 2 = brightest)
miiocli genericmiot set screen:brightness 2

# turn the device off
miiocli genericmiot air-purifier:on 0

# toggle device state
miiocli genericmiot call air-purifier:toggle

Let's keep this issue open, as it's indeed confusing that calling miiocli device errors out like that.

@rytilahti
Copy link
Owner

rytilahti commented Jan 25, 2024

To add to your API question, it really depends on what you want to do.

The easiest way is to use a repl like ipython to explore the API (besides checking out https://github.com/rytilahti/python-miio/#api-usage), as you can use a tab for autocompletion.

Here are some examples (using a simulated rma3):

In [1]: from miio import DeviceFactory

In [2]: dev = DeviceFactory.create("127.0.0.1", 32*"0")

In [3]: dev
Out[3]: <GenericMiot: 127.0.0.1 (token: 00000000000000000000000000000000)>

In [5]: status = dev.status()

In [6]: status.data
Out[6]: 
{'air-purifier:fault': 3,
 'environment:relative-humidity': 38,
 'environment:pm2.5-density': 507,
 'environment:temperature': 70,
 'environment:air-quality': 1,
 'filter:filter-life-level': 13,
 'filter:filter-used-time': 8073,
 'filter:filter-left-time': 434,
 'custom-service:moto-speed-rpm': 2388}

In [7]: status.filter_filter_used_time
Out[7]: 8073

In [8]: settings = dev.settings()

In [9]: list(settings)
Out[9]: 
['air-purifier:on',
 'air-purifier:mode',
 'alarm:alarm',
 'screen:brightness',
 'physical-controls-locked:physical-controls-locked',
 'air-purifier-favorite:fan-level',
 'custom-service:filter-used-time-dbg',
 'aqi:aqi-updata-heartbeat']

In [10]: on = settings["air-purifier:on"]

In [11]: on.setter
Out[11]: functools.partial(<bound method MiotDevice.set_property_by of <GenericMiot: 127.0.0.1 (token: 00000000000000000000000000000000)>>, 2, 1, name='air-purifier:on')

In [12]: on.setter(True)
Out[12]: 0

In [13]: on.setter(False)
Out[13]: 0

In [14]: brightness = settings["screen:brightness"]

In [15]: brightness.choices
Out[15]: <enum 'Brightness'>

In [16]: list(brightness.choices)
Out[16]: [<Brightness.Close: 0>, <Brightness.Bright: 1>, <Brightness.Brightest: 2>]

In [17]: brightness.name
Out[17]: 'Brightness'

In [18]: brightness.setter(1)
Out[18]: 0

Also, if you don't need anything fancy, you can also use the *_by() methods of the MiotDevice class to read & write individual properties.

Alas, proper documentation for API usage is missing at the moment, but let me know what you need and I'll try to help.

@technyon
Copy link
Author

Thanks, control using the set command works.

Good input for exploring the API, will do tomorrow, it's already late here. If all works out I have the idea to add an MQTT client to my code so I can control via iobroker (which runs on nodejs. There's some javascript code available but it hasn't been updated in years unfortunately).

@rytilahti
Copy link
Owner

Sure thing! I think the simplest way to explore would be using a graphical debugger (e.g., pycharm) as it can be overwhelming with all the dynamically constructed data.

@technyon
Copy link
Author

pycharm is already up and running :).

Thanks again.

@rytilahti
Copy link
Owner

Great! Let me know if you find something odd or if you have some ideas what should be included in the docs regarding the API use.

@technyon
Copy link
Author

Sure will do

@technyon
Copy link
Author

technyon commented Jan 27, 2024

I made some progress: I can read the status and publish it to MQTT. Accessing setting and controlling the device works (e. g. turn on/off with on.setter(False)). What I couldn't figure out though, is there a way to get the current value of a setting. It says read/write, so it should be possible to read and not only to write.

@technyon
Copy link
Author

I've figured out how to do it, although it's rather complicated as I did it. Is there an easier way to get the settings current value?

You can check my script at:

https://github.com/technyon/mqmiio/

I've kept it very general, so best case this works for more than just my air purifier.

Also, should we create a seperate issue for this discussion?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

2 participants