From f8a65221005c7223f186b1b19743a0dde66c586b Mon Sep 17 00:00:00 2001 From: Marcel Date: Sun, 12 Dec 2021 22:21:20 +0100 Subject: [PATCH] [miio] add BT Devices channel to chuangmi plug (#11715) * [miio] add BT Devices channel to chuangmi plug * Shows the bluetooth devices connected to the plug (plug as BT gateway) * Add refresh interval functionality to reduce load on device * Change public to private for the private functions in conversions. * Add test for new conversion * Update miio.properties Signed-off-by: Marcel Verpaalen --- bundles/org.openhab.binding.miio/README.md | 2 + .../miio/internal/MiIoSendCommand.java | 2 +- .../miio/internal/basic/Conversions.java | 44 ++++++++++---- .../miio/internal/basic/MiIoBasicChannel.java | 57 +++++++++++++++++++ .../internal/handler/MiIoBasicHandler.java | 38 ++++++++++--- .../transport/MiIoAsyncCommunication.java | 5 +- .../resources/OH-INF/i18n/basic.properties | 1 + .../database/chuangmi.plug.212a01-miot.json | 22 +++++++ .../miio/internal/ConversionsTest.java | 48 ++++++++++++++-- 9 files changed, 191 insertions(+), 28 deletions(-) diff --git a/bundles/org.openhab.binding.miio/README.md b/bundles/org.openhab.binding.miio/README.md index a0adc3d9e..7e4cd02b3 100644 --- a/bundles/org.openhab.binding.miio/README.md +++ b/bundles/org.openhab.binding.miio/README.md @@ -701,6 +701,7 @@ Note, not all the values need to be in the json file, e.g. a subset of the param | task-switch | Switch | Imilab Timer - Task Switch | | | countdown-info | Switch | Imilab Timer - Countdown Info | | | bt-gw | String | BT Gateway | Value mapping `["disable"="Disable","enable"="Enable"]` | +| bt-gw-devices | String | Connected BT Gateway Devices | Note, refreshes every 2nd refresh. Channel requires cloud connectivity to function. Sample widget to visualise the (json) output available from the widget market | ### Mi Smart Plug WiFi (chuangmi.plug.hmi205) Channels @@ -5720,6 +5721,7 @@ Number:Time countdown "Imilab Timer - Countdown" (G_plug) {channel="miio:basic:p Switch task_switch "Imilab Timer - Task Switch" (G_plug) {channel="miio:basic:plug:task-switch"} Switch countdown_info "Imilab Timer - Countdown Info" (G_plug) {channel="miio:basic:plug:countdown-info"} String bt_gw "BT Gateway" (G_plug) {channel="miio:basic:plug:bt-gw"} +String bt_gw_devices "Connected BT Gateway Devices" (G_plug) {channel="miio:basic:plug:bt-gw-devices"} ``` ### Mi Smart Plug WiFi (chuangmi.plug.hmi205) item file lines diff --git a/bundles/org.openhab.binding.miio/src/main/java/org/openhab/binding/miio/internal/MiIoSendCommand.java b/bundles/org.openhab.binding.miio/src/main/java/org/openhab/binding/miio/internal/MiIoSendCommand.java index b26bd251d..d552c8fc1 100644 --- a/bundles/org.openhab.binding.miio/src/main/java/org/openhab/binding/miio/internal/MiIoSendCommand.java +++ b/bundles/org.openhab.binding.miio/src/main/java/org/openhab/binding/miio/internal/MiIoSendCommand.java @@ -74,7 +74,7 @@ public class MiIoSendCommand { } public JsonElement getParams() { - return commandJson.has("params") ? commandJson.get("params").getAsJsonArray() : new JsonArray(); + return commandJson.has("params") ? commandJson.get("params") : new JsonArray(); } public JsonObject getResponse() { diff --git a/bundles/org.openhab.binding.miio/src/main/java/org/openhab/binding/miio/internal/basic/Conversions.java b/bundles/org.openhab.binding.miio/src/main/java/org/openhab/binding/miio/internal/basic/Conversions.java index e20487d09..ab17fdfdf 100644 --- a/bundles/org.openhab.binding.miio/src/main/java/org/openhab/binding/miio/internal/basic/Conversions.java +++ b/bundles/org.openhab.binding.miio/src/main/java/org/openhab/binding/miio/internal/basic/Conversions.java @@ -44,7 +44,7 @@ public class Conversions { * @param RGB + brightness value (note brightness in the first byte) * @return HSV */ - public static JsonElement bRGBtoHSV(JsonElement bRGB) throws ClassCastException { + private static JsonElement bRGBtoHSV(JsonElement bRGB) throws ClassCastException { if (bRGB.isJsonPrimitive() && bRGB.getAsJsonPrimitive().isNumber()) { Color rgb = new Color(bRGB.getAsInt()); HSBType hsb = HSBType.fromRGB(rgb.getRed(), rgb.getGreen(), rgb.getBlue()); @@ -62,7 +62,7 @@ public class Conversions { * @param map with device variables containing the brightness info * @return HSV */ - public static JsonElement addBrightToHSV(JsonElement rgbValue, @Nullable Map deviceVariables) + private static JsonElement addBrightToHSV(JsonElement rgbValue, @Nullable Map deviceVariables) throws ClassCastException, IllegalStateException { int bright = 100; if (deviceVariables != null) { @@ -79,12 +79,12 @@ public class Conversions { return rgbValue; } - public static JsonElement secondsToHours(JsonElement seconds) throws ClassCastException { + private static JsonElement secondsToHours(JsonElement seconds) throws ClassCastException { double value = seconds.getAsDouble() / 3600; return new JsonPrimitive(value); } - public static JsonElement yeelightSceneConversion(JsonElement intValue) + private static JsonElement yeelightSceneConversion(JsonElement intValue) throws ClassCastException, IllegalStateException { switch (intValue.getAsInt()) { case 1: @@ -104,17 +104,17 @@ public class Conversions { } } - public static JsonElement divideTen(JsonElement value10) throws ClassCastException, IllegalStateException { + private static JsonElement divideTen(JsonElement value10) throws ClassCastException, IllegalStateException { double value = value10.getAsDouble() / 10.0; return new JsonPrimitive(value); } - public static JsonElement divideHundred(JsonElement value10) throws ClassCastException, IllegalStateException { + private static JsonElement divideHundred(JsonElement value10) throws ClassCastException, IllegalStateException { double value = value10.getAsDouble() / 100.0; return new JsonPrimitive(value); } - public static JsonElement tankLevel(JsonElement value12) throws ClassCastException, IllegalStateException { + private static JsonElement tankLevel(JsonElement value12) throws ClassCastException, IllegalStateException { // 127 without water tank. 120 = 100% water if (value12.getAsInt() == 127) { return new JsonPrimitive(-1); @@ -124,7 +124,30 @@ public class Conversions { } } - public static JsonElement getJsonElement(String element, JsonElement responseValue) { + /** + * Returns the deviceId element value from the Json response. If not found, returns the input + * + * @param responseValue + * @param deviceVariables containing the deviceId + * @return + */ + private static JsonElement getDidElement(JsonElement responseValue, Map deviceVariables) { + String did = (String) deviceVariables.get("deviceId"); + if (did != null) { + return getJsonElement(did, responseValue); + } + LOGGER.debug("deviceId not Found, no conversion"); + return responseValue; + } + + /** + * Returns the element from the Json response. If not found, returns the input + * + * @param element to be found + * @param responseValue + * @return + */ + private static JsonElement getJsonElement(String element, JsonElement responseValue) { try { if (responseValue.isJsonPrimitive() || responseValue.isJsonObject()) { JsonElement jsonElement = responseValue.isJsonObject() ? responseValue @@ -143,8 +166,7 @@ public class Conversions { return responseValue; } - public static JsonElement execute(String transformation, JsonElement value, - @Nullable Map deviceVariables) { + public static JsonElement execute(String transformation, JsonElement value, Map deviceVariables) { try { if (transformation.toUpperCase().startsWith("GETJSONELEMENT")) { if (transformation.length() > 15) { @@ -168,6 +190,8 @@ public class Conversions { return addBrightToHSV(value, deviceVariables); case "BRGBTOHSV": return bRGBtoHSV(value); + case "GETDIDELEMENT": + return getDidElement(value, deviceVariables); default: LOGGER.debug("Transformation {} not found. Returning '{}'", transformation, value.toString()); return value; diff --git a/bundles/org.openhab.binding.miio/src/main/java/org/openhab/binding/miio/internal/basic/MiIoBasicChannel.java b/bundles/org.openhab.binding.miio/src/main/java/org/openhab/binding/miio/internal/basic/MiIoBasicChannel.java index 33dc57574..6190f075d 100644 --- a/bundles/org.openhab.binding.miio/src/main/java/org/openhab/binding/miio/internal/basic/MiIoBasicChannel.java +++ b/bundles/org.openhab.binding.miio/src/main/java/org/openhab/binding/miio/internal/basic/MiIoBasicChannel.java @@ -22,6 +22,7 @@ import java.util.List; import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; +import com.google.gson.JsonElement; import com.google.gson.annotations.Expose; import com.google.gson.annotations.SerializedName; @@ -66,12 +67,21 @@ public class MiIoBasicChannel { @SerializedName("refresh") @Expose private @Nullable Boolean refresh; + @SerializedName("refreshInterval") + @Expose + private @Nullable Integer refreshInterval; @SerializedName("customRefreshCommand") @Expose private @Nullable String channelCustomRefreshCommand; + @SerializedName("customRefreshParameters") + @Expose + private @Nullable JsonElement customRefreshParameters; @SerializedName("transformation") @Expose private @Nullable String transformation; + @SerializedName("transformations") + @Expose + private @Nullable List transformations; @SerializedName("ChannelGroup") @Expose private @Nullable String channelGroup; @@ -202,6 +212,23 @@ public class MiIoBasicChannel { this.refresh = refresh; } + public Integer getRefreshInterval() { + Integer refreshInterval = this.refreshInterval; + if (refreshInterval != null) { + return refreshInterval; + } + return 1; + } + + public void setRefresh(@Nullable final Integer interval) { + final Integer refreshInterval = interval; + if (refreshInterval != null && refreshInterval.intValue() != 1) { + this.refreshInterval = refreshInterval; + } else { + this.refreshInterval = null; + } + } + public String getChannelCustomRefreshCommand() { final @Nullable String channelCustomRefreshCommand = this.channelCustomRefreshCommand; return channelCustomRefreshCommand != null ? channelCustomRefreshCommand : ""; @@ -211,6 +238,14 @@ public class MiIoBasicChannel { this.channelCustomRefreshCommand = channelCustomRefreshCommand; } + public @Nullable final JsonElement getCustomRefreshParameters() { + return customRefreshParameters; + } + + public final void setCustomRefreshParameters(@Nullable JsonElement customRefreshParameters) { + this.customRefreshParameters = customRefreshParameters; + } + public String getChannelGroup() { final @Nullable String channelGroup = this.channelGroup; return channelGroup != null ? channelGroup : ""; @@ -237,6 +272,28 @@ public class MiIoBasicChannel { this.transformation = transformation; } + public final List getTransformations() { + List transformations = this.transformations; + if (transformations == null) { + transformations = new ArrayList<>(); + } + String transformation = this.transformation; + if (transformation != null) { + List allTransformation = new ArrayList<>(List.of(transformation)); + allTransformation.addAll(transformations); + return allTransformation; + } + return transformations; + } + + public final void setTransformations(@Nullable List transformations) { + if (transformations != null && !transformations.isEmpty()) { + this.transformations = transformations; + } else { + this.transformations = null; + } + } + public @Nullable String getCategory() { return category; } diff --git a/bundles/org.openhab.binding.miio/src/main/java/org/openhab/binding/miio/internal/handler/MiIoBasicHandler.java b/bundles/org.openhab.binding.miio/src/main/java/org/openhab/binding/miio/internal/handler/MiIoBasicHandler.java index d192889aa..e51083b71 100644 --- a/bundles/org.openhab.binding.miio/src/main/java/org/openhab/binding/miio/internal/handler/MiIoBasicHandler.java +++ b/bundles/org.openhab.binding.miio/src/main/java/org/openhab/binding/miio/internal/handler/MiIoBasicHandler.java @@ -105,6 +105,7 @@ public class MiIoBasicHandler extends MiIoAbstractHandler { private Map actions = new HashMap<>(); private ChannelTypeRegistry channelTypeRegistry; private BasicChannelTypeProvider basicChannelTypeProvider; + private Map customRefreshInterval = new HashMap<>(); public MiIoBasicHandler(Thing thing, MiIoDatabaseWatchService miIoDatabaseWatchService, CloudConnector cloudConnector, ChannelTypeRegistry channelTypeRegistry, @@ -352,16 +353,39 @@ public class MiIoBasicHandler extends MiIoAbstractHandler { } } + private boolean customRefreshIntervalCheck(MiIoBasicChannel miChannel) { + if (miChannel.getRefreshInterval() > 1) { + int iteration = customRefreshInterval.getOrDefault(miChannel.getChannel(), 0); + if (iteration < 1) { + customRefreshInterval.put(miChannel.getChannel(), miChannel.getRefreshInterval() - 1); + } else { + logger.debug("Skip refresh of channel {} for {}. Next refresh in {} cycles.", miChannel.getChannel(), + getThing().getUID(), iteration); + customRefreshInterval.put(miChannel.getChannel(), iteration - 1); + return true; + } + } + return false; + } + + private boolean linkedChannelCheck(MiIoBasicChannel miChannel) { + if (!isLinked(miChannel.getChannel())) { + logger.debug("Skip refresh of channel {} for {} as it is not linked", miChannel.getChannel(), + getThing().getUID()); + return false; + } + return true; + } + private void refreshCustomProperties(MiIoBasicDevice midevice) { for (MiIoBasicChannel miChannel : refreshListCustomCommands.values()) { - if (!isLinked(miChannel.getChannel())) { - logger.debug("Skip refresh of channel {} for {} as it is not linked", miChannel.getChannel(), - getThing().getUID()); + if (customRefreshIntervalCheck(miChannel) || !linkedChannelCheck(miChannel)) { continue; } - String cmd = miChannel.getChannelCustomRefreshCommand(); + final JsonElement para = miChannel.getCustomRefreshParameters(); + String cmd = miChannel.getChannelCustomRefreshCommand() + (para != null ? para.toString() : ""); if (!cmd.startsWith("/")) { - cmds.put(sendCommand(miChannel.getChannelCustomRefreshCommand()), miChannel.getChannel()); + cmds.put(sendCommand(cmd), miChannel.getChannel()); } else { if (cloudServer.isBlank()) { logger.debug("Cloudserver empty. Skipping refresh for {} channel '{}'", getThing().getUID(), @@ -378,9 +402,7 @@ public class MiIoBasicHandler extends MiIoAbstractHandler { int maxProperties = device.getDevice().getMaxProperties(); JsonArray getPropString = new JsonArray(); for (MiIoBasicChannel miChannel : refreshList) { - if (!isLinked(miChannel.getChannel())) { - logger.debug("Skip refresh of channel {} for {} as it is not linked", miChannel.getChannel(), - getThing().getUID()); + if (customRefreshIntervalCheck(miChannel) || !linkedChannelCheck(miChannel)) { continue; } JsonElement property; diff --git a/bundles/org.openhab.binding.miio/src/main/java/org/openhab/binding/miio/internal/transport/MiIoAsyncCommunication.java b/bundles/org.openhab.binding.miio/src/main/java/org/openhab/binding/miio/internal/transport/MiIoAsyncCommunication.java index 1bb9018b4..caa85b37b 100644 --- a/bundles/org.openhab.binding.miio/src/main/java/org/openhab/binding/miio/internal/transport/MiIoAsyncCommunication.java +++ b/bundles/org.openhab.binding.miio/src/main/java/org/openhab/binding/miio/internal/transport/MiIoAsyncCommunication.java @@ -182,10 +182,7 @@ public class MiIoAsyncCommunication { miIoSendCommand.getCloudServer()); updateStatus(ThingStatus.ONLINE, ThingStatusDetail.NONE); } else { - String data = miIoSendCommand.getParams().isJsonArray() - && miIoSendCommand.getParams().getAsJsonArray().size() > 0 - ? miIoSendCommand.getParams().getAsJsonArray().get(0).toString() - : ""; + String data = miIoSendCommand.getParams().toString(); logger.debug("Custom cloud request send to url '{}' with data '{}'", miIoSendCommand.getMethod(), data); decryptedResponse = cloudConnector.sendCloudCommand(miIoSendCommand.getMethod(), diff --git a/bundles/org.openhab.binding.miio/src/main/resources/OH-INF/i18n/basic.properties b/bundles/org.openhab.binding.miio/src/main/resources/OH-INF/i18n/basic.properties index 65659da0c..ec7c8d672 100644 --- a/bundles/org.openhab.binding.miio/src/main/resources/OH-INF/i18n/basic.properties +++ b/bundles/org.openhab.binding.miio/src/main/resources/OH-INF/i18n/basic.properties @@ -414,6 +414,7 @@ ch.cgllc.airmonitor.s1.pm25 = PM2.5 ch.cgllc.airmonitor.s1.temperature = Temperature ch.cgllc.airmonitor.s1.tvoc = tVOC ch.chuangmi.plug.212a01-miot.bt-gw = BT Gateway +ch.chuangmi.plug.212a01-miot.bt-gw-devices = Connected BT Gateway Devices ch.chuangmi.plug.212a01-miot.countdown = Imilab Timer - Countdown ch.chuangmi.plug.212a01-miot.countdown-info = Imilab Timer - Countdown Info ch.chuangmi.plug.212a01-miot.electric-current = Power Consumption - Electric Current diff --git a/bundles/org.openhab.binding.miio/src/main/resources/database/chuangmi.plug.212a01-miot.json b/bundles/org.openhab.binding.miio/src/main/resources/database/chuangmi.plug.212a01-miot.json index 9ee47e5e6..66f248fa2 100644 --- a/bundles/org.openhab.binding.miio/src/main/resources/database/chuangmi.plug.212a01-miot.json +++ b/bundles/org.openhab.binding.miio/src/main/resources/database/chuangmi.plug.212a01-miot.json @@ -322,7 +322,29 @@ } } ], + "category": "bluetooth", "readmeComment": "Value mapping `[\"disable\"\u003d\"Disable\",\"enable\"\u003d\"Enable\"]`" + }, + { + "property": "", + "friendlyName": "Connected BT Gateway Devices", + "channel": "bt-gw-devices", + "type": "String", + "stateDescription": { + "readOnly": true + }, + "refresh": true, + "refreshInterval": 2, + "customRefreshCommand": "/device/get_bledevice_by_gateway", + "customRefreshParameters": { + "dids": [ + "$deviceId$" + ] + }, + "transformation": "getDiDElement", + "actions": [], + "category": "bluetooth", + "readmeComment": "Note, refreshes every 2nd refresh. Channel requires cloud connectivity to function. Sample widget to visualise the (json) output available from the widget market" } ], "experimental": false diff --git a/bundles/org.openhab.binding.miio/src/test/java/org/openhab/binding/miio/internal/ConversionsTest.java b/bundles/org.openhab.binding.miio/src/test/java/org/openhab/binding/miio/internal/ConversionsTest.java index 2ee46a065..aa83988d6 100644 --- a/bundles/org.openhab.binding.miio/src/test/java/org/openhab/binding/miio/internal/ConversionsTest.java +++ b/bundles/org.openhab.binding.miio/src/test/java/org/openhab/binding/miio/internal/ConversionsTest.java @@ -15,6 +15,7 @@ package org.openhab.binding.miio.internal; import static org.junit.jupiter.api.Assertions.*; import java.util.Collections; +import java.util.HashMap; import java.util.Map; import org.eclipse.jdt.annotation.NonNullByDefault; @@ -35,6 +36,48 @@ import com.google.gson.JsonPrimitive; @NonNullByDefault public class ConversionsTest { + @Test + public void getDidElementTest() { + Map deviceVariables = new HashMap<>(); + String transformation = "getDidElement"; + JsonElement validInput = new JsonPrimitive( + "{\"361185596\":\"{\\\"C812105B04000400\\\":\\\"-92\\\",\\\"blt.3.17q3si5345k00\\\":\\\"-54\\\",\\\"blt.4.10heul64og400\\\":\\\"-73\\\"}\"}"); + + // test no did in deviceVariables + JsonElement value = validInput; + JsonElement transformedResponse = Conversions.execute(transformation, value, deviceVariables); + assertNotNull(transformedResponse); + assertEquals(value, transformedResponse); + + // test valid input & response + deviceVariables.put("deviceId", "361185596"); + value = validInput; + transformedResponse = Conversions.execute(transformation, value, deviceVariables); + assertNotNull(transformedResponse); + assertEquals(new JsonPrimitive( + "{\"C812105B04000400\":\"-92\",\"blt.3.17q3si5345k00\":\"-54\",\"blt.4.10heul64og400\":\"-73\"}"), + transformedResponse); + + // test non json + value = new JsonPrimitive("some non json value"); + transformedResponse = Conversions.execute(transformation, value, deviceVariables); + assertNotNull(transformedResponse); + assertEquals(value, transformedResponse); + + // test different did in deviceVariables + deviceVariables.put("deviceId", "ABC185596"); + value = validInput; + transformedResponse = Conversions.execute(transformation, value, deviceVariables); + assertNotNull(transformedResponse); + assertEquals(value, transformedResponse); + + // test empty input + value = new JsonPrimitive(""); + transformedResponse = Conversions.execute(transformation, value, deviceVariables); + assertNotNull(transformedResponse); + assertEquals(value, transformedResponse); + } + @Test public void getJsonElementTest() { @@ -55,11 +98,6 @@ public class ConversionsTest { transformation = "getJsonElement-test"; - // test without deviceVariables - resp = Conversions.execute(transformation, value, null); - assertNotNull(resp); - assertEquals(new JsonPrimitive("testresponse"), resp); - // test non json value = new JsonPrimitive("some non json value"); resp = Conversions.execute(transformation, value, deviceVariables);