Merge branch 'master' into fix_bug_lwm2m_redis

This commit is contained in:
nick 2024-04-23 08:48:32 +03:00
commit 8828798639
37 changed files with 9677 additions and 71 deletions

View File

@ -2,15 +2,15 @@
"widgetsBundle": {
"alias": "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.",
"order": 9000,
"externalId": null,
"name": "Status indicators"
},
"widgetTypeFqns": [
"battery_level",
"signal_strength",
"progress_bar"
"progress_bar",
"status_widget"
]
}

File diff suppressed because one or more lines are too long

View File

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

View File

@ -147,7 +147,7 @@ public class SequentialTimeseriesPersistenceTest extends AbstractControllerTest
void checkDiffBetweenLatestTsForDevicesAndAsset(List<Device> devices, Asset asset) throws ExecutionException, InterruptedException, TimeoutException {
TsKvEntry assetTsKvEntry = getTsKvLatest(asset.getId(), GENERIC_CUMULATIVE_OBJ);
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) {
Long assetValue = assetJsonObject.get(device.getName()).getAsLong();
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 jsonObject = new JsonObject();
if (tsKvEntryOpt.isPresent()) {
jsonObject = new JsonParser().parse(tsKvEntryOpt.get()).getAsJsonObject();
jsonObject = JsonParser.parseString(tsKvEntryOpt.get()).getAsJsonObject();
}
jsonObject.addProperty(key, value);
return jsonObject;

View File

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

View File

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

View File

@ -26,8 +26,6 @@ import java.util.ArrayList;
public class JsonConverterTest {
private final JsonParser JSON_PARSER = new JsonParser();
@BeforeEach
public void before() {
JsonConverter.setTypeCastEnabled(true);
@ -35,55 +33,55 @@ public class JsonConverterTest {
@Test
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());
}
@Test
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);
}
@Test
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());
}
@Test
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);
}
@Test
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);
}
@Test
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());
}
@Test
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());
}
@Test
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());
}
@Test
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());
}
@ -91,7 +89,7 @@ public class JsonConverterTest {
public void testParseBigDecimalOutOfLongRangeWithoutParsing() {
JsonConverter.setTypeCastEnabled(false);
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() {
JsonConverter.setTypeCastEnabled(false);
Assertions.assertThrows(JsonSyntaxException.class, () -> {
JsonConverter.convertToTelemetry(JSON_PARSER.parse("{\"meterReadingDelta\": 9.9701010061400066E19}"), 0L);
JsonConverter.convertToTelemetry(JsonParser.parseString("{\"meterReadingDelta\": 9.9701010061400066E19}"), 0L);
});
}
}

View File

@ -45,7 +45,7 @@ public class JsonCoapAdaptor implements CoapTransportAdaptor {
public TransportProtos.PostTelemetryMsg convertToPostTelemetry(UUID sessionId, Request inbound, Descriptors.Descriptor telemetryMsgDescriptor) throws AdaptorException {
String payload = validatePayload(sessionId, inbound, false);
try {
return JsonConverter.convertToTelemetryProto(new JsonParser().parse(payload));
return JsonConverter.convertToTelemetryProto(JsonParser.parseString(payload));
} catch (IllegalStateException | JsonSyntaxException 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 {
String payload = validatePayload(sessionId, inbound, false);
try {
return JsonConverter.convertToAttributesProto(new JsonParser().parse(payload));
return JsonConverter.convertToAttributesProto(JsonParser.parseString(payload));
} catch (IllegalStateException | JsonSyntaxException 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 {
Optional<Integer> requestId = CoapTransportResource.getRequestId(inbound);
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!")))
.setPayload(response.toString()).build();
}
@ -78,7 +78,7 @@ public class JsonCoapAdaptor implements CoapTransportAdaptor {
@Override
public TransportProtos.ToServerRpcRequestMsg convertToServerRpcRequest(UUID sessionId, Request inbound) throws AdaptorException {
String payload = validatePayload(sessionId, inbound, false);
return JsonConverter.convertToServerRpcRequest(new JsonParser().parse(payload), 0);
return JsonConverter.convertToServerRpcRequest(JsonParser.parseString(payload), 0);
}
@Override

View File

@ -45,7 +45,7 @@ public class ProtoCoapAdaptor implements CoapTransportAdaptor {
public TransportProtos.PostTelemetryMsg convertToPostTelemetry(UUID sessionId, Request inbound, Descriptors.Descriptor telemetryMsgDescriptor) throws AdaptorException {
ProtoConverter.validateDescriptor(telemetryMsgDescriptor);
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) {
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 {
ProtoConverter.validateDescriptor(attributesMsgDescriptor);
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) {
throw new AdaptorException(e);
}
@ -74,7 +74,7 @@ public class ProtoCoapAdaptor implements CoapTransportAdaptor {
} else {
ProtoConverter.validateDescriptor(rpcResponseMsgDescriptor);
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!")))
.setPayload(response.toString()).build();
} catch (Exception e) {

View File

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

View File

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

View File

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

View File

@ -50,7 +50,7 @@ public class ProtoMqttAdaptor implements MqttTransportAdaptor {
byte[] bytes = toBytes(inbound.payload());
Descriptors.Descriptor telemetryDynamicMsgDescriptor = ProtoConverter.validateDescriptor(deviceSessionCtx.getTelemetryDynamicMsgDescriptor());
try {
return JsonConverter.convertToTelemetryProto(new JsonParser().parse(ProtoConverter.dynamicMsgToJson(bytes, telemetryDynamicMsgDescriptor)));
return JsonConverter.convertToTelemetryProto(JsonParser.parseString(ProtoConverter.dynamicMsgToJson(bytes, telemetryDynamicMsgDescriptor)));
} catch (Exception e) {
log.debug("Failed to decode post telemetry request", e);
throw new AdaptorException(e);
@ -63,7 +63,7 @@ public class ProtoMqttAdaptor implements MqttTransportAdaptor {
byte[] bytes = toBytes(inbound.payload());
Descriptors.Descriptor attributesDynamicMessageDescriptor = ProtoConverter.validateDescriptor(deviceSessionCtx.getAttributesDynamicMessageDescriptor());
try {
return JsonConverter.convertToAttributesProto(new JsonParser().parse(ProtoConverter.dynamicMsgToJson(bytes, attributesDynamicMessageDescriptor)));
return JsonConverter.convertToAttributesProto(JsonParser.parseString(ProtoConverter.dynamicMsgToJson(bytes, attributesDynamicMessageDescriptor)));
} catch (Exception e) {
log.debug("Failed to decode post attributes request", e);
throw new AdaptorException(e);
@ -102,7 +102,7 @@ public class ProtoMqttAdaptor implements MqttTransportAdaptor {
Descriptors.Descriptor rpcResponseDynamicMessageDescriptor = ProtoConverter.validateDescriptor(deviceSessionCtx.getRpcResponseDynamicMessageDescriptor());
try {
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();
} catch (Exception e) {
log.debug("Failed to decode rpc response", e);

View File

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

View File

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

View File

@ -51,6 +51,7 @@ import java.net.InetSocketAddress;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.TimeUnit;
import static org.eclipse.leshan.client.object.Security.noSec;
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);
ObjectsInitializer initializer = new ObjectsInitializer(model);
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.setClassForObject(ACCESS_CONTROL, DummyInstanceEnabler.class);
DtlsConnectorConfig.Builder dtlsConfig = new DtlsConnectorConfig.Builder();

View File

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

View File

@ -25,7 +25,6 @@ import org.thingsboard.rule.engine.api.RuleNode;
import org.thingsboard.rule.engine.api.TbContext;
import org.thingsboard.rule.engine.api.TbNodeException;
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.kv.AttributeKvEntry;
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 final Map<EntityId, EntityGeofencingState> entityStates = new HashMap<>();
private final Gson gson = new Gson();
private final JsonParser parser = new JsonParser();
@Override
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())
.get(1, TimeUnit.MINUTES);
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());
} else {
return new EntityGeofencingState(false, 0L, false);

View File

@ -114,6 +114,9 @@ import { ComparisonKeyRowComponent } from '@home/components/widget/config/basic/
import {
ComparisonKeysTableComponent
} 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({
declarations: [
@ -151,7 +154,8 @@ import {
ToggleButtonBasicConfigComponent,
TimeSeriesChartBasicConfigComponent,
ComparisonKeyRowComponent,
ComparisonKeysTableComponent
ComparisonKeysTableComponent,
StatusWidgetBasicConfigComponent
],
imports: [
CommonModule,
@ -191,7 +195,8 @@ import {
PowerButtonBasicConfigComponent,
SliderBasicConfigComponent,
ToggleButtonBasicConfigComponent,
TimeSeriesChartBasicConfigComponent
TimeSeriesChartBasicConfigComponent,
StatusWidgetBasicConfigComponent
]
})
export class BasicWidgetConfigModule {
@ -225,5 +230,6 @@ export const basicWidgetConfigComponentsMap: {[key: string]: Type<IBasicWidgetCo
'tb-power-button-basic-config': PowerButtonBasicConfigComponent,
'tb-slider-basic-config': SliderBasicConfigComponent,
'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
};

View File

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

View File

@ -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');
}
}

View File

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

View File

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

View File

@ -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();
}
}

View File

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

View File

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

View File

@ -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);
}
}

View File

@ -145,6 +145,9 @@ import {
import {
TimeSeriesChartGridSettingsComponent
} 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({
declarations: [
@ -198,6 +201,7 @@ import {
TimeSeriesChartStatesPanelComponent,
TimeSeriesChartStateRowComponent,
TimeSeriesChartGridSettingsComponent,
StatusWidgetStateSettingsComponent,
DataKeyInputComponent,
EntityAliasInputComponent
],
@ -257,6 +261,7 @@ import {
TimeSeriesChartStatesPanelComponent,
TimeSeriesChartStateRowComponent,
TimeSeriesChartGridSettingsComponent,
StatusWidgetStateSettingsComponent,
DataKeyInputComponent,
EntityAliasInputComponent
],

View File

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

View File

@ -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, []]
});
}
}

View File

@ -342,6 +342,9 @@ import {
import {
TimeSeriesChartWidgetSettingsComponent
} 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({
declarations: [
@ -464,7 +467,8 @@ import {
TimeSeriesChartKeySettingsComponent,
TimeSeriesChartLineSettingsComponent,
TimeSeriesChartBarSettingsComponent,
TimeSeriesChartWidgetSettingsComponent
TimeSeriesChartWidgetSettingsComponent,
StatusWidgetSettingsComponent
],
imports: [
CommonModule,
@ -592,7 +596,8 @@ import {
TimeSeriesChartKeySettingsComponent,
TimeSeriesChartLineSettingsComponent,
TimeSeriesChartBarSettingsComponent,
TimeSeriesChartWidgetSettingsComponent
TimeSeriesChartWidgetSettingsComponent,
StatusWidgetSettingsComponent
]
})
export class WidgetSettingsModule {
@ -685,5 +690,6 @@ export const widgetSettingsComponentsMap: {[key: string]: Type<IWidgetSettingsCo
'tb-slider-widget-settings': SliderWidgetSettingsComponent,
'tb-toggle-button-widget-settings': ToggleButtonWidgetSettingsComponent,
'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
};

View File

@ -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 { 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 { StatusWidgetComponent } from '@home/components/widget/lib/indicator/status-widget.component';
@NgModule({
declarations:
@ -138,7 +139,8 @@ import { TimeSeriesChartWidgetComponent } from '@home/components/widget/lib/char
PowerButtonWidgetComponent,
SliderWidgetComponent,
ToggleButtonWidgetComponent,
TimeSeriesChartWidgetComponent
TimeSeriesChartWidgetComponent,
StatusWidgetComponent
],
imports: [
CommonModule,
@ -195,7 +197,8 @@ import { TimeSeriesChartWidgetComponent } from '@home/components/widget/lib/char
PowerButtonWidgetComponent,
SliderWidgetComponent,
ToggleButtonWidgetComponent,
TimeSeriesChartWidgetComponent
TimeSeriesChartWidgetComponent,
StatusWidgetComponent
],
providers: [
{provide: WIDGET_COMPONENTS_MODULE_TOKEN, useValue: WidgetComponentsModule }

File diff suppressed because it is too large Load Diff

View File

@ -5461,6 +5461,25 @@
"signal-strength-card-style": "Signal strength card style",
"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": {
"common-settings": "Common settings",
"enable-stacking-mode": "Enable stacking mode",
@ -7262,6 +7281,7 @@
"language": {
"language": "Language",
"locales": {
"ar_AR": "اَلْعَرَبِيَّةُ",
"ca_ES": "Catalan",
"cs_CZ": "Česky",
"da_DK": "Dansk",

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 19 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 19 KiB

View File

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