Browse Source

Merge branch 'master' into fix_bug_lwm2m_redis

pull/10613/head
nick 2 years ago
parent
commit
8828798639
  1. 6
      application/src/main/data/json/system/widget_bundles/status_indicators.json
  2. 26
      application/src/main/data/json/system/widget_types/status_widget.json
  3. 2
      application/src/main/java/org/thingsboard/server/controller/TelemetryController.java
  4. 4
      application/src/test/java/org/thingsboard/server/service/sql/SequentialTimeseriesPersistenceTest.java
  5. 19
      common/proto/src/main/java/org/thingsboard/server/common/adaptor/JsonConverter.java
  6. 5
      common/proto/src/main/java/org/thingsboard/server/common/adaptor/ProtoConverter.java
  7. 24
      common/proto/src/test/java/org/thingsboard/server/common/adaptor/JsonConverterTest.java
  8. 8
      common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/adaptors/JsonCoapAdaptor.java
  9. 6
      common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/adaptors/ProtoCoapAdaptor.java
  10. 6
      common/transport/http/src/main/java/org/thingsboard/server/transport/http/DeviceApiController.java
  11. 3
      common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/LwM2mTransportServerHelper.java
  12. 10
      common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/adaptors/JsonMqttAdaptor.java
  13. 6
      common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/adaptors/ProtoMqttAdaptor.java
  14. 2
      common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/SparkplugNodeSessionHandler.java
  15. 6
      common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/util/JsonUtils.java
  16. 3
      monitoring/src/main/java/org/thingsboard/monitoring/client/Lwm2mClient.java
  17. 3
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbSaveToCustomCassandraTableNode.java
  18. 4
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/geo/TbGpsGeofencingActionNode.java
  19. 12
      ui-ngx/src/app/modules/home/components/widget/config/basic/basic-widget-config.module.ts
  20. 101
      ui-ngx/src/app/modules/home/components/widget/config/basic/indicator/status-widget-basic-config.component.html
  21. 119
      ui-ngx/src/app/modules/home/components/widget/config/basic/indicator/status-widget-basic-config.component.ts
  22. 33
      ui-ngx/src/app/modules/home/components/widget/lib/indicator/status-widget.component.html
  23. 99
      ui-ngx/src/app/modules/home/components/widget/lib/indicator/status-widget.component.scss
  24. 242
      ui-ngx/src/app/modules/home/components/widget/lib/indicator/status-widget.component.ts
  25. 204
      ui-ngx/src/app/modules/home/components/widget/lib/indicator/status-widget.models.ts
  26. 111
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/indicator/status-widget-state-settings.component.html
  27. 158
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/indicator/status-widget-state-settings.component.ts
  28. 5
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/widget-settings-common.module.ts
  29. 82
      ui-ngx/src/app/modules/home/components/widget/lib/settings/indicator/status-widget-settings.component.html
  30. 79
      ui-ngx/src/app/modules/home/components/widget/lib/settings/indicator/status-widget-settings.component.ts
  31. 12
      ui-ngx/src/app/modules/home/components/widget/lib/settings/widget-settings.module.ts
  32. 7
      ui-ngx/src/app/modules/home/components/widget/widget-components.module.ts
  33. 8263
      ui-ngx/src/assets/locale/locale.constant-ar_AR.json
  34. 20
      ui-ngx/src/assets/locale/locale.constant-en_US.json
  35. 20
      ui-ngx/src/assets/widget/status-widget/center-layout.svg
  36. 20
      ui-ngx/src/assets/widget/status-widget/default-layout.svg
  37. 18
      ui-ngx/src/assets/widget/status-widget/icon-layout.svg

6
application/src/main/data/json/system/widget_bundles/status_indicators.json

@ -2,15 +2,15 @@
"widgetsBundle": { "widgetsBundle": {
"alias": "status_indicators", "alias": "status_indicators",
"title": "Status indicators", "title": "Status indicators",
"image": "data:image/png;base64,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", "image": "tb-image:c3RhdHVzX2luZGljYXRvcnNfc3lzdGVtX2J1bmRsZV9pbWFnZS5wbmc=:IlN0YXR1cyBpbmRpY2F0b3JzIiBzeXN0ZW0gYnVuZGxlIGltYWdl;data:image/png;base64,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",
"description": "Contains widgets displaying battery level and signal strength.", "description": "Contains widgets displaying battery level and signal strength.",
"order": 9000, "order": 9000,
"externalId": null,
"name": "Status indicators" "name": "Status indicators"
}, },
"widgetTypeFqns": [ "widgetTypeFqns": [
"battery_level", "battery_level",
"signal_strength", "signal_strength",
"progress_bar" "progress_bar",
"status_widget"
] ]
} }

26
application/src/main/data/json/system/widget_types/status_widget.json

File diff suppressed because one or more lines are too long

2
application/src/main/java/org/thingsboard/server/controller/TelemetryController.java

