[miio] Implement lumi devices support for gateways (#11688)

* [miio] Implement lumi devices support for gateways v3 WIP

Adding support for the following models:
* Aqara LED Light Bulb (Tunable White) (modelId: lumi.light.aqcn02)
* IKEA E27 white spectrum opal (modelId: ikea.light.led1545g12)
* IKEA E27 white spectrum clear (modelId: ikea.light.led1546g12)
* IKEA E14 white spectrum (modelId: ikea.light.led1536g5)
* IKEA GU10 white spectrum (modelId: ikea.light.led1537r6)
* IKEA E27 warm white (modelId: ikea.light.led1623g12)
* IKEA GU10 warm white (modelId: ikea.light.led1650r5)
* IKEA E14 warm white (modelId: ikea.light.led1649c5)
* Door lock (modelId: lumi.lock.v1)
* Aqara Door Lock (modelId: lumi.lock.aq1)
* Aqara Door Lock S2 (modelId: lumi.lock.acn02)
* Aqara Door lock S2 Pro (modelId: lumi.lock.acn03)
* Mi Smart Plug (Zigbee) (modelId: lumi.plug.mmeu01)
* Mi Temperature and Humidity Sensor (modelId: lumi.sensor_ht.v1)
* Mi Window and Door Sensor (modelId: lumi.sensor_magnet.v2)
* Mi Motion Sensor (modelId: lumi.sensor_motion.v2)
* Water Leak Sensor (modelId: lumi.sensor_wleak.aq1)
* Aqara Temperature and Humidity Sensor (modelId: lumi.weather.v1)

Signed-off-by: Marcel Verpaalen <marcel@verpaalen.com>

* Work in progress support plug

Signed-off-by: Marcel Verpaalen <marcel@verpaalen.com>

* [miio] cleanup, improve messages and initialization

Signed-off-by: Marcel Verpaalen <marcel@verpaalen.com>

* [miio] Cleanup to prepare for PR

Signed-off-by: Marcel Verpaalen <marcel@verpaalen.com>

* [miio] add missing placeholder

Signed-off-by: Marcel Verpaalen <marcel@verpaalen.com>

* [miio] resolve merge issue

Signed-off-by: Marcel Verpaalen <marcel@verpaalen.com>

* [miio] update readme after rebase

Signed-off-by: Marcel Verpaalen <marcel@verpaalen.com>

* [miio] Update from review comments and warnings/checkstyle cleanup

Signed-off-by: Marcel Verpaalen <marcel@verpaalen.com>

* [miio] update readme after merge and update json to updated format

Signed-off-by: Marcel Verpaalen <marcel@verpaalen.com>

* [miio] Improve online indication

Signed-off-by: Marcel Verpaalen <marcel@verpaalen.com>

* reset

Signed-off-by: Marcel Verpaalen <marcel@verpaalen.com>

* Update readme

Signed-off-by: Marcel Verpaalen <marcel@verpaalen.com>

* [miio] update from review comments

Signed-off-by: Marcel Verpaalen <marcel@verpaalen.com>

* [miio] feedback codereview

Signed-off-by: Marcel Verpaalen <marcel@verpaalen.com>
This commit is contained in:
Marcel 2022-01-22 18:57:01 +01:00 committed by GitHub
parent 03b53475ba
commit d196dc2c92
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
37 changed files with 1922 additions and 521 deletions

View File

@ -15,6 +15,8 @@ The following things types are available:
| miio:generic | Generic type for discovered devices. Once the token is available and the device model is determined, this ThingType will automatically change to the appropriate ThingType | | miio:generic | Generic type for discovered devices. Once the token is available and the device model is determined, this ThingType will automatically change to the appropriate ThingType |
| miio:vacuum | For Xiaomi/RoboRock Robot Vacuum products | | miio:vacuum | For Xiaomi/RoboRock Robot Vacuum products |
| miio:basic | For most other devices like yeelights, airpurifiers. Channels and commands are determined by database configuration | | miio:basic | For most other devices like yeelights, airpurifiers. Channels and commands are determined by database configuration |
| miio:gateway | Similar to basic, but with the Bridge feature, it can support to forward commands for connected devices |
| miio:lumi | Thing type for subdevices connected to the gateway. Note, these devices require a defined gateway to function |
| miio:unsupported | For experimenting with other devices which use the Mi IO protocol or to build experimental support | | miio:unsupported | For experimenting with other devices which use the Mi IO protocol or to build experimental support |
# Discovery # Discovery
@ -86,6 +88,7 @@ However, for devices that are unsupported, you may override the value and try to
Note: Suggest to use the cloud communication only for devices that require it. Note: Suggest to use the cloud communication only for devices that require it.
It is unknown at this time if Xiaomi has a rate limit or other limitations on the cloud usage. e.g. if having many devices would trigger some throttling from the cloud side. It is unknown at this time if Xiaomi has a rate limit or other limitations on the cloud usage. e.g. if having many devices would trigger some throttling from the cloud side.
Note2: communications parameter is not available for lumi devices. Lumi devices communicate using the bridge/gateway.
### Example Thing file ### Example Thing file
@ -95,6 +98,11 @@ or in case of unknown models include the model information of a similar device t
`Thing miio:vacuum:s50 "vacuum" @ "livingroom" [ host="192.168.15.20", token="xxxxxxx", deviceId="326xxxx", model="roborock.vacuum.s4", communication="direct", cloudServer="de" ]` `Thing miio:vacuum:s50 "vacuum" @ "livingroom" [ host="192.168.15.20", token="xxxxxxx", deviceId="326xxxx", model="roborock.vacuum.s4", communication="direct", cloudServer="de" ]`
in case of gateway, instead of defining it as a Thing, use Bridge
`Bridge miio:gateway:lumigateway "Mi Smarter Gateway" [ host="10.10.x.x", token="put here your token", deviceId="326xxxx", model="lumi.gateway.mieu01", communication="direct", cloudServer="de" ]`
# Advanced: Unsupported devices # Advanced: Unsupported devices
Newer devices may not yet be supported. Newer devices may not yet be supported.
@ -177,6 +185,9 @@ This will change the communication method and the Mi IO binding can communicate
# Mi IO Devices # Mi IO Devices
!!!devices !!!devices
note: Supported means we received feedback from users this device is working with the binding.
For devices with experimental support, we did not yet confirmation that channels are correctly working.
Please feedback your findings for these devices (e.g. Are all channels working, do they contain the right information, is controlling the devices working etc.)
# Channels # Channels

File diff suppressed because it is too large Load Diff

View File

@ -36,15 +36,18 @@ public final class MiIoBindingConstants {
// List of all Thing Type UIDs // List of all Thing Type UIDs
public static final ThingTypeUID THING_TYPE_MIIO = new ThingTypeUID(BINDING_ID, "generic"); public static final ThingTypeUID THING_TYPE_MIIO = new ThingTypeUID(BINDING_ID, "generic");
public static final ThingTypeUID THING_TYPE_BASIC = new ThingTypeUID(BINDING_ID, "basic"); public static final ThingTypeUID THING_TYPE_BASIC = new ThingTypeUID(BINDING_ID, "basic");
public static final ThingTypeUID THING_TYPE_LUMI = new ThingTypeUID(BINDING_ID, "lumi");
public static final ThingTypeUID THING_TYPE_GATEWAY = new ThingTypeUID(BINDING_ID, "gateway");
public static final ThingTypeUID THING_TYPE_VACUUM = new ThingTypeUID(BINDING_ID, "vacuum"); public static final ThingTypeUID THING_TYPE_VACUUM = new ThingTypeUID(BINDING_ID, "vacuum");
public static final ThingTypeUID THING_TYPE_UNSUPPORTED = new ThingTypeUID(BINDING_ID, "unsupported"); public static final ThingTypeUID THING_TYPE_UNSUPPORTED = new ThingTypeUID(BINDING_ID, "unsupported");
public static final Set<ThingTypeUID> SUPPORTED_THING_TYPES_UIDS = Collections public static final Set<ThingTypeUID> SUPPORTED_THING_TYPES_UIDS = Collections
.unmodifiableSet(Stream.of(THING_TYPE_MIIO, THING_TYPE_BASIC, THING_TYPE_VACUUM, THING_TYPE_UNSUPPORTED) .unmodifiableSet(Stream.of(THING_TYPE_MIIO, THING_TYPE_BASIC, THING_TYPE_LUMI, THING_TYPE_GATEWAY,
.collect(Collectors.toSet())); THING_TYPE_VACUUM, THING_TYPE_UNSUPPORTED).collect(Collectors.toSet()));
public static final Set<ThingTypeUID> NONGENERIC_THING_TYPES_UIDS = Collections.unmodifiableSet( public static final Set<ThingTypeUID> NONGENERIC_THING_TYPES_UIDS = Collections.unmodifiableSet(
Stream.of(THING_TYPE_BASIC, THING_TYPE_VACUUM, THING_TYPE_UNSUPPORTED).collect(Collectors.toSet())); Stream.of(THING_TYPE_BASIC, THING_TYPE_LUMI, THING_TYPE_GATEWAY, THING_TYPE_VACUUM, THING_TYPE_UNSUPPORTED)
.collect(Collectors.toSet()));
// List of all Channel IDs // List of all Channel IDs
public static final String CHANNEL_BATTERY = "status#battery"; public static final String CHANNEL_BATTERY = "status#battery";

View File

@ -97,6 +97,24 @@ public enum MiIoCommand {
GET_MULTI_MAP_LIST("get_multi_maps_list"), GET_MULTI_MAP_LIST("get_multi_maps_list"),
GET_ROOM_MAPPING("get_room_mapping"), GET_ROOM_MAPPING("get_room_mapping"),
// Gateway & child device commands
GET_ARMING("get_arming"),
GET_ARMING_TIME("get_arming_time"),
GET_DOORBEL_VOLUME("get_doorbell_volume"),
GET_GATEWAY_VOLUME("get_gateway_volume"),
GET_ALARMING_VOLUME("get_alarming_volume"),
GET_CLOCK_VOLUME("get_clock_volume"),
GET_DOORBELL_VOLUME("get_doorbell_volume"),
GET_ARM_WAIT_TIME("get_arm_wait_time"),
ALARM_TIME_LEN("alarm_time_len"),
EN_ALARM_LIGHT("en_alarm_light"),
GET_CORRIDOR_ON_TIME("get_corridor_on_time"),
GET_ZIGBEE_CHANNEL("get_zigbee_channel"),
GET_RGB("get_rgb"),
GET_NIGHTLIGHT_RGB("get_night_light_rgb"),
GET_LUMI_BIND("get_lumi_bind"),
GET_PROP_PLUG("get_prop_plug"),
UNKNOWN(""); UNKNOWN("");
private final String command; private final String command;

View File

@ -95,16 +95,35 @@ public enum MiIoDevices {
HUAYI_LIGHT_ZW131("huayi.light.zw131", "HUIZUO ZIWEI Ceiling Lamp", THING_TYPE_BASIC), HUAYI_LIGHT_ZW131("huayi.light.zw131", "HUIZUO ZIWEI Ceiling Lamp", THING_TYPE_BASIC),
HUNMI_COOKER_NORMAL3("hunmi.cooker.normal3", "MiJia Rice Cooker", THING_TYPE_UNSUPPORTED), HUNMI_COOKER_NORMAL3("hunmi.cooker.normal3", "MiJia Rice Cooker", THING_TYPE_UNSUPPORTED),
IDELAN_AIRCONDITION_V1("idelan.aircondition.v1", "Jinxing Smart Air Conditioner", THING_TYPE_UNSUPPORTED), IDELAN_AIRCONDITION_V1("idelan.aircondition.v1", "Jinxing Smart Air Conditioner", THING_TYPE_UNSUPPORTED),
IKEA_LIGHT_LED1545G12("ikea.light.led1545g12", "IKEA E27 white spectrum opal", THING_TYPE_LUMI),
IKEA_LIGHT_LED1546G12("ikea.light.led1546g12", "IKEA E27 white spectrum clear", THING_TYPE_LUMI),
IKEA_LIGHT_LED1536G5("ikea.light.led1536g5", "IKEA E14 white spectrum", THING_TYPE_LUMI),
IKEA_LIGHT_LED1537R6("ikea.light.led1537r6", "IKEA GU10 white spectrum", THING_TYPE_LUMI),
IKEA_LIGHT_LED1623G12("ikea.light.led1623g12", "IKEA E27 warm white", THING_TYPE_LUMI),
IKEA_LIGHT_LED1650R5("ikea.light.led1650r5", "IKEA GU10 warm white", THING_TYPE_LUMI),
IKEA_LIGHT_LED1649C5("ikea.light.led1649c5", "IKEA E14 warm white", THING_TYPE_LUMI),
LUMI_CTRL_NEUTRAL1_V1("lumi.ctrl_neutral1.v1", "Aqara Wall Switch(No Neutral, Single Rocker)", LUMI_CTRL_NEUTRAL1_V1("lumi.ctrl_neutral1.v1", "Aqara Wall Switch(No Neutral, Single Rocker)",
THING_TYPE_UNSUPPORTED), THING_TYPE_UNSUPPORTED),
LUMI_CTRL_NEUTRAL2_V1("lumi.ctrl_neutral2.v1", "Aqara Wall Switch (No Neutral, Double Rocker)", LUMI_CTRL_NEUTRAL2_V1("lumi.ctrl_neutral2.v1", "Aqara Wall Switch (No Neutral, Double Rocker)",
THING_TYPE_UNSUPPORTED), THING_TYPE_UNSUPPORTED),
LUMI_CURTAIN_HAGL05("lumi.curtain.hagl05", "Xiaomiyoupin Curtain Controller (Wi-Fi)", THING_TYPE_BASIC), LUMI_CURTAIN_HAGL05("lumi.curtain.hagl05", "Xiaomiyoupin Curtain Controller (Wi-Fi)", THING_TYPE_BASIC),
LUMI_GATEWAY_MGL03("lumi.gateway.mgl03", "Mi Air Purifier virtual", THING_TYPE_BASIC), LUMI_GATEWAY_MGL03("lumi.gateway.mgl03", "Mi Air Purifier virtual", THING_TYPE_GATEWAY),
LUMI_GATEWAY_V1("lumi.gateway.v1", "Mi smart Home Gateway Hub v1", THING_TYPE_BASIC), LUMI_GATEWAY_MIEU01("lumi.gateway.mieu01", "Mi smart Home Gateway Hub", THING_TYPE_GATEWAY),
LUMI_GATEWAY_V2("lumi.gateway.v2", "Mi smart Home GatewayHub v2", THING_TYPE_BASIC), LUMI_GATEWAY_V1("lumi.gateway.v1", "Mi smart Home Gateway Hub v1", THING_TYPE_GATEWAY),
LUMI_GATEWAY_V3("lumi.gateway.v3", "Mi smart Home Gateway Hub v3", THING_TYPE_BASIC), LUMI_GATEWAY_V2("lumi.gateway.v2", "Mi smart Home GatewayHub v2", THING_TYPE_GATEWAY),
LUMI_GATEWAY_MIEU01("lumi.gateway.mieu01", "Mi smart Home Gateway Hub", THING_TYPE_BASIC), LUMI_GATEWAY_V3("lumi.gateway.v3", "Mi smart Home Gateway Hub v3", THING_TYPE_GATEWAY),
LUMI_LIGHT_AQCN02("lumi.light.aqcn02", "Aqara LED Light Bulb (Tunable White)", THING_TYPE_LUMI),
LUMI_LOCK_V1("lumi.lock.v1", "Door lock", THING_TYPE_LUMI),
LUMI_LOCK_AQ1("lumi.lock.aq1", "Aqara Door Lock", THING_TYPE_LUMI),
LUMI_LOCK_ACN02("lumi.lock.acn02", "Aqara Door Lock S2", THING_TYPE_LUMI),
LUMI_LOCK_ACN03("lumi.lock.acn03", "Aqara Door lock S2 Pro", THING_TYPE_LUMI),
LUMI_PLUG_MMEU01("lumi.plug.mmeu01", "Mi Smart Plug (Zigbee)", THING_TYPE_LUMI),
LUMI_SENSOR_MAGNET_V2("lumi.sensor_magnet.v2", "Mi Window and Door Sensor", THING_TYPE_LUMI),
LUMI_SENSOR_MOTION_AQ2("lumi.sensor_motion.aq2", "Mi Motion Sensor", THING_TYPE_LUMI),
LUMI_SENSOR_MOTION_V2("lumi.sensor_motion.v2", "Mi Motion Sensor", THING_TYPE_LUMI),
LUMI_SENSOR_HT_V1("lumi.sensor_ht.v1", "Mi Temperature and Humidity Sensor", THING_TYPE_LUMI),
LUMI_SENSOR_WLEAK_AQ1("lumi.sensor_wleak.aq1", "Water Leak Sensor", THING_TYPE_LUMI),
LUMI_WEATHER_V1("lumi.weather.v1", "Aqara Temperature and Humidity Sensor", THING_TYPE_LUMI),
MIDEA_AIRCONDITION_V1("midea.aircondition.v1", "Midea AC-i Youth", THING_TYPE_UNSUPPORTED), MIDEA_AIRCONDITION_V1("midea.aircondition.v1", "Midea AC-i Youth", THING_TYPE_UNSUPPORTED),
MIDEA_AIRCONDITION_V2("midea.aircondition.v2", "Midea Air Conditioner v2", THING_TYPE_UNSUPPORTED), MIDEA_AIRCONDITION_V2("midea.aircondition.v2", "Midea Air Conditioner v2", THING_TYPE_UNSUPPORTED),
MIDEA_AIRCONDITION_XA1("midea.aircondition.xa1", "Midea AC-Cool Golden", THING_TYPE_UNSUPPORTED), MIDEA_AIRCONDITION_XA1("midea.aircondition.xa1", "Midea AC-Cool Golden", THING_TYPE_UNSUPPORTED),
@ -139,7 +158,7 @@ public enum MiIoDevices {
PHILIPS_LIGHT_MONO1("philips.light.mono1", "Philips Smart Lamp", THING_TYPE_BASIC), PHILIPS_LIGHT_MONO1("philips.light.mono1", "Philips Smart Lamp", THING_TYPE_BASIC),
PHILIPS_LIGHT_MOONLIGHT("philips.light.moonlight", "Philips ZhiRui Bedside Lamp", THING_TYPE_BASIC), PHILIPS_LIGHT_MOONLIGHT("philips.light.moonlight", "Philips ZhiRui Bedside Lamp", THING_TYPE_BASIC),
PHILIPS_LIGHT_OBCEIL("philips.light.obceil", "Zhirui Ceiling Lamp Black 80W", THING_TYPE_BASIC), PHILIPS_LIGHT_OBCEIL("philips.light.obceil", "Zhirui Ceiling Lamp Black 80W", THING_TYPE_BASIC),
PHILIPS_LIGHT_OBCEIM("philips.light.obceim", " Zhirui Ceiling Lamp Black 40W", THING_TYPE_BASIC), PHILIPS_LIGHT_OBCEIM("philips.light.obceim", "Zhirui Ceiling Lamp Black 40W", THING_TYPE_BASIC),
PHILIPS_LIGHT_OBCEIS("philips.light.obceis", "Zhirui Ceiling Lamp Black 28W", THING_TYPE_BASIC), PHILIPS_LIGHT_OBCEIS("philips.light.obceis", "Zhirui Ceiling Lamp Black 28W", THING_TYPE_BASIC),
PHILIPS_LIGHT_RWREAD("philips.light.rwread", "Mijia Philips Study Desk Lamp", THING_TYPE_BASIC), PHILIPS_LIGHT_RWREAD("philips.light.rwread", "Mijia Philips Study Desk Lamp", THING_TYPE_BASIC),
PHILIPS_LIGHT_SCEIL("philips.light.sceil", "Zhirui Ceiling Lamp Starry 80W", THING_TYPE_BASIC), PHILIPS_LIGHT_SCEIL("philips.light.sceil", "Zhirui Ceiling Lamp Starry 80W", THING_TYPE_BASIC),
@ -404,7 +423,7 @@ public enum MiIoDevices {
public static MiIoDevices getType(String modelString) { public static MiIoDevices getType(String modelString) {
for (MiIoDevices mioDev : MiIoDevices.values()) { for (MiIoDevices mioDev : MiIoDevices.values()) {
if (mioDev.getModel().equals(modelString)) { if (mioDev.getModel().equalsIgnoreCase(modelString)) {
return mioDev; return mioDev;
} }
} }

View File

@ -25,13 +25,16 @@ import org.openhab.binding.miio.internal.basic.BasicChannelTypeProvider;
import org.openhab.binding.miio.internal.basic.MiIoDatabaseWatchService; import org.openhab.binding.miio.internal.basic.MiIoDatabaseWatchService;
import org.openhab.binding.miio.internal.cloud.CloudConnector; import org.openhab.binding.miio.internal.cloud.CloudConnector;
import org.openhab.binding.miio.internal.handler.MiIoBasicHandler; import org.openhab.binding.miio.internal.handler.MiIoBasicHandler;
import org.openhab.binding.miio.internal.handler.MiIoGatewayHandler;
import org.openhab.binding.miio.internal.handler.MiIoGenericHandler; import org.openhab.binding.miio.internal.handler.MiIoGenericHandler;
import org.openhab.binding.miio.internal.handler.MiIoLumiHandler;
import org.openhab.binding.miio.internal.handler.MiIoUnsupportedHandler; import org.openhab.binding.miio.internal.handler.MiIoUnsupportedHandler;
import org.openhab.binding.miio.internal.handler.MiIoVacuumHandler; import org.openhab.binding.miio.internal.handler.MiIoVacuumHandler;
import org.openhab.core.common.ThreadPoolManager; import org.openhab.core.common.ThreadPoolManager;
import org.openhab.core.i18n.LocaleProvider; import org.openhab.core.i18n.LocaleProvider;
import org.openhab.core.i18n.TranslationProvider; import org.openhab.core.i18n.TranslationProvider;
import org.openhab.core.io.net.http.HttpClientFactory; import org.openhab.core.io.net.http.HttpClientFactory;
import org.openhab.core.thing.Bridge;
import org.openhab.core.thing.Thing; import org.openhab.core.thing.Thing;
import org.openhab.core.thing.ThingTypeUID; import org.openhab.core.thing.ThingTypeUID;
import org.openhab.core.thing.binding.BaseThingHandlerFactory; import org.openhab.core.thing.binding.BaseThingHandlerFactory;
@ -122,6 +125,14 @@ public class MiIoHandlerFactory extends BaseThingHandlerFactory {
return new MiIoBasicHandler(thing, miIoDatabaseWatchService, cloudConnector, channelTypeRegistry, return new MiIoBasicHandler(thing, miIoDatabaseWatchService, cloudConnector, channelTypeRegistry,
basicChannelTypeProvider, i18nProvider, localeProvider); basicChannelTypeProvider, i18nProvider, localeProvider);
} }
if (thingTypeUID.equals(THING_TYPE_LUMI)) {
return new MiIoLumiHandler(thing, miIoDatabaseWatchService, cloudConnector, channelTypeRegistry,
basicChannelTypeProvider, i18nProvider, localeProvider);
}
if (thingTypeUID.equals(THING_TYPE_GATEWAY)) {
return new MiIoGatewayHandler((Bridge) thing, miIoDatabaseWatchService, cloudConnector, channelTypeRegistry,
basicChannelTypeProvider, i18nProvider, localeProvider);
}
if (thingTypeUID.equals(THING_TYPE_VACUUM)) { if (thingTypeUID.equals(THING_TYPE_VACUUM)) {
return new MiIoVacuumHandler(thing, miIoDatabaseWatchService, cloudConnector, channelTypeRegistry, return new MiIoVacuumHandler(thing, miIoDatabaseWatchService, cloudConnector, channelTypeRegistry,
i18nProvider, localeProvider); i18nProvider, localeProvider);

View File

@ -22,6 +22,7 @@ import org.openhab.core.library.types.PercentType;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import com.google.gson.JsonArray;
import com.google.gson.JsonElement; import com.google.gson.JsonElement;
import com.google.gson.JsonObject; import com.google.gson.JsonObject;
import com.google.gson.JsonParseException; import com.google.gson.JsonParseException;
@ -79,6 +80,33 @@ public class Conversions {
return rgbValue; return rgbValue;
} }
public static JsonElement deviceDataTab(JsonElement deviceLog, @Nullable Map<String, Object> deviceVariables)
throws ClassCastException, IllegalStateException {
if (!deviceLog.isJsonObject() && !deviceLog.isJsonPrimitive()) {
return deviceLog;
}
JsonObject deviceLogJsonObj = deviceLog.isJsonObject() ? deviceLog.getAsJsonObject()
: (JsonObject) JsonParser.parseString(deviceLog.getAsString());
JsonArray resultLog = new JsonArray();
if (deviceLogJsonObj.has("data") && deviceLogJsonObj.get("data").isJsonArray()) {
for (JsonElement element : deviceLogJsonObj.get("data").getAsJsonArray()) {
if (element.isJsonObject()) {
JsonObject dataObject = element.getAsJsonObject();
if (dataObject.has("value")) {
String value = dataObject.get("value").getAsString();
JsonElement val = JsonParser.parseString(value);
if (val.isJsonArray()) {
resultLog.add(JsonParser.parseString(val.getAsString()));
} else {
resultLog.add(val);
}
}
}
}
}
return resultLog;
}
private static JsonElement secondsToHours(JsonElement seconds) throws ClassCastException { private static JsonElement secondsToHours(JsonElement seconds) throws ClassCastException {
double value = seconds.getAsDouble() / 3600; double value = seconds.getAsDouble() / 3600;
return new JsonPrimitive(value); return new JsonPrimitive(value);
@ -190,6 +218,8 @@ public class Conversions {
return addBrightToHSV(value, deviceVariables); return addBrightToHSV(value, deviceVariables);
case "BRGBTOHSV": case "BRGBTOHSV":
return bRGBtoHSV(value); return bRGBtoHSV(value);
case "DEVICEDATATAB":
return deviceDataTab(value, deviceVariables);
case "GETDIDELEMENT": case "GETDIDELEMENT":
return getDidElement(value, deviceVariables); return getDidElement(value, deviceVariables);
default: default:

View File

@ -114,6 +114,9 @@ public class MiIoDatabaseWatchService extends AbstractWatchService {
try { try {
JsonObject deviceMapping = Utils.convertFileToJSON(db); JsonObject deviceMapping = Utils.convertFileToJSON(db);
MiIoBasicDevice devdb = GSON.fromJson(deviceMapping, MiIoBasicDevice.class); MiIoBasicDevice devdb = GSON.fromJson(deviceMapping, MiIoBasicDevice.class);
if (devdb == null) {
continue;
}
for (String id : devdb.getDevice().getId()) { for (String id : devdb.getDevice().getId()) {
workingDatabaseList.put(id, db); workingDatabaseList.put(id, db);
} }

View File

@ -152,7 +152,7 @@ public class MiCloudConnector {
Map<String, String> map = new HashMap<String, String>(); Map<String, String> map = new HashMap<String, String>();
map.put("data", "{\"obj_name\":\"" + vacuumMap + "\"}"); map.put("data", "{\"obj_name\":\"" + vacuumMap + "\"}");
String mapResponse = request(url, map); String mapResponse = request(url, map);
logger.trace("response: {}", mapResponse); logger.trace("Response: {}", mapResponse);
String errorMsg = ""; String errorMsg = "";
try { try {
JsonElement response = JsonParser.parseString(mapResponse); JsonElement response = JsonParser.parseString(mapResponse);
@ -181,7 +181,6 @@ public class MiCloudConnector {
public String getDeviceStatus(String device, String country) throws MiCloudException { public String getDeviceStatus(String device, String country) throws MiCloudException {
final String response = request("/home/device_list", country, "{\"dids\":[\"" + device + "\"]}"); final String response = request("/home/device_list", country, "{\"dids\":[\"" + device + "\"]}");
logger.debug("response: {}", response);
return response; return response;
} }
@ -201,7 +200,6 @@ public class MiCloudConnector {
throw new MiCloudException(err, e); throw new MiCloudException(err, e);
} }
final String response = request("/home/rpc/" + id, country, command); final String response = request("/home/rpc/" + id, country, command);
logger.debug("response: {}", response);
return response; return response;
} }

View File

@ -92,6 +92,7 @@ public class MiIoDiscovery extends AbstractDiscoveryService {
} }
private String getCloudDiscoveryMode() { private String getCloudDiscoveryMode() {
final Configuration miioConfig = this.miioConfig;
if (miioConfig != null) { if (miioConfig != null) {
try { try {
Dictionary<String, @Nullable Object> properties = miioConfig.getProperties(); Dictionary<String, @Nullable Object> properties = miioConfig.getProperties();
@ -185,20 +186,23 @@ public class MiIoDiscovery extends AbstractDiscoveryService {
List<CloudDeviceDTO> dv = cloudConnector.getDevicesList(); List<CloudDeviceDTO> dv = cloudConnector.getDevicesList();
for (CloudDeviceDTO device : dv) { for (CloudDeviceDTO device : dv) {
String id = device.getDid(); String id = device.getDid();
if (cloudDiscoveryMode.contentEquals(SUPPORTED)) { if (SUPPORTED.contentEquals(cloudDiscoveryMode)) {
if (MiIoDevices.getType(device.getModel()).getThingType().equals(THING_TYPE_UNSUPPORTED)) { if (MiIoDevices.getType(device.getModel()).getThingType().equals(THING_TYPE_UNSUPPORTED)) {
logger.warn("Discovered from cloud, but ignored because not supported: {} {}", id, device); logger.debug("Discovered from cloud, but ignored because not supported: {} {}", id, device);
} }
} }
if (device.getIsOnline()) { if (device.getIsOnline() || ALL.contentEquals(cloudDiscoveryMode)) {
logger.debug("Discovered from cloud: {} {}", id, device); logger.debug("Discovered from cloud: {} {}", id, device);
cloudDevices.put(id, device.getLocalip()); cloudDevices.put(id, device.getLocalip());
String token = device.getToken(); String token = device.getToken();
String label = device.getName() + " " + id + " (" + Utils.getHexId(id) + ")"; String label = device.getName() + " (" + id + (id.contains(".") ? "" : " / " + Utils.getHexId(id))
+ ")";
String model = device.getModel();
String country = device.getServer(); String country = device.getServer();
boolean isOnline = device.getIsOnline(); boolean isOnline = device.getIsOnline();
String parent = device.getParentId();
String ip = device.getLocalip(); String ip = device.getLocalip();
submitDiscovery(ip, token, id, label, country, isOnline); submitDiscovery(ip, token, id, label, model, country, isOnline, parent);
} else { } else {
logger.debug("Discovered from cloud, but ignored because not online: {} {}", id, device); logger.debug("Discovered from cloud, but ignored because not online: {} {}", id, device);
} }
@ -212,8 +216,10 @@ public class MiIoDiscovery extends AbstractDiscoveryService {
String token = Utils.getHex(msg.getChecksum()); String token = Utils.getHex(msg.getChecksum());
String hexId = Utils.getHex(msg.getDeviceId()); String hexId = Utils.getHex(msg.getDeviceId());
String id = Utils.fromHEX(hexId); String id = Utils.fromHEX(hexId);
String label = "Xiaomi Mi Device " + id + " (" + Utils.getHexId(id) + ")"; String label = "Xiaomi Mi Device " + " (" + id + (id.contains(".") ? "" : " / " + Utils.getHexId(id)) + ")";
String model = "";
String country = ""; String country = "";
String parent = "";
boolean isOnline = false; boolean isOnline = false;
if (ip.equals(cloudDevices.get(id))) { if (ip.equals(cloudDevices.get(id))) {
logger.debug("Skipped adding local found {}. Already discovered by cloud.", label); logger.debug("Skipped adding local found {}. Already discovered by cloud.", label);
@ -226,18 +232,27 @@ public class MiIoDiscovery extends AbstractDiscoveryService {
logger.debug("Cloud Info: {}", cloudInfo); logger.debug("Cloud Info: {}", cloudInfo);
token = cloudInfo.getToken(); token = cloudInfo.getToken();
label = cloudInfo.getName() + " " + id + " (" + Utils.getHexId(id) + ")"; label = cloudInfo.getName() + " " + id + " (" + Utils.getHexId(id) + ")";
model = cloudInfo.getModel();
country = cloudInfo.getServer(); country = cloudInfo.getServer();
isOnline = cloudInfo.getIsOnline(); isOnline = cloudInfo.getIsOnline();
parent = cloudInfo.getParentId();
} }
} }
submitDiscovery(ip, token, id, label, country, isOnline); submitDiscovery(ip, token, id, label, model, country, isOnline, parent);
} }
private void submitDiscovery(String ip, String token, String id, String label, String country, boolean isOnline) { private void submitDiscovery(String ip, String token, String id, String label, String model, String country,
ThingUID uid = new ThingUID(THING_TYPE_MIIO, Utils.getHexId(id).replace(".", "_")); boolean isOnline, String parent) {
ThingUID uid;
ThingTypeUID thingType = MiIoDevices.getType(model).getThingType();
if (id.startsWith("lumi.") || THING_TYPE_GATEWAY.equals(thingType) || THING_TYPE_LUMI.equals(thingType)) {
uid = new ThingUID(thingType, Utils.getHexId(id).replace(".", "_"));
} else {
uid = new ThingUID(THING_TYPE_MIIO, Utils.getHexId(id).replace(".", "_"));
}
DiscoveryResultBuilder dr = DiscoveryResultBuilder.create(uid).withProperty(PROPERTY_HOST_IP, ip) DiscoveryResultBuilder dr = DiscoveryResultBuilder.create(uid).withProperty(PROPERTY_HOST_IP, ip)
.withProperty(PROPERTY_DID, id); .withProperty(PROPERTY_DID, id);
if (IGNORED_TOKENS.contains(token)) { if (IGNORED_TOKENS.contains(token) || token.isBlank()) {
logger.debug("Discovered Mi Device {} ({}) at {} as {}", id, Utils.getHexId(id), ip, uid); logger.debug("Discovered Mi Device {} ({}) at {} as {}", id, Utils.getHexId(id), ip, uid);
logger.debug( logger.debug(
"No token discovered for device {}. For options how to get the token, check the binding readme.", "No token discovered for device {}. For options how to get the token, check the binding readme.",
@ -249,6 +264,9 @@ public class MiIoDiscovery extends AbstractDiscoveryService {
dr = dr.withProperty(PROPERTY_TOKEN, token).withRepresentationProperty(PROPERTY_DID) dr = dr.withProperty(PROPERTY_TOKEN, token).withRepresentationProperty(PROPERTY_DID)
.withLabel(label + " with token"); .withLabel(label + " with token");
} }
if (!model.isEmpty()) {
dr = dr.withProperty(PROPERTY_MODEL, model);
}
if (!country.isEmpty() && isOnline) { if (!country.isEmpty() && isOnline) {
dr = dr.withProperty(PROPERTY_CLOUDSERVER, country); dr = dr.withProperty(PROPERTY_CLOUDSERVER, country);
} }
@ -321,9 +339,10 @@ public class MiIoDiscovery extends AbstractDiscoveryService {
* Stops the {@link ReceiverThread} thread * Stops the {@link ReceiverThread} thread
*/ */
private synchronized void stopReceiverThreat() { private synchronized void stopReceiverThreat() {
final Thread socketReceiveThread = this.socketReceiveThread;
if (socketReceiveThread != null) { if (socketReceiveThread != null) {
socketReceiveThread.interrupt(); socketReceiveThread.interrupt();
socketReceiveThread = null; this.socketReceiveThread = null;
} }
closeSocket(); closeSocket();
} }

View File

@ -88,6 +88,7 @@ public abstract class MiIoAbstractHandler extends BaseThingHandler implements Mi
protected final Bundle bundle; protected final Bundle bundle;
protected final TranslationProvider i18nProvider; protected final TranslationProvider i18nProvider;
protected final LocaleProvider localeProvider; protected final LocaleProvider localeProvider;
protected final Map<Thing, MiIoLumiHandler> childDevices = new ConcurrentHashMap<>();
protected ScheduledExecutorService miIoScheduler = new ScheduledThreadPoolExecutor(3, protected ScheduledExecutorService miIoScheduler = new ScheduledThreadPoolExecutor(3,
new NamedThreadFactory("binding-" + getThing().getUID().getAsString(), true)); new NamedThreadFactory("binding-" + getThing().getUID().getAsString(), true));
@ -159,14 +160,17 @@ public abstract class MiIoAbstractHandler extends BaseThingHandler implements Mi
final MiIoBindingConfiguration configuration = getConfigAs(MiIoBindingConfiguration.class); final MiIoBindingConfiguration configuration = getConfigAs(MiIoBindingConfiguration.class);
this.configuration = configuration; this.configuration = configuration;
if (configuration.host.isEmpty()) { if (!getThing().getThingTypeUID().equals(THING_TYPE_LUMI)) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "@text/offline.config-error-ip"); if (configuration.host.isEmpty()) {
return; updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
} "@text/offline.config-error-ip");
if (!tokenCheckPass(configuration.token)) { return;
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, }
"@text/offline.config-error-token"); if (!tokenCheckPass(configuration.token)) {
return; updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
"@text/offline.config-error-token");
return;
}
} }
this.cloudServer = configuration.cloudServer; this.cloudServer = configuration.cloudServer;
this.deviceId = configuration.deviceId; this.deviceId = configuration.deviceId;
@ -303,6 +307,10 @@ public abstract class MiIoAbstractHandler extends BaseThingHandler implements Mi
*/ */
protected int sendCommand(String command, String params, String cloudServer, String sender) { protected int sendCommand(String command, String params, String cloudServer, String sender) {
try { try {
if (!sender.isBlank()) {
logger.debug("Received child command from {} : {} - {} (via: {})", sender, command, params,
getThing().getUID());
}
final MiIoAsyncCommunication connection = getConnection(); final MiIoAsyncCommunication connection = getConnection();
return (connection != null) ? connection.queueCommand(command, params, cloudServer, sender) : 0; return (connection != null) ? connection.queueCommand(command, params, cloudServer, sender) : 0;
} catch (MiIoCryptoException | IOException e) { } catch (MiIoCryptoException | IOException e) {
@ -623,8 +631,25 @@ public abstract class MiIoAbstractHandler extends BaseThingHandler implements Mi
@Override @Override
public void onMessageReceived(MiIoSendCommand response) { public void onMessageReceived(MiIoSendCommand response) {
if (!response.getSender().isBlank() && !response.getSender().contentEquals(getThing().getUID().getAsString())) {
for (Entry<Thing, MiIoLumiHandler> entry : childDevices.entrySet()) {
if (entry.getKey().getUID().getAsString().contentEquals(response.getSender())) {
logger.trace("Submit response to to child {} -> {}", response.getSender(), entry.getKey().getUID());
entry.getValue().onMessageReceived(response);
return;
}
}
logger.debug("{} Could not find match in {} child devices for submitter {}", getThing().getUID(),
childDevices.size(), response.getSender());
return;
}
logger.debug("Received response for device {} type: {}, result: {}, fullresponse: {}", logger.debug("Received response for device {} type: {}, result: {}, fullresponse: {}",
getThing().getUID().getId(), response.getCommand(), response.getResult(), response.getResponse()); getThing().getUID().getId(),
MiIoCommand.UNKNOWN.equals(response.getCommand())
? response.getCommand().toString() + "(" + response.getCommandString() + ")"
: response.getCommand(),
response.getResult(), response.getResponse());
if (response.isError()) { if (response.isError()) {
logger.debug("Error received for command '{}': {}.", response.getCommandString(), logger.debug("Error received for command '{}': {}.", response.getCommandString(),
response.getResponse().get("error")); response.getResponse().get("error"));

View File

@ -90,21 +90,21 @@ import com.google.gson.JsonSyntaxException;
*/ */
@NonNullByDefault @NonNullByDefault
public class MiIoBasicHandler extends MiIoAbstractHandler { public class MiIoBasicHandler extends MiIoAbstractHandler {
private final Logger logger = LoggerFactory.getLogger(MiIoBasicHandler.class); protected final Logger logger = LoggerFactory.getLogger(MiIoBasicHandler.class);
private boolean hasChannelStructure; protected boolean hasChannelStructure;
private final ExpiringCache<Boolean> updateDataCache = new ExpiringCache<>(CACHE_EXPIRY, () -> { protected final ExpiringCache<Boolean> updateDataCache = new ExpiringCache<>(CACHE_EXPIRY, () -> {
miIoScheduler.schedule(this::updateData, 0, TimeUnit.SECONDS); miIoScheduler.schedule(this::updateData, 0, TimeUnit.SECONDS);
return true; return true;
}); });
List<MiIoBasicChannel> refreshList = new ArrayList<>(); protected List<MiIoBasicChannel> refreshList = new ArrayList<>();
private Map<String, MiIoBasicChannel> refreshListCustomCommands = new HashMap<>(); protected Map<String, MiIoBasicChannel> refreshListCustomCommands = new HashMap<>();
private @Nullable MiIoBasicDevice miioDevice; protected @Nullable MiIoBasicDevice miioDevice;
private Map<ChannelUID, MiIoBasicChannel> actions = new HashMap<>(); protected Map<ChannelUID, MiIoBasicChannel> actions = new HashMap<>();
private ChannelTypeRegistry channelTypeRegistry; protected ChannelTypeRegistry channelTypeRegistry;
private BasicChannelTypeProvider basicChannelTypeProvider; protected BasicChannelTypeProvider basicChannelTypeProvider;
private Map<String, Integer> customRefreshInterval = new HashMap<>(); private Map<String, Integer> customRefreshInterval = new HashMap<>();
public MiIoBasicHandler(Thing thing, MiIoDatabaseWatchService miIoDatabaseWatchService, public MiIoBasicHandler(Thing thing, MiIoDatabaseWatchService miIoDatabaseWatchService,
@ -301,14 +301,14 @@ public class MiIoBasicHandler extends MiIoAbstractHandler {
} }
} }
private void forceStatusUpdate() { protected void forceStatusUpdate() {
updateDataCache.invalidateValue(); updateDataCache.invalidateValue();
miIoScheduler.schedule(() -> { miIoScheduler.schedule(() -> {
updateData(); updateData();
}, 3000, TimeUnit.MILLISECONDS); }, 3000, TimeUnit.MILLISECONDS);
} }
private @Nullable JsonElement miotTransform(MiIoBasicChannel miIoBasicChannel, @Nullable JsonElement value) { protected @Nullable JsonElement miotTransform(MiIoBasicChannel miIoBasicChannel, @Nullable JsonElement value) {
JsonObject json = new JsonObject(); JsonObject json = new JsonObject();
json.addProperty("did", miIoBasicChannel.getChannel()); json.addProperty("did", miIoBasicChannel.getChannel());
json.addProperty("siid", miIoBasicChannel.getSiid()); json.addProperty("siid", miIoBasicChannel.getSiid());
@ -317,7 +317,7 @@ public class MiIoBasicHandler extends MiIoAbstractHandler {
return json; return json;
} }
private @Nullable JsonElement miotActionTransform(MiIoDeviceAction action, MiIoBasicChannel miIoBasicChannel, protected @Nullable JsonElement miotActionTransform(MiIoDeviceAction action, MiIoBasicChannel miIoBasicChannel,
@Nullable JsonElement value) { @Nullable JsonElement value) {
JsonObject json = new JsonObject(); JsonObject json = new JsonObject();
json.addProperty("did", miIoBasicChannel.getChannel()); json.addProperty("did", miIoBasicChannel.getChannel());
@ -344,8 +344,8 @@ public class MiIoBasicHandler extends MiIoAbstractHandler {
final MiIoBasicDevice midevice = miioDevice; final MiIoBasicDevice midevice = miioDevice;
if (midevice != null) { if (midevice != null) {
deviceVariables.put(TIMESTAMP, Instant.now().getEpochSecond()); deviceVariables.put(TIMESTAMP, Instant.now().getEpochSecond());
refreshProperties(midevice); refreshProperties(midevice, "");
refreshCustomProperties(midevice); refreshCustomProperties(midevice, false);
refreshNetwork(); refreshNetwork();
} }
} catch (Exception e) { } catch (Exception e) {
@ -377,14 +377,16 @@ public class MiIoBasicHandler extends MiIoAbstractHandler {
return true; return true;
} }
private void refreshCustomProperties(MiIoBasicDevice midevice) { protected void refreshCustomProperties(MiIoBasicDevice midevice, boolean cloudOnly) {
logger.debug("Custom Refresh for device '{}': {} channels ", getThing().getUID(),
refreshListCustomCommands.size());
for (MiIoBasicChannel miChannel : refreshListCustomCommands.values()) { for (MiIoBasicChannel miChannel : refreshListCustomCommands.values()) {
if (customRefreshIntervalCheck(miChannel) || !linkedChannelCheck(miChannel)) { if (customRefreshIntervalCheck(miChannel) || !linkedChannelCheck(miChannel)) {
continue; continue;
} }
final JsonElement para = miChannel.getCustomRefreshParameters(); final JsonElement para = miChannel.getCustomRefreshParameters();
String cmd = miChannel.getChannelCustomRefreshCommand() + (para != null ? para.toString() : ""); String cmd = miChannel.getChannelCustomRefreshCommand() + (para != null ? para.toString() : "");
if (!cmd.startsWith("/")) { if (!cmd.startsWith("/") && !cloudOnly) {
cmds.put(sendCommand(cmd), miChannel.getChannel()); cmds.put(sendCommand(cmd), miChannel.getChannel());
} else { } else {
if (cloudServer.isBlank()) { if (cloudServer.isBlank()) {
@ -397,10 +399,14 @@ public class MiIoBasicHandler extends MiIoAbstractHandler {
} }
} }
private boolean refreshProperties(MiIoBasicDevice device) { protected boolean refreshProperties(MiIoBasicDevice device, String childId) {
MiIoCommand command = MiIoCommand.getCommand(device.getDevice().getPropertyMethod()); String command = device.getDevice().getPropertyMethod();
int maxProperties = device.getDevice().getMaxProperties(); int maxProperties = device.getDevice().getMaxProperties();
JsonArray getPropString = new JsonArray(); JsonArray getPropString = new JsonArray();
if (!childId.isBlank()) {
getPropString.add(childId);
maxProperties++;
}
for (MiIoBasicChannel miChannel : refreshList) { for (MiIoBasicChannel miChannel : refreshList) {
if (customRefreshIntervalCheck(miChannel) || !linkedChannelCheck(miChannel)) { if (customRefreshIntervalCheck(miChannel) || !linkedChannelCheck(miChannel)) {
continue; continue;
@ -419,22 +425,32 @@ public class MiIoBasicHandler extends MiIoAbstractHandler {
if (getPropString.size() >= maxProperties) { if (getPropString.size() >= maxProperties) {
sendRefreshProperties(command, getPropString); sendRefreshProperties(command, getPropString);
getPropString = new JsonArray(); getPropString = new JsonArray();
if (!childId.isBlank()) {
getPropString.add(childId);
}
} }
} }
if (getPropString.size() > 0) { if (getPropString.size() > (childId.isBlank() ? 0 : 1)) {
sendRefreshProperties(command, getPropString); sendRefreshProperties(command, getPropString);
} }
return true; return true;
} }
private void sendRefreshProperties(MiIoCommand command, JsonArray getPropString) { protected void sendRefreshProperties(String command, JsonArray getPropString) {
sendCommand(command, getPropString.toString()); JsonArray para = getPropString;
if (MiIoCommand.GET_DEVICE_PROPERTY_EXP.getCommand().contentEquals(command)) {
logger.debug("This seems a subdevice propery refresh for {}... ({} {})", getThing().getUID(), command,
getPropString.toString());
para = new JsonArray();
para.add(getPropString);
}
sendCommand(command, para.toString(), getCloudServer());
} }
/** /**
* Checks if the channel structure has been build already based on the model data. If not build it. * Checks if the channel structure has been build already based on the model data. If not build it.
*/ */
private void checkChannelStructure() { protected void checkChannelStructure() {
final MiIoBindingConfiguration configuration = this.configuration; final MiIoBindingConfiguration configuration = this.configuration;
if (configuration == null) { if (configuration == null) {
return; return;
@ -457,17 +473,16 @@ public class MiIoBasicHandler extends MiIoAbstractHandler {
if (miChannel.getChannelCustomRefreshCommand().isBlank()) { if (miChannel.getChannelCustomRefreshCommand().isBlank()) {
refreshList.add(miChannel); refreshList.add(miChannel);
} else { } else {
String i = miChannel.getChannelCustomRefreshCommand().split("\\[")[0]; String cm = miChannel.getChannelCustomRefreshCommand();
refreshListCustomCommands.put(i.trim(), miChannel); refreshListCustomCommands.put(cm.trim(), miChannel);
} }
} }
} }
} }
} }
} }
private boolean buildChannelStructure(String deviceName) { protected boolean buildChannelStructure(String deviceName) {
logger.debug("Building Channel Structure for {} - Model: {}", getThing().getUID().toString(), deviceName); logger.debug("Building Channel Structure for {} - Model: {}", getThing().getUID().toString(), deviceName);
URL fn = miIoDatabaseWatchService.getDatabaseUrl(deviceName); URL fn = miIoDatabaseWatchService.getDatabaseUrl(deviceName);
if (fn == null) { if (fn == null) {
@ -531,7 +546,7 @@ public class MiIoBasicHandler extends MiIoAbstractHandler {
return false; return false;
} }
private @Nullable ChannelUID addChannel(ThingBuilder thingBuilder, MiIoBasicChannel miChannel, String model, protected @Nullable ChannelUID addChannel(ThingBuilder thingBuilder, MiIoBasicChannel miChannel, String model,
String key) { String key) {
String channel = miChannel.getChannel(); String channel = miChannel.getChannel();
String dataType = miChannel.getType(); String dataType = miChannel.getType();
@ -571,7 +586,7 @@ public class MiIoBasicHandler extends MiIoAbstractHandler {
return channelUID; return channelUID;
} }
private @Nullable MiIoBasicChannel getChannel(String parameter) { protected @Nullable MiIoBasicChannel getChannel(String parameter) {
for (MiIoBasicChannel refreshEntry : refreshList) { for (MiIoBasicChannel refreshEntry : refreshList) {
if (refreshEntry.getProperty().equals(parameter)) { if (refreshEntry.getProperty().equals(parameter)) {
return refreshEntry; return refreshEntry;
@ -591,13 +606,22 @@ public class MiIoBasicHandler extends MiIoAbstractHandler {
return null; return null;
} }
private void updatePropsFromJsonArray(MiIoSendCommand response) { protected void updatePropsFromJsonArray(MiIoSendCommand response) {
boolean isSubdeviceUpdate = false;
JsonArray res = response.getResult().getAsJsonArray(); JsonArray res = response.getResult().getAsJsonArray();
JsonArray para = JsonParser.parseString(response.getCommandString()).getAsJsonObject().get("params") JsonArray para = JsonParser.parseString(response.getCommandString()).getAsJsonObject().get("params")
.getAsJsonArray(); .getAsJsonArray();
if (para.get(0).isJsonArray()) {
isSubdeviceUpdate = true;
para = para.get(0).getAsJsonArray();
para.remove(0);
if (res.get(0).isJsonArray()) {
res = res.get(0).getAsJsonArray();
}
}
if (res.size() != para.size()) { if (res.size() != para.size()) {
logger.debug("Unexpected size different. Request size {}, response size {}. (Req: {}, Resp:{})", logger.debug("Unexpected size different{}. Request size {}, response size {}. (Req: {}, Resp:{})",
para.size(), res.size(), para, res); isSubdeviceUpdate ? " for childdevice refresh" : "", para.size(), res.size(), para, res);
return; return;
} }
for (int i = 0; i < para.size(); i++) { for (int i = 0; i < para.size(); i++) {
@ -621,7 +645,7 @@ public class MiIoBasicHandler extends MiIoAbstractHandler {
} }
} }
private void updatePropsFromJsonObject(MiIoSendCommand response) { protected void updatePropsFromJsonObject(MiIoSendCommand response) {
JsonObject res = response.getResult().getAsJsonObject(); JsonObject res = response.getResult().getAsJsonObject();
for (Object k : res.keySet()) { for (Object k : res.keySet()) {
String param = (String) k; String param = (String) k;
@ -635,7 +659,7 @@ public class MiIoBasicHandler extends MiIoAbstractHandler {
} }
} }
private void updateChannel(@Nullable MiIoBasicChannel basicChannel, String param, JsonElement value) { protected void updateChannel(@Nullable MiIoBasicChannel basicChannel, String param, JsonElement value) {
JsonElement val = value; JsonElement val = value;
deviceVariables.put(param, val); deviceVariables.put(param, val);
if (basicChannel == null) { if (basicChannel == null) {
@ -712,7 +736,7 @@ public class MiIoBasicHandler extends MiIoAbstractHandler {
} }
} }
private void quantityTypeUpdate(MiIoBasicChannel basicChannel, JsonElement val, String type) { protected void quantityTypeUpdate(MiIoBasicChannel basicChannel, JsonElement val, String type) {
if (!basicChannel.getUnit().isBlank()) { if (!basicChannel.getUnit().isBlank()) {
Unit<?> unit = MiIoQuantiyTypes.get(basicChannel.getUnit()); Unit<?> unit = MiIoQuantiyTypes.get(basicChannel.getUnit());
if (unit != null) { if (unit != null) {
@ -751,7 +775,10 @@ public class MiIoBasicHandler extends MiIoAbstractHandler {
@Override @Override
public void onMessageReceived(MiIoSendCommand response) { public void onMessageReceived(MiIoSendCommand response) {
super.onMessageReceived(response); super.onMessageReceived(response);
if (response.isError()) { if (response.isError() || (!response.getSender().isBlank()
&& !response.getSender().contentEquals(getThing().getUID().getAsString()))) {
logger.trace("Device {} is not processing command {} as no match. Sender id:'{}'", getThing().getUID(),
response.getId(), response.getSender());
return; return;
} }
try { try {
@ -790,6 +817,9 @@ public class MiIoBasicHandler extends MiIoAbstractHandler {
} }
} }
cmds.remove(response.getId()); cmds.remove(response.getId());
} else {
logger.debug("Could not identify channel for {}. Device {} has {} commands in queue.",
response.getMethod(), getThing().getUID(), cmds.size());
} }
break; break;
} }

View File

@ -0,0 +1,86 @@
/**
* Copyright (c) 2010-2022 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.miio.internal.handler;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.miio.internal.basic.BasicChannelTypeProvider;
import org.openhab.binding.miio.internal.basic.MiIoDatabaseWatchService;
import org.openhab.binding.miio.internal.cloud.CloudConnector;
import org.openhab.core.i18n.LocaleProvider;
import org.openhab.core.i18n.TranslationProvider;
import org.openhab.core.thing.Bridge;
import org.openhab.core.thing.Thing;
import org.openhab.core.thing.binding.BaseThingHandler;
import org.openhab.core.thing.binding.BridgeHandler;
import org.openhab.core.thing.binding.ThingHandler;
import org.openhab.core.thing.binding.builder.BridgeBuilder;
import org.openhab.core.thing.type.ChannelTypeRegistry;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The {@link MiIoGatewayHandler} is responsible for handling commands, which are
* sent to one of the channels.
*
* @author Marcel Verpaalen - Initial contribution
*/
@NonNullByDefault
public class MiIoGatewayHandler extends MiIoBasicHandler implements BridgeHandler {
private final Logger logger = LoggerFactory.getLogger(MiIoGatewayHandler.class);
public MiIoGatewayHandler(Bridge thing, MiIoDatabaseWatchService miIoDatabaseWatchService,
CloudConnector cloudConnector, ChannelTypeRegistry channelTypeRegistry,
BasicChannelTypeProvider basicChannelTypeProvider, TranslationProvider i18nProvider,
LocaleProvider localeProvider) {
super(thing, miIoDatabaseWatchService, cloudConnector, channelTypeRegistry, basicChannelTypeProvider,
i18nProvider, localeProvider);
}
@Override
public Bridge getThing() {
return (Bridge) super.getThing();
}
/**
* Creates a bridge builder, which allows to modify the bridge. The method
* {@link BaseThingHandler#updateThing(Thing)} must be called to persist the changes.
*
* @return {@link BridgeBuilder} which builds an exact copy of the bridge
*/
@Override
protected BridgeBuilder editThing() {
return BridgeBuilder.create(thing.getThingTypeUID(), thing.getUID()).withBridge(thing.getBridgeUID())
.withChannels(thing.getChannels()).withConfiguration(thing.getConfiguration())
.withLabel(thing.getLabel()).withLocation(thing.getLocation()).withProperties(thing.getProperties());
}
@Override
public void childHandlerInitialized(ThingHandler childHandler, Thing childThing) {
logger.debug("Child registered with gateway: {} {} -> {} {}", childThing.getUID(), childThing.getLabel(),
getThing().getUID(), getThing().getLabel());
childDevices.put(childThing, (MiIoLumiHandler) childHandler);
}
@Override
public void childHandlerDisposed(ThingHandler childHandler, Thing childThing) {
logger.debug("Child released from gateway: {} {} -> {} {}", childThing.getUID(), childThing.getLabel(),
getThing().getUID(), getThing().getLabel());
childDevices.remove(childThing);
}
public @Nullable BridgeHandler getHandler() {
return this;
}
}

View File

@ -0,0 +1,162 @@
/**
* Copyright (c) 2010-2022 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.miio.internal.handler;
import java.time.Instant;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.miio.internal.MiIoBindingConfiguration;
import org.openhab.binding.miio.internal.MiIoSendCommand;
import org.openhab.binding.miio.internal.basic.BasicChannelTypeProvider;
import org.openhab.binding.miio.internal.basic.MiIoBasicDevice;
import org.openhab.binding.miio.internal.basic.MiIoDatabaseWatchService;
import org.openhab.binding.miio.internal.cloud.CloudConnector;
import org.openhab.core.i18n.LocaleProvider;
import org.openhab.core.i18n.TranslationProvider;
import org.openhab.core.thing.Bridge;
import org.openhab.core.thing.Thing;
import org.openhab.core.thing.ThingStatus;
import org.openhab.core.thing.ThingStatusDetail;
import org.openhab.core.thing.type.ChannelTypeRegistry;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The {@link MiIoLumiHandler} is responsible for handling commands, which are
* sent to one of the channels.
*
* @author Marcel Verpaalen - Initial contribution
*/
@NonNullByDefault
public class MiIoLumiHandler extends MiIoBasicHandler {
private final Logger logger = LoggerFactory.getLogger(MiIoLumiHandler.class);
private @Nullable MiIoGatewayHandler bridgeHandler;
public MiIoLumiHandler(Thing thing, MiIoDatabaseWatchService miIoDatabaseWatchService,
CloudConnector cloudConnector, ChannelTypeRegistry channelTypeRegistry,
BasicChannelTypeProvider basicChannelTypeProvider, TranslationProvider i18nProvider,
LocaleProvider localeProvider) {
super(thing, miIoDatabaseWatchService, cloudConnector, channelTypeRegistry, basicChannelTypeProvider,
i18nProvider, localeProvider);
}
@Override
public void initialize() {
super.initialize();
isIdentified = false;
updateStatus(ThingStatus.UNKNOWN);
final MiIoBindingConfiguration config = this.configuration;
if (config != null && config.deviceId.isBlank()) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "Missing required deviceId");
return;
}
Bridge bridge = getBridge();
if (bridge == null) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
"No device bridge has been configured");
return;
} else {
logger.debug("Bridge for {} {} = {} {} ({})", getThing().getUID(), getThing().getLabel(),
bridge.getBridgeUID(), bridge.getLabel(), bridge.getHandler());
}
bridgeHandler = null;
}
@Nullable
MiIoGatewayHandler getBridgeHandler() {
if (bridgeHandler == null) {
Bridge bridge = getBridge();
if (bridge != null) {
final MiIoGatewayHandler bridgeHandler = (MiIoGatewayHandler) bridge.getHandler();
if (bridgeHandler != null) {
if (!bridgeHandler.childDevices.containsKey(getThing())) {
logger.warn(("Child device {} missing at bridge {}. We should not see this"),
getThing().getUID(), bridgeHandler.getThing().getUID());
bridgeHandler.childDevices.forEach((k, v) -> logger.debug("Devices in bridge: {} : {}", k, v));
}
this.bridgeHandler = bridgeHandler;
return bridgeHandler;
} else {
logger.debug("Bridge is defined, but bridge handler not found for {} {}.", getThing().getUID(),
getThing().getLabel());
}
}
logger.debug("Bridge is missing for {} {}", getThing().getUID(), getThing().getLabel());
}
return this.bridgeHandler;
}
@Override
public String getCloudServer() {
final MiIoGatewayHandler bh = this.bridgeHandler;
if (bh != null) {
return bh.getCloudServer();
} else {
final MiIoBindingConfiguration config = this.configuration;
return config != null ? config.cloudServer : "";
}
}
// Override to inject the sender
@Override
protected int sendCommand(String command, String params, String cloudServer, String sender) {
final MiIoGatewayHandler bridge = getBridgeHandler();
if (bridge != null) {
logger.debug("Send via bridge {} {} (Cloudserver {})", command, params, cloudServer);
return bridge.sendCommand(command, params, cloudServer, getThing().getUID().getAsString());
} else {
logger.debug("Bridge handler is null. This is unexpected and prevents sending the update");
}
return 0;
}
@Override
protected synchronized void updateData() {
logger.debug("Periodic update for '{}' ({})", getThing().getUID(), getThing().getThingTypeUID());
try {
checkChannelStructure();
final MiIoBindingConfiguration config = this.configuration;
final MiIoBasicDevice midevice = miioDevice;
if (midevice != null && configuration != null && config != null) {
Bridge bridge = getBridge();
if (bridge == null || !bridge.getStatus().equals(ThingStatus.ONLINE)) {
logger.debug("Bridge {} offline, skipping regular refresh for {}", getThing().getBridgeUID(),
getThing().getUID());
refreshCustomProperties(midevice, true);
return;
}
deviceVariables.put(TIMESTAMP, Instant.now().getEpochSecond());
logger.debug("Refresh properties for child device {}", getThing().getLabel());
refreshProperties(midevice, config.deviceId);
logger.debug("Refresh custom commands for child device {}", getThing().getLabel());
refreshCustomProperties(midevice, false);
} else {
logger.debug("Null value occured for device {}: {}", midevice, config);
}
} catch (Exception e) {
logger.debug("Error while performing periodic refresh for '{}': {}", getThing().getUID(), e.getMessage());
}
}
@Override
public void onMessageReceived(MiIoSendCommand response) {
super.onMessageReceived(response);
if (!response.isError() && (!response.getSender().isBlank()
&& response.getSender().contentEquals(getThing().getUID().getAsString()))) {
updateStatus(ThingStatus.ONLINE);
}
}
}

View File

@ -301,7 +301,9 @@ public class MiIoUnsupportedHandler extends MiIoAbstractHandler {
JsonObject deviceMapping = Utils.convertFileToJSON(fn); JsonObject deviceMapping = Utils.convertFileToJSON(fn);
logger.debug("Using device database: {} for device {}", fn.getFile(), deviceName); logger.debug("Using device database: {} for device {}", fn.getFile(), deviceName);
final MiIoBasicDevice device = GSONP.fromJson(deviceMapping, MiIoBasicDevice.class); final MiIoBasicDevice device = GSONP.fromJson(deviceMapping, MiIoBasicDevice.class);
return device.getDevice().getChannels(); if (device != null) {
return device.getDevice().getChannels();
}
} catch (JsonIOException | JsonSyntaxException e) { } catch (JsonIOException | JsonSyntaxException e) {
logger.warn("Error parsing database Json", e); logger.warn("Error parsing database Json", e);
} catch (IOException e) { } catch (IOException e) {

View File

@ -279,6 +279,9 @@ public class MiIoVacuumHandler extends MiIoAbstractHandler {
private boolean updateVacuumStatus(JsonObject statusData) { private boolean updateVacuumStatus(JsonObject statusData) {
StatusDTO statusInfo = GSON.fromJson(statusData, StatusDTO.class); StatusDTO statusInfo = GSON.fromJson(statusData, StatusDTO.class);
if (statusInfo == null) {
return false;
}
safeUpdateState(CHANNEL_BATTERY, statusInfo.getBattery()); safeUpdateState(CHANNEL_BATTERY, statusInfo.getBattery());
if (statusInfo.getCleanArea() != null) { if (statusInfo.getCleanArea() != null) {
updateState(CHANNEL_CLEAN_AREA, updateState(CHANNEL_CLEAN_AREA,

View File

@ -66,7 +66,6 @@ public class MiotParser {
private static final String BASEURL = "https://miot-spec.org/miot-spec-v2/"; private static final String BASEURL = "https://miot-spec.org/miot-spec-v2/";
private static final Gson GSON = new GsonBuilder().setPrettyPrinting().create(); private static final Gson GSON = new GsonBuilder().setPrettyPrinting().create();
private static final boolean SKIP_SIID_1 = true; private static final boolean SKIP_SIID_1 = true;
private static final boolean INCLUDE_MANUAL_ACTIONS_COMMENT = false;
private String model; private String model;
private @Nullable String urn; private @Nullable String urn;
@ -98,7 +97,7 @@ public class MiotParser {
* @param device * @param device
* @return * @return
*/ */
static public String toJson(MiIoBasicDevice device) { public static String toJson(MiIoBasicDevice device) {
String usersJson = GSON.toJson(device); String usersJson = GSON.toJson(device);
usersJson = usersJson.replace(".0,\n", ",\n"); usersJson = usersJson.replace(".0,\n", ",\n");
usersJson = usersJson.replace("\n", "\r\n").replace(" ", "\t"); usersJson = usersJson.replace("\n", "\r\n").replace(" ", "\t");
@ -329,11 +328,6 @@ public class MiotParser {
} }
deviceMapping.setChannels(miIoBasicChannels); deviceMapping.setChannels(miIoBasicChannels);
device.setDevice(deviceMapping); device.setDevice(deviceMapping);
if (actionText.length() > 35 && INCLUDE_MANUAL_ACTIONS_COMMENT) {
deviceMapping.setReadmeComment(
"Identified " + actionText.toString().replace("Manual", "manual").replace("\r\n", "<br />")
+ "Please test and feedback if they are working so they can be linked to a channel.");
}
logger.info(channelConfigText.toString()); logger.info(channelConfigText.toString());
if (actionText.length() > 30) { if (actionText.length() > 30) {
logger.info("{}", actionText); logger.info("{}", actionText);
@ -422,6 +416,9 @@ public class MiotParser {
.send(); .send();
JsonElement json = JsonParser.parseString(response.getContentAsString()); JsonElement json = JsonParser.parseString(response.getContentAsString());
UrnsDTO data = GSON.fromJson(json, UrnsDTO.class); UrnsDTO data = GSON.fromJson(json, UrnsDTO.class);
if (data == null) {
return null;
}
for (ModelUrnsDTO device : data.getInstances()) { for (ModelUrnsDTO device : data.getInstances()) {
if (device.getModel().contentEquals(model)) { if (device.getModel().contentEquals(model)) {
this.urn = device.getType(); this.urn = device.getType();
@ -433,7 +430,6 @@ public class MiotParser {
} catch (JsonParseException e) { } catch (JsonParseException e) {
logger.debug("Failed parsing downloading models: {}", e.getMessage()); logger.debug("Failed parsing downloading models: {}", e.getMessage());
} }
return null; return null;
} }

View File

@ -27,7 +27,7 @@
<options> <options>
<option value="disabled">Local discovery only (Default)</option> <option value="disabled">Local discovery only (Default)</option>
<option value="supportedOnly">Discover online supported devices from Xiaomi cloud</option> <option value="supportedOnly">Discover online supported devices from Xiaomi cloud</option>
<option value="all">Discover all online devices from Xiaomi cloud</option> <option value="all">Discover all on &amp; offline devices from Xiaomi cloud (advanced, see readme for usage)</option>
</options> </options>
</parameter> </parameter>
</config-description> </config-description>

View File

@ -46,7 +46,9 @@
<advanced>true</advanced> <advanced>true</advanced>
</parameter> </parameter>
<parameter name="cloudServer" type="text" required="false"> <parameter name="cloudServer" type="text" required="false">
<label>Xiaomi cloud Server (county code)</label> <label>Cloud Server Country Code</label>
<description>Country code (2 characters) of the Xiaomi cloud server. See binding documentation for mapping of the
country to cloud server</description>
<advanced>true</advanced> <advanced>true</advanced>
</parameter> </parameter>
</config-description> </config-description>

View File

@ -0,0 +1,37 @@
<?xml version="1.0" encoding="UTF-8"?>
<config-description:config-descriptions
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:config-description="https://openhab.org/schemas/config-description/v1.0.0"
xsi:schemaLocation="https://openhab.org/schemas/config-description/v1.0.0 https://openhab.org/schemas/config-description-1.0.0.xsd">
<config-description uri="thing-type:miio:configGatewayDevices">
<parameter name="deviceId" type="text" required="true">
<label>Device ID</label>
<description>Device ID number for communication (in Hex)</description>
<advanced>false</advanced>
</parameter>
<parameter name="model" type="text" required="false">
<label>Device Model String</label>
<description>Device model string, used to determine the subtype.</description>
<advanced>false</advanced>
</parameter>
<parameter name="refreshInterval" type="integer" min="0" max="9999" required="false">
<label>Refresh Interval</label>
<description>Refresh interval for refreshing the data in seconds. (0=disabled)</description>
<default>30</default>
<advanced>true</advanced>
</parameter>
<parameter name="timeout" type="integer" min="1000" max="60000" required="false">
<label>Timeout</label>
<description>Timeout time in milliseconds</description>
<default>15000</default>
<advanced>true</advanced>
</parameter>
<parameter name="cloudServer" type="text" required="false">
<label>Cloud Server Country Code</label>
<description>Country code (2 characters) of the Xiaomi cloud server. See binding documentation for mapping of the
country to cloud server</description>
<advanced>true</advanced>
</parameter>
</config-description>
</config-description:config-descriptions>

View File

@ -72,14 +72,33 @@ thing.huayi.light.wyheat = HUIZUO Heating Lamp
thing.huayi.light.zw131 = HUIZUO ZIWEI Ceiling Lamp thing.huayi.light.zw131 = HUIZUO ZIWEI Ceiling Lamp
thing.hunmi.cooker.normal3 = MiJia Rice Cooker thing.hunmi.cooker.normal3 = MiJia Rice Cooker
thing.idelan.aircondition.v1 = Jinxing Smart Air Conditioner thing.idelan.aircondition.v1 = Jinxing Smart Air Conditioner
thing.ikea.light.led1545g12 = IKEA E27 white spectrum opal
thing.ikea.light.led1546g12 = IKEA E27 white spectrum clear
thing.ikea.light.led1536g5 = IKEA E14 white spectrum
thing.ikea.light.led1537r6 = IKEA GU10 white spectrum
thing.ikea.light.led1623g12 = IKEA E27 warm white
thing.ikea.light.led1650r5 = IKEA GU10 warm white
thing.ikea.light.led1649c5 = IKEA E14 warm white
thing.lumi.ctrl_neutral1.v1 = Aqara Wall Switch(No Neutral, Single Rocker) thing.lumi.ctrl_neutral1.v1 = Aqara Wall Switch(No Neutral, Single Rocker)
thing.lumi.ctrl_neutral2.v1 = Aqara Wall Switch (No Neutral, Double Rocker) thing.lumi.ctrl_neutral2.v1 = Aqara Wall Switch (No Neutral, Double Rocker)
thing.lumi.curtain.hagl05 = Xiaomiyoupin Curtain Controller (Wi-Fi) thing.lumi.curtain.hagl05 = Xiaomiyoupin Curtain Controller (Wi-Fi)
thing.lumi.gateway.mgl03 = Mi Air Purifier virtual thing.lumi.gateway.mgl03 = Mi Air Purifier virtual
thing.lumi.gateway.mieu01 = Mi smart Home Gateway Hub
thing.lumi.gateway.v1 = Mi smart Home Gateway Hub v1 thing.lumi.gateway.v1 = Mi smart Home Gateway Hub v1
thing.lumi.gateway.v2 = Mi smart Home GatewayHub v2 thing.lumi.gateway.v2 = Mi smart Home GatewayHub v2
thing.lumi.gateway.v3 = Mi smart Home Gateway Hub v3 thing.lumi.gateway.v3 = Mi smart Home Gateway Hub v3
thing.lumi.gateway.mieu01 = Mi smart Home Gateway Hub thing.lumi.light.aqcn02 = Aqara LED Light Bulb (Tunable White)
thing.lumi.lock.v1 = Door lock
thing.lumi.lock.aq1 = Aqara Door Lock
thing.lumi.lock.acn02 = Aqara Door Lock S2
thing.lumi.lock.acn03 = Aqara Door lock S2 Pro
thing.lumi.plug.mmeu01 = Mi Smart Plug (Zigbee)
thing.lumi.sensor_magnet.v2 = Mi Window and Door Sensor
thing.lumi.sensor_motion.aq2 = Mi Motion Sensor
thing.lumi.sensor_motion.v2 = Mi Motion Sensor
thing.lumi.sensor_ht.v1 = Mi Temperature and Humidity Sensor
thing.lumi.sensor_wleak.aq1 = Water Leak Sensor
thing.lumi.weather.v1 = Aqara Temperature and Humidity Sensor
thing.midea.aircondition.v1 = Midea AC-i Youth thing.midea.aircondition.v1 = Midea AC-i Youth
thing.midea.aircondition.v2 = Midea Air Conditioner v2 thing.midea.aircondition.v2 = Midea Air Conditioner v2
thing.midea.aircondition.xa1 = Midea AC-Cool Golden thing.midea.aircondition.xa1 = Midea AC-Cool Golden
@ -114,7 +133,7 @@ thing.philips.light.mceils = Zhirui Ceiling Lamp Nordic 28W
thing.philips.light.mono1 = Philips Smart Lamp thing.philips.light.mono1 = Philips Smart Lamp
thing.philips.light.moonlight = Philips ZhiRui Bedside Lamp thing.philips.light.moonlight = Philips ZhiRui Bedside Lamp
thing.philips.light.obceil = Zhirui Ceiling Lamp Black 80W thing.philips.light.obceil = Zhirui Ceiling Lamp Black 80W
thing.philips.light.obceim = Zhirui Ceiling Lamp Black 40W thing.philips.light.obceim = Zhirui Ceiling Lamp Black 40W
thing.philips.light.obceis = Zhirui Ceiling Lamp Black 28W thing.philips.light.obceis = Zhirui Ceiling Lamp Black 28W
thing.philips.light.rwread = Mijia Philips Study Desk Lamp thing.philips.light.rwread = Mijia Philips Study Desk Lamp
thing.philips.light.sceil = Zhirui Ceiling Lamp Starry 80W thing.philips.light.sceil = Zhirui Ceiling Lamp Starry 80W
@ -896,6 +915,26 @@ ch.lumi.gateway.mieu01.nightlight = Night Light
ch.lumi.gateway.mieu01.rgb = Colored Light ch.lumi.gateway.mieu01.rgb = Colored Light
ch.lumi.gateway.mieu01.zigbee_channel = Zigbee Channel ch.lumi.gateway.mieu01.zigbee_channel = Zigbee Channel
ch.lumi.gateway.telnetEnable = Enable Telnet ch.lumi.gateway.telnetEnable = Enable Telnet
ch.lumi.light.aqcn02.brightness = Brightness
ch.lumi.light.aqcn02.colour_temperature = Color Temperature
ch.lumi.light.aqcn02.power = Power
ch.lumi.lock.log = Device Log
ch.lumi.lock.status = Status
ch.lumi.plug.mmeu01.en_night_tip_light = Led Light
ch.lumi.plug.mmeu01.load_power = Load Power
ch.lumi.plug.mmeu01.log = Device Log
ch.lumi.plug.mmeu01.max_power = Max Power
ch.lumi.plug.mmeu01.power = Power
ch.lumi.plug.mmeu01.poweroff_memory = Poweroff Memory
ch.lumi.sensor_ht.v1.humidity = Humidity
ch.lumi.sensor_ht.v1.temperature = Temperature
ch.lumi.sensor_magnet.v2.log = Device Log
ch.lumi.sensor_motion.v2.log = Device Log
ch.lumi.sensor_wleak.aq1.leak = Leaking
ch.lumi.sensor_wleak.aq1.log = Device Log
ch.lumi.weather.v1.humidity = Humidity
ch.lumi.weather.v1.pressure = pressure
ch.lumi.weather.v1.temperature = Temperature
ch.mijia.vacuum.v2-miot.alarm = Alarm - Alarm ch.mijia.vacuum.v2-miot.alarm = Alarm - Alarm
ch.mijia.vacuum.v2-miot.battery-level = Battery - Battery Level ch.mijia.vacuum.v2-miot.battery-level = Battery - Battery Level
ch.mijia.vacuum.v2-miot.brush-left-time = Brush Cleaner - Brush Left Time ch.mijia.vacuum.v2-miot.brush-left-time = Brush Cleaner - Brush Left Time

View File

@ -0,0 +1,30 @@
<?xml version="1.0" encoding="UTF-8"?>
<thing:thing-descriptions bindingId="miio"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
<bridge-type id="gateway">
<label>Xiaomi Mi Gateway</label>
<channel-groups>
<channel-group id="network" typeId="network"/>
<channel-group id="actions" typeId="basicactions"/>
</channel-groups>
<properties>
<property name="vendor">Xiaomi</property>
</properties>
<config-description-ref uri="thing-type:miio:config"/>
</bridge-type>
<channel-group-type id="basicactions">
<label>Actions</label>
<channels>
<channel id="commands" typeId="commands"/>
<channel id="rpc" typeId="rpc"/>
</channels>
</channel-group-type>
</thing:thing-descriptions>

View File

@ -0,0 +1,33 @@
<?xml version="1.0" encoding="UTF-8"?>
<thing:thing-descriptions bindingId="miio"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
<thing-type id="lumi">
<supported-bridge-type-refs>
<bridge-type-ref id="gateway"/>
</supported-bridge-type-refs>
<label>Xiaomi Mi Lumi Device</label>
<channel-groups>
<channel-group id="actions" typeId="basicactions"/>
</channel-groups>
<properties>
<property name="vendor">Xiaomi</property>
</properties>
<config-description-ref uri="thing-type:miio:configGatewayDevices"/>
</thing-type>
<channel-group-type id="basicactions">
<label>Actions</label>
<channels>
<channel id="commands" typeId="commands"/>
<channel id="rpc" typeId="rpc"/>
</channels>
</channel-group-type>
</thing:thing-descriptions>

View File

@ -76,7 +76,7 @@
] ]
} }
], ],
"readmeComment": "Used to control the gateway itself. Use the mihome binding to control devices connected to the Xiaomi gateway.", "readmeComment": "Used to control the gateway itself. Use the mihome binding to control devices connected to the Xiaomi gateway if you have the developer key. Otherwise this binding provides experimental support for lumi subdevices",
"experimental": true "experimental": true
} }
} }

View File

@ -223,7 +223,7 @@
"category": "settings" "category": "settings"
} }
], ],
"readmeComment": "Used to control the gateway itself. Controlling child devices currently only possible via rules", "readmeComment": "Used to control the gateway itself. Experimental support for controlling lumi subdevices",
"experimental": false "experimental": false
} }
} }

View File

@ -0,0 +1,58 @@
{
"deviceMapping": {
"id": [
"lumi.light.aqcn02",
"ikea.light.led1545g12",
"ikea.light.led1546g12",
"ikea.light.led1536g5",
"ikea.light.led1537r6",
"ikea.light.led1623g12",
"ikea.light.led1650r5",
"ikea.light.led1649c5"
],
"propertyMethod": "get_device_prop_exp",
"maxProperties": 3,
"channels": [
{
"property": "power_status",
"friendlyName": "Power",
"channel": "power",
"type": "Switch",
"refresh": true,
"actions": [],
"category": "switch",
"tags": [
"Switch"
]
},
{
"property": "light_level",
"friendlyName": "Brightness",
"channel": "brightness",
"type": "Dimmer",
"refresh": true,
"actions": [],
"category": "lightbulb",
"tags": [
"Setpoint",
"Light"
]
},
{
"property": "colour_temperature",
"friendlyName": "Color Temperature",
"channel": "colour_temperature",
"type": "Number",
"refresh": true,
"actions": [],
"category": "colorlight",
"tags": [
"Setpoint",
"Temperature"
]
}
],
"readmeComment": "Needs to have the Xiaomi gateway configured in the binding as bridge.",
"experimental": true
}
}

View File

@ -0,0 +1,50 @@
{
"deviceMapping": {
"id": [
"lumi.lock.v1",
"lumi.lock.aq1",
"lumi.lock.acn02",
"lumi.lock.acn03"
],
"propertyMethod": "get_device_prop_exp",
"maxProperties": 3,
"channels": [
{
"property": "status",
"friendlyName": "Status",
"channel": "status",
"type": "String",
"refresh": true,
"actions": [],
"category": "lock",
"tags": [
"Lock"
]
},
{
"property": "log",
"friendlyName": "Device Log",
"channel": "log",
"type": "String",
"refresh": true,
"customRefreshCommand": "/v2/user/getuserdevicedatatab",
"customRefreshParameters": {
"limit": 10,
"timestamp": "$timestamp$",
"did": "$deviceId$",
"type": "prop",
"key": "device_log"
},
"transformation": "deviceDataTab",
"actions": [],
"category": "setting",
"tags": [
"Point"
],
"readmeComment": "This channel uses cloud to get data. See widget market place for suitable widget to display the data"
}
],
"readmeComment": "Needs to have the Xiaomi gateway configured in the binding as bridge.",
"experimental": true
}
}

View File

@ -0,0 +1,117 @@
{
"deviceMapping": {
"id": [
"lumi.plug.mmeu01"
],
"propertyMethod": "get_device_prop_exp",
"maxProperties": 5,
"channels": [
{
"property": "power",
"friendlyName": "Power",
"channel": "power",
"type": "Switch",
"refresh": true,
"customRefreshCommand": "{\"sid\":$deviceId$,\"method\":\"get_prop_plug\"} [\"channel_0\"]",
"actions": [
{
"command": "{\"sid\":$deviceId$,\"method\":\"toggle_plug\"}",
"parameterType": "ONOFF",
"parameters": [
"channel_0",
"$value$"
]
}
],
"category": "switch",
"tags": [
"Switch"
]
},
{
"property": "load_power",
"friendlyName": "Load Power",
"channel": "load_power",
"type": "Number",
"refresh": true,
"customRefreshCommand": "{\"sid\":$deviceId$,\"method\":\"get_prop_plug\"} [\"load_power\"]",
"actions": [],
"category": "switch",
"tags": [
"Measurement"
]
},
{
"property": "en_night_tip_light",
"friendlyName": "Led Light",
"channel": "en_night_tip_light",
"type": "Switch",
"refresh": true,
"actions": [],
"category": "light",
"tags": [
"Switch"
]
},
{
"property": "poweroff_memory",
"friendlyName": "Poweroff Memory",
"channel": "poweroff_memory",
"type": "Switch",
"refresh": true,
"actions": [
{
"command": "set_device_prop",
"parameterType": "EMPTY",
"parameters": [
{
"sid": "$deviceId$",
"poweroff_memory": "$value$"
}
]
}
],
"category": "setting",
"tags": [
"Switch"
]
},
{
"property": "max_power",
"friendlyName": "Max Power",
"channel": "max_power",
"type": "Number",
"refresh": true,
"actions": [],
"category": "power",
"tags": [
"Setpoint"
]
},
{
"property": "log",
"friendlyName": "Device Log",
"channel": "log",
"type": "String",
"refresh": true,
"customRefreshCommand": "/v2/user/getuserdevicedatatab",
"customRefreshParameters": {
"limit": 10,
"timestamp": "$timestamp$",
"did": "$deviceId$",
"type": "prop",
"key": "device_log"
},
"transformation": "deviceDataTab",
"actions": [],
"category": "setting",
"tags": [
"Point"
],
"readmeComment": "This channel uses cloud to get data. See widget market place for suitable widget to display the data."
}
],
"readmeComment": "Needs to have the Xiaomi gateway configured in the binding as bridge.",
"experimental": false
}
}

View File

@ -0,0 +1,49 @@
{
"deviceMapping": {
"id": [
"lumi.sensor_ht.v1"
],
"propertyMethod": "get_device_prop_exp",
"maxProperties": 3,
"channels": [
{
"property": "temperature",
"friendlyName": "Temperature",
"channel": "temperature",
"type": "Number:Temperature",
"unit": "CELSIUS",
"stateDescription": {
"pattern": "%.1f %unit%"
},
"refresh": true,
"transformation": "/100",
"actions": [],
"category": "temperature",
"tags": [
"Measurement",
"Temperature"
]
},
{
"property": "humidity",
"friendlyName": "Humidity",
"channel": "humidity",
"type": "Number:Dimensionless",
"unit": "PERCENT",
"stateDescription": {
"pattern": "%.0f%%"
},
"refresh": true,
"transformation": "/100",
"actions": [],
"category": "humidity",
"tags": [
"Measurement",
"Humidity"
]
}
],
"readmeComment": "Needs to have the Xiaomi gateway configured in the binding as bridge.",
"experimental": true
}
}

View File

@ -0,0 +1,35 @@
{
"deviceMapping": {
"id": [
"lumi.sensor_magnet.v2"
],
"propertyMethod": "get_device_prop_exp",
"maxProperties": 5,
"channels": [
{
"property": "log",
"friendlyName": "Device Log",
"channel": "log",
"type": "String",
"refresh": true,
"customRefreshCommand": "/v2/user/getuserdevicedatatab",
"customRefreshParameters": {
"limit": 10,
"timestamp": "$timestamp$",
"did": "$deviceId$",
"type": "prop",
"key": "device_log"
},
"transformation": "deviceDataTab",
"actions": [],
"category": "setting",
"tags": [
"Point"
],
"readmeComment": "This channel uses cloud to get data. See widget market place for suitable widget to display the data."
}
],
"readmeComment": "Needs to have the Xiaomi gateway configured in the binding as bridge. Note: Won\u0027t display the current status. Log only\u0027",
"experimental": false
}
}

View File

@ -0,0 +1,36 @@
{
"deviceMapping": {
"id": [
"lumi.sensor_motion.v2",
"lumi.sensor_motion.aq2"
],
"propertyMethod": "get_device_prop_exp",
"maxProperties": 5,
"channels": [
{
"property": "log",
"friendlyName": "Device Log",
"channel": "log",
"type": "String",
"refresh": true,
"customRefreshCommand": "/v2/user/getuserdevicedatatab",
"customRefreshParameters": {
"limit": 10,
"timestamp": "$timestamp$",
"did": "$deviceId$",
"type": "prop",
"key": "device_log"
},
"transformation": "deviceDataTab",
"actions": [],
"category": "setting",
"tags": [
"Point"
],
"readmeComment": "This channel uses cloud to get data. See widget market place for suitable widget to display the data"
}
],
"readmeComment": "Needs to have the Xiaomi gateway configured in the binding as bridge.Note: Won\u0027t display the current status, nor trigger events. Log only",
"experimental": false
}
}

View File

@ -0,0 +1,40 @@
{
"deviceMapping": {
"id": [
"lumi.sensor_wleak.aq1"
],
"propertyMethod": "get_device_prop_exp",
"maxProperties": 5,
"channels": [
{
"property": "leak",
"friendlyName": "Leaking",
"channel": "leak",
"type": "Switch",
"refresh": true,
"actions": [],
"category": "water",
"tags": [
"Switch"
]
},
{
"property": "log",
"friendlyName": "Device Log",
"channel": "log",
"type": "String",
"refresh": true,
"customRefreshCommand": "/v2/user/getuserdevicedatatab [{\"limit\":10,\"timestamp\": $timestamp$,\"did\":\"$deviceId$\",\"type\":\"prop\",\"key\":\"device_log\"}]",
"transformation": "deviceDataTab",
"actions": [],
"category": "setting",
"tags": [
"Point"
],
"readmeComment": "This channel uses cloud to get data. See widget market place for suitable widget to display the data"
}
],
"readmeComment": "Needs to have the Xiaomi gateway configured in the binding as bridge.",
"experimental": false
}
}

View File

@ -0,0 +1,67 @@
{
"deviceMapping": {
"id": [
"lumi.weather.v1"
],
"propertyMethod": "get_device_prop_exp",
"maxProperties": 3,
"channels": [
{
"property": "temperature",
"friendlyName": "Temperature",
"channel": "temperature",
"type": "Number:Temperature",
"unit": "CELSIUS",
"stateDescription": {
"pattern": "%.1f %unit%"
},
"refresh": true,
"transformation": "/100",
"actions": [],
"category": "temperature",
"tags": [
"Measurement",
"Temperature"
]
},
{
"property": "humidity",
"friendlyName": "Humidity",
"channel": "humidity",
"type": "Number:Dimensionless",
"unit": "PERCENT",
"stateDescription": {
"pattern": "%.0f%%"
},
"refresh": true,
"transformation": "/100",
"actions": [],
"category": "humidity",
"tags": [
"Measurement",
"Humidity"
]
},
{
"property": "pressure",
"friendlyName": "pressure",
"channel": "pressure",
"type": "Number:Pressure",
"unit": "hPa",
"stateDescription": {
"pattern": "%.1f %unit%"
},
"refresh": true,
"transformation": "/100",
"actions": [],
"category": "pressure",
"tags": [
"Measurement",
"Pressure"
]
}
],
"readmeComment": "Needs to have the Xiaomi gateway configured in the binding as bridge.",
"experimental": false
}
}

View File

@ -80,7 +80,6 @@ public class ConversionsTest {
@Test @Test
public void getJsonElementTest() { public void getJsonElementTest() {
Map<String, Object> deviceVariables = Collections.emptyMap(); Map<String, Object> deviceVariables = Collections.emptyMap();
// test invalid missing element // test invalid missing element

View File

@ -28,21 +28,19 @@ import org.openhab.binding.miio.internal.miot.MiIoQuantiyTypesConversion;
public class MiIoQuantiyTypesConversionTest { public class MiIoQuantiyTypesConversionTest {
@Test @Test
public void UnknownUnitTest() { public void unknownUnitTest() {
String unitName = "some none existent unit"; String unitName = "some none existent unit";
assertNull(MiIoQuantiyTypesConversion.getType(unitName)); assertNull(MiIoQuantiyTypesConversion.getType(unitName));
} }
@Test @Test
public void NullUnitTest() { public void nullUnitTest() {
String unitName = null; String unitName = null;
assertNull(MiIoQuantiyTypesConversion.getType(unitName)); assertNull(MiIoQuantiyTypesConversion.getType(unitName));
} }
@Test @Test
public void regularsUnitTest() { public void regularsUnitTest() {
String unitName = "minute"; String unitName = "minute";
assertEquals("Time", MiIoQuantiyTypesConversion.getType(unitName)); assertEquals("Time", MiIoQuantiyTypesConversion.getType(unitName));

View File

@ -57,7 +57,6 @@ public class MiotJsonFileCreator {
@Disabled @Disabled
public static void main(String[] args) { public static void main(String[] args) {
LinkedHashMap<String, String> checksums = new LinkedHashMap<>(); LinkedHashMap<String, String> checksums = new LinkedHashMap<>();
LinkedHashSet<String> models = new LinkedHashSet<>(); LinkedHashSet<String> models = new LinkedHashSet<>();
if (args.length > 0) { if (args.length > 0) {

View File

@ -25,11 +25,15 @@ import java.nio.file.Files;
import java.nio.file.Paths; import java.nio.file.Paths;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap; import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Map.Entry; import java.util.Map.Entry;
import java.util.Set;
import java.util.TreeMap; import java.util.TreeMap;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable; import org.eclipse.jdt.annotation.Nullable;
@ -38,6 +42,7 @@ import org.openhab.binding.miio.internal.basic.MiIoBasicChannel;
import org.openhab.binding.miio.internal.basic.MiIoBasicDevice; import org.openhab.binding.miio.internal.basic.MiIoBasicDevice;
import org.openhab.binding.miio.internal.basic.OptionsValueListDTO; import org.openhab.binding.miio.internal.basic.OptionsValueListDTO;
import org.openhab.binding.miio.internal.basic.StateDescriptionDTO; import org.openhab.binding.miio.internal.basic.StateDescriptionDTO;
import org.openhab.core.thing.ThingTypeUID;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
@ -68,15 +73,19 @@ public class ReadmeHelper {
private static final String I18N_CHANNEL_FILE = "./src/main/resources/OH-INF/i18n/basic.properties"; private static final String I18N_CHANNEL_FILE = "./src/main/resources/OH-INF/i18n/basic.properties";
private static final boolean UPDATE_OPTION_MAPPING_README_COMMENTS = true; private static final boolean UPDATE_OPTION_MAPPING_README_COMMENTS = true;
public static final Set<ThingTypeUID> DATABASE_THING_TYPES = Collections
.unmodifiableSet(Stream.of(MiIoBindingConstants.THING_TYPE_BASIC, MiIoBindingConstants.THING_TYPE_LUMI,
MiIoBindingConstants.THING_TYPE_GATEWAY).collect(Collectors.toSet()));
@Disabled @Disabled
public static void main(String[] args) { public static void main(String[] args) {
ReadmeHelper rm = new ReadmeHelper(); ReadmeHelper rm = new ReadmeHelper();
LOGGER.info("## Creating device list"); LOGGER.info("## Creating device list");
StringWriter deviceList = rm.deviceList(); StringWriter deviceList = rm.deviceList();
rm.checkDatabaseEntrys(); rm.checkDatabaseEntrys();
LOGGER.info("## Creating channel list for basic devices"); LOGGER.info("## Creating channel list for json database driven devices");
StringWriter channelList = rm.channelList(); StringWriter channelList = rm.channelList();
LOGGER.info("## Creating Item Files for miio:basic devices"); LOGGER.info("## Creating Item Files for json database driven devices");
StringWriter itemFileExamples = rm.itemFileExamples(); StringWriter itemFileExamples = rm.itemFileExamples();
try { try {
String baseDoc = new String(Files.readAllBytes(Paths.get(BASEFILE)), StandardCharsets.UTF_8); String baseDoc = new String(Files.readAllBytes(Paths.get(BASEFILE)), StandardCharsets.UTF_8);
@ -125,35 +134,37 @@ public class ReadmeHelper {
sw.write(devicesCount); sw.write(devicesCount);
sw.write("\n\n"); sw.write("\n\n");
sw.write( sw.write(
"| Device | ThingType | Device Model | Supported | Remark |\n"); "| Device | ThingType | Device Model | Supported | Remark |\n");
sw.write( sw.write(
"|------------------------------|------------------|------------------------|-----------|------------|\n"); "|------------------------------------|------------------|------------------------|--------------|------------|\n");
Arrays.asList(MiIoDevices.values()).forEach(device -> { Arrays.asList(MiIoDevices.values()).forEach(device -> {
if (!device.getModel().equals("unknown")) { if (!device.getModel().equals("unknown")) {
String link = device.getModel().replace(".", "-"); String link = device.getModel().replace(".", "-");
boolean isSupported = device.getThingType().equals(MiIoBindingConstants.THING_TYPE_UNSUPPORTED); boolean isSupported = device.getThingType().equals(MiIoBindingConstants.THING_TYPE_UNSUPPORTED);
Boolean experimental = false;
String remark = ""; String remark = "";
if (device.getThingType().equals(MiIoBindingConstants.THING_TYPE_BASIC)) { if (DATABASE_THING_TYPES.contains(device.getThingType())) {
MiIoBasicDevice dev = findDatabaseEntry(device.getModel()); MiIoBasicDevice dev = findDatabaseEntry(device.getModel());
if (dev != null) { if (dev != null) {
remark = dev.getDevice().getReadmeComment(); remark = dev.getDevice().getReadmeComment();
final Boolean experimental = dev.getDevice().getExperimental(); final Boolean experimentalDev = dev.getDevice().getExperimental();
if (experimental != null && experimental.booleanValue()) { experimental = experimentalDev != null && experimentalDev.booleanValue();
if (experimental) {
remark += (remark.isBlank() ? "" : "<br />") remark += (remark.isBlank() ? "" : "<br />")
+ "Experimental support. Please report back if all channels are functional. Preferably share the debug log of property refresh and command responses"; + "Experimental support. Please report back if all channels are functional. Preferably share the debug log of property refresh and command responses";
} }
} }
} }
sw.write("| "); sw.write("| ");
sw.write(minLengthString(device.getDescription(), 28)); sw.write(minLengthString(device.getDescription(), 34));
sw.write(" | "); sw.write(" | ");
sw.write(minLengthString(device.getThingType().toString(), 16)); sw.write(minLengthString(device.getThingType().toString(), 16));
sw.write(" | "); sw.write(" | ");
String model = isSupported ? device.getModel() : "[" + device.getModel() + "](#" + link + ")"; String model = isSupported ? device.getModel() : "[" + device.getModel() + "](#" + link + ")";
sw.write(minLengthString(model, 22)); sw.write(minLengthString(model, 22));
sw.write(" | "); sw.write(" | ");
sw.write(isSupported ? "No " : "Yes "); sw.write(isSupported ? "No " : (experimental ? "Experimental" : "Yes "));
sw.write(" | "); sw.write(" | ");
sw.write(minLengthString(remark, 10)); sw.write(minLengthString(remark, 10));
sw.write(" |\n"); sw.write(" |\n");
@ -165,7 +176,7 @@ public class ReadmeHelper {
private StringWriter channelList() { private StringWriter channelList() {
StringWriter sw = new StringWriter(); StringWriter sw = new StringWriter();
Arrays.asList(MiIoDevices.values()).forEach(device -> { Arrays.asList(MiIoDevices.values()).forEach(device -> {
if (device.getThingType().equals(MiIoBindingConstants.THING_TYPE_BASIC)) { if (DATABASE_THING_TYPES.contains(device.getThingType())) {
MiIoBasicDevice dev = findDatabaseEntry(device.getModel()); MiIoBasicDevice dev = findDatabaseEntry(device.getModel());
if (dev != null) { if (dev != null) {
String link = device.getModel().replace(".", "-"); String link = device.getModel().replace(".", "-");
@ -217,7 +228,7 @@ public class ReadmeHelper {
private StringWriter itemFileExamples() { private StringWriter itemFileExamples() {
StringWriter sw = new StringWriter(); StringWriter sw = new StringWriter();
Arrays.asList(MiIoDevices.values()).forEach(device -> { Arrays.asList(MiIoDevices.values()).forEach(device -> {
if (device.getThingType().equals(MiIoBindingConstants.THING_TYPE_BASIC)) { if (DATABASE_THING_TYPES.contains(device.getThingType())) {
MiIoBasicDevice dev = findDatabaseEntry(device.getModel()); MiIoBasicDevice dev = findDatabaseEntry(device.getModel());
if (dev != null) { if (dev != null) {
sw.write("### " + device.getDescription() + " (" + device.getModel() + ") item file lines\n\n"); sw.write("### " + device.getDescription() + " (" + device.getModel() + ") item file lines\n\n");
@ -231,7 +242,8 @@ public class ReadmeHelper {
for (MiIoBasicChannel ch : dev.getDevice().getChannels()) { for (MiIoBasicChannel ch : dev.getDevice().getChannels()) {
sw.write(ch.getType() + " " + ch.getChannel().replace("-", "_") + " \"" + ch.getFriendlyName() sw.write(ch.getType() + " " + ch.getChannel().replace("-", "_") + " \"" + ch.getFriendlyName()
+ "\" (" + gr + ") {channel=\"miio:basic:" + id + ":" + ch.getChannel() + "\"}\n"); + "\" (" + gr + ") {channel=\"" + device.getThingType().toString() + ":" + id + ":"
+ ch.getChannel() + "\"}\n");
} }
sw.write("```\n\n"); sw.write("```\n\n");
} }
@ -242,6 +254,7 @@ public class ReadmeHelper {
private void checkDatabaseEntrys() { private void checkDatabaseEntrys() {
StringBuilder sb = new StringBuilder(); StringBuilder sb = new StringBuilder();
StringBuilder commentSb = new StringBuilder("Adding support for the following models:\r\n");
Gson gson = new GsonBuilder().setPrettyPrinting().create(); Gson gson = new GsonBuilder().setPrettyPrinting().create();
HashMap<String, String> names = new HashMap<String, String>(); HashMap<String, String> names = new HashMap<String, String>();
try { try {
@ -253,7 +266,12 @@ public class ReadmeHelper {
for (MiIoBasicDevice entry : findDatabaseEntrys()) { for (MiIoBasicDevice entry : findDatabaseEntrys()) {
for (String id : entry.getDevice().getId()) { for (String id : entry.getDevice().getId()) {
if (!MiIoDevices.getType(id).getThingType().equals(MiIoBindingConstants.THING_TYPE_BASIC)) { if (!DATABASE_THING_TYPES.contains(MiIoDevices.getType(id).getThingType())) {
commentSb.append("* ");
commentSb.append(names.get(id));
commentSb.append(" (modelId: ");
commentSb.append(id);
commentSb.append(")\r\n");
sb.append(id.toUpperCase().replace(".", "_")); sb.append(id.toUpperCase().replace(".", "_"));
sb.append("(\""); sb.append("(\"");
sb.append(id); sb.append(id);
@ -267,12 +285,17 @@ public class ReadmeHelper {
"id: {} not found in MiIoDevices.java and name unavilable in the device names list.", "id: {} not found in MiIoDevices.java and name unavilable in the device names list.",
id); id);
} }
sb.append("\", THING_TYPE_BASIC),\r\n"); sb.append("\", ");
sb.append(id.startsWith("lumi.")
? (id.startsWith("lumi.gateway") ? "THING_TYPE_GATEWAY" : "THING_TYPE_LUMI")
: "THING_TYPE_BASIC");
sb.append("),\r\n");
} }
} }
} }
if (sb.length() > 0) { if (sb.length() > 0) {
LOGGER.info("Model(s) not found. Suggested lines to add to MiIoDevices.java\r\n{}", sb.toString()); LOGGER.info("Model(s) not found. Suggested lines to add to MiIoDevices.java\r\n{}", sb);
LOGGER.info("Model(s) not found. Suggested lines to add to the change log\r\n{}", commentSb);
} }
} }
@ -362,7 +385,7 @@ public class ReadmeHelper {
return i18nEntries; return i18nEntries;
} }
public static <K extends Comparable, V> Map<K, V> sortByKeys(Map<K, V> map) { public static <K extends Comparable<?>, V> Map<K, V> sortByKeys(Map<K, V> map) {
return new TreeMap<>(map); return new TreeMap<>(map);
} }