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

[miio] Add support for Xiaomi Vacuum Mop 2S #12723

Merged
merged 1 commit into from
Aug 14, 2022
Merged

Conversation

Tiller
Copy link
Contributor

@Tiller Tiller commented May 12, 2022

Hello,
I needed support for the new Xiaomi Vacuum Mop 2S but it didn't exist yet. So I tried to add support by myself and ended-up with this result.
This is not a complete PR as it's missing at the very least an entry in the README, but I wanted your feedback before proceeding.

As I'm quite lazy, I did not write the JSON spec file myself. I extracted the JSON model from https://home.miot-spec.com/spec?type=urn:miot-spec-v2:device:vacuum:0000A006:ijai-v19:1 and wrote this little script to do the conversion for me:

var f = {
	"deviceMapping": {
		"id": [
			"ijai.vacuum.v19"
		],
		"propertyMethod": "get_properties",
		"maxProperties": 1,
		"channels": [
			{
				"property": "",
				"friendlyName": "Actions",
				"channel": "actions",
				"type": "String",
				"stateDescription": {
					"options": []
                },
                "actions": []
            }
        ]
    }
};


var allFormat = [];
var langs = [];

function mapType(property) {
    var format = property.format;

    if (! allFormat.includes(format)) {
        allFormat.push(format);
    }

    if (/^u?int/.test(format)) {
        if (/hour|minute|second/i.test(property.unit)) {
            return "Number:Time";
        } else if (/percent/i.test(property.unit)) {
            return "Number:Dimensionless";
        } else if (/square.?meter/i.test(property.unit)) {
            return "Number:Area";
        }

        return "Number";
    } else if (format == 'string') {
        return "String";
    } else if (format == 'bool') {
        return "Switch";
    }

    return "Number";
}

function mapPropertyType(property) {
    var format = property.format;

    if (/^u?int/.test(format)) {
        return "NUMBER";
    } else if (format == 'string') {
        return "STRING";
    } else if (format == 'bool') {
        return "ONOFFBOOL";
    }
    
    return "Number";
}

for (var siid in s) {
    var service = s[siid];

    for (var aiid in service.actions) {
        var action = service.actions[aiid];

        var res = {
            "command": "action",
            "parameterType": "EMPTY",
            "siid": service.iid,
            "aiid": action.iid,
            "condition": {
                "name": "matchValue",
                "parameters": [
                    {
                        "matchValue": service.name + "-" + action.name
                    }
                ]
            }
        };

        if (action.in.length > 0) {
            res.parameterType = 'NUMBER';
            res.parameters = action.in.map(d => d*1.0);
        }

        f.deviceMapping.channels[0].actions.push(res);
        f.deviceMapping.channels[0].stateDescription.options.push({
            "value": service.name + "-" + action.name,
            "label": action.description
        });
    }

    for (var piid in service.properties) {
        var property = service.properties[piid];

        var channelName = property.prop;
        if (f.deviceMapping.channels.filter(ch => ch.channel == channelName).length > 0) {
            channelName = service.name + '_' + property.prop;
        }

        var res = {
            "property": property.name,
            "siid": service.iid,
            "piid": property.iid,
            "friendlyName": service.description + " - " + property.description,
            "channel": channelName,
            "type": mapType(property),
            "unit": property.unit,
            "stateDescription": {},
            "refresh": true,
            "actions": []
        };

        if (! (property.access || []).includes('write')) {
            res.stateDescription.readOnly = true;
        } else {
            res.actions.push({
                "command": "set_properties",
                "parameterType": mapPropertyType(property)
            });
        }
        if (property['value-range']) {
            var r = property['value-range'];
            res.stateDescription.minimum = r[0];
            res.stateDescription.maximum = r[1];
            res.stateDescription.step = r[2];
        }
        if (property['value-list']) {
            res.stateDescription.options = property['value-list'].map(v => {
                return {
                    value: v.value + '',
                    label: v.description
                };
            });
        }

        f.deviceMapping.channels.push(res);
    }
}

for (var channel of f.deviceMapping.channels) {
    langs.push('ch.' + f.deviceMapping.id + '.' + channel.channel + ' = ' + channel.friendlyName);
}
for (var channel of f.deviceMapping.channels) {
    for (var o of (channel.stateDescription.options||[])) {
        langs.push('option.' + f.deviceMapping.id + '.' + channel.channel + '-' + o.value + ' = ' + o.label);
    }
}