@ -647,7 +647,7 @@ public class TelemetryController extends BaseController {
Map<Long, List<KvEntry>> telemetryRequest; Map<Long, List<KvEntry>> telemetryRequest;
JsonElement telemetryJson; JsonElement telemetryJson;
try { try {
telemetryJson = new JsonParser().parse(requestBody); telemetryJson = JsonParser.parseString(requestBody);
} catch (Exception e) { } catch (Exception e) {
return getImmediateDeferredResult("Unable to parse timeseries payload: Invalid JSON body!", HttpStatus.BAD_REQUEST); return getImmediateDeferredResult("Unable to parse timeseries payload: Invalid JSON body!", HttpStatus.BAD_REQUEST);
} }

4
application/src/test/java/org/thingsboard/server/service/sql/SequentialTimeseriesPersistenceTest.java

@ -147,7 +147,7 @@ public class SequentialTimeseriesPersistenceTest extends AbstractControllerTest
void checkDiffBetweenLatestTsForDevicesAndAsset(List<Device> devices, Asset asset) throws ExecutionException, InterruptedException, TimeoutException { void checkDiffBetweenLatestTsForDevicesAndAsset(List<Device> devices, Asset asset) throws ExecutionException, InterruptedException, TimeoutException {
TsKvEntry assetTsKvEntry = getTsKvLatest(asset.getId(), GENERIC_CUMULATIVE_OBJ); TsKvEntry assetTsKvEntry = getTsKvLatest(asset.getId(), GENERIC_CUMULATIVE_OBJ);
Assert.assertTrue(assetTsKvEntry.getJsonValue().isPresent()); Assert.assertTrue(assetTsKvEntry.getJsonValue().isPresent());
JsonObject assetJsonObject = new JsonParser().parse(assetTsKvEntry.getJsonValue().get()).getAsJsonObject(); JsonObject assetJsonObject = JsonParser.parseString(assetTsKvEntry.getJsonValue().get()).getAsJsonObject();
for (Device device : devices) { for (Device device : devices) {
Long assetValue = assetJsonObject.get(device.getName()).getAsLong(); Long assetValue = assetJsonObject.get(device.getName()).getAsLong();
TsKvEntry deviceLatest = getTsKvLatest(device.getId(), TOTALIZER); TsKvEntry deviceLatest = getTsKvLatest(device.getId(), TOTALIZER);
@ -182,7 +182,7 @@ public class SequentialTimeseriesPersistenceTest extends AbstractControllerTest
JsonObject getJsonObject(String key, long value, Optional<String> tsKvEntryOpt) { JsonObject getJsonObject(String key, long value, Optional<String> tsKvEntryOpt) {
JsonObject jsonObject = new JsonObject(); JsonObject jsonObject = new JsonObject();
if (tsKvEntryOpt.isPresent()) { if (tsKvEntryOpt.isPresent()) {
jsonObject = new JsonParser().parse(tsKvEntryOpt.get()).getAsJsonObject(); jsonObject = JsonParser.parseString(tsKvEntryOpt.get()).getAsJsonObject();
} }
jsonObject.addProperty(key, value); jsonObject.addProperty(key, value);
return jsonObject; return jsonObject;

19
common/proto/src/main/java/org/thingsboard/server/common/adaptor/JsonConverter.java

@ -66,7 +66,6 @@ import java.util.stream.Collectors;
public class JsonConverter { public class JsonConverter {
private static final Gson GSON = new Gson(); private static final Gson GSON = new Gson();
private static final JsonParser JSON_PARSER = new JsonParser();
private static final String CAN_T_PARSE_VALUE = "Can't parse value: "; private static final String CAN_T_PARSE_VALUE = "Can't parse value: ";
private static final String DEVICE_PROPERTY = "device"; private static final String DEVICE_PROPERTY = "device";
@ -111,7 +110,7 @@ public class JsonConverter {
public static ClaimDeviceMsg convertToClaimDeviceProto(DeviceId deviceId, String json) { public static ClaimDeviceMsg convertToClaimDeviceProto(DeviceId deviceId, String json) {
long durationMs = 0L; long durationMs = 0L;
if (json != null && !json.isEmpty()) { if (json != null && !json.isEmpty()) {
return convertToClaimDeviceProto(deviceId, JSON_PARSER.parse(json)); return convertToClaimDeviceProto(deviceId, JsonParser.parseString(json));
} }
return buildClaimDeviceMsg(deviceId, DataConstants.DEFAULT_SECRET_KEY, durationMs); return buildClaimDeviceMsg(deviceId, DataConstants.DEFAULT_SECRET_KEY, durationMs);
} }
@ -160,7 +159,7 @@ public class JsonConverter {
result.addProperty("id", msg.getRequestId()); result.addProperty("id", msg.getRequestId());
} }
result.addProperty("method", msg.getMethodName()); result.addProperty("method", msg.getMethodName());
result.add("params", JSON_PARSER.parse(msg.getParams())); result.add("params", JsonParser.parseString(msg.getParams()));
return result; return result;
} }
@ -367,7 +366,7 @@ public class JsonConverter {
json.addProperty(name, entry.getLongV()); json.addProperty(name, entry.getLongV());
break; break;
case JSON_V: case JSON_V:
json.add(name, JSON_PARSER.parse(entry.getJsonV())); json.add(name, JsonParser.parseString(entry.getJsonV()));
break; break;
} }
} }
@ -388,7 +387,7 @@ public class JsonConverter {
result.add(de.getKv().getKey(), new JsonPrimitive(de.getKv().getStringV())); result.add(de.getKv().getKey(), new JsonPrimitive(de.getKv().getStringV()));
break; break;
case JSON_V: case JSON_V:
result.add(de.getKv().getKey(), JSON_PARSER.parse(de.getKv().getJsonV())); result.add(de.getKv().getKey(), JsonParser.parseString(de.getKv().getJsonV()));
break; break;
default: default:
throw new IllegalArgumentException("Unsupported data type: " + de.getKv().getType()); throw new IllegalArgumentException("Unsupported data type: " + de.getKv().getType());
@ -412,7 +411,7 @@ public class JsonConverter {
result.add(de.getKey(), new JsonPrimitive(de.getStrValue().get())); result.add(de.getKey(), new JsonPrimitive(de.getStrValue().get()));
break; break;
case JSON: case JSON:
result.add(de.getKey(), JSON_PARSER.parse(de.getJsonValue().get())); result.add(de.getKey(), JsonParser.parseString(de.getJsonValue().get()));
break; break;
default: default:
throw new IllegalArgumentException("Unsupported data type: " + de.getDataType()); throw new IllegalArgumentException("Unsupported data type: " + de.getDataType());
@ -422,7 +421,7 @@ public class JsonConverter {
public static JsonElement toJson(TransportProtos.ToServerRpcResponseMsg msg) { public static JsonElement toJson(TransportProtos.ToServerRpcResponseMsg msg) {
if (StringUtils.isEmpty(msg.getError())) { if (StringUtils.isEmpty(msg.getError())) {
return JSON_PARSER.parse(msg.getPayload()); return JsonParser.parseString(msg.getPayload());
} else { } else {
JsonObject errorMsg = new JsonObject(); JsonObject errorMsg = new JsonObject();
errorMsg.addProperty("error", msg.getError()); errorMsg.addProperty("error", msg.getError());
@ -456,7 +455,7 @@ public class JsonConverter {
result.addProperty("credentialsValue", payload.getCredentialsValue()); result.addProperty("credentialsValue", payload.getCredentialsValue());
break; break;
case MQTT_BASIC: case MQTT_BASIC:
result.add("credentialsValue", JSON_PARSER.parse(payload.getCredentialsValue()).getAsJsonObject()); result.add("credentialsValue", JsonParser.parseString(payload.getCredentialsValue()).getAsJsonObject());
break; break;
case LWM2M_CREDENTIALS: case LWM2M_CREDENTIALS:
break; break;
@ -572,7 +571,7 @@ public class JsonConverter {
} }
public static JsonElement parse(String json) { public static JsonElement parse(String json) {
return JSON_PARSER.parse(json); return JsonParser.parseString(json);
} }
public static <T> T parse(String json, Class<T> clazz) { public static <T> T parse(String json, Class<T> clazz) {
@ -600,7 +599,7 @@ public class JsonConverter {
} }
public static TransportProtos.ProvisionDeviceRequestMsg convertToProvisionRequestMsg(String json) { public static TransportProtos.ProvisionDeviceRequestMsg convertToProvisionRequestMsg(String json) {
JsonElement jsonElement = JSON_PARSER.parse(json); JsonElement jsonElement = JsonParser.parseString(json);
if (jsonElement.isJsonObject()) { if (jsonElement.isJsonObject()) {
return buildProvisionRequestMsg(jsonElement.getAsJsonObject()); return buildProvisionRequestMsg(jsonElement.getAsJsonObject());
} else { } else {

5
common/proto/src/main/java/org/thingsboard/server/common/adaptor/ProtoConverter.java

@ -39,7 +39,6 @@ import java.util.List;
public class ProtoConverter { public class ProtoConverter {
public static final Gson GSON = new Gson(); public static final Gson GSON = new Gson();
public static final JsonParser JSON_PARSER = new JsonParser();
public static TransportProtos.PostTelemetryMsg convertToTelemetryProto(byte[] payload) throws InvalidProtocolBufferException, IllegalArgumentException { public static TransportProtos.PostTelemetryMsg convertToTelemetryProto(byte[] payload) throws InvalidProtocolBufferException, IllegalArgumentException {
TransportProtos.TsKvListProto protoPayload = TransportProtos.TsKvListProto.parseFrom(payload); TransportProtos.TsKvListProto protoPayload = TransportProtos.TsKvListProto.parseFrom(payload);
@ -165,7 +164,7 @@ public class ProtoConverter {
break; break;
case JSON_V: case JSON_V:
try { try {
JSON_PARSER.parse(keyValueProto.getJsonV()); JsonParser.parseString(keyValueProto.getJsonV());
} catch (Exception e) { } catch (Exception e) {
throw new IllegalArgumentException("Can't parse value: " + keyValueProto.getJsonV() + " for key: " + key + "!"); throw new IllegalArgumentException("Can't parse value: " + keyValueProto.getJsonV() + " for key: " + key + "!");
} }
@ -184,7 +183,7 @@ public class ProtoConverter {
rpcRequestJson.addProperty("requestId", toDeviceRpcRequestMsg.getRequestId()); rpcRequestJson.addProperty("requestId", toDeviceRpcRequestMsg.getRequestId());
String params = toDeviceRpcRequestMsg.getParams(); String params = toDeviceRpcRequestMsg.getParams();
try { try {
JsonElement paramsElement = JSON_PARSER.parse(params); JsonElement paramsElement = JsonParser.parseString(params);
rpcRequestJson.add("params", paramsElement); rpcRequestJson.add("params", paramsElement);
DynamicMessage dynamicRpcRequest = DynamicProtoUtils.jsonToDynamicMessage(rpcRequestDynamicMessageBuilder, GSON.toJson(rpcRequestJson)); DynamicMessage dynamicRpcRequest = DynamicProtoUtils.jsonToDynamicMessage(rpcRequestDynamicMessageBuilder, GSON.toJson(rpcRequestJson));
return dynamicRpcRequest.toByteArray(); return dynamicRpcRequest.toByteArray();

24
common/proto/src/test/java/org/thingsboard/server/common/adaptor/JsonConverterTest.java

@ -26,8 +26,6 @@ import java.util.ArrayList;
public class JsonConverterTest { public class JsonConverterTest {
private final JsonParser JSON_PARSER = new JsonParser();
@BeforeEach @BeforeEach
public void before() { public void before() {
JsonConverter.setTypeCastEnabled(true); JsonConverter.setTypeCastEnabled(true);
@ -35,55 +33,55 @@ public class JsonConverterTest {
@Test @Test
public void testParseBigDecimalAsLong() { public void testParseBigDecimalAsLong() {
var result = JsonConverter.convertToTelemetry(JSON_PARSER.parse("{\"meterReadingDelta\": 1E+1}"), 0L); var result = JsonConverter.convertToTelemetry(JsonParser.parseString("{\"meterReadingDelta\": 1E+1}"), 0L);
Assert.assertEquals(10L, result.get(0L).get(0).getLongValue().get().longValue()); Assert.assertEquals(10L, result.get(0L).get(0).getLongValue().get().longValue());
} }
@Test @Test
public void testParseBigDecimalAsDouble() { public void testParseBigDecimalAsDouble() {
var result = JsonConverter.convertToTelemetry(JSON_PARSER.parse("{\"meterReadingDelta\": 101E-1}"), 0L); var result = JsonConverter.convertToTelemetry(JsonParser.parseString("{\"meterReadingDelta\": 101E-1}"), 0L);
Assert.assertEquals(10.1, result.get(0L).get(0).getDoubleValue().get(), 0.0); Assert.assertEquals(10.1, result.get(0L).get(0).getDoubleValue().get(), 0.0);
} }
@Test @Test
public void testParseAttributesBigDecimalAsLong() { public void testParseAttributesBigDecimalAsLong() {
var result = new ArrayList<>(JsonConverter.convertToAttributes(JSON_PARSER.parse("{\"meterReadingDelta\": 1E1}"))); var result = new ArrayList<>(JsonConverter.convertToAttributes(JsonParser.parseString("{\"meterReadingDelta\": 1E1}")));
Assert.assertEquals(10L, result.get(0).getLongValue().get().longValue()); Assert.assertEquals(10L, result.get(0).getLongValue().get().longValue());
} }
@Test @Test
public void testParseAsDoubleWithZero() { public void testParseAsDoubleWithZero() {
var result = JsonConverter.convertToTelemetry(JSON_PARSER.parse("{\"meterReadingDelta\": 42.0}"), 0L); var result = JsonConverter.convertToTelemetry(JsonParser.parseString("{\"meterReadingDelta\": 42.0}"), 0L);
Assert.assertEquals(42.0, result.get(0L).get(0).getDoubleValue().get(), 0.0); Assert.assertEquals(42.0, result.get(0L).get(0).getDoubleValue().get(), 0.0);
} }
@Test @Test
public void testParseAsDouble() { public void testParseAsDouble() {
var result = JsonConverter.convertToTelemetry(JSON_PARSER.parse("{\"meterReadingDelta\": 1.1}"), 0L); var result = JsonConverter.convertToTelemetry(JsonParser.parseString("{\"meterReadingDelta\": 1.1}"), 0L);
Assert.assertEquals(1.1, result.get(0L).get(0).getDoubleValue().get(), 0.0); Assert.assertEquals(1.1, result.get(0L).get(0).getDoubleValue().get(), 0.0);
} }
@Test @Test
public void testParseAsLong() { public void testParseAsLong() {
var result = JsonConverter.convertToTelemetry(JSON_PARSER.parse("{\"meterReadingDelta\": 11}"), 0L); var result = JsonConverter.convertToTelemetry(JsonParser.parseString("{\"meterReadingDelta\": 11}"), 0L);
Assert.assertEquals(11L, result.get(0L).get(0).getLongValue().get().longValue()); Assert.assertEquals(11L, result.get(0L).get(0).getLongValue().get().longValue());
} }
@Test @Test
public void testParseBigDecimalAsStringOutOfLongRange() { public void testParseBigDecimalAsStringOutOfLongRange() {
var result = JsonConverter.convertToTelemetry(JSON_PARSER.parse("{\"meterReadingDelta\": 9.9701010061400066E19}"), 0L); var result = JsonConverter.convertToTelemetry(JsonParser.parseString("{\"meterReadingDelta\": 9.9701010061400066E19}"), 0L);
Assert.assertEquals("99701010061400066000", result.get(0L).get(0).getStrValue().get()); Assert.assertEquals("99701010061400066000", result.get(0L).get(0).getStrValue().get());
} }
@Test @Test
public void testParseBigDecimalAsStringOutOfLongRange2() { public void testParseBigDecimalAsStringOutOfLongRange2() {
var result = JsonConverter.convertToTelemetry(JSON_PARSER.parse("{\"meterReadingDelta\": 99701010061400066001}"), 0L); var result = JsonConverter.convertToTelemetry(JsonParser.parseString("{\"meterReadingDelta\": 99701010061400066001}"), 0L);
Assert.assertEquals("99701010061400066001", result.get(0L).get(0).getStrValue().get()); Assert.assertEquals("99701010061400066001", result.get(0L).get(0).getStrValue().get());
} }
@Test @Test
public void testParseBigDecimalAsStringOutOfLongRange3() { public void testParseBigDecimalAsStringOutOfLongRange3() {
var result = JsonConverter.convertToTelemetry(JSON_PARSER.parse("{\"meterReadingDelta\": 1E19}"), 0L); var result = JsonConverter.convertToTelemetry(JsonParser.parseString("{\"meterReadingDelta\": 1E19}"), 0L);
Assert.assertEquals("10000000000000000000", result.get(0L).get(0).getStrValue().get()); Assert.assertEquals("10000000000000000000", result.get(0L).get(0).getStrValue().get());
} }
@ -91,7 +89,7 @@ public class JsonConverterTest {
public void testParseBigDecimalOutOfLongRangeWithoutParsing() { public void testParseBigDecimalOutOfLongRangeWithoutParsing() {
JsonConverter.setTypeCastEnabled(false); JsonConverter.setTypeCastEnabled(false);
Assertions.assertThrows(JsonSyntaxException.class, () -> { Assertions.assertThrows(JsonSyntaxException.class, () -> {
JsonConverter.convertToTelemetry(JSON_PARSER.parse("{\"meterReadingDelta\": 89701010051400054084}"), 0L); JsonConverter.convertToTelemetry(JsonParser.parseString("{\"meterReadingDelta\": 89701010051400054084}"), 0L);
}); });
} }
@ -99,7 +97,7 @@ public class JsonConverterTest {
public void testParseBigDecimalOutOfLongRangeWithoutParsing2() { public void testParseBigDecimalOutOfLongRangeWithoutParsing2() {
JsonConverter.setTypeCastEnabled(false); JsonConverter.setTypeCastEnabled(false);
Assertions.assertThrows(JsonSyntaxException.class, () -> { Assertions.assertThrows(JsonSyntaxException.class, () -> {
JsonConverter.convertToTelemetry(JSON_PARSER.parse("{\"meterReadingDelta\": 9.9701010061400066E19}"), 0L); JsonConverter.convertToTelemetry(JsonParser.parseString("{\"meterReadingDelta\": 9.9701010061400066E19}"), 0L);
}); });
} }
} }

8
common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/adaptors/JsonCoapAdaptor.java

@ -45,7 +45,7 @@ public class JsonCoapAdaptor implements CoapTransportAdaptor {
public TransportProtos.PostTelemetryMsg convertToPostTelemetry(UUID sessionId, Request inbound, Descriptors.Descriptor telemetryMsgDescriptor) throws AdaptorException { public TransportProtos.PostTelemetryMsg convertToPostTelemetry(UUID sessionId, Request inbound, Descriptors.Descriptor telemetryMsgDescriptor) throws AdaptorException {
String payload = validatePayload(sessionId, inbound, false); String payload = validatePayload(sessionId, inbound, false);
try { try {
return JsonConverter.convertToTelemetryProto(new JsonParser().parse(payload)); return JsonConverter.convertToTelemetryProto(JsonParser.parseString(payload));
} catch (IllegalStateException | JsonSyntaxException ex) { } catch (IllegalStateException | JsonSyntaxException ex) {
throw new AdaptorException(ex); throw new AdaptorException(ex);
} }
@ -55,7 +55,7 @@ public class JsonCoapAdaptor implements CoapTransportAdaptor {
public TransportProtos.PostAttributeMsg convertToPostAttributes(UUID sessionId, Request inbound, Descriptors.Descriptor attributesMsgDescriptor) throws AdaptorException { public TransportProtos.PostAttributeMsg convertToPostAttributes(UUID sessionId, Request inbound, Descriptors.Descriptor attributesMsgDescriptor) throws AdaptorException {
String payload = validatePayload(sessionId, inbound, false); String payload = validatePayload(sessionId, inbound, false);
try { try {
return JsonConverter.convertToAttributesProto(new JsonParser().parse(payload)); return JsonConverter.convertToAttributesProto(JsonParser.parseString(payload));
} catch (IllegalStateException | JsonSyntaxException ex) { } catch (IllegalStateException | JsonSyntaxException ex) {
throw new AdaptorException(ex); throw new AdaptorException(ex);
} }
@ -70,7 +70,7 @@ public class JsonCoapAdaptor implements CoapTransportAdaptor {
public TransportProtos.ToDeviceRpcResponseMsg convertToDeviceRpcResponse(UUID sessionId, Request inbound, Descriptors.Descriptor rpcResponseMsgDescriptor) throws AdaptorException { public TransportProtos.ToDeviceRpcResponseMsg convertToDeviceRpcResponse(UUID sessionId, Request inbound, Descriptors.Descriptor rpcResponseMsgDescriptor) throws AdaptorException {
Optional<Integer> requestId = CoapTransportResource.getRequestId(inbound); Optional<Integer> requestId = CoapTransportResource.getRequestId(inbound);
String payload = validatePayload(sessionId, inbound, false); String payload = validatePayload(sessionId, inbound, false);
JsonObject response = new JsonParser().parse(payload).getAsJsonObject(); JsonObject response = JsonParser.parseString(payload).getAsJsonObject();
return TransportProtos.ToDeviceRpcResponseMsg.newBuilder().setRequestId(requestId.orElseThrow(() -> new AdaptorException("Request id is missing!"))) return TransportProtos.ToDeviceRpcResponseMsg.newBuilder().setRequestId(requestId.orElseThrow(() -> new AdaptorException("Request id is missing!")))
.setPayload(response.toString()).build(); .setPayload(response.toString()).build();
} }
@ -78,7 +78,7 @@ public class JsonCoapAdaptor implements CoapTransportAdaptor {
@Override @Override
public TransportProtos.ToServerRpcRequestMsg convertToServerRpcRequest(UUID sessionId, Request inbound) throws AdaptorException { public TransportProtos.ToServerRpcRequestMsg convertToServerRpcRequest(UUID sessionId, Request inbound) throws AdaptorException {
String payload = validatePayload(sessionId, inbound, false); String payload = validatePayload(sessionId, inbound, false);
return JsonConverter.convertToServerRpcRequest(new JsonParser().parse(payload), 0); return JsonConverter.convertToServerRpcRequest(JsonParser.parseString(payload), 0);
} }
@Override @Override

6
common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/adaptors/ProtoCoapAdaptor.java

@ -45,7 +45,7 @@ public class ProtoCoapAdaptor implements CoapTransportAdaptor {
public TransportProtos.PostTelemetryMsg convertToPostTelemetry(UUID sessionId, Request inbound, Descriptors.Descriptor telemetryMsgDescriptor) throws AdaptorException { public TransportProtos.PostTelemetryMsg convertToPostTelemetry(UUID sessionId, Request inbound, Descriptors.Descriptor telemetryMsgDescriptor) throws AdaptorException {
ProtoConverter.validateDescriptor(telemetryMsgDescriptor); ProtoConverter.validateDescriptor(telemetryMsgDescriptor);
try { try {
return JsonConverter.convertToTelemetryProto(new JsonParser().parse(ProtoConverter.dynamicMsgToJson(inbound.getPayload(), telemetryMsgDescriptor))); return JsonConverter.convertToTelemetryProto(JsonParser.parseString(ProtoConverter.dynamicMsgToJson(inbound.getPayload(), telemetryMsgDescriptor)));
} catch (Exception e) { } catch (Exception e) {
throw new AdaptorException(e); throw new AdaptorException(e);
} }
@ -55,7 +55,7 @@ public class ProtoCoapAdaptor implements CoapTransportAdaptor {
public TransportProtos.PostAttributeMsg convertToPostAttributes(UUID sessionId, Request inbound, Descriptors.Descriptor attributesMsgDescriptor) throws AdaptorException { public TransportProtos.PostAttributeMsg convertToPostAttributes(UUID sessionId, Request inbound, Descriptors.Descriptor attributesMsgDescriptor) throws AdaptorException {
ProtoConverter.validateDescriptor(attributesMsgDescriptor); ProtoConverter.validateDescriptor(attributesMsgDescriptor);
try { try {
return JsonConverter.convertToAttributesProto(new JsonParser().parse(ProtoConverter.dynamicMsgToJson(inbound.getPayload(), attributesMsgDescriptor))); return JsonConverter.convertToAttributesProto(JsonParser.parseString(ProtoConverter.dynamicMsgToJson(inbound.getPayload(), attributesMsgDescriptor)));
} catch (Exception e) { } catch (Exception e) {
throw new AdaptorException(e); throw new AdaptorException(e);
} }
@ -74,7 +74,7 @@ public class ProtoCoapAdaptor implements CoapTransportAdaptor {
} else { } else {
ProtoConverter.validateDescriptor(rpcResponseMsgDescriptor); ProtoConverter.validateDescriptor(rpcResponseMsgDescriptor);
try { try {
JsonElement response = new JsonParser().parse(ProtoConverter.dynamicMsgToJson(inbound.getPayload(), rpcResponseMsgDescriptor)); JsonElement response = JsonParser.parseString(ProtoConverter.dynamicMsgToJson(inbound.getPayload(), rpcResponseMsgDescriptor));
return TransportProtos.ToDeviceRpcResponseMsg.newBuilder().setRequestId(requestId.orElseThrow(() -> new AdaptorException("Request id is missing!"))) return TransportProtos.ToDeviceRpcResponseMsg.newBuilder().setRequestId(requestId.orElseThrow(() -> new AdaptorException("Request id is missing!")))
.setPayload(response.toString()).build(); .setPayload(response.toString()).build();
} catch (Exception e) { } catch (Exception e) {

6
common/transport/http/src/main/java/org/thingsboard/server/transport/http/DeviceApiController.java

@ -185,7 +185,7 @@ public class DeviceApiController implements TbTransportService {
transportContext.getTransportService().process(DeviceTransportType.DEFAULT, ValidateDeviceTokenRequestMsg.newBuilder().setToken(deviceToken).build(), transportContext.getTransportService().process(DeviceTransportType.DEFAULT, ValidateDeviceTokenRequestMsg.newBuilder().setToken(deviceToken).build(),
new DeviceAuthCallback(transportContext, responseWriter, sessionInfo -> { new DeviceAuthCallback(transportContext, responseWriter, sessionInfo -> {
TransportService transportService = transportContext.getTransportService(); TransportService transportService = transportContext.getTransportService();
transportService.process(sessionInfo, JsonConverter.convertToAttributesProto(new JsonParser().parse(json)), transportService.process(sessionInfo, JsonConverter.convertToAttributesProto(JsonParser.parseString(json)),
new HttpOkCallback(responseWriter)); new HttpOkCallback(responseWriter));
})); }));
return responseWriter; return responseWriter;
@ -205,7 +205,7 @@ public class DeviceApiController implements TbTransportService {
transportContext.getTransportService().process(DeviceTransportType.DEFAULT, ValidateDeviceTokenRequestMsg.newBuilder().setToken(deviceToken).build(), transportContext.getTransportService().process(DeviceTransportType.DEFAULT, ValidateDeviceTokenRequestMsg.newBuilder().setToken(deviceToken).build(),
new DeviceAuthCallback(transportContext, responseWriter, sessionInfo -> { new DeviceAuthCallback(transportContext, responseWriter, sessionInfo -> {
TransportService transportService = transportContext.getTransportService(); TransportService transportService = transportContext.getTransportService();
transportService.process(sessionInfo, JsonConverter.convertToTelemetryProto(new JsonParser().parse(json)), transportService.process(sessionInfo, JsonConverter.convertToTelemetryProto(JsonParser.parseString(json)),
new HttpOkCallback(responseWriter)); new HttpOkCallback(responseWriter));
})); }));
return responseWriter; return responseWriter;
@ -301,7 +301,7 @@ public class DeviceApiController implements TbTransportService {
DeferredResult<ResponseEntity> responseWriter = new DeferredResult<ResponseEntity>(); DeferredResult<ResponseEntity> responseWriter = new DeferredResult<ResponseEntity>();
transportContext.getTransportService().process(DeviceTransportType.DEFAULT, ValidateDeviceTokenRequestMsg.newBuilder().setToken(deviceToken).build(), transportContext.getTransportService().process(DeviceTransportType.DEFAULT, ValidateDeviceTokenRequestMsg.newBuilder().setToken(deviceToken).build(),
new DeviceAuthCallback(transportContext, responseWriter, sessionInfo -> { new DeviceAuthCallback(transportContext, responseWriter, sessionInfo -> {
JsonObject request = new JsonParser().parse(json).getAsJsonObject(); JsonObject request = JsonParser.parseString(json).getAsJsonObject();
TransportService transportService = transportContext.getTransportService(); TransportService transportService = transportContext.getTransportService();
transportService.registerSyncSession(sessionInfo, transportService.registerSyncSession(sessionInfo,
new HttpSessionListener(responseWriter, transportContext.getTransportService(), sessionInfo), new HttpSessionListener(responseWriter, transportContext.getTransportService(), sessionInfo),

3
common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/LwM2mTransportServerHelper.java

@ -50,7 +50,6 @@ import static org.thingsboard.server.gen.transport.TransportProtos.KeyValueType.
public class LwM2mTransportServerHelper { public class LwM2mTransportServerHelper {
private final LwM2mTransportContext context; private final LwM2mTransportContext context;
private final static JsonParser JSON_PARSER = new JsonParser();
public void sendParametersOnThingsboardAttribute(List<TransportProtos.KeyValueProto> result, SessionInfoProto sessionInfo) { public void sendParametersOnThingsboardAttribute(List<TransportProtos.KeyValueProto> result, SessionInfoProto sessionInfo) {
PostAttributeMsg.Builder request = PostAttributeMsg.newBuilder(); PostAttributeMsg.Builder request = PostAttributeMsg.newBuilder();
@ -231,7 +230,7 @@ public class LwM2mTransportServerHelper {
return kv.getStringV(); return kv.getStringV();
case JSON_V: case JSON_V:
try { try {
return JSON_PARSER.parse(kv.getJsonV()); return JsonParser.parseString(kv.getJsonV());
} catch (Exception e) { } catch (Exception e) {
return null; return null;
} }

10
common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/adaptors/JsonMqttAdaptor.java

@ -59,7 +59,7 @@ public class JsonMqttAdaptor implements MqttTransportAdaptor {
public TransportProtos.PostTelemetryMsg convertToPostTelemetry(MqttDeviceAwareSessionContext ctx, MqttPublishMessage inbound) throws AdaptorException { public TransportProtos.PostTelemetryMsg convertToPostTelemetry(MqttDeviceAwareSessionContext ctx, MqttPublishMessage inbound) throws AdaptorException {
String payload = validatePayload(ctx.getSessionId(), inbound.payload(), false); String payload = validatePayload(ctx.getSessionId(), inbound.payload(), false);
try { try {
return JsonConverter.convertToTelemetryProto(new JsonParser().parse(payload)); return JsonConverter.convertToTelemetryProto(JsonParser.parseString(payload));
} catch (IllegalStateException | JsonSyntaxException ex) { } catch (IllegalStateException | JsonSyntaxException ex) {
log.debug("Failed to decode post telemetry request", ex); log.debug("Failed to decode post telemetry request", ex);
throw new AdaptorException(ex); throw new AdaptorException(ex);
@ -70,7 +70,7 @@ public class JsonMqttAdaptor implements MqttTransportAdaptor {
public TransportProtos.PostAttributeMsg convertToPostAttributes(MqttDeviceAwareSessionContext ctx, MqttPublishMessage inbound) throws AdaptorException { public TransportProtos.PostAttributeMsg convertToPostAttributes(MqttDeviceAwareSessionContext ctx, MqttPublishMessage inbound) throws AdaptorException {
String payload = validatePayload(ctx.getSessionId(), inbound.payload(), false); String payload = validatePayload(ctx.getSessionId(), inbound.payload(), false);
try { try {
return JsonConverter.convertToAttributesProto(new JsonParser().parse(payload)); return JsonConverter.convertToAttributesProto(JsonParser.parseString(payload));
} catch (IllegalStateException | JsonSyntaxException ex) { } catch (IllegalStateException | JsonSyntaxException ex) {
log.debug("Failed to decode post attributes request", ex); log.debug("Failed to decode post attributes request", ex);
throw new AdaptorException(ex); throw new AdaptorException(ex);
@ -162,7 +162,7 @@ public class JsonMqttAdaptor implements MqttTransportAdaptor {
public static JsonElement validateJsonPayload(UUID sessionId, ByteBuf payloadData) throws AdaptorException { public static JsonElement validateJsonPayload(UUID sessionId, ByteBuf payloadData) throws AdaptorException {
String payload = validatePayload(sessionId, payloadData, false); String payload = validatePayload(sessionId, payloadData, false);
try { try {
return new JsonParser().parse(payload); return JsonParser.parseString(payload);
} catch (JsonSyntaxException ex) { } catch (JsonSyntaxException ex) {
log.debug("Payload is in incorrect format: {}", payload); log.debug("Payload is in incorrect format: {}", payload);
throw new AdaptorException(ex); throw new AdaptorException(ex);
@ -175,7 +175,7 @@ public class JsonMqttAdaptor implements MqttTransportAdaptor {
TransportProtos.GetAttributeRequestMsg.Builder result = TransportProtos.GetAttributeRequestMsg.newBuilder(); TransportProtos.GetAttributeRequestMsg.Builder result = TransportProtos.GetAttributeRequestMsg.newBuilder();
result.setRequestId(getRequestId(topicName, topicBase)); result.setRequestId(getRequestId(topicName, topicBase));
String payload = inbound.payload().toString(UTF8); String payload = inbound.payload().toString(UTF8);
JsonElement requestBody = new JsonParser().parse(payload); JsonElement requestBody = JsonParser.parseString(payload);
Set<String> clientKeys = toStringSet(requestBody, "clientKeys"); Set<String> clientKeys = toStringSet(requestBody, "clientKeys");
Set<String> sharedKeys = toStringSet(requestBody, "sharedKeys"); Set<String> sharedKeys = toStringSet(requestBody, "sharedKeys");
if (clientKeys != null) { if (clientKeys != null) {
@ -208,7 +208,7 @@ public class JsonMqttAdaptor implements MqttTransportAdaptor {
String payload = validatePayload(ctx.getSessionId(), inbound.payload(), false); String payload = validatePayload(ctx.getSessionId(), inbound.payload(), false);
try { try {
int requestId = getRequestId(topicName, topicBase); int requestId = getRequestId(topicName, topicBase);
return JsonConverter.convertToServerRpcRequest(new JsonParser().parse(payload), requestId); return JsonConverter.convertToServerRpcRequest(JsonParser.parseString(payload), requestId);
} catch (IllegalStateException | JsonSyntaxException ex) { } catch (IllegalStateException | JsonSyntaxException ex) {
log.debug("Failed to decode to server rpc request", ex); log.debug("Failed to decode to server rpc request", ex);
throw new AdaptorException(ex); throw new AdaptorException(ex);

6
common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/adaptors/ProtoMqttAdaptor.java

@ -50,7 +50,7 @@ public class ProtoMqttAdaptor implements MqttTransportAdaptor {
byte[] bytes = toBytes(inbound.payload()); byte[] bytes = toBytes(inbound.payload());
Descriptors.Descriptor telemetryDynamicMsgDescriptor = ProtoConverter.validateDescriptor(deviceSessionCtx.getTelemetryDynamicMsgDescriptor()); Descriptors.Descriptor telemetryDynamicMsgDescriptor = ProtoConverter.validateDescriptor(deviceSessionCtx.getTelemetryDynamicMsgDescriptor());
try { try {
return JsonConverter.convertToTelemetryProto(new JsonParser().parse(ProtoConverter.dynamicMsgToJson(bytes, telemetryDynamicMsgDescriptor))); return JsonConverter.convertToTelemetryProto(JsonParser.parseString(ProtoConverter.dynamicMsgToJson(bytes, telemetryDynamicMsgDescriptor)));
} catch (Exception e) { } catch (Exception e) {
log.debug("Failed to decode post telemetry request", e); log.debug("Failed to decode post telemetry request", e);
throw new AdaptorException(e); throw new AdaptorException(e);
@ -63,7 +63,7 @@ public class ProtoMqttAdaptor implements MqttTransportAdaptor {
byte[] bytes = toBytes(inbound.payload()); byte[] bytes = toBytes(inbound.payload());
Descriptors.Descriptor attributesDynamicMessageDescriptor = ProtoConverter.validateDescriptor(deviceSessionCtx.getAttributesDynamicMessageDescriptor()); Descriptors.Descriptor attributesDynamicMessageDescriptor = ProtoConverter.validateDescriptor(deviceSessionCtx.getAttributesDynamicMessageDescriptor());
try { try {
return JsonConverter.convertToAttributesProto(new JsonParser().parse(ProtoConverter.dynamicMsgToJson(bytes, attributesDynamicMessageDescriptor))); return JsonConverter.convertToAttributesProto(JsonParser.parseString(ProtoConverter.dynamicMsgToJson(bytes, attributesDynamicMessageDescriptor)));
} catch (Exception e) { } catch (Exception e) {
log.debug("Failed to decode post attributes request", e); log.debug("Failed to decode post attributes request", e);
throw new AdaptorException(e); throw new AdaptorException(e);
@ -102,7 +102,7 @@ public class ProtoMqttAdaptor implements MqttTransportAdaptor {
Descriptors.Descriptor rpcResponseDynamicMessageDescriptor = ProtoConverter.validateDescriptor(deviceSessionCtx.getRpcResponseDynamicMessageDescriptor()); Descriptors.Descriptor rpcResponseDynamicMessageDescriptor = ProtoConverter.validateDescriptor(deviceSessionCtx.getRpcResponseDynamicMessageDescriptor());
try { try {
int requestId = getRequestId(topicName, topicBase); int requestId = getRequestId(topicName, topicBase);
JsonElement response = new JsonParser().parse(ProtoConverter.dynamicMsgToJson(bytes, rpcResponseDynamicMessageDescriptor)); JsonElement response = JsonParser.parseString(ProtoConverter.dynamicMsgToJson(bytes, rpcResponseDynamicMessageDescriptor));
return TransportProtos.ToDeviceRpcResponseMsg.newBuilder().setRequestId(requestId).setPayload(response.toString()).build(); return TransportProtos.ToDeviceRpcResponseMsg.newBuilder().setRequestId(requestId).setPayload(response.toString()).build();
} catch (Exception e) { } catch (Exception e) {
log.debug("Failed to decode rpc response", e); log.debug("Failed to decode rpc response", e);

2
common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/SparkplugNodeSessionHandler.java

@ -94,7 +94,7 @@ public class SparkplugNodeSessionHandler extends AbstractGatewaySessionHandler<S
byte[] bytes = getBytes(inbound.payload()); byte[] bytes = getBytes(inbound.payload());
Descriptors.Descriptor telemetryDynamicMsgDescriptor = ProtoConverter.validateDescriptor(deviceSessionCtx.getTelemetryDynamicMsgDescriptor()); Descriptors.Descriptor telemetryDynamicMsgDescriptor = ProtoConverter.validateDescriptor(deviceSessionCtx.getTelemetryDynamicMsgDescriptor());
try { try {
return JsonConverter.convertToTelemetryProto(new JsonParser().parse(ProtoConverter.dynamicMsgToJson(bytes, telemetryDynamicMsgDescriptor))); return JsonConverter.convertToTelemetryProto(JsonParser.parseString(ProtoConverter.dynamicMsgToJson(bytes, telemetryDynamicMsgDescriptor)));
} catch (Exception e) { } catch (Exception e) {
log.debug("Failed to decode post telemetry request", e); log.debug("Failed to decode post telemetry request", e);
throw new AdaptorException(e); throw new AdaptorException(e);

6
common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/util/JsonUtils.java

@ -24,8 +24,6 @@ import java.util.List;
public class JsonUtils { public class JsonUtils {
private static final JsonParser jsonParser = new JsonParser();
public static JsonObject getJsonObject(List<KeyValueProto> tsKv) { public static JsonObject getJsonObject(List<KeyValueProto> tsKv) {
JsonObject json = new JsonObject(); JsonObject json = new JsonObject();
for (KeyValueProto kv : tsKv) { for (KeyValueProto kv : tsKv) {
@ -43,7 +41,7 @@ public class JsonUtils {
json.addProperty(kv.getKey(), kv.getStringV()); json.addProperty(kv.getKey(), kv.getStringV());
break; break;
case JSON_V: case JSON_V:
json.add(kv.getKey(), jsonParser.parse(kv.getJsonV())); json.add(kv.getKey(), JsonParser.parseString(kv.getJsonV()));
break; break;
} }
} }
@ -51,7 +49,7 @@ public class JsonUtils {
} }
public static JsonElement parse(String params) { public static JsonElement parse(String params) {
return jsonParser.parse(params); return JsonParser.parseString(params);
} }
} }

3
monitoring/src/main/java/org/thingsboard/monitoring/client/Lwm2mClient.java

@ -51,6 +51,7 @@ import java.net.InetSocketAddress;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collections; import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.concurrent.TimeUnit;
import static org.eclipse.leshan.client.object.Security.noSec; import static org.eclipse.leshan.client.object.Security.noSec;
import static org.eclipse.leshan.core.LwM2mId.ACCESS_CONTROL; import static org.eclipse.leshan.core.LwM2mId.ACCESS_CONTROL;
@ -95,7 +96,7 @@ public class Lwm2mClient extends BaseInstanceEnabler implements Destroyable {
LwM2mModel model = new StaticModel(models); LwM2mModel model = new StaticModel(models);
ObjectsInitializer initializer = new ObjectsInitializer(model); ObjectsInitializer initializer = new ObjectsInitializer(model);
initializer.setInstancesForObject(SECURITY, security); initializer.setInstancesForObject(SECURITY, security);
initializer.setInstancesForObject(SERVER, new Server(123, 300)); initializer.setInstancesForObject(SERVER, new Server(123, TimeUnit.MINUTES.toSeconds(60)));
initializer.setInstancesForObject(DEVICE, this); initializer.setInstancesForObject(DEVICE, this);
initializer.setClassForObject(ACCESS_CONTROL, DummyInstanceEnabler.class); initializer.setClassForObject(ACCESS_CONTROL, DummyInstanceEnabler.class);
DtlsConnectorConfig.Builder dtlsConfig = new DtlsConnectorConfig.Builder(); DtlsConnectorConfig.Builder dtlsConfig = new DtlsConnectorConfig.Builder();

3
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbSaveToCustomCassandraTableNode.java

@ -72,7 +72,6 @@ import static org.thingsboard.common.util.DonAsynchron.withCallback;
public class TbSaveToCustomCassandraTableNode implements TbNode { public class TbSaveToCustomCassandraTableNode implements TbNode {
private static final String TABLE_PREFIX = "cs_tb_"; private static final String TABLE_PREFIX = "cs_tb_";
private static final JsonParser parser = new JsonParser();
private static final String ENTITY_ID = "$entityId"; private static final String ENTITY_ID = "$entityId";
private TbSaveToCustomCassandraTableNodeConfiguration config; private TbSaveToCustomCassandraTableNodeConfiguration config;
@ -168,7 +167,7 @@ public class TbSaveToCustomCassandraTableNode implements TbNode {
} }
private ListenableFuture<Void> save(TbMsg msg, TbContext ctx) { private ListenableFuture<Void> save(TbMsg msg, TbContext ctx) {
JsonElement data = parser.parse(msg.getData()); JsonElement data = JsonParser.parseString(msg.getData());
if (!data.isJsonObject()) { if (!data.isJsonObject()) {
throw new IllegalStateException("Invalid message structure, it is not a JSON Object:" + data); throw new IllegalStateException("Invalid message structure, it is not a JSON Object:" + data);
} else { } else {

4
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/geo/TbGpsGeofencingActionNode.java

@ -25,7 +25,6 @@ import org.thingsboard.rule.engine.api.RuleNode;
import org.thingsboard.rule.engine.api.TbContext; import org.thingsboard.rule.engine.api.TbContext;
import org.thingsboard.rule.engine.api.TbNodeException; import org.thingsboard.rule.engine.api.TbNodeException;
import org.thingsboard.server.common.data.AttributeScope; import org.thingsboard.server.common.data.AttributeScope;
import org.thingsboard.server.common.data.DataConstants;
import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.kv.AttributeKvEntry; import org.thingsboard.server.common.data.kv.AttributeKvEntry;
import org.thingsboard.server.common.data.kv.BaseAttributeKvEntry; import org.thingsboard.server.common.data.kv.BaseAttributeKvEntry;
@ -75,7 +74,6 @@ public class TbGpsGeofencingActionNode extends AbstractGeofencingNode<TbGpsGeofe
private static final String REPORT_PRESENCE_STATUS_ON_EACH_MESSAGE = "reportPresenceStatusOnEachMessage"; private static final String REPORT_PRESENCE_STATUS_ON_EACH_MESSAGE = "reportPresenceStatusOnEachMessage";
private final Map<EntityId, EntityGeofencingState> entityStates = new HashMap<>(); private final Map<EntityId, EntityGeofencingState> entityStates = new HashMap<>();
private final Gson gson = new Gson(); private final Gson gson = new Gson();
private final JsonParser parser = new JsonParser();
@Override @Override
public void onMsg(TbContext ctx, TbMsg msg) throws TbNodeException { public void onMsg(TbContext ctx, TbMsg msg) throws TbNodeException {
@ -88,7 +86,7 @@ public class TbGpsGeofencingActionNode extends AbstractGeofencingNode<TbGpsGeofe
.find(ctx.getTenantId(), msg.getOriginator(), AttributeScope.SERVER_SCOPE, ctx.getServiceId()) .find(ctx.getTenantId(), msg.getOriginator(), AttributeScope.SERVER_SCOPE, ctx.getServiceId())
.get(1, TimeUnit.MINUTES); .get(1, TimeUnit.MINUTES);
if (entry.isPresent()) { if (entry.isPresent()) {
JsonObject element = parser.parse(entry.get().getValueAsString()).getAsJsonObject(); JsonObject element = JsonParser.parseString(entry.get().getValueAsString()).getAsJsonObject();
return new EntityGeofencingState(element.get("inside").getAsBoolean(), element.get("stateSwitchTime").getAsLong(), element.get("stayed").getAsBoolean()); return new EntityGeofencingState(element.get("inside").getAsBoolean(), element.get("stateSwitchTime").getAsLong(), element.get("stayed").getAsBoolean());
} else { } else {
return new EntityGeofencingState(false, 0L, false); return new EntityGeofencingState(false, 0L, false);

12
ui-ngx/src/app/modules/home/components/widget/config/basic/basic-widget-config.module.ts

@ -114,6 +114,9 @@ import { ComparisonKeyRowComponent } from '@home/components/widget/config/basic/
import { import {
ComparisonKeysTableComponent ComparisonKeysTableComponent
} from '@home/components/widget/config/basic/chart/comparison-keys-table.component'; } from '@home/components/widget/config/basic/chart/comparison-keys-table.component';
import {
StatusWidgetBasicConfigComponent
} from '@home/components/widget/config/basic/indicator/status-widget-basic-config.component';
@NgModule({ @NgModule({
declarations: [ declarations: [
@ -151,7 +154,8 @@ import {
ToggleButtonBasicConfigComponent, ToggleButtonBasicConfigComponent,
TimeSeriesChartBasicConfigComponent, TimeSeriesChartBasicConfigComponent,
ComparisonKeyRowComponent, ComparisonKeyRowComponent,
ComparisonKeysTableComponent ComparisonKeysTableComponent,
StatusWidgetBasicConfigComponent
], ],
imports: [ imports: [
CommonModule, CommonModule,
@ -191,7 +195,8 @@ import {
PowerButtonBasicConfigComponent, PowerButtonBasicConfigComponent,
SliderBasicConfigComponent, SliderBasicConfigComponent,
ToggleButtonBasicConfigComponent, ToggleButtonBasicConfigComponent,
TimeSeriesChartBasicConfigComponent TimeSeriesChartBasicConfigComponent,
StatusWidgetBasicConfigComponent
] ]
}) })
export class BasicWidgetConfigModule { export class BasicWidgetConfigModule {
@ -225,5 +230,6 @@ export const basicWidgetConfigComponentsMap: {[key: string]: Type<IBasicWidgetCo
'tb-power-button-basic-config': PowerButtonBasicConfigComponent, 'tb-power-button-basic-config': PowerButtonBasicConfigComponent,
'tb-slider-basic-config': SliderBasicConfigComponent, 'tb-slider-basic-config': SliderBasicConfigComponent,
'tb-toggle-button-basic-config': ToggleButtonBasicConfigComponent, 'tb-toggle-button-basic-config': ToggleButtonBasicConfigComponent,
'tb-time-series-chart-basic-config': TimeSeriesChartBasicConfigComponent 'tb-time-series-chart-basic-config': TimeSeriesChartBasicConfigComponent,
'tb-status-widget-basic-config': StatusWidgetBasicConfigComponent
}; };

101
ui-ngx/src/app/modules/home/components/widget/config/basic/indicator/status-widget-basic-config.component.html

@ -0,0 +1,101 @@
<!--
Copyright © 2016-2024 The Thingsboard Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<ng-container [formGroup]="statusWidgetConfigForm">
<tb-target-device formControlName="targetDevice"></tb-target-device>
<div class="tb-form-panel">
<div class="tb-form-panel-title" translate>widgets.status-widget.behavior</div>
<div class="tb-form-row">
<div class="fixed-title-width" tb-hint-tooltip-icon="{{'widgets.rpc-state.initial-state-hint' | translate}}" translate>widgets.rpc-state.initial-state</div>
<tb-get-value-action-settings fxFlex
panelTitle="widgets.rpc-state.initial-state"
[valueType]="valueType.BOOLEAN"
trueLabel="widgets.rpc-state.on"
falseLabel="widgets.rpc-state.off"
stateLabel="widgets.rpc-state.on"
[aliasController]="aliasController"
[targetDevice]="targetDevice"
[widgetType]="widgetType"
formControlName="initialState"></tb-get-value-action-settings>
</div>
<div class="tb-form-row">
<div class="fixed-title-width" tb-hint-tooltip-icon="{{'widgets.rpc-state.disabled-state-hint' | translate}}" translate>widgets.rpc-state.disabled-state</div>
<tb-get-value-action-settings fxFlex
panelTitle="widgets.rpc-state.disabled-state"
[valueType]="valueType.BOOLEAN"
stateLabel="widgets.rpc-state.disabled"
[aliasController]="aliasController"
[targetDevice]="targetDevice"
[widgetType]="widgetType"
formControlName="disabledState"></tb-get-value-action-settings>
</div>
</div>
<div class="tb-form-panel">
<div class="tb-form-panel-title" translate>widget-config.appearance</div>
<tb-image-cards-select rowHeight="1:1"
[cols]="{columns: 3,
breakpoints: {
'lt-sm': 1,
'lt-md': 2
}}"
label="{{ 'widgets.status-widget.layout' | translate }}" formControlName="layout">
<tb-image-cards-select-option *ngFor="let layout of statusWidgetLayouts"
[value]="layout"
[image]="statusWidgetLayoutImageMap.get(layout)">
{{ statusWidgetLayoutTranslationMap.get(layout) | translate }}
</tb-image-cards-select-option>
</tb-image-cards-select>
</div>
<div class="tb-form-panel">
<div fxLayout="row" fxLayoutAlign="space-between center">
<div class="tb-form-panel-title" translate>widget-config.card-style</div>
<tb-toggle-select [(ngModel)]="cardStyleMode"
[ngModelOptions]="{ standalone: true }">
<tb-toggle-option value="on">{{ 'widgets.status-widget.on' | translate }}</tb-toggle-option>
<tb-toggle-option value="off">{{ 'widgets.status-widget.off' | translate }}</tb-toggle-option>
</tb-toggle-select>
</div>
<tb-status-widget-state-settings
*ngIf="cardStyleMode === 'on'"
[layout]="statusWidgetConfigForm.get('layout').value"
formControlName="onState">
</tb-status-widget-state-settings>
<tb-status-widget-state-settings
*ngIf="cardStyleMode === 'off'"
[layout]="statusWidgetConfigForm.get('layout').value"
formControlName="offState">
</tb-status-widget-state-settings>
</div>
<div class="tb-form-panel">
<div class="tb-form-panel-title" translate>widget-config.card-appearance</div>
<div class="tb-form-row space-between column-lt-md">
<div translate>widget-config.show-card-buttons</div>
<mat-chip-listbox multiple formControlName="cardButtons">
<mat-chip-option value="fullscreen">{{ 'fullscreen.fullscreen' | translate }}</mat-chip-option>
</mat-chip-listbox>
</div>
<div class="tb-form-row space-between">
<div>{{ 'widget-config.card-border-radius' | translate }}</div>
<mat-form-field appearance="outline" subscriptSizing="dynamic">
<input matInput formControlName="borderRadius" placeholder="{{ 'widget-config.set' | translate }}">
</mat-form-field>
</div>
</div>
<tb-widget-actions-panel
formControlName="actions">
</tb-widget-actions-panel>
</ng-container>

119
ui-ngx/src/app/modules/home/components/widget/config/basic/indicator/status-widget-basic-config.component.ts

@ -0,0 +1,119 @@
///
/// Copyright © 2016-2024 The Thingsboard Authors
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
///
import { Component } from '@angular/core';
import { UntypedFormBuilder, UntypedFormGroup } from '@angular/forms';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { BasicWidgetConfigComponent } from '@home/components/widget/config/widget-config.component.models';
import { WidgetConfigComponentData } from '@home/models/widget-component.models';
import { TargetDevice, WidgetConfig, } from '@shared/models/widget.models';
import { WidgetConfigComponent } from '@home/components/widget/widget-config.component';
import { isUndefined } from '@core/utils';
import { ValueType } from '@shared/models/constants';
import {
statusWidgetDefaultSettings,
statusWidgetLayoutImages,
statusWidgetLayouts,
statusWidgetLayoutTranslations,
StatusWidgetSettings
} from '@home/components/widget/lib/indicator/status-widget.models';
@Component({
selector: 'tb-status-widget-basic-config',
templateUrl: './status-widget-basic-config.component.html',
styleUrls: ['../basic-config.scss']
})
export class StatusWidgetBasicConfigComponent extends BasicWidgetConfigComponent {
get targetDevice(): TargetDevice {
return this.statusWidgetConfigForm.get('targetDevice').value;
}
statusWidgetLayouts = statusWidgetLayouts;
statusWidgetLayoutTranslationMap = statusWidgetLayoutTranslations;
statusWidgetLayoutImageMap = statusWidgetLayoutImages;
valueType = ValueType;
statusWidgetConfigForm: UntypedFormGroup;
cardStyleMode = 'on';
constructor(protected store: Store<AppState>,
protected widgetConfigComponent: WidgetConfigComponent,
private fb: UntypedFormBuilder) {
super(store, widgetConfigComponent);
}
protected configForm(): UntypedFormGroup {
return this.statusWidgetConfigForm;
}
protected onConfigSet(configData: WidgetConfigComponentData) {
const settings: StatusWidgetSettings = {...statusWidgetDefaultSettings, ...(configData.config.settings || {})};
this.statusWidgetConfigForm = this.fb.group({
targetDevice: [configData.config.targetDevice, []],
initialState: [settings.initialState, []],
disabledState: [settings.disabledState, []],
layout: [settings.layout, []],
onState: [settings.onState, []],
offState: [settings.offState, []],
cardButtons: [this.getCardButtons(configData.config), []],
borderRadius: [configData.config.borderRadius, []],
actions: [configData.config.actions || {}, []]
});
}
protected prepareOutputConfig(config: any): WidgetConfigComponentData {
this.widgetConfig.config.targetDevice = config.targetDevice;
this.widgetConfig.config.settings = this.widgetConfig.config.settings || {};
this.widgetConfig.config.settings.initialState = config.initialState;
this.widgetConfig.config.settings.disabledState = config.disabledState;
this.widgetConfig.config.settings.layout = config.layout;
this.widgetConfig.config.settings.onState = config.onState;
this.widgetConfig.config.settings.offState = config.offState;
this.setCardButtons(config.cardButtons, this.widgetConfig.config);
this.widgetConfig.config.borderRadius = config.borderRadius;
this.widgetConfig.config.actions = config.actions;
return this.widgetConfig;
}
private getCardButtons(config: WidgetConfig): string[] {
const buttons: string[] = [];
if (isUndefined(config.enableFullscreen) || config.enableFullscreen) {
buttons.push('fullscreen');
}
return buttons;
}
private setCardButtons(buttons: string[], config: WidgetConfig) {
config.enableFullscreen = buttons.includes('fullscreen');
}
}

33
ui-ngx/src/app/modules/home/components/widget/lib/indicator/status-widget.component.html

@ -0,0 +1,33 @@
<!--
Copyright © 2016-2024 The Thingsboard Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<div #statusWidgetPanel class="tb-status-widget-panel" [style]="backgroundStyle$ | async">
<div class="tb-status-widget-overlay" [style]="overlayStyle" [style.inset]="overlayInset"></div>
<div class="tb-status-widget-title-panel">
<ng-container *ngTemplateOutlet="widgetTitlePanel"></ng-container>
</div>
<div #statusWidgetContent class="tb-status-widget-content" [class]="this.layout">
<div class="tb-status-widget-icon-container">
<tb-icon [style]="iconStyle">{{ icon }}</tb-icon>
</div>
<div class="tb-status-widget-labels-container">
<div *ngIf="showLabel" class="tb-status-widget-label" [style]="labelStyle">{{ label$ | async }}</div>
<div *ngIf="showStatus" class="tb-status-widget-status" [style]="statusStyle">{{ status$ | async }}</div>
</div>
</div>
<mat-progress-bar class="tb-action-widget-progress" style="height: 4px;" color="accent" mode="indeterminate" *ngIf="loading$ | async"></mat-progress-bar>
</div>

99
ui-ngx/src/app/modules/home/components/widget/lib/indicator/status-widget.component.scss

@ -0,0 +1,99 @@
/**
* Copyright © 2016-2024 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
.tb-status-widget-panel {
width: 100%;
height: 100%;
position: relative;
display: flex;
align-items: center;
justify-content: center;
padding: 0;
> div:not(.tb-status-widget-overlay), > tb-icon {
z-index: 1;
}
.tb-status-widget-overlay {
position: absolute;
inset: 12px;
}
> div.tb-status-widget-title-panel {
position: absolute;
top: 12px;
left: 12px;
right: 12px;
z-index: 2;
}
.tb-status-widget-content {
width: 100%;
height: 100%;
padding: 16px;
position: relative;
display: flex;
flex-direction: column;
.tb-status-widget-icon-container {
display: flex;
width: 100%;
flex-direction: column;
}
.tb-status-widget-labels-container {
display: flex;
width: 100%;
flex-direction: column;
.tb-status-widget-label {
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
text-overflow: ellipsis;
}
.tb-status-widget-status {
text-transform: uppercase;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
}
&.default {
place-content: flex-start space-between;
align-items: flex-start;
}
&.center {
place-content: center flex-start;
align-items: center;
.tb-status-widget-icon-container {
flex: 1;
place-content: center;
align-items: center;
}
.tb-status-widget-labels-container {
flex-direction: column-reverse;
place-content: center flex-start;
align-items: center;
}
}
&.icon {
place-content: center;
align-items: center;
.tb-status-widget-icon-container {
flex: 1;
place-content: center;
align-items: center;
}
.tb-status-widget-labels-container {
display: none;
}
}
}
}

242
ui-ngx/src/app/modules/home/components/widget/lib/indicator/status-widget.component.ts

@ -0,0 +1,242 @@
///
/// Copyright © 2016-2024 The Thingsboard Authors
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
///
import {
AfterViewInit,
ChangeDetectorRef,
Component,
ElementRef,
OnDestroy,
OnInit,
Renderer2, ViewChild,
ViewEncapsulation
} from '@angular/core';
import { BasicActionWidgetComponent } from '@home/components/widget/lib/action/action-widget.models';
import {
statusWidgetDefaultSettings,
StatusWidgetLayout,
StatusWidgetSettings, StatusWidgetStateSettings
} from '@home/components/widget/lib/indicator/status-widget.models';
import { Observable } from 'rxjs';
import {
backgroundStyle,
ComponentStyle,
iconStyle,
overlayStyle,
textStyle
} from '@shared/models/widget-settings.models';
import { ResizeObserver } from '@juggle/resize-observer';
import { ImagePipe } from '@shared/pipe/image.pipe';
import { DomSanitizer } from '@angular/platform-browser';
import { UtilsService } from '@core/services/utils.service';
import { ValueType } from '@shared/models/constants';
const initialStatusWidgetSize = 147;
@Component({
selector: 'tb-status-widget',
templateUrl: './status-widget.component.html',
styleUrls: ['../action/action-widget.scss', './status-widget.component.scss'],
encapsulation: ViewEncapsulation.None
})
export class StatusWidgetComponent extends
BasicActionWidgetComponent implements OnInit, AfterViewInit, OnDestroy {
@ViewChild('statusWidgetPanel', {static: false})
statusWidgetPanel: ElementRef<HTMLElement>;
@ViewChild('statusWidgetContent', {static: false})
statusWidgetContent: ElementRef<HTMLElement>;
settings: StatusWidgetSettings;
backgroundStyle$: Observable<ComponentStyle>;
overlayStyle: ComponentStyle = {};
overlayInset = '12px';
borderRadius = '';
layout: StatusWidgetLayout;
showLabel = true;
label$: Observable<string>;
labelStyle: ComponentStyle = {};
showStatus = true;
status$: Observable<string>;
statusStyle: ComponentStyle = {};
icon = '';
iconStyle: ComponentStyle = {};
private panelResize$: ResizeObserver;
private onLabel$: Observable<string>;
private onStatus$: Observable<string>;
private onBackground$: Observable<ComponentStyle>;
private onBackgroundDisabled$: Observable<ComponentStyle>;
private offLabel$: Observable<string>;
private offStatus$: Observable<string>;
private offBackground$: Observable<ComponentStyle>;
private offBackgroundDisabled$: Observable<ComponentStyle>;
private state = false;
private disabled = false;
private disabledState = false;
constructor(protected imagePipe: ImagePipe,
protected sanitizer: DomSanitizer,
private renderer: Renderer2,
private utils: UtilsService,
protected cd: ChangeDetectorRef,
private elementRef: ElementRef) {
super(cd);
}
ngOnInit(): void {
super.ngOnInit();
this.settings = {...statusWidgetDefaultSettings, ...this.ctx.settings};
this.layout = this.settings.layout;
this.onLabel$ = this.ctx.registerLabelPattern(this.settings.onState.label, this.onLabel$);
this.onStatus$ = this.ctx.registerLabelPattern(this.settings.onState.status, this.onStatus$);
this.onBackground$ = backgroundStyle(this.settings.onState.background, this.imagePipe, this.sanitizer);
this.onBackgroundDisabled$ = backgroundStyle(this.settings.onState.backgroundDisabled, this.imagePipe, this.sanitizer);
this.offLabel$ = this.ctx.registerLabelPattern(this.settings.offState.label, this.offLabel$);
this.offStatus$ = this.ctx.registerLabelPattern(this.settings.offState.status, this.offStatus$);
this.offBackground$ = backgroundStyle(this.settings.offState.background, this.imagePipe, this.sanitizer);
this.offBackgroundDisabled$ = backgroundStyle(this.settings.offState.backgroundDisabled, this.imagePipe, this.sanitizer);
const getInitialStateSettings =
{...this.settings.initialState, actionLabel: this.ctx.translate.instant('widgets.rpc-state.initial-state')};
this.createValueGetter(getInitialStateSettings, ValueType.BOOLEAN, {
next: (value) => this.onState(value)
});
const disabledStateSettings =
{...this.settings.disabledState, actionLabel: this.ctx.translate.instant('widgets.rpc-state.disabled-state')};
this.createValueGetter(disabledStateSettings, ValueType.BOOLEAN, {
next: (value) => this.onDisabled(value)
});
this.loading$.subscribe((loading) => {
this.updateDisabledState(loading || this.disabled);
});
this.updateStyle(this.state, this.disabled);
}
ngAfterViewInit(): void {
this.renderer.setStyle(this.statusWidgetContent.nativeElement, 'overflow', 'visible');
this.renderer.setStyle(this.statusWidgetContent.nativeElement, 'position', 'absolute');
this.panelResize$ = new ResizeObserver(() => {
this.onResize();
});
this.panelResize$.observe(this.statusWidgetPanel.nativeElement);
if (this.showLabel) {
this.panelResize$.observe(this.statusWidgetPanel.nativeElement);
}
this.onResize();
super.ngAfterViewInit();
}
ngOnDestroy() {
if (this.panelResize$) {
this.panelResize$.disconnect();
}
super.ngOnDestroy();
}
public onInit() {
super.onInit();
this.borderRadius = this.ctx.$widgetElement.css('borderRadius');
this.overlayStyle = {...this.overlayStyle, ...{borderRadius: this.borderRadius}};
this.cd.detectChanges();
}
private onState(value: boolean): void {
const newState = !!value;
if (this.state !== newState) {
this.state = newState;
this.updateStyle(this.state, this.disabled || this.disabledState);
}
}
private onDisabled(value: boolean): void {
const newDisabled = !!value;
if (this.disabled !== newDisabled) {
this.disabled = newDisabled;
this.updateDisabledState(this.disabled);
}
}
private updateDisabledState(disabled: boolean) {
this.disabledState = disabled;
this.updateStyle(this.state, this.disabledState);
}
private onResize() {
const panelWidth = this.statusWidgetPanel.nativeElement.getBoundingClientRect().width;
const panelHeight = this.statusWidgetPanel.nativeElement.getBoundingClientRect().height;
const targetSize = Math.min(panelWidth, panelHeight);
const scale = targetSize / initialStatusWidgetSize;
const width = initialStatusWidgetSize;
const height = initialStatusWidgetSize;
this.renderer.setStyle(this.statusWidgetContent.nativeElement, 'width', width + 'px');
this.renderer.setStyle(this.statusWidgetContent.nativeElement, 'height', height + 'px');
this.renderer.setStyle(this.statusWidgetContent.nativeElement, 'transform', `scale(${scale})`);
this.overlayInset = (Math.floor(12 * scale * 100) / 100) + 'px';
this.cd.markForCheck();
}
private updateStyle(state: boolean, disabled: boolean) {
let stateSettings: StatusWidgetStateSettings;
if (state) {
this.label$ = this.onLabel$;
this.status$ = this.onStatus$;
this.backgroundStyle$ = disabled ? this.onBackgroundDisabled$ : this.onBackground$;
stateSettings = this.settings.onState;
} else {
this.label$ = this.offLabel$;
this.status$ = this.offStatus$;
this.backgroundStyle$ = disabled ? this.offBackgroundDisabled$ : this.offBackground$;
stateSettings = this.settings.offState;
}
this.showLabel = stateSettings.showLabel && this.layout !== StatusWidgetLayout.icon;
this.showStatus = stateSettings.showStatus && this.layout !== StatusWidgetLayout.icon;
this.icon = stateSettings.icon;
const primaryColor = disabled ? stateSettings.primaryColorDisabled : stateSettings.primaryColor;
const secondaryColor = disabled ? stateSettings.secondaryColorDisabled : stateSettings.secondaryColor;
this.labelStyle = textStyle(stateSettings.labelFont);
this.labelStyle.color = primaryColor;
this.statusStyle = textStyle(stateSettings.statusFont);
this.statusStyle.color = secondaryColor;
this.iconStyle = iconStyle(stateSettings.iconSize, stateSettings.iconSizeUnit);
this.iconStyle.color = primaryColor;
this.overlayStyle = overlayStyle(disabled ? stateSettings.backgroundDisabled.overlay : stateSettings.background.overlay);
if (this.borderRadius) {
this.overlayStyle = {...this.overlayStyle, ...{borderRadius: this.borderRadius}};
}
this.cd.detectChanges();
}
}

204
ui-ngx/src/app/modules/home/components/widget/lib/indicator/status-widget.models.ts

@ -0,0 +1,204 @@
///
/// Copyright © 2016-2024 The Thingsboard Authors
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
///
import { DataToValueType, GetValueAction, GetValueSettings } from '@shared/models/action-widget-settings.models';
import { BackgroundSettings, BackgroundType, cssUnit, Font } from '@shared/models/widget-settings.models';
export enum StatusWidgetLayout {
default = 'default',
center = 'center',
icon = 'icon'
}
export const statusWidgetLayouts = Object.keys(StatusWidgetLayout) as StatusWidgetLayout[];
export const statusWidgetLayoutTranslations = new Map<StatusWidgetLayout, string>(
[
[StatusWidgetLayout.default, 'widgets.status-widget.layout-default'],
[StatusWidgetLayout.center, 'widgets.status-widget.layout-center'],
[StatusWidgetLayout.icon, 'widgets.status-widget.layout-icon']
]
);
export const statusWidgetLayoutImages = new Map<StatusWidgetLayout, string>(
[
[StatusWidgetLayout.default, 'assets/widget/status-widget/default-layout.svg'],
[StatusWidgetLayout.center, 'assets/widget/status-widget/center-layout.svg'],
[StatusWidgetLayout.icon, 'assets/widget/status-widget/icon-layout.svg']
]
);
export interface StatusWidgetStateSettings {
showLabel: boolean;
label: string;
labelFont: Font;
showStatus: boolean;
status: string;
statusFont: Font;
icon: string;
iconSize: number;
iconSizeUnit: cssUnit;
primaryColor: string;
secondaryColor: string;
background: BackgroundSettings;
primaryColorDisabled: string;
secondaryColorDisabled: string;
backgroundDisabled: BackgroundSettings;
}
export interface StatusWidgetSettings {
initialState: GetValueSettings<boolean>;
disabledState: GetValueSettings<boolean>;
layout: StatusWidgetLayout;
onState: StatusWidgetStateSettings;
offState: StatusWidgetStateSettings;
}
export const statusWidgetDefaultSettings: StatusWidgetSettings = {
initialState: {
action: GetValueAction.EXECUTE_RPC,
defaultValue: false,
executeRpc: {
method: 'getState',
requestTimeout: 5000,
requestPersistent: false,
persistentPollingInterval: 1000
},
getAttribute: {
key: 'state',
scope: null
},
getTimeSeries: {
key: 'state'
},
dataToValue: {
type: DataToValueType.NONE,
compareToValue: true,
dataToValueFunction: '/* Should return boolean value */\nreturn data;'
}
},
disabledState: {
action: GetValueAction.DO_NOTHING,
defaultValue: false,
getAttribute: {
key: 'state',
scope: null
},
getTimeSeries: {
key: 'state'
},
dataToValue: {
type: DataToValueType.NONE,
compareToValue: true,
dataToValueFunction: '/* Should return boolean value */\nreturn data;'
}
},
layout: StatusWidgetLayout.default,
onState: {
showLabel: true,
label: 'Window left corner',
labelFont: {
family: 'Roboto',
size: 12,
sizeUnit: 'px',
style: 'normal',
weight: '500',
lineHeight: '16px'
},
showStatus: true,
status: 'Opened',
statusFont: {
family: 'Roboto',
size: 10,
sizeUnit: 'px',
style: 'normal',
weight: '500',
lineHeight: '20px'
},
icon: 'mdi:curtains',
iconSize: 32,
iconSizeUnit: 'px',
primaryColor: '#fff',
secondaryColor: 'rgba(255, 255, 255, 0.80)',
background: {
type: BackgroundType.color,
color: '#3F52DD',
overlay: {
enabled: false,
color: 'rgba(255,255,255,0.72)',
blur: 3
}
},
primaryColorDisabled: 'rgba(0, 0, 0, 0.38)',
secondaryColorDisabled: 'rgba(0, 0, 0, 0.38)',
backgroundDisabled: {
type: BackgroundType.color,
color: '#CACACA',
overlay: {
enabled: false,
color: 'rgba(255,255,255,0.72)',
blur: 3
}
}
},
offState: {
showLabel: true,
label: 'Window left corner',
labelFont: {
family: 'Roboto',
size: 12,
sizeUnit: 'px',
style: 'normal',
weight: '500',
lineHeight: '16px'
},
showStatus: true,
status: 'Closed',
statusFont: {
family: 'Roboto',
size: 10,
sizeUnit: 'px',
style: 'normal',
weight: '500',
lineHeight: '20px'
},
icon: 'mdi:curtains-closed',
iconSize: 32,
iconSizeUnit: 'px',
primaryColor: 'rgba(0, 0, 0, 0.87)',
secondaryColor: 'rgba(0, 0, 0, 0.54)',
background: {
type: BackgroundType.color,
color: '#FFF',
overlay: {
enabled: false,
color: 'rgba(255,255,255,0.72)',
blur: 3
}
},
primaryColorDisabled: 'rgba(0, 0, 0, 0.38)',
secondaryColorDisabled: 'rgba(0, 0, 0, 0.38)',
backgroundDisabled: {
type: BackgroundType.color,
color: '#CACACA',
overlay: {
enabled: false,
color: 'rgba(255,255,255,0.72)',
blur: 3
}
}
}
};

111
ui-ngx/src/app/modules/home/components/widget/lib/settings/common/indicator/status-widget-state-settings.component.html

@ -0,0 +1,111 @@
<!--
Copyright © 2016-2024 The Thingsboard Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<ng-container [formGroup]="stateSettingsFormGroup">
<div *ngIf="layout !== StatusWidgetLayout.icon" class="tb-form-row column-xs">
<mat-slide-toggle class="mat-slide fixed-title-width" formControlName="showLabel">
{{ 'widgets.status-widget.label' | translate }}
</mat-slide-toggle>
<div fxFlex fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="8px">
<mat-form-field class="flex" appearance="outline" subscriptSizing="dynamic">
<input matInput formControlName="label" placeholder="{{ 'widget-config.set' | translate }}">
</mat-form-field>
<tb-font-settings formControlName="labelFont"
[previewText]="stateSettingsFormGroup.get('label').value">
</tb-font-settings>
</div>
</div>
<div *ngIf="layout !== StatusWidgetLayout.icon" class="tb-form-row column-xs">
<mat-slide-toggle class="mat-slide fixed-title-width" formControlName="showStatus">
{{ 'widgets.status-widget.status' | translate }}
</mat-slide-toggle>
<div fxFlex fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="8px">
<mat-form-field class="flex" appearance="outline" subscriptSizing="dynamic">
<input matInput formControlName="status" placeholder="{{ 'widget-config.set' | translate }}">
</mat-form-field>
<tb-font-settings formControlName="statusFont"
[previewText]="stateSettingsFormGroup.get('status').value">
</tb-font-settings>
</div>
</div>
<div class="tb-form-row">
<div class="fixed-title-width">
{{ 'widgets.status-widget.icon' | translate }}
</div>
<div fxFlex fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="8px">
<mat-form-field appearance="outline" class="flex number" subscriptSizing="dynamic">
<input matInput type="number" min="0" formControlName="iconSize" placeholder="{{ 'widget-config.set' | translate }}">
</mat-form-field>
<tb-css-unit-select fxFlex formControlName="iconSizeUnit"></tb-css-unit-select>
<tb-material-icon-select asBoxInput
formControlName="icon">
</tb-material-icon-select>
</div>
</div>
<div class="tb-form-row space-between" [class]="{'column-xs': layout === StatusWidgetLayout.icon, 'column-lt-md': layout !== StatusWidgetLayout.icon}">
<div>{{ 'widgets.status-widget.color-palette' | translate }}</div>
<div fxLayout="row wrap" fxLayoutAlign="start center" fxLayoutAlign.lt-sm="space-between center"
[fxLayoutAlign.lt-md]="layout !== StatusWidgetLayout.icon ? 'space-between center': 'start center'"
style="gap: 12px;">
<div fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="8px">
<div tb-hint-tooltip-icon="{{'widgets.status-widget.primary-color-hint' | translate}}" translate>widgets.status-widget.primary</div>
<tb-color-input asBoxInput
formControlName="primaryColor">
</tb-color-input>
</div>
<mat-divider *ngIf="layout !== StatusWidgetLayout.icon" vertical fxHide.lt-md></mat-divider>
<div *ngIf="layout !== StatusWidgetLayout.icon" fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="8px">
<div tb-hint-tooltip-icon="{{'widgets.status-widget.secondary-color-hint' | translate}}" translate>widgets.status-widget.secondary</div>
<tb-color-input asBoxInput
formControlName="secondaryColor">
</tb-color-input>
</div>
<mat-divider vertical fxHide.lt-sm [fxHide.lt-md]="layout !== StatusWidgetLayout.icon"></mat-divider>
<div fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="8px">
<div translate>widgets.status-widget.background</div>
<tb-background-settings formControlName="background">
</tb-background-settings>
</div>
</div>
</div>
<div class="tb-form-row space-between" [class]="{'column-xs': layout === StatusWidgetLayout.icon, 'column-lt-md': layout !== StatusWidgetLayout.icon}">
<div>{{ 'widgets.status-widget.disabled-color-palette' | translate }}</div>
<div fxLayout="row wrap" fxLayoutAlign="start center" fxLayoutAlign.lt-sm="space-between center"
[fxLayoutAlign.lt-md]="layout !== StatusWidgetLayout.icon ? 'space-between center': 'start center'"
style="gap: 12px;">
<div fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="8px">
<div tb-hint-tooltip-icon="{{'widgets.status-widget.primary-color-hint' | translate}}" translate>widgets.status-widget.primary</div>
<tb-color-input asBoxInput
formControlName="primaryColorDisabled">
</tb-color-input>
</div>
<mat-divider *ngIf="layout !== StatusWidgetLayout.icon" vertical fxHide.lt-md></mat-divider>
<div *ngIf="layout !== StatusWidgetLayout.icon" fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="8px">
<div tb-hint-tooltip-icon="{{'widgets.status-widget.secondary-color-hint' | translate}}" translate>widgets.status-widget.secondary</div>
<tb-color-input asBoxInput
formControlName="secondaryColorDisabled">
</tb-color-input>
</div>
<mat-divider vertical fxHide.lt-sm [fxHide.lt-md]="layout !== StatusWidgetLayout.icon"></mat-divider>
<div fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="8px">
<div translate>widgets.status-widget.background</div>
<tb-background-settings formControlName="backgroundDisabled">
</tb-background-settings>
</div>
</div>
</div>
</ng-container>

158
ui-ngx/src/app/modules/home/components/widget/lib/settings/common/indicator/status-widget-state-settings.component.ts

@ -0,0 +1,158 @@
///
/// Copyright © 2016-2024 The Thingsboard Authors
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
///
import { Component, forwardRef, Input, OnChanges, OnInit, SimpleChanges } from '@angular/core';
import { ControlValueAccessor, NG_VALUE_ACCESSOR, UntypedFormBuilder, UntypedFormGroup } from '@angular/forms';
import { merge } from 'rxjs';
import {
StatusWidgetLayout,
StatusWidgetStateSettings
} from '@home/components/widget/lib/indicator/status-widget.models';
@Component({
selector: 'tb-status-widget-state-settings',
templateUrl: './status-widget-state-settings.component.html',
styleUrls: ['./../../widget-settings.scss'],
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => StatusWidgetStateSettingsComponent),
multi: true
}
]
})
export class StatusWidgetStateSettingsComponent implements OnInit, OnChanges, ControlValueAccessor {
StatusWidgetLayout = StatusWidgetLayout;
@Input()
disabled: boolean;
@Input()
layout: StatusWidgetLayout;
private modelValue: StatusWidgetStateSettings;
private propagateChange = null;
public stateSettingsFormGroup: UntypedFormGroup;
constructor(private fb: UntypedFormBuilder) {
}
ngOnInit(): void {
this.stateSettingsFormGroup = this.fb.group({
showLabel: [null, []],
label: [null, []],
labelFont: [null, []],
showStatus: [null, []],
status: [null, []],
statusFont: [null, []],
icon: [null, []],
iconSize: [null, []],
iconSizeUnit: [null, []],
primaryColor: [null, []],
secondaryColor: [null, []],
background: [null, []],
primaryColorDisabled: [null, []],
secondaryColorDisabled: [null, []],
backgroundDisabled: [null, []]
});
this.stateSettingsFormGroup.valueChanges.subscribe(() => {
this.updateModel();
});
merge(this.stateSettingsFormGroup.get('showLabel').valueChanges,
this.stateSettingsFormGroup.get('showStatus').valueChanges)
.subscribe(() => {
this.updateValidators();
});
}
ngOnChanges(changes: SimpleChanges): void {
for (const propName of Object.keys(changes)) {
const change = changes[propName];
if (!change.firstChange && change.currentValue !== change.previousValue) {
if (['layout'].includes(propName)) {
this.updateValidators();
}
}
}
}
registerOnChange(fn: any): void {
this.propagateChange = fn;
}
registerOnTouched(_fn: any): void {
}
setDisabledState(isDisabled: boolean): void {
this.disabled = isDisabled;
if (isDisabled) {
this.stateSettingsFormGroup.disable({emitEvent: false});
} else {
this.stateSettingsFormGroup.enable({emitEvent: false});
this.updateValidators();
}
}
writeValue(value: StatusWidgetStateSettings): void {
this.modelValue = value;
this.stateSettingsFormGroup.patchValue(
value, {emitEvent: false}
);
this.updateValidators();
}
private updateValidators() {
if (this.layout === StatusWidgetLayout.icon) {
this.stateSettingsFormGroup.get('showLabel').disable({emitEvent: false});
this.stateSettingsFormGroup.get('label').disable({emitEvent: false});
this.stateSettingsFormGroup.get('labelFont').disable({emitEvent: false});
this.stateSettingsFormGroup.get('showStatus').disable({emitEvent: false});
this.stateSettingsFormGroup.get('status').disable({emitEvent: false});
this.stateSettingsFormGroup.get('statusFont').disable({emitEvent: false});
this.stateSettingsFormGroup.get('secondaryColor').disable({emitEvent: false});
this.stateSettingsFormGroup.get('secondaryColorDisabled').disable({emitEvent: false});
} else {
this.stateSettingsFormGroup.get('showLabel').enable({emitEvent: false});
this.stateSettingsFormGroup.get('showStatus').enable({emitEvent: false});
this.stateSettingsFormGroup.get('secondaryColor').enable({emitEvent: false});
this.stateSettingsFormGroup.get('secondaryColorDisabled').enable({emitEvent: false});
const showLabel: boolean = this.stateSettingsFormGroup.get('showLabel').value;
const showStatus: boolean = this.stateSettingsFormGroup.get('showStatus').value;
if (showLabel) {
this.stateSettingsFormGroup.get('label').enable({emitEvent: false});
this.stateSettingsFormGroup.get('labelFont').enable({emitEvent: false});
} else {
this.stateSettingsFormGroup.get('label').disable({emitEvent: false});
this.stateSettingsFormGroup.get('labelFont').disable({emitEvent: false});
}
if (showStatus) {
this.stateSettingsFormGroup.get('status').enable({emitEvent: false});
this.stateSettingsFormGroup.get('statusFont').enable({emitEvent: false});
} else {
this.stateSettingsFormGroup.get('status').disable({emitEvent: false});
this.stateSettingsFormGroup.get('statusFont').disable({emitEvent: false});
}
}
}
private updateModel() {
this.modelValue = this.stateSettingsFormGroup.getRawValue();
this.propagateChange(this.modelValue);
}
}

5
ui-ngx/src/app/modules/home/components/widget/lib/settings/common/widget-settings-common.module.ts

@ -145,6 +145,9 @@ import {
import { import {
TimeSeriesChartGridSettingsComponent TimeSeriesChartGridSettingsComponent
} from '@home/components/widget/lib/settings/common/chart/time-series-chart-grid-settings.component'; } from '@home/components/widget/lib/settings/common/chart/time-series-chart-grid-settings.component';
import {
StatusWidgetStateSettingsComponent
} from '@home/components/widget/lib/settings/common/indicator/status-widget-state-settings.component';
@NgModule({ @NgModule({
declarations: [ declarations: [
@ -198,6 +201,7 @@ import {
TimeSeriesChartStatesPanelComponent, TimeSeriesChartStatesPanelComponent,
TimeSeriesChartStateRowComponent, TimeSeriesChartStateRowComponent,
TimeSeriesChartGridSettingsComponent, TimeSeriesChartGridSettingsComponent,
StatusWidgetStateSettingsComponent,
DataKeyInputComponent, DataKeyInputComponent,
EntityAliasInputComponent EntityAliasInputComponent
], ],
@ -257,6 +261,7 @@ import {
TimeSeriesChartStatesPanelComponent, TimeSeriesChartStatesPanelComponent,
TimeSeriesChartStateRowComponent, TimeSeriesChartStateRowComponent,
TimeSeriesChartGridSettingsComponent, TimeSeriesChartGridSettingsComponent,
StatusWidgetStateSettingsComponent,
DataKeyInputComponent, DataKeyInputComponent,
EntityAliasInputComponent EntityAliasInputComponent
], ],

82
ui-ngx/src/app/modules/home/components/widget/lib/settings/indicator/status-widget-settings.component.html

@ -0,0 +1,82 @@
<!--
Copyright © 2016-2024 The Thingsboard Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<ng-container [formGroup]="statusWidgetSettingsForm">
<div class="tb-form-panel">
<div class="tb-form-panel-title" translate>widgets.status-widget.behavior</div>
<div class="tb-form-row">
<div class="fixed-title-width" tb-hint-tooltip-icon="{{'widgets.rpc-state.initial-state-hint' | translate}}" translate>widgets.rpc-state.initial-state</div>
<tb-get-value-action-settings fxFlex
panelTitle="widgets.rpc-state.initial-state"
[valueType]="valueType.BOOLEAN"
trueLabel="widgets.rpc-state.on"
falseLabel="widgets.rpc-state.off"
stateLabel="widgets.rpc-state.on"
[aliasController]="aliasController"
[targetDevice]="targetDevice"
[widgetType]="widgetType"
formControlName="initialState"></tb-get-value-action-settings>
</div>
<div class="tb-form-row">
<div class="fixed-title-width" tb-hint-tooltip-icon="{{'widgets.rpc-state.disabled-state-hint' | translate}}" translate>widgets.rpc-state.disabled-state</div>
<tb-get-value-action-settings fxFlex
panelTitle="widgets.rpc-state.disabled-state"
[valueType]="valueType.BOOLEAN"
stateLabel="widgets.rpc-state.disabled"
[aliasController]="aliasController"
[targetDevice]="targetDevice"
[widgetType]="widgetType"
formControlName="disabledState"></tb-get-value-action-settings>
</div>
</div>
<div class="tb-form-panel">
<div class="tb-form-panel-title" translate>widget-config.appearance</div>
<tb-image-cards-select rowHeight="1:1"
[cols]="{columns: 3,
breakpoints: {
'lt-sm': 1,
'lt-md': 2
}}"
label="{{ 'widgets.status-widget.layout' | translate }}" formControlName="layout">
<tb-image-cards-select-option *ngFor="let layout of statusWidgetLayouts"
[value]="layout"
[image]="statusWidgetLayoutImageMap.get(layout)">
{{ statusWidgetLayoutTranslationMap.get(layout) | translate }}
</tb-image-cards-select-option>
</tb-image-cards-select>
</div>
<div class="tb-form-panel">
<div fxLayout="row" fxLayoutAlign="space-between center">
<div class="tb-form-panel-title" translate>widget-config.card-style</div>
<tb-toggle-select [(ngModel)]="cardStyleMode"
[ngModelOptions]="{ standalone: true }">
<tb-toggle-option value="on">{{ 'widgets.status-widget.on' | translate }}</tb-toggle-option>
<tb-toggle-option value="off">{{ 'widgets.status-widget.off' | translate }}</tb-toggle-option>
</tb-toggle-select>
</div>
<tb-status-widget-state-settings
*ngIf="cardStyleMode === 'on'"
[layout]="statusWidgetSettingsForm.get('layout').value"
formControlName="onState">
</tb-status-widget-state-settings>
<tb-status-widget-state-settings
*ngIf="cardStyleMode === 'off'"
[layout]="statusWidgetSettingsForm.get('layout').value"
formControlName="offState">
</tb-status-widget-state-settings>
</div>
</ng-container>

79
ui-ngx/src/app/modules/home/components/widget/lib/settings/indicator/status-widget-settings.component.ts

@ -0,0 +1,79 @@
///
/// Copyright © 2016-2024 The Thingsboard Authors
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
///
import { Component, Injector } from '@angular/core';
import { TargetDevice, WidgetSettings, WidgetSettingsComponent, widgetType } from '@shared/models/widget.models';
import { UntypedFormBuilder, UntypedFormGroup } from '@angular/forms';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import {
statusWidgetDefaultSettings,
statusWidgetLayoutImages,
statusWidgetLayouts,
statusWidgetLayoutTranslations
} from '@home/components/widget/lib/indicator/status-widget.models';
import { ValueType } from '@shared/models/constants';
@Component({
selector: 'tb-status-widget-settings',
templateUrl: './status-widget-settings.component.html',
styleUrls: ['./../widget-settings.scss'],
})
export class StatusWidgetSettingsComponent extends WidgetSettingsComponent {
get targetDevice(): TargetDevice {
return this.widgetConfig?.config?.targetDevice;
}
get widgetType(): widgetType {
return this.widgetConfig?.widgetType;
}
statusWidgetLayouts = statusWidgetLayouts;
statusWidgetLayoutTranslationMap = statusWidgetLayoutTranslations;
statusWidgetLayoutImageMap = statusWidgetLayoutImages;
valueType = ValueType;
statusWidgetSettingsForm: UntypedFormGroup;
cardStyleMode = 'on';
constructor(protected store: Store<AppState>,
private $injector: Injector,
private fb: UntypedFormBuilder) {
super(store);
}
protected settingsForm(): UntypedFormGroup {
return this.statusWidgetSettingsForm;
}
protected defaultSettings(): WidgetSettings {
return {...statusWidgetDefaultSettings};
}
protected onSettingsSet(settings: WidgetSettings) {
this.statusWidgetSettingsForm = this.fb.group({
initialState: [settings.initialState, []],
disabledState: [settings.disabledState, []],
layout: [settings.layout, []],
onState: [settings.onState, []],
offState: [settings.offState, []]
});
}
}

12
ui-ngx/src/app/modules/home/components/widget/lib/settings/widget-settings.module.ts

@ -342,6 +342,9 @@ import {
import { import {
TimeSeriesChartWidgetSettingsComponent TimeSeriesChartWidgetSettingsComponent
} from '@home/components/widget/lib/settings/chart/time-series-chart-widget-settings.component'; } from '@home/components/widget/lib/settings/chart/time-series-chart-widget-settings.component';
import {
StatusWidgetSettingsComponent
} from '@home/components/widget/lib/settings/indicator/status-widget-settings.component';
@NgModule({ @NgModule({
declarations: [ declarations: [
@ -464,7 +467,8 @@ import {
TimeSeriesChartKeySettingsComponent, TimeSeriesChartKeySettingsComponent,
TimeSeriesChartLineSettingsComponent, TimeSeriesChartLineSettingsComponent,
TimeSeriesChartBarSettingsComponent, TimeSeriesChartBarSettingsComponent,
TimeSeriesChartWidgetSettingsComponent TimeSeriesChartWidgetSettingsComponent,
StatusWidgetSettingsComponent
], ],
imports: [ imports: [
CommonModule, CommonModule,
@ -592,7 +596,8 @@ import {
TimeSeriesChartKeySettingsComponent, TimeSeriesChartKeySettingsComponent,
TimeSeriesChartLineSettingsComponent, TimeSeriesChartLineSettingsComponent,
TimeSeriesChartBarSettingsComponent, TimeSeriesChartBarSettingsComponent,
TimeSeriesChartWidgetSettingsComponent TimeSeriesChartWidgetSettingsComponent,
StatusWidgetSettingsComponent
] ]
}) })
export class WidgetSettingsModule { export class WidgetSettingsModule {
@ -685,5 +690,6 @@ export const widgetSettingsComponentsMap: {[key: string]: Type<IWidgetSettingsCo
'tb-slider-widget-settings': SliderWidgetSettingsComponent, 'tb-slider-widget-settings': SliderWidgetSettingsComponent,
'tb-toggle-button-widget-settings': ToggleButtonWidgetSettingsComponent, 'tb-toggle-button-widget-settings': ToggleButtonWidgetSettingsComponent,
'tb-time-series-chart-key-settings': TimeSeriesChartKeySettingsComponent, 'tb-time-series-chart-key-settings': TimeSeriesChartKeySettingsComponent,
'tb-time-series-chart-widget-settings': TimeSeriesChartWidgetSettingsComponent 'tb-time-series-chart-widget-settings': TimeSeriesChartWidgetSettingsComponent,
'tb-status-widget-settings': StatusWidgetSettingsComponent
}; };

7
ui-ngx/src/app/modules/home/components/widget/widget-components.module.ts

@ -86,6 +86,7 @@ import { PowerButtonWidgetComponent } from '@home/components/widget/lib/rpc/powe
import { SliderWidgetComponent } from '@home/components/widget/lib/rpc/slider-widget.component'; import { SliderWidgetComponent } from '@home/components/widget/lib/rpc/slider-widget.component';
import { ToggleButtonWidgetComponent } from '@home/components/widget/lib/button/toggle-button-widget.component'; import { ToggleButtonWidgetComponent } from '@home/components/widget/lib/button/toggle-button-widget.component';
import { TimeSeriesChartWidgetComponent } from '@home/components/widget/lib/chart/time-series-chart-widget.component'; import { TimeSeriesChartWidgetComponent } from '@home/components/widget/lib/chart/time-series-chart-widget.component';
import { StatusWidgetComponent } from '@home/components/widget/lib/indicator/status-widget.component';
@NgModule({ @NgModule({
declarations: declarations:
@ -138,7 +139,8 @@ import { TimeSeriesChartWidgetComponent } from '@home/components/widget/lib/char
PowerButtonWidgetComponent, PowerButtonWidgetComponent,
SliderWidgetComponent, SliderWidgetComponent,
ToggleButtonWidgetComponent, ToggleButtonWidgetComponent,
TimeSeriesChartWidgetComponent TimeSeriesChartWidgetComponent,
StatusWidgetComponent
], ],
imports: [ imports: [
CommonModule, CommonModule,
@ -195,7 +197,8 @@ import { TimeSeriesChartWidgetComponent } from '@home/components/widget/lib/char
PowerButtonWidgetComponent, PowerButtonWidgetComponent,
SliderWidgetComponent, SliderWidgetComponent,
ToggleButtonWidgetComponent, ToggleButtonWidgetComponent,
TimeSeriesChartWidgetComponent TimeSeriesChartWidgetComponent,
StatusWidgetComponent
], ],
providers: [ providers: [
{provide: WIDGET_COMPONENTS_MODULE_TOKEN, useValue: WidgetComponentsModule } {provide: WIDGET_COMPONENTS_MODULE_TOKEN, useValue: WidgetComponentsModule }

8263
ui-ngx/src/assets/locale/locale.constant-ar_AR.json

File diff suppressed because it is too large

20
ui-ngx/src/assets/locale/locale.constant-en_US.json

@ -5461,6 +5461,25 @@
"signal-strength-card-style": "Signal strength card style", "signal-strength-card-style": "Signal strength card style",
"no-signal-rssi-value": "\"No signal\" rssi value" "no-signal-rssi-value": "\"No signal\" rssi value"
}, },
"status-widget": {
"behavior": "Behavior",
"layout": "Layout",
"layout-default": "Default",
"layout-center": "Center",
"layout-icon": "Icon",
"on": "On",
"off": "Off",
"label": "Label",
"status": "Status",
"icon": "Icon",
"color-palette": "Color palette",
"disabled-color-palette": "Disabled color palette",
"primary": "Primary",
"primary-color-hint": "Color of icon and label",
"secondary": "Secondary",
"secondary-color-hint": "Color of status",
"background": "Background"
},
"chart": { "chart": {
"common-settings": "Common settings", "common-settings": "Common settings",
"enable-stacking-mode": "Enable stacking mode", "enable-stacking-mode": "Enable stacking mode",
@ -7262,6 +7281,7 @@
"language": { "language": {
"language": "Language", "language": "Language",
"locales": { "locales": {
"ar_AR": "اَلْعَرَبِيَّةُ",
"ca_ES": "Catalan", "ca_ES": "Catalan",
"cs_CZ": "Česky", "cs_CZ": "Česky",
"da_DK": "Dansk", "da_DK": "Dansk",

20
ui-ngx/src/assets/widget/status-widget/center-layout.svg

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 19 KiB

20
ui-ngx/src/assets/widget/status-widget/default-layout.svg

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 19 KiB

18
ui-ngx/src/assets/widget/status-widget/icon-layout.svg

@ -0,0 +1,18 @@
<svg width="141" height="140" viewBox="0 0 141 140" fill="none" xmlns="http://www.w3.org/2000/svg">
<g filter="url(#filter0_d_5952_143941)">
<rect x="8.66675" y="4" width="124" height="124" rx="4" fill="#3F52DD"/>
<path d="M83.0387 55.8776H58.2949V53.6282H83.0387V55.8776ZM59.4196 77.2472H63.9185C63.9185 73.8731 61.6691 71.6236 61.6691 71.6236C68.4174 67.1248 69.5421 57.0023 69.5421 57.0023H59.4196V77.2472ZM81.914 57.0023H71.7915C71.7915 57.0023 72.9162 67.1248 79.6645 71.6236C79.6645 71.6236 77.4151 73.8731 77.4151 77.2472H81.914V57.0023Z" fill="white"/>
</g>
<defs>
<filter id="filter0_d_5952_143941" x="0.666748" y="0" width="140" height="140" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
<feOffset dy="4"/>
<feGaussianBlur stdDeviation="4"/>
<feComposite in2="hardAlpha" operator="out"/>
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.04 0"/>
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_5952_143941"/>
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_5952_143941" result="shape"/>
</filter>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

Loading…
Cancel
Save