[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: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: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 |
# 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.
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
@ -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" ]`
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
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
!!!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

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
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_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_UNSUPPORTED = new ThingTypeUID(BINDING_ID, "unsupported");
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)
.collect(Collectors.toSet()));
.unmodifiableSet(Stream.of(THING_TYPE_MIIO, THING_TYPE_BASIC, THING_TYPE_LUMI, THING_TYPE_GATEWAY,
THING_TYPE_VACUUM, THING_TYPE_UNSUPPORTED).collect(Collectors.toSet()));
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
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_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("");
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),
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),
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)",
THING_TYPE_UNSUPPORTED),
LUMI_CTRL_NEUTRAL2_V1("lumi.ctrl_neutral2.v1", "Aqara Wall Switch (No Neutral, Double Rocker)",
THING_TYPE_UNSUPPORTED),
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_V1("lumi.gateway.v1", "Mi smart Home Gateway Hub v1", THING_TYPE_BASIC),
LUMI_GATEWAY_V2("lumi.gateway.v2", "Mi smart Home GatewayHub v2", THING_TYPE_BASIC),
LUMI_GATEWAY_V3("lumi.gateway.v3", "Mi smart Home Gateway Hub v3", THING_TYPE_BASIC),
LUMI_GATEWAY_MIEU01("lumi.gateway.mieu01", "Mi smart Home Gateway Hub", THING_TYPE_BASIC),
LUMI_GATEWAY_MGL03("lumi.gateway.mgl03", "Mi Air Purifier virtual", THING_TYPE_GATEWAY),
LUMI_GATEWAY_MIEU01("lumi.gateway.mieu01", "Mi smart Home Gateway Hub", THING_TYPE_GATEWAY),
LUMI_GATEWAY_V1("lumi.gateway.v1", "Mi smart Home Gateway Hub v1", THING_TYPE_GATEWAY),
LUMI_GATEWAY_V2("lumi.gateway.v2", "Mi smart Home GatewayHub v2", THING_TYPE_GATEWAY),
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_V2("midea.aircondition.v2", "Midea Air Conditioner v2", 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_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_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_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),
@ -404,7 +423,7 @@ public enum MiIoDevices {
public static MiIoDevices getType(String modelString) {
for (MiIoDevices mioDev : MiIoDevices.values()) {
if (mioDev.getModel().equals(modelString)) {
if (mioDev.getModel().equalsIgnoreCase(modelString)) {
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.cloud.CloudConnector;
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.MiIoLumiHandler;
import org.openhab.binding.miio.internal.handler.MiIoUnsupportedHandler;
import org.openhab.binding.miio.internal.handler.MiIoVacuumHandler;
import org.openhab.core.common.ThreadPoolManager;
import org.openhab.core.i18n.LocaleProvider;
import org.openhab.core.i18n.TranslationProvider;
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.ThingTypeUID;
import org.openhab.core.thing.binding.BaseThingHandlerFactory;
@ -122,6 +125,14 @@ public class MiIoHandlerFactory extends BaseThingHandlerFactory {
return new MiIoBasicHandler(thing, miIoDatabaseWatchService, cloudConnector, channelTypeRegistry,
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)) {
return new MiIoVacuumHandler(thing, miIoDatabaseWatchService, cloudConnector, channelTypeRegistry,
i18nProvider, localeProvider);

View File

@ -22,6 +22,7 @@ import org.openhab.core.library.types.PercentType;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonParseException;
@ -79,6 +80,33 @@ public class Conversions {
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 {
double value = seconds.getAsDouble() / 3600;
return new JsonPrimitive(value);
@ -190,6 +218,8 @@ public class Conversions {
return addBrightToHSV(value, deviceVariables);
case "BRGBTOHSV":
return bRGBtoHSV(value);
case "DEVICEDATATAB":
return deviceDataTab(value, deviceVariables);
case "GETDIDELEMENT":
return getDidElement(value, deviceVariables);
default:

View File

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

View File

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

View File

@ -92,6 +92,7 @@ public class MiIoDiscovery extends AbstractDiscoveryService {
}
private String getCloudDiscoveryMode() {
final Configuration miioConfig = this.miioConfig;
if (miioConfig != null) {
try {
Dictionary<String, @Nullable Object> properties = miioConfig.getProperties();
@ -185,20 +186,23 @@ public class MiIoDiscovery extends AbstractDiscoveryService {
List<CloudDeviceDTO> dv = cloudConnector.getDevicesList();
for (CloudDeviceDTO device : dv) {
String id = device.getDid();
if (cloudDiscoveryMode.contentEquals(SUPPORTED)) {
if (SUPPORTED.contentEquals(cloudDiscoveryMode)) {
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);
cloudDevices.put(id, device.getLocalip());
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();
boolean isOnline = device.getIsOnline();
String parent = device.getParentId();
String ip = device.getLocalip();
submitDiscovery(ip, token, id, label, country, isOnline);
submitDiscovery(ip, token, id, label, model, country, isOnline, parent);
} else {
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 hexId = Utils.getHex(msg.getDeviceId());
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 parent = "";
boolean isOnline = false;
if (ip.equals(cloudDevices.get(id))) {
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);
token = cloudInfo.getToken();
label = cloudInfo.getName() + " " + id + " (" + Utils.getHexId(id) + ")";
model = cloudInfo.getModel();
country = cloudInfo.getServer();
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) {
ThingUID uid = new ThingUID(THING_TYPE_MIIO, Utils.getHexId(id).replace(".", "_"));
private void submitDiscovery(String ip, String token, String id, String label, String model, String country,
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)
.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(
"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)
.withLabel(label + " with token");
}
if (!model.isEmpty()) {
dr = dr.withProperty(PROPERTY_MODEL, model);
}
if (!country.isEmpty() && isOnline) {
dr = dr.withProperty(PROPERTY_CLOUDSERVER, country);
}
@ -321,9 +339,10 @@ public class MiIoDiscovery extends AbstractDiscoveryService {
* Stops the {@link ReceiverThread} thread
*/
private synchronized void stopReceiverThreat() {
final Thread socketReceiveThread = this.socketReceiveThread;
if (socketReceiveThread != null) {
socketReceiveThread.interrupt();
socketReceiveThread = null;
this.socketReceiveThread = null;
}
closeSocket();
}

View File

@ -88,6 +88,7 @@ public abstract class MiIoAbstractHandler extends BaseThingHandler implements Mi
protected final Bundle bundle;
protected final TranslationProvider i18nProvider;
protected final LocaleProvider localeProvider;
protected final Map<Thing, MiIoLumiHandler> childDevices = new ConcurrentHashMap<>();
protected ScheduledExecutorService miIoScheduler = new ScheduledThreadPoolExecutor(3,
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);
this.configuration = configuration;
if (configuration.host.isEmpty()) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "@text/offline.config-error-ip");
return;
}
if (!tokenCheckPass(configuration.token)) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
"@text/offline.config-error-token");
return;
if (!getThing().getThingTypeUID().equals(THING_TYPE_LUMI)) {
if (configuration.host.isEmpty()) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
"@text/offline.config-error-ip");
return;
}
if (!tokenCheckPass(configuration.token)) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
"@text/offline.config-error-token");
return;
}
}
this.cloudServer = configuration.cloudServer;
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) {
try {
if (!sender.isBlank()) {
logger.debug("Received child command from {} : {} - {} (via: {})", sender, command, params,
getThing().getUID());
}
final MiIoAsyncCommunication connection = getConnection();
return (connection != null) ? connection.queueCommand(command, params, cloudServer, sender) : 0;
} catch (MiIoCryptoException | IOException e) {
@ -623,8 +631,25 @@ public abstract class MiIoAbstractHandler extends BaseThingHandler implements Mi
@Override
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: {}",
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()) {
logger.debug("Error received for command '{}': {}.", response.getCommandString(),
response.getResponse().get("error"));

View File

@ -90,21 +90,21 @@ import com.google.gson.JsonSyntaxException;
*/
@NonNullByDefault
public class MiIoBasicHandler extends MiIoAbstractHandler {
private final Logger logger = LoggerFactory.getLogger(MiIoBasicHandler.class);
private boolean hasChannelStructure;
protected final Logger logger = LoggerFactory.getLogger(MiIoBasicHandler.class);
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);
return true;
});
List<MiIoBasicChannel> refreshList = new ArrayList<>();
private Map<String, MiIoBasicChannel> refreshListCustomCommands = new HashMap<>();
protected List<MiIoBasicChannel> refreshList = new ArrayList<>();
protected Map<String, MiIoBasicChannel> refreshListCustomCommands = new HashMap<>();
private @Nullable MiIoBasicDevice miioDevice;
private Map<ChannelUID, MiIoBasicChannel> actions = new HashMap<>();
private ChannelTypeRegistry channelTypeRegistry;
private BasicChannelTypeProvider basicChannelTypeProvider;
protected @Nullable MiIoBasicDevice miioDevice;
protected Map<ChannelUID, MiIoBasicChannel> actions = new HashMap<>();
protected ChannelTypeRegistry channelTypeRegistry;
protected BasicChannelTypeProvider basicChannelTypeProvider;
private Map<String, Integer> customRefreshInterval = new HashMap<>();
public MiIoBasicHandler(Thing thing, MiIoDatabaseWatchService miIoDatabaseWatchService,
@ -301,14 +301,14 @@ public class MiIoBasicHandler extends MiIoAbstractHandler {
}
}
private void forceStatusUpdate() {
protected void forceStatusUpdate() {
updateDataCache.invalidateValue();
miIoScheduler.schedule(() -> {
updateData();
}, 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();
json.addProperty("did", miIoBasicChannel.getChannel());
json.addProperty("siid", miIoBasicChannel.getSiid());
@ -317,7 +317,7 @@ public class MiIoBasicHandler extends MiIoAbstractHandler {
return json;
}
private @Nullable JsonElement miotActionTransform(MiIoDeviceAction action, MiIoBasicChannel miIoBasicChannel,
protected @Nullable JsonElement miotActionTransform(MiIoDeviceAction action, MiIoBasicChannel miIoBasicChannel,
@Nullable JsonElement value) {
JsonObject json = new JsonObject();
json.addProperty("did", miIoBasicChannel.getChannel());
@ -344,8 +344,8 @@ public class MiIoBasicHandler extends MiIoAbstractHandler {
final MiIoBasicDevice midevice = miioDevice;
if (midevice != null) {
deviceVariables.put(TIMESTAMP, Instant.now().getEpochSecond());
refreshProperties(midevice);
refreshCustomProperties(midevice);
refreshProperties(midevice, "");
refreshCustomProperties(midevice, false);
refreshNetwork();
}
} catch (Exception e) {
@ -377,14 +377,16 @@ public class MiIoBasicHandler extends MiIoAbstractHandler {
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()) {
if (customRefreshIntervalCheck(miChannel) || !linkedChannelCheck(miChannel)) {
continue;
}
final JsonElement para = miChannel.getCustomRefreshParameters();
String cmd = miChannel.getChannelCustomRefreshCommand() + (para != null ? para.toString() : "");
if (!cmd.startsWith("/")) {
if (!cmd.startsWith("/") && !cloudOnly) {
cmds.put(sendCommand(cmd), miChannel.getChannel());
} else {
if (cloudServer.isBlank()) {
@ -397,10 +399,14 @@ public class MiIoBasicHandler extends MiIoAbstractHandler {
}
}
private boolean refreshProperties(MiIoBasicDevice device) {
MiIoCommand command = MiIoCommand.getCommand(device.getDevice().getPropertyMethod());
protected boolean refreshProperties(MiIoBasicDevice device, String childId) {
String command = device.getDevice().getPropertyMethod();
int maxProperties = device.getDevice().getMaxProperties();
JsonArray getPropString = new JsonArray();
if (!childId.isBlank()) {
getPropString.add(childId);
maxProperties++;
}
for (MiIoBasicChannel miChannel : refreshList) {
if (customRefreshIntervalCheck(miChannel) || !linkedChannelCheck(miChannel)) {
continue;
@ -419,22 +425,32 @@ public class MiIoBasicHandler extends MiIoAbstractHandler {
if (getPropString.size() >= maxProperties) {
sendRefreshProperties(command, getPropString);
getPropString = new JsonArray();
if (!childId.isBlank()) {
getPropString.add(childId);
}
}
}
if (getPropString.size() > 0) {
if (getPropString.size() > (childId.isBlank() ? 0 : 1)) {
sendRefreshProperties(command, getPropString);
}
return true;
}
private void sendRefreshProperties(MiIoCommand command, JsonArray getPropString) {
sendCommand(command, getPropString.toString());
protected void sendRefreshProperties(String command, JsonArray getPropString) {
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.
*/
private void checkChannelStructure() {
protected void checkChannelStructure() {
final MiIoBindingConfiguration configuration = this.configuration;
if (configuration == null) {
return;
@ -457,17 +473,16 @@ public class MiIoBasicHandler extends MiIoAbstractHandler {
if (miChannel.getChannelCustomRefreshCommand().isBlank()) {
refreshList.add(miChannel);
} else {
String i = miChannel.getChannelCustomRefreshCommand().split("\\[")[0];
refreshListCustomCommands.put(i.trim(), miChannel);
String cm = miChannel.getChannelCustomRefreshCommand();
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);
URL fn = miIoDatabaseWatchService.getDatabaseUrl(deviceName);
if (fn == null) {
@ -531,7 +546,7 @@ public class MiIoBasicHandler extends MiIoAbstractHandler {
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 channel = miChannel.getChannel();
String dataType = miChannel.getType();
@ -571,7 +586,7 @@ public class MiIoBasicHandler extends MiIoAbstractHandler {
return channelUID;
}
private @Nullable MiIoBasicChannel getChannel(String parameter) {
protected @Nullable MiIoBasicChannel getChannel(String parameter) {
for (MiIoBasicChannel refreshEntry : refreshList) {
if (refreshEntry.getProperty().equals(parameter)) {
return refreshEntry;
@ -591,13 +606,22 @@ public class MiIoBasicHandler extends MiIoAbstractHandler {
return null;
}
private void updatePropsFromJsonArray(MiIoSendCommand response) {
protected void updatePropsFromJsonArray(MiIoSendCommand response) {
boolean isSubdeviceUpdate = false;
JsonArray res = response.getResult().getAsJsonArray();
JsonArray para = JsonParser.parseString(response.getCommandString()).getAsJsonObject().get("params")
.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()) {
logger.debug("Unexpected size different. Request size {}, response size {}. (Req: {}, Resp:{})",
para.size(), res.size(), para, res);
logger.debug("Unexpected size different{}. Request size {}, response size {}. (Req: {}, Resp:{})",
isSubdeviceUpdate ? " for childdevice refresh" : "", para.size(), res.size(), para, res);
return;
}
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();
for (Object k : res.keySet()) {
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;
deviceVariables.put(param, val);
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()) {
Unit<?> unit = MiIoQuantiyTypes.get(basicChannel.getUnit());
if (unit != null) {
@ -751,7 +775,10 @@ public class MiIoBasicHandler extends MiIoAbstractHandler {
@Override
public void onMessageReceived(MiIoSendCommand 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;
}
try {
@ -790,6 +817,9 @@ public class MiIoBasicHandler extends MiIoAbstractHandler {
}
}
cmds.remove(response.getId());
} else {
logger.debug("Could not identify channel for {}. Device {} has {} commands in queue.",
response.getMethod(), getThing().getUID(), cmds.size());
}
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);
logger.debug("Using device database: {} for device {}", fn.getFile(), deviceName);
final MiIoBasicDevice device = GSONP.fromJson(deviceMapping, MiIoBasicDevice.class);
return device.getDevice().getChannels();
if (device != null) {
return device.getDevice().getChannels();
}
} catch (JsonIOException | JsonSyntaxException e) {
logger.warn("Error parsing database Json", e);
} catch (IOException e) {

View File

@ -279,6 +279,9 @@ public class MiIoVacuumHandler extends MiIoAbstractHandler {
private boolean updateVacuumStatus(JsonObject statusData) {
StatusDTO statusInfo = GSON.fromJson(statusData, StatusDTO.class);
if (statusInfo == null) {
return false;
}
safeUpdateState(CHANNEL_BATTERY, statusInfo.getBattery());
if (statusInfo.getCleanArea() != null) {
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 Gson GSON = new GsonBuilder().setPrettyPrinting().create();
private static final boolean SKIP_SIID_1 = true;
private static final boolean INCLUDE_MANUAL_ACTIONS_COMMENT = false;
private String model;
private @Nullable String urn;
@ -98,7 +97,7 @@ public class MiotParser {
* @param device
* @return
*/
static public String toJson(MiIoBasicDevice device) {
public static String toJson(MiIoBasicDevice device) {
String usersJson = GSON.toJson(device);
usersJson = usersJson.replace(".0,\n", ",\n");
usersJson = usersJson.replace("\n", "\r\n").replace(" ", "\t");
@ -329,11 +328,6 @@ public class MiotParser {
}
deviceMapping.setChannels(miIoBasicChannels);
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());
if (actionText.length() > 30) {
logger.info("{}", actionText);
@ -422,6 +416,9 @@ public class MiotParser {
.send();
JsonElement json = JsonParser.parseString(response.getContentAsString());
UrnsDTO data = GSON.fromJson(json, UrnsDTO.class);
if (data == null) {
return null;
}
for (ModelUrnsDTO device : data.getInstances()) {
if (device.getModel().contentEquals(model)) {
this.urn = device.getType();
@ -433,7 +430,6 @@ public class MiotParser {
} catch (JsonParseException e) {
logger.debug("Failed parsing downloading models: {}", e.getMessage());
}
return null;
}

View File

@ -27,7 +27,7 @@
<options>
<option value="disabled">Local discovery only (Default)</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>
</parameter>
</config-description>

View File

@ -46,7 +46,9 @@
<advanced>true</advanced>
</parameter>
<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>
</parameter>
</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.hunmi.cooker.normal3 = MiJia Rice Cooker
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_neutral2.v1 = Aqara Wall Switch (No Neutral, Double Rocker)
thing.lumi.curtain.hagl05 = Xiaomiyoupin Curtain Controller (Wi-Fi)
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.v2 = Mi smart Home GatewayHub v2
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.v2 = Midea Air Conditioner v2
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.moonlight = Philips ZhiRui Bedside Lamp
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.rwread = Mijia Philips Study Desk Lamp
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.zigbee_channel = Zigbee Channel
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.battery-level = Battery - Battery Level
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
}
}

View File

@ -223,7 +223,7 @@
"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
}
}

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
public void getJsonElementTest() {
Map<String, Object> deviceVariables = Collections.emptyMap();
// test invalid missing element

View File

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

View File

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

View File

@ -25,11 +25,15 @@ import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
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.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.OptionsValueListDTO;
import org.openhab.binding.miio.internal.basic.StateDescriptionDTO;
import org.openhab.core.thing.ThingTypeUID;
import org.slf4j.Logger;
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 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
public static void main(String[] args) {
ReadmeHelper rm = new ReadmeHelper();
LOGGER.info("## Creating device list");
StringWriter deviceList = rm.deviceList();
rm.checkDatabaseEntrys();
LOGGER.info("## Creating channel list for basic devices");
LOGGER.info("## Creating channel list for json database driven devices");
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();
try {
String baseDoc = new String(Files.readAllBytes(Paths.get(BASEFILE)), StandardCharsets.UTF_8);
@ -125,35 +134,37 @@ public class ReadmeHelper {
sw.write(devicesCount);
sw.write("\n\n");
sw.write(
"| Device | ThingType | Device Model | Supported | Remark |\n");
"| Device | ThingType | Device Model | Supported | Remark |\n");
sw.write(
"|------------------------------|------------------|------------------------|-----------|------------|\n");
"|------------------------------------|------------------|------------------------|--------------|------------|\n");
Arrays.asList(MiIoDevices.values()).forEach(device -> {
if (!device.getModel().equals("unknown")) {
String link = device.getModel().replace(".", "-");
boolean isSupported = device.getThingType().equals(MiIoBindingConstants.THING_TYPE_UNSUPPORTED);
Boolean experimental = false;
String remark = "";
if (device.getThingType().equals(MiIoBindingConstants.THING_TYPE_BASIC)) {
if (DATABASE_THING_TYPES.contains(device.getThingType())) {
MiIoBasicDevice dev = findDatabaseEntry(device.getModel());
if (dev != null) {
remark = dev.getDevice().getReadmeComment();
final Boolean experimental = dev.getDevice().getExperimental();
if (experimental != null && experimental.booleanValue()) {
final Boolean experimentalDev = dev.getDevice().getExperimental();
experimental = experimentalDev != null && experimentalDev.booleanValue();
if (experimental) {
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";
}
}
}
sw.write("| ");
sw.write(minLengthString(device.getDescription(), 28));
sw.write(minLengthString(device.getDescription(), 34));
sw.write(" | ");
sw.write(minLengthString(device.getThingType().toString(), 16));
sw.write(" | ");
String model = isSupported ? device.getModel() : "[" + device.getModel() + "](#" + link + ")";
sw.write(minLengthString(model, 22));
sw.write(" | ");
sw.write(isSupported ? "No " : "Yes ");
sw.write(isSupported ? "No " : (experimental ? "Experimental" : "Yes "));
sw.write(" | ");
sw.write(minLengthString(remark, 10));
sw.write(" |\n");
@ -165,7 +176,7 @@ public class ReadmeHelper {
private StringWriter channelList() {
StringWriter sw = new StringWriter();
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());
if (dev != null) {
String link = device.getModel().replace(".", "-");
@ -217,7 +228,7 @@ public class ReadmeHelper {
private StringWriter itemFileExamples() {
StringWriter sw = new StringWriter();
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());
if (dev != null) {
sw.write("### " + device.getDescription() + " (" + device.getModel() + ") item file lines\n\n");
@ -231,7 +242,8 @@ public class ReadmeHelper {
for (MiIoBasicChannel ch : dev.getDevice().getChannels()) {
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");
}
@ -242,6 +254,7 @@ public class ReadmeHelper {
private void checkDatabaseEntrys() {
StringBuilder sb = new StringBuilder();
StringBuilder commentSb = new StringBuilder("Adding support for the following models:\r\n");
Gson gson = new GsonBuilder().setPrettyPrinting().create();
HashMap<String, String> names = new HashMap<String, String>();
try {
@ -253,7 +266,12 @@ public class ReadmeHelper {
for (MiIoBasicDevice entry : findDatabaseEntrys()) {
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("(\"");
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);
}
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) {
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;
}
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);
}