console.log(JSON.stringify(f));
console.log(langs.join("\n"));

And it seems to work (I had time to only test some actions, but it did work).

But! I ended-up with a lot of actions and channels. A lot more than the others vacuums. So my main question is : how do I filter the actions / channels? Is there a criteria I could use? What's the rule on that usually? Can I keep them all?

Thanks

Note: the script could be used to import other devices from miot-spec.com :)

@Tiller Tiller requested a review from marcelrv as a code owner May 12, 2022 08:20
@marcelrv
Copy link
Contributor

marcelrv commented May 12, 2022

As I'm quite lazy, I did not write the JSON spec file myself. I extracted the JSON model from https://home.miot-spec.com/spec?type=urn:miot-spec-v2:device:vacuum:0000A006:ijai-v19:1 and wrote this little script to do the conversion for me:

I would not call that lazy at all ;-)

I think you essentially recreated another version of the miot file creator, right?

https://github.com/openhab/openhab-addons/blob/main/bundles/org.openhab.binding.miio/src/test/java/org/openhab/binding/miio/internal/MiotJsonFileCreator.java

which uses this to do most of the conversion:
https://github.com/openhab/openhab-addons/blob/main/bundles/org.openhab.binding.miio/src/main/java/org/openhab/binding/miio/internal/miot/MiotParser.java

The intention is that this can be even triggered by users if there is an unsupported device. triggering the 'create experimental support' channel aims do make a json file from the online spec.

@marcelrv
Copy link
Contributor

Wrt to the contribution, looks good.

Suggest to try if your device accepts a "maxProperties":1, with a bit higher number as that may speed up the refresh.
If all channels give values you can keep them. the miot versions of the vacuums seem to have indeed many more channels, but some devices choke on having too many requests.

For the regular vacuums I marked several of the channels as 'advanced' but that is currently not possible with the json file created channels. Suggest to keep them unless they don't produce any data or they are really useless. (e.g. I can imagine the download progress channels are not really something that is usable for normal users)

Would be great if you can also run the readme maker, so that the miio readme is updated with the right channels/info

@Tiller
Copy link
Contributor Author

Tiller commented May 12, 2022

Oh... I didn't know it already existed... Would you suggest that I regenerate the json file using the referenced generator instead?
I guess I'll give it a try and compare with mine.

I'll also have a look whether or not all channels returns something or not

@marcelrv
Copy link
Contributor

marcelrv commented May 12, 2022

Oh... I didn't know it already existed... Would you suggest that I regenerate the json file using the referenced generator instead?
I guess I'll give it a try and compare with mine.

Yes, That is always good to do. It will also help with creating the comments for the readme file.

What I normally do is to run the automatic thing and do a comparison with a similar device for validation.
Also the specs have many times missing values (though I did not directly see that in your file) or Chinese descriptions which than can be fixed. Likewise the tags/category can be re-used where applicable (so the users sees right icons etc).

The typical final result is 90% auto generated and 10% manual edits/fixes etc.

@Tiller
Copy link
Contributor Author

Tiller commented May 12, 2022

FYI, the two output are quite similar, but you have some weird behavior on yours:
image
(right side is yours)

