Merge branch 'rc'

This commit is contained in:
Igor Kulikov 2024-10-03 13:49:30 +03:00
commit a4ff61b623
31 changed files with 553 additions and 200 deletions

View File

@ -38,7 +38,7 @@ import org.thingsboard.server.service.edge.rpc.utils.EdgeVersionUtils;
public class OAuth2EdgeProcessor extends BaseEdgeProcessor {
public DownlinkMsg convertOAuth2DomainEventToDownlink(EdgeEvent edgeEvent, EdgeVersion edgeVersion) {
if (EdgeVersionUtils.isEdgeVersionOlderThan(edgeVersion, EdgeVersion.V_3_7_1)) {
if (EdgeVersionUtils.isEdgeVersionOlderThan(edgeVersion, EdgeVersion.V_3_8_0)) {
return null;
}
DomainId domainId = new DomainId(edgeEvent.getEntityId());
@ -73,7 +73,7 @@ public class OAuth2EdgeProcessor extends BaseEdgeProcessor {
}
public DownlinkMsg convertOAuth2ClientEventToDownlink(EdgeEvent edgeEvent, EdgeVersion edgeVersion) {
if (EdgeVersionUtils.isEdgeVersionOlderThan(edgeVersion, EdgeVersion.V_3_7_1)) {
if (EdgeVersionUtils.isEdgeVersionOlderThan(edgeVersion, EdgeVersion.V_3_8_0)) {
return null;
}
OAuth2ClientId oAuth2ClientId = new OAuth2ClientId(edgeEvent.getEntityId());

View File

@ -62,7 +62,7 @@ public class DefaultCacheCleanupService implements CacheCleanupService {
clearAll();
break;
case "3.7.0":
log.info("Clearing cache to upgrade from version 3.7.0 to 3.7.1");
log.info("Clearing cache to upgrade from version 3.7.0 to 3.8.0");
clearAll();
break;
default:

View File

@ -136,7 +136,7 @@ public class EdgeGrpcClient implements EdgeRpcClient {
.setConnectRequestMsg(ConnectRequestMsg.newBuilder()
.setEdgeRoutingKey(edgeKey)
.setEdgeSecret(edgeSecret)
.setEdgeVersion(EdgeVersion.V_3_7_1)
.setEdgeVersion(EdgeVersion.V_3_8_0)
.setMaxInboundMessageSize(maxInboundMessageSize)
.build())
.build());

View File

@ -39,7 +39,7 @@ enum EdgeVersion {
V_3_6_2 = 5;
V_3_6_4 = 6;
V_3_7_0 = 7;
V_3_7_1 = 8;
V_3_8_0 = 8;
}
/**

View File

@ -46,12 +46,15 @@ import org.thingsboard.server.transport.coap.CoapTransportContext;
import org.thingsboard.server.transport.coap.callback.CoapDeviceAuthCallback;
import org.thingsboard.server.transport.coap.callback.CoapEfentoCallback;
import org.thingsboard.server.transport.coap.efento.utils.CoapEfentoUtils;
import org.thingsboard.server.transport.coap.efento.utils.PulseCounterType;
import java.nio.ByteBuffer;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.TreeMap;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
@ -62,6 +65,19 @@ import static org.thingsboard.server.transport.coap.CoapTransportService.CONFIGU
import static org.thingsboard.server.transport.coap.CoapTransportService.CURRENT_TIMESTAMP;
import static org.thingsboard.server.transport.coap.CoapTransportService.DEVICE_INFO;
import static org.thingsboard.server.transport.coap.CoapTransportService.MEASUREMENTS;
import static org.thingsboard.server.transport.coap.efento.utils.CoapEfentoUtils.BREATH_VOC_METADATA_FACTOR;
import static org.thingsboard.server.transport.coap.efento.utils.CoapEfentoUtils.CO2_EQUIVALENT_METADATA_FACTOR;
import static org.thingsboard.server.transport.coap.efento.utils.CoapEfentoUtils.CO2_GAS_METADATA_FACTOR;
import static org.thingsboard.server.transport.coap.efento.utils.CoapEfentoUtils.ELEC_METER_ACC_MAJOR_METADATA_FACTOR;
import static org.thingsboard.server.transport.coap.efento.utils.CoapEfentoUtils.ELEC_METER_ACC_MINOR_METADATA_FACTOR;
import static org.thingsboard.server.transport.coap.efento.utils.CoapEfentoUtils.IAQ_METADATA_FACTOR;
import static org.thingsboard.server.transport.coap.efento.utils.CoapEfentoUtils.PULSE_CNT_ACC_MAJOR_METADATA_FACTOR;
import static org.thingsboard.server.transport.coap.efento.utils.CoapEfentoUtils.PULSE_CNT_ACC_MINOR_METADATA_FACTOR;
import static org.thingsboard.server.transport.coap.efento.utils.CoapEfentoUtils.PULSE_CNT_ACC_WIDE_MAJOR_METADATA_FACTOR;
import static org.thingsboard.server.transport.coap.efento.utils.CoapEfentoUtils.PULSE_CNT_ACC_WIDE_MINOR_METADATA_FACTOR;
import static org.thingsboard.server.transport.coap.efento.utils.CoapEfentoUtils.STATIC_IAQ_METADATA_FACTOR;
import static org.thingsboard.server.transport.coap.efento.utils.CoapEfentoUtils.WATER_METER_ACC_MAJOR_METADATA_FACTOR;
import static org.thingsboard.server.transport.coap.efento.utils.CoapEfentoUtils.WATER_METER_ACC_MINOR_METADATA_FACTOR;
import static org.thingsboard.server.transport.coap.efento.utils.CoapEfentoUtils.isBinarySensor;
import static org.thingsboard.server.transport.coap.efento.utils.CoapEfentoUtils.isSensorError;
@ -84,6 +100,7 @@ public class CoapEfentoTransportResource extends AbstractCoapTransportResource {
List<String> uriPath = request.getOptions().getUriPath();
boolean validPath = uriPath.size() == CHILD_RESOURCE_POSITION && uriPath.get(1).equals(CURRENT_TIMESTAMP);
if (!validPath) {
log.trace("Invalid path: [{}]", uriPath);
exchange.respond(CoAP.ResponseCode.BAD_REQUEST);
} else {
int dateInSec = (int) (System.currentTimeMillis() / 1000);
@ -98,6 +115,7 @@ public class CoapEfentoTransportResource extends AbstractCoapTransportResource {
Request request = advanced.getRequest();
List<String> uriPath = request.getOptions().getUriPath();
if (uriPath.size() != CHILD_RESOURCE_POSITION) {
log.trace("Unexpected uri path size, uri path: [{}]", uriPath);
exchange.respond(CoAP.ResponseCode.BAD_REQUEST);
return;
}
@ -113,6 +131,7 @@ public class CoapEfentoTransportResource extends AbstractCoapTransportResource {
processConfigurationRequest(exchange);
break;
default:
log.trace("Unexpected request type: [{}]", requestType);
exchange.respond(CoAP.ResponseCode.BAD_REQUEST);
break;
}
@ -179,6 +198,7 @@ public class CoapEfentoTransportResource extends AbstractCoapTransportResource {
log.error("[{}] Failed to decode Efento ProtoConfig: ", sessionId, e);
exchange.respond(CoAP.ResponseCode.BAD_REQUEST);
} catch (InvalidProtocolBufferException e) {
log.error("[{}] Error while processing efento message: ", sessionId, e);
throw new RuntimeException(e);
}
});
@ -280,6 +300,12 @@ public class CoapEfentoTransportResource extends AbstractCoapTransportResource {
}
}
valuesMap.values().forEach(jsonObject -> {
for (PulseCounterType pulseCounterType : PulseCounterType.values()) {
calculatePulseCounterTotalValue(jsonObject, pulseCounterType);
}
});
if (CollectionUtils.isEmpty(valuesMap)) {
throw new IllegalStateException("[" + sessionId + "]: Failed to collect Efento measurements, reason, values map is empty!");
}
@ -312,7 +338,7 @@ public class CoapEfentoTransportResource extends AbstractCoapTransportResource {
values.addProperty("pulse_cnt_" + channelNumber, (double) (startPoint + sampleOffset));
break;
case MEASUREMENT_TYPE_IAQ:
values.addProperty("iaq_" + channelNumber, (startPoint + sampleOffset));
addPulseCounterProperties(values, "iaq_", channelNumber, startPoint + sampleOffset, IAQ_METADATA_FACTOR);
break;
case MEASUREMENT_TYPE_ELECTRICITY_METER:
values.addProperty("watt_hour_" + channelNumber, (double) (startPoint + sampleOffset));
@ -330,22 +356,25 @@ public class CoapEfentoTransportResource extends AbstractCoapTransportResource {
values.addProperty("distance_mm_" + channelNumber, (double) (startPoint + sampleOffset));
break;
case MEASUREMENT_TYPE_WATER_METER_ACC_MINOR:
values.addProperty("acc_counter_water_minor_" + channelNumber, (double) (startPoint + sampleOffset));
addPulseCounterProperties(values, "water_cnt_acc_minor_", channelNumber, startPoint + sampleOffset, WATER_METER_ACC_MINOR_METADATA_FACTOR);
break;
case MEASUREMENT_TYPE_WATER_METER_ACC_MAJOR:
values.addProperty("acc_counter_water_major_" + channelNumber, (double) (startPoint + sampleOffset));
addPulseCounterProperties(values, "water_cnt_acc_major_", channelNumber, startPoint + sampleOffset, WATER_METER_ACC_MAJOR_METADATA_FACTOR);
break;
case MEASUREMENT_TYPE_HUMIDITY_ACCURATE:
values.addProperty("humidity_relative_" + channelNumber, (double) (startPoint + sampleOffset) / 10f);
break;
case MEASUREMENT_TYPE_STATIC_IAQ:
values.addProperty("static_iaq_" + channelNumber, (double) (startPoint + sampleOffset));
addPulseCounterProperties(values, "static_iaq_", channelNumber, startPoint + sampleOffset, STATIC_IAQ_METADATA_FACTOR);
break;
case MEASUREMENT_TYPE_CO2_GAS:
addPulseCounterProperties(values, "co2_gas_", channelNumber, startPoint + sampleOffset, CO2_GAS_METADATA_FACTOR);
break;
case MEASUREMENT_TYPE_CO2_EQUIVALENT:
values.addProperty("co2_ppm_" + channelNumber, (double) (startPoint + sampleOffset));
addPulseCounterProperties(values, "co2_", channelNumber, startPoint + sampleOffset, CO2_EQUIVALENT_METADATA_FACTOR);
break;
case MEASUREMENT_TYPE_BREATH_VOC:
values.addProperty("breath_voc_ppm_" + channelNumber, (double) (startPoint + sampleOffset));
addPulseCounterProperties(values, "breath_voc_", channelNumber, startPoint + sampleOffset, BREATH_VOC_METADATA_FACTOR);
break;
case MEASUREMENT_TYPE_PERCENTAGE:
values.addProperty("percentage_" + channelNumber, (double) (startPoint + sampleOffset) / 100f);
@ -357,25 +386,25 @@ public class CoapEfentoTransportResource extends AbstractCoapTransportResource {
values.addProperty("current_" + channelNumber, (double) (startPoint + sampleOffset) / 100f);
break;
case MEASUREMENT_TYPE_PULSE_CNT_ACC_MINOR:
values.addProperty("pulse_cnt_minor_" + channelNumber, (double) (startPoint + sampleOffset));
addPulseCounterProperties(values, "pulse_cnt_acc_minor_", channelNumber, startPoint + sampleOffset, PULSE_CNT_ACC_MINOR_METADATA_FACTOR);
break;
case MEASUREMENT_TYPE_PULSE_CNT_ACC_MAJOR:
values.addProperty("pulse_cnt_major_" + channelNumber, (double) (startPoint + sampleOffset));
addPulseCounterProperties(values, "pulse_cnt_acc_major_", channelNumber, startPoint + sampleOffset, PULSE_CNT_ACC_MAJOR_METADATA_FACTOR);
break;
case MEASUREMENT_TYPE_ELEC_METER_ACC_MINOR:
values.addProperty("elec_meter_minor_" + channelNumber, (double) (startPoint + sampleOffset));
addPulseCounterProperties(values, "elec_meter_acc_minor_", channelNumber, startPoint + sampleOffset, ELEC_METER_ACC_MINOR_METADATA_FACTOR);
break;
case MEASUREMENT_TYPE_ELEC_METER_ACC_MAJOR:
values.addProperty("elec_meter_major_" + channelNumber, (double) (startPoint + sampleOffset));
addPulseCounterProperties(values, "elec_meter_acc_major_", channelNumber, startPoint + sampleOffset, ELEC_METER_ACC_MAJOR_METADATA_FACTOR);
break;
case MEASUREMENT_TYPE_PULSE_CNT_ACC_WIDE_MINOR:
values.addProperty("pulse_cnt_wide_minor_" + channelNumber, (double) (startPoint + sampleOffset));
addPulseCounterProperties(values, "pulse_cnt_acc_wide_minor_", channelNumber, startPoint + sampleOffset, PULSE_CNT_ACC_WIDE_MINOR_METADATA_FACTOR);
break;
case MEASUREMENT_TYPE_PULSE_CNT_ACC_WIDE_MAJOR:
values.addProperty("pulse_cnt_wide_major_" + channelNumber, (double) (startPoint + sampleOffset));
addPulseCounterProperties(values, "pulse_cnt_acc_wide_major_", channelNumber, startPoint + sampleOffset, PULSE_CNT_ACC_WIDE_MAJOR_METADATA_FACTOR);
break;
case MEASUREMENT_TYPE_CURRENT_PRECISE:
values.addProperty("current_precise_" + channelNumber, (double) (startPoint + sampleOffset)/1000f);
values.addProperty("current_precise_" + channelNumber, (double) (startPoint + sampleOffset) / 1000f);
break;
case MEASUREMENT_TYPE_NO_SENSOR:
case UNRECOGNIZED:
@ -387,6 +416,20 @@ public class CoapEfentoTransportResource extends AbstractCoapTransportResource {
}
}
private void addPulseCounterProperties(JsonObject values, String prefix, int channelNumber, int value, int metadataFactor) {
values.addProperty(prefix + channelNumber, value / metadataFactor);
values.addProperty(prefix + "metadata_" + channelNumber, value % metadataFactor);
}
private void calculatePulseCounterTotalValue(JsonObject value, PulseCounterType pulseCounterType) {
Set<String> keys = value.keySet();
Optional<String> major = keys.stream().filter(s -> s.startsWith(pulseCounterType.getPrefix() + "major_")).findAny();
Optional<String> minor = keys.stream().filter(s -> s.startsWith(pulseCounterType.getPrefix() + "minor_")).findAny();
if (major.isPresent() && minor.isPresent()) {
value.addProperty(pulseCounterType.getPrefix() + "total_value", value.get(major.get()).getAsInt() * pulseCounterType.getMajorResolution() + value.get(minor.get()).getAsInt());
}
}
private void addBinarySample(ProtoChannel protoChannel, boolean valueIsOk, JsonObject values, int channel, UUID sessionId) {
switch (protoChannel.getType()) {
case MEASUREMENT_TYPE_OK_ALARM:

View File

@ -28,6 +28,21 @@ import static org.thingsboard.server.gen.transport.coap.MeasurementTypeProtos.Me
public class CoapEfentoUtils {
public static final int PULSE_CNT_ACC_MINOR_METADATA_FACTOR = 6;
public static final int PULSE_CNT_ACC_MAJOR_METADATA_FACTOR = 4;
public static final int ELEC_METER_ACC_MINOR_METADATA_FACTOR = 6;
public static final int ELEC_METER_ACC_MAJOR_METADATA_FACTOR = 4;
public static final int PULSE_CNT_ACC_WIDE_MINOR_METADATA_FACTOR = 6;
public static final int PULSE_CNT_ACC_WIDE_MAJOR_METADATA_FACTOR = 4;
public static final int WATER_METER_ACC_MINOR_METADATA_FACTOR = 6;
public static final int WATER_METER_ACC_MAJOR_METADATA_FACTOR = 4;
public static final int IAQ_METADATA_FACTOR = 3;
public static final int STATIC_IAQ_METADATA_FACTOR = 3;
public static final int CO2_GAS_METADATA_FACTOR = 3;
public static final int CO2_EQUIVALENT_METADATA_FACTOR = 3;
public static final int BREATH_VOC_METADATA_FACTOR = 3;
public static String convertByteArrayToString(byte[] a) {
StringBuilder out = new StringBuilder();
for (byte b : a) {

View File

@ -0,0 +1,40 @@
/**
* 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.
*/
package org.thingsboard.server.transport.coap.efento.utils;
public enum PulseCounterType {
WATER_CNT_ACC("water_cnt_acc_", 100),
PULSE_CNT_ACC("pulse_cnt_acc_", 1000),
ELEC_METER_ACC("elec_meter_acc_", 1000),
PULSE_CNT_ACC_WIDE("pulse_cnt_acc_wide_", 1000000);
private final String prefix;
private final int majorResolution;
PulseCounterType(String prefix, int majorResolution) {
this.prefix = prefix;
this.majorResolution = majorResolution;
}
public String getPrefix() {
return prefix;
}
public int getMajorResolution() {
return majorResolution;
}
}

View File

@ -70,7 +70,7 @@ import static org.thingsboard.server.gen.transport.coap.MeasurementTypeProtos.Me
import static org.thingsboard.server.gen.transport.coap.MeasurementTypeProtos.MeasurementType.MEASUREMENT_TYPE_WATER_METER_ACC_MINOR;
import static org.thingsboard.server.transport.coap.efento.utils.CoapEfentoUtils.convertTimestampToUtcString;
class CoapEfentTransportResourceTest {
class CoapEfentoTransportResourceTest {
private static CoapEfentoTransportResource coapEfentoTransportResource;
@ -152,31 +152,69 @@ class CoapEfentTransportResourceTest {
Arguments.of(MEASUREMENT_TYPE_ATMOSPHERIC_PRESSURE, List.of(1013), "pressure_1", 101.3),
Arguments.of(MEASUREMENT_TYPE_DIFFERENTIAL_PRESSURE, List.of(500), "pressure_diff_1", 500),
Arguments.of(MEASUREMENT_TYPE_PULSE_CNT, List.of(300), "pulse_cnt_1", 300),
Arguments.of(MEASUREMENT_TYPE_IAQ, List.of(150), "iaq_1", 150),
Arguments.of(MEASUREMENT_TYPE_IAQ, List.of(150), "iaq_1", 50.0),
Arguments.of(MEASUREMENT_TYPE_ELECTRICITY_METER, List.of(1200), "watt_hour_1", 1200),
Arguments.of(MEASUREMENT_TYPE_SOIL_MOISTURE, List.of(35), "soil_moisture_1", 35),
Arguments.of(MEASUREMENT_TYPE_AMBIENT_LIGHT, List.of(500), "ambient_light_1", 50),
Arguments.of(MEASUREMENT_TYPE_HIGH_PRESSURE, List.of(200000), "high_pressure_1", 200000),
Arguments.of(MEASUREMENT_TYPE_DISTANCE_MM, List.of(1500), "distance_mm_1", 1500),
Arguments.of(MEASUREMENT_TYPE_WATER_METER_ACC_MINOR, List.of(125), "acc_counter_water_minor_1", 125),
Arguments.of(MEASUREMENT_TYPE_WATER_METER_ACC_MAJOR, List.of(2500), "acc_counter_water_major_1", 2500),
Arguments.of(MEASUREMENT_TYPE_HUMIDITY_ACCURATE, List.of(525), "humidity_relative_1", 52.5),
Arguments.of(MEASUREMENT_TYPE_STATIC_IAQ, List.of(110), "static_iaq_1", 110),
Arguments.of(MEASUREMENT_TYPE_CO2_EQUIVALENT, List.of(450), "co2_ppm_1", 450),
Arguments.of(MEASUREMENT_TYPE_BREATH_VOC, List.of(220), "breath_voc_ppm_1", 220),
Arguments.of(MEASUREMENT_TYPE_PERCENTAGE, List.of(80), "percentage_1", 0.80),
Arguments.of(MEASUREMENT_TYPE_VOLTAGE, List.of(2400), "voltage_1", 240),
Arguments.of(MEASUREMENT_TYPE_STATIC_IAQ, List.of(110), "static_iaq_1", 36),
Arguments.of(MEASUREMENT_TYPE_CO2_EQUIVALENT, List.of(450), "co2_1", 150),
Arguments.of(MEASUREMENT_TYPE_BREATH_VOC, List.of(220), "breath_voc_1", 73),
Arguments.of(MEASUREMENT_TYPE_PERCENTAGE, List.of(80), "percentage_1", 0.8),
Arguments.of(MEASUREMENT_TYPE_VOLTAGE, List.of(2400), "voltage_1", 240.0),
Arguments.of(MEASUREMENT_TYPE_CURRENT, List.of(550), "current_1", 5.5),
Arguments.of(MEASUREMENT_TYPE_PULSE_CNT_ACC_MINOR, List.of(180), "pulse_cnt_minor_1", 180),
Arguments.of(MEASUREMENT_TYPE_PULSE_CNT_ACC_MAJOR, List.of(1200), "pulse_cnt_major_1", 1200),
Arguments.of(MEASUREMENT_TYPE_ELEC_METER_ACC_MINOR, List.of(550), "elec_meter_minor_1", 550),
Arguments.of(MEASUREMENT_TYPE_ELEC_METER_ACC_MAJOR, List.of(5500), "elec_meter_major_1", 5500),
Arguments.of(MEASUREMENT_TYPE_PULSE_CNT_ACC_WIDE_MINOR, List.of(230), "pulse_cnt_wide_minor_1", 230),
Arguments.of(MEASUREMENT_TYPE_PULSE_CNT_ACC_WIDE_MAJOR, List.of(1700), "pulse_cnt_wide_major_1", 1700),
Arguments.of(MEASUREMENT_TYPE_CURRENT_PRECISE, List.of(275), "current_precise_1", 0.275)
);
}
@ParameterizedTest
@MethodSource
void checkPulseCounterSensors(MeasurementType minorType, List<Integer> minorSampleOffsets, MeasurementType majorType, List<Integer> majorSampleOffsets,
String propertyPrefix, double expectedValue) {
long tsInSec = Instant.now().getEpochSecond();
ProtoMeasurements measurements = ProtoMeasurements.newBuilder()
.setSerialNum(integerToByteString(1234))
.setCloudToken("test_token")
.setMeasurementPeriodBase(180)
.setMeasurementPeriodFactor(0)
.setBatteryStatus(true)
.setSignal(0)
.setNextTransmissionAt(1000)
.setTransferReason(0)
.setHash(0)
.addAllChannels(List.of(MeasurementsProtos.ProtoChannel.newBuilder()
.setType(minorType)
.setTimestamp(Math.toIntExact(tsInSec))
.addAllSampleOffsets(minorSampleOffsets)
.build(),
MeasurementsProtos.ProtoChannel.newBuilder()
.setType(majorType)
.setTimestamp(Math.toIntExact(tsInSec))
.addAllSampleOffsets(majorSampleOffsets)
.build()))
.build();
List<CoapEfentoTransportResource.EfentoTelemetry> efentoMeasurements = coapEfentoTransportResource.getEfentoMeasurements(measurements, UUID.randomUUID());
assertThat(efentoMeasurements).hasSize(1);
assertThat(efentoMeasurements.get(0).getTs()).isEqualTo(tsInSec * 1000);
assertThat(efentoMeasurements.get(0).getValues().getAsJsonObject().get(propertyPrefix + "_total_value").getAsDouble()).isEqualTo(expectedValue);
checkDefaultMeasurements(measurements, efentoMeasurements, 180, false);
}
private static Stream<Arguments> checkPulseCounterSensors() {
return Stream.of(
Arguments.of(MEASUREMENT_TYPE_WATER_METER_ACC_MINOR, List.of(125), MEASUREMENT_TYPE_WATER_METER_ACC_MAJOR,
List.of(2500), "water_cnt_acc", 62520.0),
Arguments.of(MEASUREMENT_TYPE_PULSE_CNT_ACC_MINOR, List.of(180), MEASUREMENT_TYPE_PULSE_CNT_ACC_MAJOR,
List.of(1200), "pulse_cnt_acc", 300030.0),
Arguments.of(MEASUREMENT_TYPE_ELEC_METER_ACC_MINOR, List.of(550), MEASUREMENT_TYPE_ELEC_METER_ACC_MAJOR,
List.of(5500), "elec_meter_acc", 1375091.0),
Arguments.of(MEASUREMENT_TYPE_PULSE_CNT_ACC_WIDE_MINOR, List.of(230), MEASUREMENT_TYPE_PULSE_CNT_ACC_WIDE_MAJOR,
List.of(1700), "pulse_cnt_acc_wide", 425000038.0));
}
@Test
void checkBinarySensor() {
long tsInSec = Instant.now().getEpochSecond();

View File

@ -0,0 +1,130 @@
/*
* 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.
*/
const fs = require('fs');
const path = require('path');
const materialIconDir = path.join('.', 'src', 'assets', 'metadata');
const mdiMetadata = path.join('.', 'node_modules', '@mdi', 'svg', 'meta.json');
async function init() {
const iconsBundle = JSON.parse(await fs.promises.readFile(path.join(materialIconDir, 'material-icons.json')));
await getMaterialIconMetadataAndUpdated(iconsBundle);
await getMDIMetadataAndUpdated(iconsBundle);
await fs.promises.writeFile(path.join(materialIconDir, 'material-icons.json'), JSON.stringify(iconsBundle), 'utf8')
}
async function getMaterialIconMetadataAndUpdated(iconsBundle){
const iconsResponse = await fetch('https://fonts.google.com/metadata/icons?key=material_symbols&incomplete=true');
const iconsText = await iconsResponse.text();
const clearText = iconsText.substring(iconsText.indexOf("\n") + 1);
const icons = JSON.parse(clearText).icons;
let prevItem;
const filterIcons = icons.filter((item) => {
if (prevItem?.name !== item.name && !item.unsupported_families.includes('Material Icons')) {
prevItem = item;
return true;
}
return false;
});
filterIcons.forEach((item, index) => {
const findItem = iconsBundle.find((el) => el.name === item.name);
if (!findItem) {
let prevIndexIcon = 0;
if (index === 0) {
prevIndexIcon = 45;
} else {
let iteration = 0;
while (prevIndexIcon < 45) {
iteration++;
const prevIconName = filterIcons[index - iteration].name;
prevIndexIcon = findPreviousIcon(iconsBundle, prevIconName);
}
}
if (prevIndexIcon >= 0) {
iconsBundle.splice(prevIndexIcon + 1, 0, {name:item.name, tags:item.tags});
}
console.log('Not found icon:', item.name);
console.count('Not found material icon');
return;
}
if (JSON.stringify(item.tags) !== JSON.stringify(findItem.tags)) {
findItem.tags = item.tags;
console.log('Difference tags in', item.name);
console.count('Difference tags in material icon');
}
});
}
async function getMDIMetadataAndUpdated(iconsBundle){
const mdiBundle = JSON.parse(await fs.promises.readFile(mdiMetadata));
iconsBundle
.filter(item => item.name.startsWith('mdi:'))
.forEach(item => {
const iconName = item.name.substring(item.name.indexOf(":") + 1);
const findItem = mdiBundle.find((el) => el.name === iconName);
if (!findItem) {
console.error('Delete icon:', item.name);
}
});
mdiBundle.forEach((item, index) => {
const iconName = `mdi:${item.name}`
let iconTags = item.tags;
const iconAliases = item.aliases.map(item => item.replaceAll('-', ' '));
if (!iconTags.length && item.aliases.length) {
iconTags = iconAliases;
} else if (item.aliases.length) {
iconTags = iconTags.concat(iconAliases);
}
iconTags = iconTags.map(item => item.toLowerCase());
const findItem = iconsBundle.find((el) => el.name === iconName);
if (!findItem) {
let prevIndexIcon;
if (index === 0) {
prevIndexIcon = iconsBundle.findIndex(item => item.name.startsWith('mdi:'))
} else {
const prevIconName = `mdi:${mdiBundle[index - 1].name}`;
prevIndexIcon = findPreviousIcon(iconsBundle, prevIconName);
}
if (prevIndexIcon >= 0) {
iconsBundle.splice(prevIndexIcon + 1, 0, {name:iconName, tags:iconTags});
}
console.log('Not found icon:', iconName);
console.count('Not found mdi icon');
return;
}
if (JSON.stringify(iconTags) !== JSON.stringify(findItem.tags)) {
findItem.tags = iconTags;
console.log('Difference tags in', iconName);
console.count('Difference tags in mdi icon');
}
});
}
function findPreviousIcon(iconsBundle, findName) {
return iconsBundle.findIndex(item => item.name === findName);
}
init();

View File

@ -7,6 +7,7 @@
"build": "ng build",
"build:prod": "node --max_old_space_size=4096 ./node_modules/@angular/cli/bin/ng build --configuration production --vendor-chunk",
"build:types": "node generate-types.js",
"build:icon-metadata": "node generate-icon-metadata.js",
"test": "ng test",
"lint": "ng lint",
"e2e": "ng e2e",

View File

@ -15,15 +15,17 @@
///
import { GatewayConnector, GatewayVersion } from '@home/components/widget/lib/gateway/gateway-widget.models';
import { isNumber, isString } from '@core/utils';
import {
GatewayConnectorVersionMappingUtil
} from '@home/components/widget/lib/gateway/utils/gateway-connector-version-mapping.util';
export abstract class GatewayConnectorVersionProcessor<BasicConfig> {
gatewayVersion: number;
configVersion: number;
protected constructor(protected gatewayVersionIn: string | number, protected connector: GatewayConnector<BasicConfig>) {
this.gatewayVersion = this.parseVersion(this.gatewayVersionIn);
this.configVersion = this.parseVersion(this.connector.configVersion);
this.gatewayVersion = GatewayConnectorVersionMappingUtil.parseVersion(this.gatewayVersionIn);
this.configVersion = GatewayConnectorVersionMappingUtil.parseVersion(this.connector.configVersion);
}
getProcessedByVersion(): GatewayConnector<BasicConfig> {
@ -53,19 +55,13 @@ export abstract class GatewayConnectorVersionProcessor<BasicConfig> {
}
private isVersionUpgradeNeeded(): boolean {
return this.gatewayVersionIn === GatewayVersion.Current && (!this.configVersion || this.configVersion < this.gatewayVersion);
return this.gatewayVersion >= GatewayConnectorVersionMappingUtil.parseVersion(GatewayVersion.Current)
&& (!this.configVersion || this.configVersion < this.gatewayVersion);
}
private isVersionDowngradeNeeded(): boolean {
return this.configVersion && this.connector.configVersion === GatewayVersion.Current && (this.configVersion > this.gatewayVersion);
}
private parseVersion(version: string | number): number {
if (isNumber(version)) {
return version as number;
}
return isString(version) ? parseFloat((version as string).replace(/\./g, '').slice(0, 3)) / 100 : 0;
return this.configVersion && this.configVersion >= GatewayConnectorVersionMappingUtil.parseVersion(GatewayVersion.Current)
&& (this.configVersion > this.gatewayVersion);
}
protected abstract getDowngradedVersion(): GatewayConnector<BasicConfig>;

View File

@ -175,7 +175,7 @@
<div class="tb-form-row" fxLayoutAlign="space-between center">
<mat-slide-toggle class="mat-slide" formControlName="sendDataToThingsBoard">
<mat-label>
{{ 'gateway.send-data-TB' | translate }}
{{ 'gateway.send-data-to-platform' | translate }}
</mat-label>
</mat-slide-toggle>
</div>

View File

@ -126,6 +126,7 @@ export class SecurityConfigComponent implements ControlValueAccessor, OnInit, On
if (!securityInfo.type) {
securityInfo.type = SecurityType.ANONYMOUS;
}
this.updateValidators(securityInfo.type);
this.securityFormGroup.reset(securityInfo, {emitEvent: false});
}
this.cdr.markForCheck();

View File

@ -37,6 +37,7 @@ import { Observable, Subject } from 'rxjs';
import { ResourcesService } from '@core/services/resources.service';
import { takeUntil, tap } from 'rxjs/operators';
import { helpBaseUrl } from '@shared/models/constants';
import { LatestVersionConfigPipe } from '@home/components/widget/lib/gateway/pipes/latest-version-config.pipe';
@Component({
selector: 'tb-add-connector-dialog',
@ -63,6 +64,7 @@ export class AddConnectorDialogComponent
@Inject(MAT_DIALOG_DATA) public data: AddConnectorConfigData,
public dialogRef: MatDialogRef<AddConnectorDialogComponent, CreatedConnectorConfigData>,
private fb: FormBuilder,
private isLatestVersionConfig: LatestVersionConfigPipe,
private resourcesService: ResourcesService) {
super(store, router, dialogRef);
this.connectorForm = this.fb.group({
@ -103,9 +105,9 @@ export class AddConnectorDialogComponent
if (gatewayVersion) {
value.configVersion = gatewayVersion;
}
value.configurationJson = (gatewayVersion === GatewayVersion.Current
? defaultConfig[this.data.gatewayVersion]
: defaultConfig.legacy)
value.configurationJson = (this.isLatestVersionConfig.transform(gatewayVersion)
? defaultConfig[GatewayVersion.Current]
: defaultConfig[GatewayVersion.Legacy])
?? defaultConfig;
if (this.connectorForm.valid) {
this.dialogRef.close(value);

View File

@ -178,7 +178,7 @@
<ng-container [ngSwitch]="initialConnector.type">
<ng-container *ngSwitchCase="ConnectorType.MQTT">
<tb-mqtt-basic-config
*ngIf="connectorForm.get('configVersion').value === GatewayVersion.Current else legacy"
*ngIf="connectorForm.get('configVersion').value | isLatestVersionConfig else legacy"
formControlName="basicConfig"
[generalTabContent]="generalTabContent"
(initialized)="basicConfigInitSubject.next()"
@ -193,7 +193,7 @@
</ng-container>
<ng-container *ngSwitchCase="ConnectorType.OPCUA">
<tb-opc-ua-basic-config
*ngIf="connectorForm.get('configVersion').value === GatewayVersion.Current else legacy"
*ngIf="connectorForm.get('configVersion').value | isLatestVersionConfig else legacy"
formControlName="basicConfig"
[generalTabContent]="generalTabContent"
(initialized)="basicConfigInitSubject.next()"
@ -208,7 +208,7 @@
</ng-container>
<ng-container *ngSwitchCase="ConnectorType.MODBUS">
<tb-modbus-basic-config
*ngIf="connectorForm.get('configVersion').value === GatewayVersion.Current else legacy"
*ngIf="connectorForm.get('configVersion').value | isLatestVersionConfig else legacy"
formControlName="basicConfig"
[generalTabContent]="generalTabContent"
(initialized)="basicConfigInitSubject.next()"
@ -316,7 +316,7 @@
</div>
<tb-report-strategy
[defaultValue]="ReportStrategyDefaultValue.Connector"
*ngIf="connectorForm.get('type').value === ConnectorType.MODBUS && connectorForm.get('configVersion').value === GatewayVersion.Current"
*ngIf="connectorForm.get('type').value === ConnectorType.MODBUS && (connectorForm.get('configVersion').value | isLatestVersionConfig)"
formControlName="reportStrategy"
/>
</section>

View File

@ -59,7 +59,6 @@ import {
GatewayConnectorDefaultTypesTranslatesMap,
GatewayLogLevel,
noLeadTrailSpacesRegex,
GatewayVersion,
ReportStrategyDefaultValue,
ReportStrategyType,
} from './gateway-widget.models';
@ -71,6 +70,7 @@ import { PageData } from '@shared/models/page/page-data';
import {
GatewayConnectorVersionMappingUtil
} from '@home/components/widget/lib/gateway/utils/gateway-connector-version-mapping.util';
import { LatestVersionConfigPipe } from '@home/components/widget/lib/gateway/pipes/latest-version-config.pipe';
export class ForceErrorStateMatcher implements ErrorStateMatcher {
isErrorState(control: FormControl | null): boolean {
@ -104,7 +104,6 @@ export class GatewayConnectorComponent extends PageComponent implements AfterVie
readonly displayedColumns = ['enabled', 'key', 'type', 'syncStatus', 'errors', 'actions'];
readonly GatewayConnectorTypesTranslatesMap = GatewayConnectorDefaultTypesTranslatesMap;
readonly ConnectorConfigurationModes = ConfigurationModes;
readonly GatewayVersion = GatewayVersion;
readonly ReportStrategyDefaultValue = ReportStrategyDefaultValue;
pageLink: PageLink;
@ -149,6 +148,7 @@ export class GatewayConnectorComponent extends PageComponent implements AfterVie
private telemetryWsService: TelemetryWebsocketService,
private zone: NgZone,
private utils: UtilsService,
private isLatestVersionConfig: LatestVersionConfigPipe,
private cd: ChangeDetectorRef) {
super(store);
@ -255,7 +255,7 @@ export class GatewayConnectorComponent extends PageComponent implements AfterVie
delete value.class;
}
if (value.type === ConnectorType.MODBUS && value.configVersion === GatewayVersion.Current) {
if (value.type === ConnectorType.MODBUS && this.isLatestVersionConfig.transform(value.configVersion)) {
if (!value.reportStrategy) {
value.reportStrategy = {
type: ReportStrategyType.OnReportPeriod,
@ -508,6 +508,9 @@ export class GatewayConnectorComponent extends PageComponent implements AfterVie
if (!connector.configurationJson) {
connector.configurationJson = {} as ConnectorBaseConfig;
}
if (this.gatewayVersion && !connector.configVersion) {
connector.configVersion = this.gatewayVersion;
}
connector.basicConfig = connector.configurationJson;
this.initialConnector = connector;
@ -517,7 +520,7 @@ export class GatewayConnectorComponent extends PageComponent implements AfterVie
this.saveConnector(this.getUpdatedConnectorData(connector));
if (!previousType || previousType === connector.type || !this.allowBasicConfig.has(connector.type)) {
if (previousType === connector.type || !this.allowBasicConfig.has(connector.type)) {
this.patchBasicConfigConnector(connector);
} else {
this.basicConfigInitSubject.pipe(take(1)).subscribe(() => {
@ -527,24 +530,26 @@ export class GatewayConnectorComponent extends PageComponent implements AfterVie
}
private setInitialConnectorValues(connector: GatewayConnector): void {
const {basicConfig, mode, ...initialConnector} = connector;
this.toggleReportStrategy(connector.type);
this.connectorForm.get('mode').setValue(this.allowBasicConfig.has(connector.type)
? connector.mode ?? ConfigurationModes.BASIC
: null, {emitEvent: false}
);
this.connectorForm.get('configVersion').setValue(connector.configVersion, {emitEvent: false});
this.connectorForm.get('type').setValue(connector.type, {emitEvent: false});
this.connectorForm.patchValue(initialConnector, {emitEvent: false});
}
private openAddConnectorDialog(): Observable<GatewayConnector> {
return this.dialog.open<AddConnectorDialogComponent, AddConnectorConfigData>(AddConnectorDialogComponent, {
disableClose: true,
panelClass: ['tb-dialog', 'tb-fullscreen-dialog'],
data: {
dataSourceData: this.dataSource.data,
gatewayVersion: this.gatewayVersion,
}
}).afterClosed();
return this.ctx.ngZone.run(() =>
this.dialog.open<AddConnectorDialogComponent, AddConnectorConfigData>(AddConnectorDialogComponent, {
disableClose: true,
panelClass: ['tb-dialog', 'tb-fullscreen-dialog'],
data: {
dataSourceData: this.dataSource.data,
gatewayVersion: this.gatewayVersion,
}
}).afterClosed()
);
}
uniqNameRequired(): ValidatorFn {
@ -650,13 +655,8 @@ export class GatewayConnectorComponent extends PageComponent implements AfterVie
private observeModeChange(): void {
this.connectorForm.get('mode').valueChanges
.pipe(takeUntil(this.destroy$))
.subscribe((mode) => {
.subscribe(() => {
this.connectorForm.get('mode').markAsPristine();
if (mode === ConfigurationModes.BASIC) {
this.basicConfigInitSubject.pipe(take(1)).subscribe(() => {
this.patchBasicConfigConnector({...this.initialConnector, mode: ConfigurationModes.BASIC});
});
}
});
}
@ -827,12 +827,17 @@ export class GatewayConnectorComponent extends PageComponent implements AfterVie
...connector,
}, this.gatewayVersion);
if (this.gatewayVersion && !connectorState.configVersion) {
connectorState.configVersion = this.gatewayVersion;
}
connectorState.basicConfig = connectorState.configurationJson;
this.initialConnector = connectorState;
this.updateConnector(connectorState);
}
private updateConnector(connector: GatewayConnector): void {
this.jsonConfigSub?.unsubscribe();
switch (connector.type) {
case ConnectorType.MQTT:
case ConnectorType.OPCUA:
@ -842,18 +847,21 @@ export class GatewayConnectorComponent extends PageComponent implements AfterVie
default:
this.connectorForm.patchValue({...connector, mode: null});
this.connectorForm.markAsPristine();
this.createJsonConfigWatcher();
}
this.createJsonConfigWatcher();
}
private updateBasicConfigConnector(connector: GatewayConnector): void {
this.basicConfigSub?.unsubscribe();
const previousType = this.connectorForm.get('type').value;
this.setInitialConnectorValues(connector);
if ((!connector.mode || connector.mode === ConfigurationModes.BASIC) && this.connectorForm.get('type').value !== connector.type) {
if (previousType === connector.type || !this.allowBasicConfig.has(connector.type)) {
this.patchBasicConfigConnector(connector);
} else {
this.basicConfigInitSubject.asObservable().pipe(take(1)).subscribe(() => {
this.patchBasicConfigConnector(connector);
});
} else {
this.patchBasicConfigConnector(connector);
}
}
@ -861,6 +869,7 @@ export class GatewayConnectorComponent extends PageComponent implements AfterVie
this.connectorForm.patchValue(connector, {emitEvent: false});
this.connectorForm.markAsPristine();
this.createBasicConfigWatcher();
this.createJsonConfigWatcher();
}
private toggleReportStrategy(type: ConnectorType): void {

View File

@ -33,6 +33,8 @@ export class GatewayHelpLinkPipe implements PipeTransform {
} else {
return;
}
} else if (field === 'attributes' || field === 'timeseries') {
return 'widget/lib/gateway/attributes_timeseries_expressions_fn';
}
return 'widget/lib/gateway/expressions_fn';
}

View File

@ -0,0 +1,32 @@
///
/// 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 { Pipe, PipeTransform } from '@angular/core';
import { GatewayVersion } from '@home/components/widget/lib/gateway/gateway-widget.models';
import {
GatewayConnectorVersionMappingUtil
} from '@home/components/widget/lib/gateway/utils/gateway-connector-version-mapping.util';
@Pipe({
name: 'isLatestVersionConfig',
standalone: true,
})
export class LatestVersionConfigPipe implements PipeTransform {
transform(configVersion: number | string): boolean {
return GatewayConnectorVersionMappingUtil.parseVersion(configVersion)
>= GatewayConnectorVersionMappingUtil.parseVersion(GatewayVersion.Current);
}
}

View File

@ -24,6 +24,7 @@ import {
import { MqttVersionProcessor } from '@home/components/widget/lib/gateway/abstract/mqtt-version-processor.abstract';
import { OpcVersionProcessor } from '@home/components/widget/lib/gateway/abstract/opc-version-processor.abstract';
import { ModbusVersionProcessor } from '@home/components/widget/lib/gateway/abstract/modbus-version-processor.abstract';
import { isNumber, isString } from '@core/utils';
export abstract class GatewayConnectorVersionMappingUtil {
@ -39,4 +40,12 @@ export abstract class GatewayConnectorVersionMappingUtil {
return connector;
}
}
static parseVersion(version: string | number): number {
if (isNumber(version)) {
return version as number;
}
return isString(version) ? parseFloat((version as string).replace(/\./g, '').slice(0, 3)) / 100 : 0;
}
}

View File

@ -56,7 +56,7 @@ export class OpcVersionMappingUtil {
static mapMappingToUpgradedVersion(mapping: LegacyDeviceConnectorMapping[]): DeviceConnectorMapping[] {
return mapping.map((legacyMapping: LegacyDeviceConnectorMapping) => ({
...legacyMapping,
deviceNodeSource: this.getTypeSourceByValue(legacyMapping.deviceNodePattern),
deviceNodeSource: this.getDeviceNodeSourceByValue(legacyMapping.deviceNodePattern),
deviceInfo: {
deviceNameExpression: legacyMapping.deviceNamePattern,
deviceNameExpressionSource: this.getTypeSourceByValue(legacyMapping.deviceNamePattern),
@ -122,6 +122,14 @@ export class OpcVersionMappingUtil {
return OPCUaSourceType.CONST;
}
private static getDeviceNodeSourceByValue(value: string): OPCUaSourceType {
if (value.includes('${')) {
return OPCUaSourceType.IDENTIFIER;
} else {
return OPCUaSourceType.PATH;
}
}
private static getArgumentType(arg: unknown): string {
switch (typeof arg) {
case 'boolean':

View File

@ -1168,6 +1168,22 @@ class CssScadaSymbolAnimation implements ScadaSymbolAnimation {
private element: Element,
duration = 1000) {
this._duration = duration;
this.fixPatternAnimationForChromeBelow128();
}
private fixPatternAnimationForChromeBelow128(): void {
try {
const userAgent = window.navigator.userAgent;
if (+(/Chrome\/(\d+)/i.exec(userAgent)[1]) <= 127) {
if (this.svgShape.defs().findOne('pattern') && !this.svgShape.defs().findOne('pattern.empty-animation')) {
this.svgShape.defs().add(SVG('<pattern class="empty-animation"></pattern>'));
this.svgShape.style()
.rule('.' + 'empty-animation',
{'animation-name': 'empty-animation', 'animation-duration': '1000ms', 'animation-iteration-count': 'infinite'})
.addText('@keyframes empty-animation {0% {<!--opacity:1;-->}100% {<!--opacity:1;-->}}');
}
}
} catch (e) {}
}
public running(): boolean {

View File

@ -167,9 +167,10 @@ import {
ModbusRpcParametersComponent
} from '@home/components/widget/lib/gateway/connectors-configuration/rpc-parameters/modbus-rpc-parameters/modbus-rpc-parameters.component';
import { RpcTemplateArrayViewPipe } from '@home/components/widget/lib/gateway/pipes/rpc-template-array-view.pipe';
import {
import {
ReportStrategyComponent
} from '@home/components/widget/lib/gateway/connectors-configuration/report-strategy/report-strategy.component';
import { LatestVersionConfigPipe } from '@home/components/widget/lib/gateway/pipes/latest-version-config.pipe';
@NgModule({
declarations: [
@ -268,6 +269,7 @@ import {
ModbusRpcParametersComponent,
RpcTemplateArrayViewPipe,
ReportStrategyComponent,
LatestVersionConfigPipe,
],
exports: [
EntitiesTableWidgetComponent,
@ -337,7 +339,8 @@ import {
ScadaSymbolWidgetComponent
],
providers: [
{provide: WIDGET_COMPONENTS_MODULE_TOKEN, useValue: WidgetComponentsModule}
{provide: WIDGET_COMPONENTS_MODULE_TOKEN, useValue: WidgetComponentsModule},
{provide: LatestVersionConfigPipe}
]
})
export class WidgetComponentsModule {

View File

@ -162,28 +162,12 @@
</mat-slide-toggle>
</div>
</section>
<div class="tb-form-row tb-standard-fields no-border no-padding column-xs">
<mat-form-field class="flex">
<mat-label translate>admin.oauth2.scope</mat-label>
<mat-chip-grid #scopeList required [disabled]="!this.createNewDialog && !(this.isEdit || this.isAdd)">
<mat-chip-row *ngFor="let scope of entityForm.get('scope').value; let k = index; trackBy: trackByParams"
removable (removed)="removeScope(k, entityForm)">
{{scope}}
<mat-icon matChipRemove>cancel</mat-icon>
</mat-chip-row>
<input [matChipInputFor]="scopeList"
[matChipInputSeparatorKeyCodes]="separatorKeysCodes"
matChipInputAddOnBlur
(matChipInputTokenEnd)="addScope($event, entityForm)">
</mat-chip-grid>
<mat-error *ngIf="entityForm.get('scope').hasError('required')">
{{ 'admin.oauth2.scope-required' | translate }}
</mat-error>
</mat-form-field>
</div>
<tb-error style="display: block; margin-top: -24px;"
[error]="entityForm.get('scope').hasError('required') ? ('admin.oauth2.scope-required' | translate) : ''">
</tb-error>
<tb-string-items-list
formControlName="scope"
label="{{ 'admin.oauth2.scope' | translate }}"
requiredText="{{ 'admin.oauth2.scope-required' | translate }}"
required>
</tb-string-items-list>
</section>
<section class="tb-form-panel no-border no-padding no-gap" *ngIf="!generalSettingsMode">
<div class="tb-form-row tb-standard-fields no-border no-padding">

View File

@ -33,18 +33,10 @@ import { AppState } from '@core/core.state';
import { EntityTableConfig } from '@home/models/entity/entities-table-config.models';
import { TranslateService } from '@ngx-translate/core';
import { Store } from '@ngrx/store';
import {
AbstractControl,
UntypedFormArray,
UntypedFormBuilder,
UntypedFormGroup,
ValidationErrors,
Validators
} from '@angular/forms';
import { AbstractControl, UntypedFormBuilder, UntypedFormGroup, Validators } from '@angular/forms';
import { isDefinedAndNotNull } from '@core/utils';
import { OAuth2Service } from '@core/http/oauth2.service';
import { Subscription } from 'rxjs';
import { MatChipInputEvent } from '@angular/material/chips';
import { COMMA, ENTER } from '@angular/cdk/keycodes';
import { PageLink } from '@shared/models/page/page-link';
import { coerceBoolean } from '@app/shared/decorators/coercion';
@ -84,16 +76,6 @@ export class ClientComponent extends EntityComponent<OAuth2Client, PageLink, OAu
private subscriptions: Array<Subscription> = [];
public static validateScope(control: AbstractControl): ValidationErrors | null {
const scope: string[] = control.value;
if (!scope || !scope.length) {
return {
required: true
};
}
return null;
}
readonly separatorKeysCodes: number[] = [ENTER, COMMA];
clientAuthenticationMethods = Object.keys(ClientAuthenticationMethod);
@ -146,7 +128,7 @@ export class ClientComponent extends EntityComponent<OAuth2Client, PageLink, OAu
loginButtonLabel: [entity?.loginButtonLabel ? entity.loginButtonLabel : null, Validators.required],
loginButtonIcon: [entity?.loginButtonIcon ? entity.loginButtonIcon : null],
userNameAttributeName: [entity?.userNameAttributeName ? entity.userNameAttributeName : 'email', Validators.required],
scope: this.fb.array(entity?.scope ? entity.scope : [], ClientComponent.validateScope),
scope: [entity?.scope ? entity.scope : [], Validators.required],
mapperConfig: this.fb.group({
allowUserCreation: [isDefinedAndNotNull(entity?.mapperConfig?.allowUserCreation) ?
entity.mapperConfig.allowUserCreation : true],
@ -166,6 +148,7 @@ export class ClientComponent extends EntityComponent<OAuth2Client, PageLink, OAu
clientId: entity.clientId,
clientSecret: entity.clientSecret,
accessTokenUri: entity.accessTokenUri,
scope: entity.scope,
authorizationUri: entity.authorizationUri,
jwkSetUri: entity.jwkSetUri,
userInfoUri: entity.userInfoUri,
@ -181,19 +164,6 @@ export class ClientComponent extends EntityComponent<OAuth2Client, PageLink, OAu
}, {emitEvent: false});
this.changeMapperConfigType(this.entityForm, this.entityValue.mapperConfig.type, this.entityValue.mapperConfig);
const scopeControls = this.entityForm.get('scope') as UntypedFormArray;
if (entity.scope.length === scopeControls.length) {
scopeControls.patchValue(entity.scope, {emitEvent: false});
} else {
const newScopeControls: Array<AbstractControl> = [];
if (entity.scope) {
for (const scope of entity.scope) {
newScopeControls.push(this.fb.control(scope, [Validators.required]));
}
}
this.entityForm.setControl('scope', this.fb.array(newScopeControls));
}
}
getProviderName(): string {
@ -204,35 +174,6 @@ export class ClientComponent extends EntityComponent<OAuth2Client, PageLink, OAu
return this.getProviderName() === 'Custom';
}
trackByParams(index: number): number {
return index;
}
trackByItem(i, item) {
return item;
}
addScope(event: MatChipInputEvent, control: AbstractControl): void {
const input = event.chipInput.inputElement;
const value = event.value;
const controller = control.get('scope') as UntypedFormArray;
if ((value.trim() !== '')) {
controller.push(this.fb.control(value.trim()));
controller.markAsDirty();
}
if (input) {
input.value = '';
}
}
removeScope(i: number, control: AbstractControl): void {
const controller = control.get('scope') as UntypedFormArray;
controller.removeAt(i);
controller.markAsTouched();
controller.markAsDirty();
}
private initTemplates(templates: OAuth2ClientRegistrationTemplate[]): void {
templates.map(provider => {
delete provider.additionalInfo;
@ -264,7 +205,7 @@ export class ClientComponent extends EntityComponent<OAuth2Client, PageLink, OAu
}));
this.subscriptions.push(this.entityForm.get('additionalInfo.providerName').valueChanges.subscribe((provider) => {
(this.entityForm.get('scope') as UntypedFormArray).clear();
this.entityForm.get('scope').setValue([]);
this.setProviderDefaultValue(provider, this.entityForm);
}));
}
@ -355,9 +296,6 @@ export class ClientComponent extends EntityComponent<OAuth2Client, PageLink, OAu
const template = this.templates.get(provider);
template.clientId = '';
template.clientSecret = '';
template.scope.forEach(() => {
(clientRegistration.get('scope') as UntypedFormArray).push(this.fb.control(''));
});
clientRegistration.patchValue(template);
}
}

View File

@ -53,7 +53,16 @@ export const svgIcons: {[key: string]: string} = {
'<path fill="#fff" d="M9 4V2H4a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h5v-2H4V4h5z"/>' +
'<path fill="#fff" d="M7 18V6h2v12H7zM11 6v12h2V6h-2zM15 20v2h5a2 2 0 0 0 2-2V4a2 2 0 0 0-2-2h-5v2h5v16h-5z"/>' +
'<path fill="#fff" d="M15 18V6h2v12h-2z"/>' +
'</svg>'
'</svg>',
trendz: '<svg viewBox="0 0 24 24"><path fill-rule="evenodd" clip-rule="evenodd" d="m 17.329936,0 1.999613,2.003952 -2.674056,' +
'2.679916 2.661746,2.6765649 2.678508,-2.684351 1.999613,2.0039449 -2.682055,2.6878227 2.661607,2.6763665 -2.641298,2.656033 ' +
'2.661746,2.66751 -1.999613,2.004017 -2.658338,-2.664181 -2.661607,2.676508 2.653887,2.659575 -1.999614,2.003804 L 14.679666,' +
'21.39152 11.997681,24.088432 9.3156944,21.39152 6.6653477,24.047482 4.6657413,22.043678 7.3194891,19.384103 4.6578333,16.707382 ' +
'1.9996133,19.371421 0,17.367405 2.6616488,14.700107 0.02053398,12.044216 2.6820275,9.3678495 1.4263538e-5,6.6800126 1.9996273,' +
'4.6760606 4.678212,7.3604329 7.3397982,4.6839955 4.6657413,2.0041717 6.6653477,2.2309572e-4 9.3360035,2.6766286 11.997681,' +
'0 14.659287,2.6765011 Z m -5.332255,4.0079963 1.999613,2.003945 -7.99844,8.0158157 -1.9996133,-2.004017 z m 1.676684,4.3522483 ' +
'1.999613,2.0039454 -6.6654242,6.679793 -1.9996133,-2.003874 z m 2.988987,7.0033574 -1.999544,-2.003945 -4.6658108,4.675848 ' +
'1.9996128,2.004015 z"/></svg>'
};
export const svgIconsUrl: { [key: string]: string } = {

View File

@ -0,0 +1,65 @@
### Expressions
#### JSON Path:
The expression field is used to extract data from the MQTT message. There are various available options for different parts of the messages:
- The JSONPath format can be used to extract data from the message body.
- The regular expression format can be used to extract data from the topic where the message will arrive.
- Slices can only be used in the expression fields of bytes converters.
JSONPath expressions specify the items within a JSON structure (which could be an object, array, or nested combination of both) that you want to access. These expressions can select elements from JSON data on specific criteria. Here's a basic overview of how JSONPath expressions are structured:
- `$`: The root element of the JSON document;
- `.`: Child operator used to select child elements. For example, $.store.book ;
- `[]`: Child operator used to select child elements. $['store']['book'] accesses the book array within a store object;
##### Examples:
For example, if we want to extract the device name from the following message, we can use the expression below:
MQTT message:
```
{
"sensorModelInfo": {
"sensorName": "AM-123",
"sensorType": "myDeviceType"
},
"data": {
"temp": 12.2,
"hum": 56,
"status": "ok"
}
}
{:copy-code}
```
Expression:
`${sensorModelInfo.sensorName}`
Converted data:
`AM-123`
If we want to extract all data from the message above, we can use the following expression:
`${data}`
Converted data:
`{"temp": 12.2, "hum": 56, "status": "ok"}`
Or if we want to extract specific data (for example “temperature”), you can use the following expression:
`${data.temp}`
And as a converted data we will get:
`12.2`
<br/>

View File

@ -3277,7 +3277,7 @@
},
"select-connector": "Select connector to display config",
"send-change-data": "Send data only on change",
"send-data-TB": "Send data to ThingsBoard",
"send-data-to-platform": "Send data to platform",
"send-data-on-change": "Send data only on change",
"send-change-data-hint": "The values will be saved to the database only if they are different from the corresponding values in the previous converted message. This functionality applies to both attributes and time series in the converter output.",
"server": "Server",

View File

@ -20,13 +20,21 @@
"connectAttemptTimeMs": 5000,
"connectAttemptCount": 5,
"waitAfterFailedAttemptsMs": 300000,
"reportStrategy": {
"type": "ON_REPORT_PERIOD",
"reportPeriod": 30000
},
"attributes": [
{
"tag": "string_read",
"type": "string",
"functionCode": 4,
"objectsCount": 4,
"address": 1
"address": 1,
"reportStrategy": {
"type": "ON_REPORT_PERIOD",
"reportPeriod": 15000
}
},
{
"tag": "bits_read",
@ -86,7 +94,11 @@
"type": "8uint",
"functionCode": 4,
"objectsCount": 1,
"address": 17
"address": 17,
"reportStrategy": {
"type": "ON_REPORT_PERIOD",
"reportPeriod": 15000
}
},
{
"tag": "16uint_read",

File diff suppressed because one or more lines are too long

View File

@ -5434,7 +5434,7 @@ ecc-jsbn@~0.1.1:
"echarts@https://github.com/thingsboard/echarts/archive/5.5.0-TB.tar.gz":
version "5.5.0-TB"
resolved "https://github.com/thingsboard/echarts/archive/5.5.0-TB.tar.gz#d1017728576b3fd65532eb17e503d1349f7feaed"
resolved "https://github.com/thingsboard/echarts/archive/5.5.0-TB.tar.gz#0b707b5cd2ae4699e9ced8b07ca49cb70189ae2a"
dependencies:
tslib "2.3.0"
zrender "5.5.0"