Repository: rytilahti/python-miio

Issue:
Roidmi Eva (roidmi.vacuum.v66) with genericmiot
**Describe the bug**
When I pass a command I have the following answer :

miiocli genericmiot --ip xxxxxxxx --token xxxxxxxx  status 
 Running command status
ERROR    Exception: 1 validation error for DeviceModel                                                      click_common.py:57
         services -> 1
           'NoneType' object is not iterable (type=type_error)
         Traceback (most recent call last):
           File "/usr/local/lib/python3.9/dist-packages/miio/click_common.py", line 55, in __call__
             return self.main(*args, **kwargs)
           File "/usr/local/lib/python3.9/dist-packages/click/core.py", line 1078, in main
             rv = self.invoke(ctx)
           File "/usr/local/lib/python3.9/dist-packages/click/core.py", line 1688, in invoke
             return _process_result(sub_ctx.command.invoke(sub_ctx))
           File "/usr/local/lib/python3.9/dist-packages/click/core.py", line 1688, in invoke
             return _process_result(sub_ctx.command.invoke(sub_ctx))
           File "/usr/local/lib/python3.9/dist-packages/click/core.py", line 1434, in invoke
             return ctx.invoke(self.callback, **ctx.params)
           File "/usr/local/lib/python3.9/dist-packages/click/core.py", line 783, in invoke
             return __callback(*args, **kwargs)
           File "/usr/local/lib/python3.9/dist-packages/miio/click_common.py", line 306, in wrap
             result = kwargs["result"] = func(*args, **kwargs)
           File "/usr/local/lib/python3.9/dist-packages/click/decorators.py", line 92, in new_func
             return ctx.invoke(f, obj, *args, **kwargs)
           File "/usr/local/lib/python3.9/dist-packages/click/core.py", line 783, in invoke
             return __callback(*args, **kwargs)
           File "/usr/local/lib/python3.9/dist-packages/miio/click_common.py", line 271, in
         command_callback
             return miio_command.call(miio_device, *args, **kwargs)
           File "/usr/local/lib/python3.9/dist-packages/miio/click_common.py", line 219, in call
             return method(*args, **kwargs)
           File "/usr/local/lib/python3.9/dist-packages/miio/click_common.py", line 186, in _wrap
             return func(self, *args, **kwargs)
           File "/usr/local/lib/python3.9/dist-packages/miio/integrations/genericmiot/genericmiot.py", line
         64, in status
             self._initialize_descriptors()
           File "/usr/local/lib/python3.9/dist-packages/miio/integrations/genericmiot/genericmiot.py", line
         143, in _initialize_descriptors
             self.initialize_model()
           File "/usr/local/lib/python3.9/dist-packages/miio/integrations/genericmiot/genericmiot.py", line
         56, in initialize_model
             self._miot_model = miotcloud.get_device_model(self.model)
           File "/usr/local/lib/python3.9/dist-packages/miio/miot_cloud.py", line 90, in get_device_model
             return DeviceModel.parse_obj(spec)
           File "/usr/local/lib/python3.9/dist-packages/pydantic/v1/main.py", line 526, in parse_obj
             return cls(**obj)
           File "/usr/local/lib/python3.9/dist-packages/miio/miot_models.py", line 456, in __init__
             super().__init__(*args, **kwargs)
           File "/usr/local/lib/python3.9/dist-packages/pydantic/v1/main.py", line 341, in __init__
             raise validation_error
         pydantic.v1.error_wrappers.ValidationError: 1 validation error for DeviceModel
         services -> 1
           'NoneType' object is not iterable (type=type_error)

Maybe because there is 4 version of the roidmi v66?

**Version information (please complete the following information):**
 - OS: Debian 11
 - python-miio:  miiocli, version 0.6.0.dev0

**Device information:**
Model: roidmi.vacuum.v66
Hardware version: Linux
Firmware version: 1.4.6_3271
Supported using: GenericMiot
Supported by genericmiot: True

**To Reproduce**
Steps to reproduce the behavior:
1. Execute the command : miiocli genericmiot --ip xxxxxxxx --token xxxxxxxx  status

Hints from issue comments:
@rytilahti: Thanks for the report! Looks like the schema file contains something unexpected causing it to fail (undefined output for an action), which should be fixed by the linked PR.@JCinside13: Thanks It's working now, perfect@rytilahti: Great, thanks for reporting & testing the fix!

Solution patch:
diff --git a/README.md b/README.md
index e4a93b5..8c0c9c5 100644
--- a/README.md
+++ b/README.md
@@ -50,7 +50,7 @@ Alternatively, you can install the latest development version from GitHub:
     pip install git+https://github.com/rytilahti/python-miio.git
 
 **This project is currently ongoing [a major refactoring effort](https://github.com/rytilahti/python-miio/issues/1114).
-If you are interested in controlling modern (MIoT) devices, you want to use the git version until version 0.6.0 is released.**
+If you are interested in controlling modern (MIoT) devices, you want to use the git version (or pre-releases, `pip install --pre python-miio`) until version 0.6.0 is released.**
 
 ## Getting started
 
diff --git a/miio/miot_models.py b/miio/miot_models.py
index f70e4c9..53592a5 100644
--- a/miio/miot_models.py
+++ b/miio/miot_models.py
@@ -159,6 +159,15 @@ class MiotAction(MiotBaseModel):
     inputs: Any = Field(alias="in")
     outputs: Any = Field(alias="out")
 
+    @root_validator(pre=True)
+    def default_null_to_empty(cls, values):
+        """Coerce null values for in&out to empty lists."""
+        if values["in"] is None:
+            values["in"] = []
+        if values["out"] is None:
+            values["out"] = []
+        return values
+
     def fill_from_parent(self, service: "MiotService"):
         """Overridden to convert inputs and outputs to property references."""
         super().fill_from_parent(service)


Test patch:
diff --git a/miio/tests/test_miot_models.py b/miio/tests/test_miot_models.py
index eaf8d9e..32afb76 100644
--- a/miio/tests/test_miot_models.py
+++ b/miio/tests/test_miot_models.py
@@ -138,6 +138,26 @@ def test_action():
     assert act.plain_name == "dummy-action"
 
 
+def test_action_with_nulls():
+    """Test that actions with null ins and outs are parsed correctly."""
+    simple_action = """\
+    {
+        "iid": 1,
+        "type": "urn:miot-spec-v2:action:dummy-action:0000001:dummy:1",
+        "description": "Description",
+        "in": null,
+        "out": null
+    }"""
+    act = MiotAction.parse_raw(simple_action)
+    assert act.aiid == 1
+    assert act.urn.type == "action"
+    assert act.description == "Description"
+    assert act.inputs == []
+    assert act.outputs == []
+
+    assert act.plain_name == "dummy-action"
+
+
 @pytest.mark.parametrize(
     ("urn_string", "unexpected"),
     [