The parameterType of the action & the max in the stateDescription is broken (I might have a look into the generator and try to fix it, but I can't promise anything)
Edit: the max might be intentional and '-1' meaning unlimited

@Tiller
Copy link
Contributor Author

Tiller commented May 13, 2022

@marcelrv do you know if there is a way I can convert properties like this in a boolean in openhab?

                        {
                                "property":"repeat-state",
                                "siid":7,
                                "piid":1,
                                "friendlyName":"Sweep - Repeat State",
                                "channel":"repeat_state",
                                "type":"Number",
                                "stateDescription":{
                                        "options":[
                                                {
                                                        "value":"0",
                                                        "label":"Close"
                                                },
                                                {
                                                        "value":"1",
                                                        "label":"Open"
                                                }
                                        ]
                                },
                                "refresh":true,
                                "actions":[
                                        {
                                                "command":"set_properties",
                                                "parameterType":"NUMBER"
                                        }
                                ],
                                "readmeComment":"Value mapping [\"0\"\u003d\"Close\",\"1\"\u003d\"Open\"]"
                        }

I guess if I just put "boolean" it won't work as it'll send a boolean to the vacuum
Also, how can I test the maxProperties? Will I get an error log if the value is too high for the device?

@marcelrv
Copy link
Contributor

marcelrv commented May 13, 2022

@marcelrv do you know if there is a way I can convert properties like this in a boolean in openhab?

I think you can just make it a switch as type.

in that case remove the statedescription section
In the action parameterType you need to use ONOFFNUMBER

@marcelrv
Copy link
Contributor

Also, how can I test the maxProperties? Will I get an error log if the value is too high for the device?

Just set to something like 4 or 5, that is accepted by most devices. If it gives strange responses (e.g. leave out responses or timeouts )you known it is too high

@marcelrv
Copy link
Contributor

The parameterType of the action & the max in the stateDescription is broken (I might have a look into the generator and try to fix it, but I can't promise anything)
Edit: the max might be intentional and '-1' meaning unlimited

Yes, looks like something is off. There are a few shortcuts in the conversion. I have seen the -1 before. I assumed it had something to do with unsigned vs signed number formats, but so far never really digged deep. I mostly remove max like that as it is not useful

@Tiller Tiller force-pushed the 3.2.x branch 4 times, most recently from fd3986f to 02f41bc Compare May 13, 2022 20:07
@Tiller Tiller changed the title [WIP][miio] adding support for ijai.vacuum.v19 : Xiaomi Vacuum Mop 2S [miio] adding support for ijai.vacuum.v19 : Xiaomi Vacuum Mop 2S May 13, 2022
@Tiller
Copy link
Contributor Author

Tiller commented May 13, 2022

Ok, I've tried my best to provide the cleanest mapping possible. You can now review it to merge it

Thanks for the guidances

"piid":1,
"friendlyName":"Disturb - Dnd Enable",
"channel":"dnd_enable",
"type":"Contact",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should this not be a switch? Is the contact working properly?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

From what I saw in openhab's documentation, a Contact is basically a read-only Switch. It seems to work properly, but it does not show a toggle switch, just a text with OPENED/CLOSED. But when I analyze the item, I see the on/off pattern fine.
image

image

Copy link
Contributor

@marcelrv marcelrv left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

few small comments. But looks good.

@marcelrv marcelrv requested a review from fwolter May 13, 2022 20:50
@marcelrv
Copy link
Contributor

as I don't have commit rights, I added another reviewer.

@fwolter
Copy link
Member

fwolter commented May 14, 2022

@Tiller You requested to merge your code into the 3.2.x branch in the repo. This branch is only for bug fixes. main would be correct.

@Tiller
Copy link
Contributor Author

Tiller commented May 14, 2022

@Tiller You requested to merge your code into the 3.2.x branch in the repo. This branch is only for bug fixes. main would be correct.

No possible backport I guess? :(

Signed-off-by: Tiller <github.me@tiller.fr>
@Tiller Tiller changed the base branch from 3.2.x to main May 14, 2022 07:08
@wborn wborn added the enhancement An enhancement or new feature for an existing add-on label Jul 10, 2022
Copy link
Contributor

@jlaur jlaur left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM - thanks!

@jlaur jlaur merged commit bb3be91 into openhab:main Aug 14, 2022
@jlaur jlaur added this to the 3.4 milestone Aug 14, 2022
@jlaur jlaur changed the title [miio] adding support for ijai.vacuum.v19 : Xiaomi Vacuum Mop 2S [miio] Add support for ijai.vacuum.v19: Xiaomi Vacuum Mop 2S Aug 14, 2022
@jlaur jlaur changed the title [miio] Add support for ijai.vacuum.v19: Xiaomi Vacuum Mop 2S [miio] Add support for Xiaomi Vacuum Mop 2S Aug 14, 2022
leifbladt pushed a commit to leifbladt/openhab-addons that referenced this pull request Oct 15, 2022
andan67 pushed a commit to andan67/openhab-addons that referenced this pull request Nov 6, 2022
andrasU pushed a commit to andrasU/openhab-addons that referenced this pull request Nov 12, 2022
…nhab#12723)

Signed-off-by: Tiller <github.me@tiller.fr>
Signed-off-by: Andras Uhrin <andras.uhrin@gmail.com>
psmedley pushed a commit to psmedley/openhab-addons that referenced this pull request Feb 23, 2023
nemerdaud pushed a commit to nemerdaud/openhab-addons that referenced this pull request Feb 28, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement An enhancement or new feature for an existing add-on
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants