[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 <marcel@verpaalen.com>
This commit is contained in:
Marcel 2021-12-12 22:21:20 +01:00 committed by GitHub
parent ba58f96d33
commit f8a6522100
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 191 additions and 28 deletions

View File

@ -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 (<a name="chuangmi-plug-hmi205">chuangmi.plug.hmi205</a>) 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

View File

@ -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() {

View File

@ -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<String, Object> deviceVariables)
private static JsonElement addBrightToHSV(JsonElement rgbValue, @Nullable Map<String, Object> 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<String, Object> 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<String, Object> deviceVariables) {
public static JsonElement execute(String transformation, JsonElement value, Map<String, Object> 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;

View File

@ -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<String> 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<String> getTransformations() {
List<String> transformations = this.transformations;
if (transformations == null) {
transformations = new ArrayList<>();
}
String transformation = this.transformation;
if (transformation != null) {
List<String> allTransformation = new ArrayList<>(List.of(transformation));
allTransformation.addAll(transformations);
return allTransformation;
}
return transformations;
}
public final void setTransformations(@Nullable List<String> transformations) {
if (transformations != null && !transformations.isEmpty()) {
this.transformations = transformations;
} else {
this.transformations = null;
}
}
public @Nullable String getCategory() {
return category;
}

View File

@ -105,6 +105,7 @@ public class MiIoBasicHandler extends MiIoAbstractHandler {
private Map<ChannelUID, MiIoBasicChannel> actions = new HashMap<>();
private ChannelTypeRegistry channelTypeRegistry;
private BasicChannelTypeProvider basicChannelTypeProvider;
private Map<String, Integer> 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;

View File

@ -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(),

View File

@ -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

View File

@ -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

View File

@ -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<String, Object> 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);