diff --git a/application/pom.xml b/application/pom.xml
index af5d26070d..8e5e6b2564 100644
--- a/application/pom.xml
+++ b/application/pom.xml
@@ -20,7 +20,7 @@
4.0.0
org.thingsboard
- 4.2.2.1-SNAPSHOT
+ 4.2.2.2-SNAPSHOT
thingsboard
application
diff --git a/application/src/main/data/upgrade/lts/schema_update.sql b/application/src/main/data/upgrade/lts/schema_update.sql
new file mode 100644
index 0000000000..3140b4f253
--- /dev/null
+++ b/application/src/main/data/upgrade/lts/schema_update.sql
@@ -0,0 +1,19 @@
+--
+-- Copyright © 2016-2026 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.
+--
+
+-- LTS cumulative schema update file.
+-- All statements must be idempotent (use IF NOT EXISTS, ADD COLUMN IF NOT EXISTS, DO $$ ... END $$ guards, etc.).
+-- This file is executed by SystemPatchApplier on every version increase within the LTS family.
diff --git a/application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java b/application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java
index a9692f4b9d..925b60de76 100644
--- a/application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java
+++ b/application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java
@@ -35,6 +35,7 @@ import org.thingsboard.rule.engine.api.DeviceStateManager;
import org.thingsboard.rule.engine.api.JobManager;
import org.thingsboard.rule.engine.api.MailService;
import org.thingsboard.rule.engine.api.MqttClientSettings;
+import org.thingsboard.rule.engine.api.TbHttpClientSettings;
import org.thingsboard.rule.engine.api.NotificationCenter;
import org.thingsboard.rule.engine.api.RuleEngineAiChatModelService;
import org.thingsboard.rule.engine.api.SmsService;
@@ -684,6 +685,10 @@ public class ActorSystemContext {
@Getter
private MqttClientSettings mqttClientSettings;
+ @Autowired(required = false)
+ @Getter
+ private TbHttpClientSettings tbHttpClientSettings;
+
@Getter
@Setter
private TbActorSystem actorSystem;
diff --git a/application/src/main/java/org/thingsboard/server/actors/ruleChain/DefaultTbContext.java b/application/src/main/java/org/thingsboard/server/actors/ruleChain/DefaultTbContext.java
index 66a15a651e..3a572503d6 100644
--- a/application/src/main/java/org/thingsboard/server/actors/ruleChain/DefaultTbContext.java
+++ b/application/src/main/java/org/thingsboard/server/actors/ruleChain/DefaultTbContext.java
@@ -27,6 +27,7 @@ import org.thingsboard.rule.engine.api.DeviceStateManager;
import org.thingsboard.rule.engine.api.JobManager;
import org.thingsboard.rule.engine.api.MailService;
import org.thingsboard.rule.engine.api.MqttClientSettings;
+import org.thingsboard.rule.engine.api.TbHttpClientSettings;
import org.thingsboard.rule.engine.api.NotificationCenter;
import org.thingsboard.rule.engine.api.RuleEngineAiChatModelService;
import org.thingsboard.rule.engine.api.RuleEngineAlarmService;
@@ -1062,6 +1063,11 @@ public class DefaultTbContext implements TbContext {
return mainCtx.getMqttClientSettings();
}
+ @Override
+ public TbHttpClientSettings getTbHttpClientSettings() {
+ return mainCtx.getTbHttpClientSettings();
+ }
+
private TbMsgMetaData getActionMetaData(RuleNodeId ruleNodeId) {
TbMsgMetaData metaData = new TbMsgMetaData();
metaData.putValue("ruleNodeId", ruleNodeId.toString());
diff --git a/application/src/main/java/org/thingsboard/server/config/TbHttpClientSettingsComponent.java b/application/src/main/java/org/thingsboard/server/config/TbHttpClientSettingsComponent.java
new file mode 100644
index 0000000000..a5ad4e8742
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/config/TbHttpClientSettingsComponent.java
@@ -0,0 +1,51 @@
+/**
+ * Copyright © 2016-2026 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.config;
+
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.stereotype.Component;
+import org.thingsboard.rule.engine.api.TbHttpClientSettings;
+import org.thingsboard.server.queue.util.TbRuleEngineComponent;
+
+@TbRuleEngineComponent
+@Component
+public class TbHttpClientSettingsComponent implements TbHttpClientSettings {
+
+ @Value("${actors.rule.external.http_client.max_parallel_requests:0}")
+ private int maxParallelRequests;
+
+ @Value("${actors.rule.external.http_client.max_pending_requests:0}")
+ private int maxPendingRequests;
+
+ @Value("${actors.rule.external.http_client.pool_max_connections:0}")
+ private int poolMaxConnections;
+
+ @Override
+ public int getMaxParallelRequests() {
+ return maxParallelRequests;
+ }
+
+ @Override
+ public int getMaxPendingRequests() {
+ return maxPendingRequests;
+ }
+
+ @Override
+ public int getPoolMaxConnections() {
+ return poolMaxConnections;
+ }
+
+}
diff --git a/application/src/main/java/org/thingsboard/server/controller/AlarmCommentController.java b/application/src/main/java/org/thingsboard/server/controller/AlarmCommentController.java
index ae8305c070..8a113fb424 100644
--- a/application/src/main/java/org/thingsboard/server/controller/AlarmCommentController.java
+++ b/application/src/main/java/org/thingsboard/server/controller/AlarmCommentController.java
@@ -30,6 +30,7 @@ import org.springframework.web.bind.annotation.RestController;
import org.thingsboard.server.common.data.alarm.Alarm;
import org.thingsboard.server.common.data.alarm.AlarmComment;
import org.thingsboard.server.common.data.alarm.AlarmCommentInfo;
+import org.thingsboard.server.common.data.alarm.AlarmCommentType;
import org.thingsboard.server.common.data.exception.ThingsboardException;
import org.thingsboard.server.common.data.id.AlarmCommentId;
import org.thingsboard.server.common.data.id.AlarmId;
@@ -77,6 +78,7 @@ public class AlarmCommentController extends BaseController {
AlarmId alarmId = new AlarmId(toUUID(strAlarmId));
Alarm alarm = checkAlarmInfoId(alarmId, Operation.WRITE);
alarmComment.setAlarmId(alarmId);
+ alarmComment.setType(AlarmCommentType.OTHER);
return tbAlarmCommentService.saveAlarmComment(alarm, alarmComment, getCurrentUser());
}
diff --git a/application/src/main/java/org/thingsboard/server/controller/plugin/TbWebSocketHandler.java b/application/src/main/java/org/thingsboard/server/controller/plugin/TbWebSocketHandler.java
index 73315be73f..133584201c 100644
--- a/application/src/main/java/org/thingsboard/server/controller/plugin/TbWebSocketHandler.java
+++ b/application/src/main/java/org/thingsboard/server/controller/plugin/TbWebSocketHandler.java
@@ -115,7 +115,7 @@ public class TbWebSocketHandler extends TextWebSocketHandler implements WebSocke
private final ConcurrentMap> tenantSessionsMap = new ConcurrentHashMap<>();
private final ConcurrentMap> customerSessionsMap = new ConcurrentHashMap<>();
private final ConcurrentMap> regularUserSessionsMap = new ConcurrentHashMap<>();
- private final ConcurrentMap> publicUserSessionsMap = new ConcurrentHashMap<>();
+ private final ConcurrentMap> publicUserSessionsMap = new ConcurrentHashMap<>();
private Cache pendingSessions;
@@ -611,7 +611,7 @@ public class TbWebSocketHandler extends TextWebSocketHandler implements WebSocke
}
if (tenantProfileConfiguration.getMaxWsSessionsPerPublicUser() > 0
&& UserPrincipal.Type.PUBLIC_ID.equals(sessionRef.getSecurityCtx().getUserPrincipal().getType())) {
- Set publicUserSessions = publicUserSessionsMap.computeIfAbsent(sessionRef.getSecurityCtx().getId(), id -> ConcurrentHashMap.newKeySet());
+ Set publicUserSessions = publicUserSessionsMap.computeIfAbsent(sessionRef.getSecurityCtx().getTenantId(), id -> ConcurrentHashMap.newKeySet());
synchronized (publicUserSessions) {
limitAllowed = publicUserSessions.size() < tenantProfileConfiguration.getMaxWsSessionsPerPublicUser();
if (limitAllowed) {
@@ -655,7 +655,7 @@ public class TbWebSocketHandler extends TextWebSocketHandler implements WebSocke
}
}
if (tenantProfileConfiguration.getMaxWsSessionsPerPublicUser() > 0 && UserPrincipal.Type.PUBLIC_ID.equals(sessionRef.getSecurityCtx().getUserPrincipal().getType())) {
- Set publicUserSessions = publicUserSessionsMap.computeIfAbsent(sessionRef.getSecurityCtx().getId(), id -> ConcurrentHashMap.newKeySet());
+ Set publicUserSessions = publicUserSessionsMap.computeIfAbsent(sessionRef.getSecurityCtx().getTenantId(), id -> ConcurrentHashMap.newKeySet());
synchronized (publicUserSessions) {
publicUserSessions.remove(sessionId);
}
diff --git a/application/src/main/java/org/thingsboard/server/service/ai/Langchain4jChatModelConfigurerImpl.java b/application/src/main/java/org/thingsboard/server/service/ai/Langchain4jChatModelConfigurerImpl.java
index 28d696468c..103617c890 100644
--- a/application/src/main/java/org/thingsboard/server/service/ai/Langchain4jChatModelConfigurerImpl.java
+++ b/application/src/main/java/org/thingsboard/server/service/ai/Langchain4jChatModelConfigurerImpl.java
@@ -177,8 +177,8 @@ class Langchain4jChatModelConfigurerImpl implements Langchain4jChatModelConfigur
if (chatModelConfig.frequencyPenalty() != null) {
generationConfigBuilder.setFrequencyPenalty(chatModelConfig.frequencyPenalty().floatValue());
}
- if (chatModelConfig.frequencyPenalty() != null) {
- generationConfigBuilder.setPresencePenalty(chatModelConfig.frequencyPenalty().floatValue());
+ if (chatModelConfig.presencePenalty() != null) {
+ generationConfigBuilder.setPresencePenalty(chatModelConfig.presencePenalty().floatValue());
}
if (chatModelConfig.maxOutputTokens() != null) {
generationConfigBuilder.setMaxOutputTokens(chatModelConfig.maxOutputTokens());
diff --git a/application/src/main/java/org/thingsboard/server/service/cf/DefaultCalculatedFieldCache.java b/application/src/main/java/org/thingsboard/server/service/cf/DefaultCalculatedFieldCache.java
index 1eba2c4549..fc54b4b0db 100644
--- a/application/src/main/java/org/thingsboard/server/service/cf/DefaultCalculatedFieldCache.java
+++ b/application/src/main/java/org/thingsboard/server/service/cf/DefaultCalculatedFieldCache.java
@@ -19,6 +19,7 @@ import lombok.Getter;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
+import org.springframework.context.event.EventListener;
import org.springframework.stereotype.Service;
import org.springframework.util.ConcurrentReferenceHashMap;
import org.thingsboard.script.api.tbel.TbelInvokeService;
@@ -29,18 +30,23 @@ import org.thingsboard.server.common.data.id.CalculatedFieldId;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.page.PageDataIterable;
+import org.thingsboard.server.common.data.plugin.ComponentLifecycleEvent;
+import org.thingsboard.server.common.msg.plugin.ComponentLifecycleMsg;
import org.thingsboard.server.dao.cf.CalculatedFieldService;
import org.thingsboard.server.dao.usagerecord.ApiLimitService;
import org.thingsboard.server.queue.util.AfterStartUp;
import org.thingsboard.server.service.cf.ctx.state.CalculatedFieldCtx;
import java.util.Collections;
+import java.util.HashSet;
import java.util.List;
+import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
+import java.util.stream.Collectors;
@Service
@Slf4j
@@ -183,6 +189,85 @@ public class DefaultCalculatedFieldCache implements CalculatedFieldCache {
log.debug("[{}] evict calculated field links from cached links by entity id: {}", calculatedFieldId, oldCalculatedField);
}
+ @EventListener(ComponentLifecycleMsg.class)
+ public void onComponentLifecycleEvent(ComponentLifecycleMsg event) {
+ switch (event.getEntityId().getEntityType()) {
+ case TENANT:
+ if (event.getEvent() == ComponentLifecycleEvent.DELETED) {
+ evictTenantCfs(event.getTenantId());
+ }
+ break;
+ case DEVICE, ASSET, DEVICE_PROFILE, ASSET_PROFILE:
+ if (event.getEvent() == ComponentLifecycleEvent.DELETED) {
+ evictEntityCfs(event.getEntityId());
+ }
+ break;
+ case CALCULATED_FIELD:
+ if (event.getEvent() == ComponentLifecycleEvent.CREATED) {
+ addCalculatedField(event.getTenantId(), (CalculatedFieldId) event.getEntityId());
+ } else if (event.getEvent() == ComponentLifecycleEvent.UPDATED) {
+ updateCalculatedField(event.getTenantId(), (CalculatedFieldId) event.getEntityId());
+ } else if (event.getEvent() == ComponentLifecycleEvent.DELETED) {
+ evict((CalculatedFieldId) event.getEntityId());
+ }
+ break;
+ }
+ }
+
+ private void evictTenantCfs(TenantId tenantId) {
+ var removedCfEntityIds = new HashSet();
+ var removedLinkEntityIds = new HashSet();
+ var toRemove = calculatedFields.entrySet().stream()
+ .filter(e -> e.getValue().getTenantId().equals(tenantId))
+ .map(Map.Entry::getKey)
+ .collect(Collectors.toSet());
+ toRemove.forEach(cfId -> {
+ CalculatedField cf = calculatedFields.remove(cfId);
+ List links = calculatedFieldLinks.remove(cfId);
+ if (links != null) {
+ links.forEach(link -> removedLinkEntityIds.add(link.getEntityId()));
+ }
+ calculatedFieldsCtx.remove(cfId);
+ if (cf != null) {
+ removedCfEntityIds.add(cf.getEntityId());
+ }
+ });
+ removedCfEntityIds.forEach(entityId -> {
+ entityIdCalculatedFields.compute(entityId, (k, cfs) -> {
+ if (cfs != null) {
+ cfs.removeIf(cf -> toRemove.contains(cf.getId()));
+ return cfs.isEmpty() ? null : cfs;
+ }
+ return null;
+ });
+ });
+ removedLinkEntityIds.forEach(entityId -> {
+ entityIdCalculatedFieldLinks.compute(entityId, ((entityId1, links) -> {
+ if (links != null) {
+ links.removeIf(link -> toRemove.contains(link.getCalculatedFieldId()));
+ return links.isEmpty() ? null : links;
+ }
+ return null;
+ }));
+ });
+ }
+
+ private void evictEntityCfs(EntityId entityId) {
+ List cfs = entityIdCalculatedFields.remove(entityId);
+ if (cfs != null) {
+ var cfIds = new HashSet();
+ cfs.forEach(cf -> {
+ calculatedFields.remove(cf.getId());
+ calculatedFieldLinks.remove(cf.getId());
+ calculatedFieldsCtx.remove(cf.getId());
+ cfIds.add(cf.getId());
+ log.debug("[{}] evict calculated field from cache on entity deletion: {}", cf.getId(), cf);
+ });
+ entityIdCalculatedFieldLinks.values().forEach(list -> list.removeIf(link -> cfIds.contains(link.getCalculatedFieldId())));
+ }
+ entityIdCalculatedFieldLinks.remove(entityId);
+ }
+
private Lock getFetchLock(CalculatedFieldId id) {
return calculatedFieldFetchLocks.computeIfAbsent(id, __ -> new ReentrantLock());
}
diff --git a/application/src/main/java/org/thingsboard/server/service/install/SqlDatabaseUpgradeService.java b/application/src/main/java/org/thingsboard/server/service/install/SqlDatabaseUpgradeService.java
index 2babc3feeb..3932aa9dcf 100644
--- a/application/src/main/java/org/thingsboard/server/service/install/SqlDatabaseUpgradeService.java
+++ b/application/src/main/java/org/thingsboard/server/service/install/SqlDatabaseUpgradeService.java
@@ -54,6 +54,7 @@ public class SqlDatabaseUpgradeService implements DatabaseEntitiesUpgradeService
public void upgradeDatabase() {
log.info("Updating schema...");
loadSql(getSchemaUpdateFile("basic"));
+ loadSql(getSchemaUpdateFile("lts"));
log.info("Schema updated.");
}
diff --git a/application/src/main/java/org/thingsboard/server/service/profile/DefaultTbAssetProfileCache.java b/application/src/main/java/org/thingsboard/server/service/profile/DefaultTbAssetProfileCache.java
index 28fa68d803..e0a9917509 100644
--- a/application/src/main/java/org/thingsboard/server/service/profile/DefaultTbAssetProfileCache.java
+++ b/application/src/main/java/org/thingsboard/server/service/profile/DefaultTbAssetProfileCache.java
@@ -16,6 +16,7 @@
package org.thingsboard.server.service.profile;
import lombok.extern.slf4j.Slf4j;
+import org.springframework.context.event.EventListener;
import org.springframework.stereotype.Service;
import org.thingsboard.server.common.data.asset.Asset;
import org.thingsboard.server.common.data.asset.AssetProfile;
@@ -23,15 +24,19 @@ import org.thingsboard.server.common.data.id.AssetId;
import org.thingsboard.server.common.data.id.AssetProfileId;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.TenantId;
+import org.thingsboard.server.common.data.plugin.ComponentLifecycleEvent;
+import org.thingsboard.server.common.msg.plugin.ComponentLifecycleMsg;
import org.thingsboard.server.dao.asset.AssetProfileService;
import org.thingsboard.server.dao.asset.AssetService;
+import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import java.util.function.BiConsumer;
import java.util.function.Consumer;
+import java.util.stream.Collectors;
@Service
@Slf4j
@@ -143,6 +148,25 @@ public class DefaultTbAssetProfileCache implements TbAssetProfileCache {
}
}
+ @EventListener(ComponentLifecycleMsg.class)
+ public void onComponentLifecycleEvent(ComponentLifecycleMsg event) {
+ switch (event.getEntityId().getEntityType()) {
+ case TENANT:
+ if (event.getEvent() == ComponentLifecycleEvent.DELETED) {
+ TenantId tenantId = event.getTenantId();
+ Set toRemove = assetProfilesMap.values().stream()
+ .filter(assetProfile -> assetProfile.getTenantId().equals(tenantId))
+ .map(AssetProfile::getId)
+ .collect(Collectors.toSet());
+ assetProfilesMap.keySet().removeAll(toRemove);
+ assetsMap.entrySet().removeIf(entry -> toRemove.contains(entry.getValue()));
+ profileListeners.remove(tenantId);
+ assetProfileListeners.remove(tenantId);
+ }
+ break;
+ }
+ }
+
private void notifyProfileListeners(AssetProfile profile) {
ConcurrentMap> tenantListeners = profileListeners.get(profile.getTenantId());
if (tenantListeners != null) {
diff --git a/application/src/main/java/org/thingsboard/server/service/profile/DefaultTbDeviceProfileCache.java b/application/src/main/java/org/thingsboard/server/service/profile/DefaultTbDeviceProfileCache.java
index 6b356adf94..4729a8c118 100644
--- a/application/src/main/java/org/thingsboard/server/service/profile/DefaultTbDeviceProfileCache.java
+++ b/application/src/main/java/org/thingsboard/server/service/profile/DefaultTbDeviceProfileCache.java
@@ -16,6 +16,7 @@
package org.thingsboard.server.service.profile;
import lombok.extern.slf4j.Slf4j;
+import org.springframework.context.event.EventListener;
import org.springframework.stereotype.Service;
import org.thingsboard.server.common.data.Device;
import org.thingsboard.server.common.data.DeviceProfile;
@@ -23,15 +24,19 @@ import org.thingsboard.server.common.data.id.DeviceId;
import org.thingsboard.server.common.data.id.DeviceProfileId;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.TenantId;
+import org.thingsboard.server.common.data.plugin.ComponentLifecycleEvent;
+import org.thingsboard.server.common.msg.plugin.ComponentLifecycleMsg;
import org.thingsboard.server.dao.device.DeviceProfileService;
import org.thingsboard.server.dao.device.DeviceService;
+import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import java.util.function.BiConsumer;
import java.util.function.Consumer;
+import java.util.stream.Collectors;
@Service
@Slf4j
@@ -143,6 +148,25 @@ public class DefaultTbDeviceProfileCache implements TbDeviceProfileCache {
}
}
+ @EventListener(ComponentLifecycleMsg.class)
+ public void onComponentLifecycleEvent(ComponentLifecycleMsg event) {
+ switch (event.getEntityId().getEntityType()) {
+ case TENANT:
+ if (event.getEvent() == ComponentLifecycleEvent.DELETED) {
+ TenantId tenantId = event.getTenantId();
+ Set toRemove = deviceProfilesMap.values().stream()
+ .filter(deviceProfile -> deviceProfile.getTenantId().equals(tenantId))
+ .map(DeviceProfile::getId)
+ .collect(Collectors.toSet());
+ deviceProfilesMap.keySet().removeAll(toRemove);
+ devicesMap.entrySet().removeIf(entry -> toRemove.contains(entry.getValue()));
+ profileListeners.remove(tenantId);
+ deviceProfileListeners.remove(tenantId);
+ }
+ break;
+ }
+ }
+
private void notifyProfileListeners(DeviceProfile profile) {
ConcurrentMap> tenantListeners = profileListeners.get(profile.getTenantId());
if (tenantListeners != null) {
diff --git a/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbCalculatedFieldConsumerService.java b/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbCalculatedFieldConsumerService.java
index 60e60b5444..ba4ab74f37 100644
--- a/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbCalculatedFieldConsumerService.java
+++ b/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbCalculatedFieldConsumerService.java
@@ -90,9 +90,8 @@ public class DefaultTbCalculatedFieldConsumerService extends AbstractPartitionBa
PartitionService partitionService,
ApplicationEventPublisher eventPublisher,
JwtSettingsService jwtSettingsService,
- CalculatedFieldCache calculatedFieldCache,
CalculatedFieldStateService stateService) {
- super(actorContext, tenantProfileCache, deviceProfileCache, assetProfileCache, tbResourceDataCache, calculatedFieldCache, apiUsageStateService, partitionService,
+ super(actorContext, tenantProfileCache, deviceProfileCache, assetProfileCache, tbResourceDataCache, apiUsageStateService, partitionService,
eventPublisher, jwtSettingsService);
this.queueFactory = tbQueueFactory;
this.stateService = stateService;
diff --git a/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbCoreConsumerService.java b/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbCoreConsumerService.java
index 6399e55e03..de2e65723e 100644
--- a/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbCoreConsumerService.java
+++ b/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbCoreConsumerService.java
@@ -179,9 +179,8 @@ public class DefaultTbCoreConsumerService extends AbstractConsumerService consumer = requests.remove(requestId);
if (consumer != null) {
- consumer.accept(TbMsg.fromProto(null, restApiCallResponseMsg.getResponseProto(), restApiCallResponseMsg.getResponse(), TbMsgCallback.EMPTY));
+ consumer.accept(TbMsg.fromProto(null, restApiCallResponseMsg.getResponseProto(), TbMsgCallback.EMPTY));
} else {
log.trace("[{}] Unknown or stale rest api call response received", requestId);
}
diff --git a/application/src/main/java/org/thingsboard/server/service/system/SystemPatchApplier.java b/application/src/main/java/org/thingsboard/server/service/system/SystemPatchApplier.java
index d80961923d..bb5052fd76 100644
--- a/application/src/main/java/org/thingsboard/server/service/system/SystemPatchApplier.java
+++ b/application/src/main/java/org/thingsboard/server/service/system/SystemPatchApplier.java
@@ -28,6 +28,7 @@ import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.common.util.ThingsBoardThreadFactory;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.widget.WidgetTypeDetails;
+import org.thingsboard.server.dao.resource.ImageService;
import org.thingsboard.server.dao.widget.WidgetTypeService;
import org.thingsboard.server.queue.util.TbCoreComponent;
import org.thingsboard.server.service.install.DatabaseSchemaSettingsService;
@@ -40,7 +41,9 @@ import java.net.URL;
import java.nio.file.Files;
import java.nio.file.NoSuchFileException;
import java.nio.file.Path;
+import java.nio.file.Paths;
import java.util.Objects;
+import java.util.Set;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.atomic.AtomicInteger;
@@ -48,7 +51,7 @@ import java.util.stream.Stream;
/**
* Runs at application startup and applies no-downtime data updates
- * when the package PATCH version increases (e.g., 4.2.1.0 -> 4.2.1.1).
+ * when the package version increases within the same LTS family (e.g., 4.3.0.0 -> 4.3.1.0 or 4.3.0.0 -> 4.3.0.1).
*/
@Slf4j
@Component
@@ -64,6 +67,7 @@ public class SystemPatchApplier {
private final InstallScripts installScripts;
private final DatabaseSchemaSettingsService schemaSettingsService;
private final WidgetTypeService widgetTypeService;
+ private final ImageService imageService;
@PostConstruct
private void init() {
@@ -91,12 +95,17 @@ public class SystemPatchApplier {
}
try {
+ updateLtsSqlSchema();
+
updateSqlViews();
log.info("Updated sql database views");
int updated = updateWidgetTypes();
log.info("Updated {} widget types", updated);
+ int createdImages = createMissingSystemImages();
+ log.info("Created {} new system images", createdImages);
+
schemaSettingsService.updateSchemaVersion();
log.info("System data patch update completed successfully");
@@ -119,17 +128,37 @@ public class SystemPatchApplier {
return false;
}
- if (!isPatchVersionChanged(packageVersionInfo, dbVersionInfo)) {
+ if (!isVersionIncreased(packageVersionInfo, dbVersionInfo)) {
return false;
}
- log.info("Patch version increased from {} to {}. Starting system data update.", dbVersion, packageVersion);
+ log.info("Version increased from {} to {}. Starting system data update.", dbVersion, packageVersion);
return true;
}
- private boolean isPatchVersionChanged(VersionInfo packageVersion, VersionInfo dbVersion) {
- return packageVersion.major == dbVersion.major && packageVersion.minor == dbVersion.minor
- && packageVersion.maintenance == dbVersion.maintenance && packageVersion.patch > dbVersion.patch;
+ private boolean isVersionIncreased(VersionInfo packageVersion, VersionInfo dbVersion) {
+ if (packageVersion.major != dbVersion.major || packageVersion.minor != dbVersion.minor) {
+ return false;
+ }
+ if (packageVersion.maintenance != dbVersion.maintenance) {
+ return packageVersion.maintenance > dbVersion.maintenance;
+ }
+ return packageVersion.patch > dbVersion.patch;
+ }
+
+ private void updateLtsSqlSchema() {
+ Path sqlFile = Paths.get(installScripts.getDataDir(), "upgrade", "lts", "schema_update.sql");
+ if (!Files.exists(sqlFile)) {
+ log.trace("LTS schema update file does not exist: {}", sqlFile);
+ return;
+ }
+ try {
+ String sql = Files.readString(sqlFile);
+ jdbcTemplate.execute(sql);
+ log.info("Applied LTS SQL schema update from {}", sqlFile);
+ } catch (IOException e) {
+ throw new RuntimeException("Failed to read LTS schema update file: " + sqlFile, e);
+ }
}
private void updateSqlViews() {
@@ -192,6 +221,39 @@ public class SystemPatchApplier {
return false;
}
+ private int createMissingSystemImages() {
+ AtomicInteger created = new AtomicInteger();
+ Path imagesDir = Paths.get(installScripts.getDataDir(), InstallScripts.RESOURCES_DIR, "images");
+
+ if (!Files.exists(imagesDir)) {
+ log.warn("System images directory does not exist: {}", imagesDir);
+ return 0;
+ }
+
+ Set existingKeys = imageService.getAllImageKeysByTenantId(TenantId.SYS_TENANT_ID);
+
+ try (Stream dirStream = listDir(imagesDir).filter(Files::isRegularFile)) {
+ dirStream.forEach(path -> {
+ String resourceKey = path.getFileName().toString();
+ if (existingKeys.contains(resourceKey)) {
+ log.trace("System image already exists, skipping: {}", resourceKey);
+ return;
+ }
+ try {
+ byte[] data = Files.readAllBytes(path);
+ imageService.createOrUpdateSystemImage(resourceKey, data);
+ created.incrementAndGet();
+ log.trace("Created system image: {}", resourceKey);
+ } catch (Exception e) {
+ log.error("Unable to create system image from file: [{}]", path);
+ throw new RuntimeException("Unable to create system image " + resourceKey, e);
+ }
+ });
+ }
+
+ return created.get();
+ }
+
private boolean isWidgetTypeChanged(WidgetTypeDetails existing, WidgetTypeDetails file) {
if (!isDescriptorEqual(existing.getDescriptor(), file.getDescriptor())) {
return true;
diff --git a/application/src/main/java/org/thingsboard/server/service/ws/DefaultWebSocketService.java b/application/src/main/java/org/thingsboard/server/service/ws/DefaultWebSocketService.java
index 4b1a81dd2d..27a8ca275c 100644
--- a/application/src/main/java/org/thingsboard/server/service/ws/DefaultWebSocketService.java
+++ b/application/src/main/java/org/thingsboard/server/service/ws/DefaultWebSocketService.java
@@ -144,7 +144,7 @@ public class DefaultWebSocketService implements WebSocketService {
private final ConcurrentMap> tenantSubscriptionsMap = new ConcurrentHashMap<>();
private final ConcurrentMap> customerSubscriptionsMap = new ConcurrentHashMap<>();
private final ConcurrentMap> regularUserSubscriptionsMap = new ConcurrentHashMap<>();
- private final ConcurrentMap> publicUserSubscriptionsMap = new ConcurrentHashMap<>();
+ private final ConcurrentMap> publicUserSubscriptionsMap = new ConcurrentHashMap<>();
private final ConcurrentMap> sessionCmdMap = new ConcurrentHashMap<>();
private ExecutorService executor;
@@ -315,7 +315,7 @@ public class DefaultWebSocketService implements WebSocketService {
}
}
- private void processSessionClose(WebSocketSessionRef sessionRef) {
+ void processSessionClose(WebSocketSessionRef sessionRef) {
var tenantProfileConfiguration = getTenantProfileConfiguration(sessionRef);
if (tenantProfileConfiguration != null) {
String sessionId = "[" + sessionRef.getSessionId() + "]";
@@ -340,7 +340,7 @@ public class DefaultWebSocketService implements WebSocketService {
}
}
if (tenantProfileConfiguration.getMaxWsSubscriptionsPerPublicUser() > 0 && UserPrincipal.Type.PUBLIC_ID.equals(sessionRef.getSecurityCtx().getUserPrincipal().getType())) {
- Set publicUserSessions = publicUserSubscriptionsMap.computeIfAbsent(sessionRef.getSecurityCtx().getId(), id -> ConcurrentHashMap.newKeySet());
+ Set publicUserSessions = publicUserSubscriptionsMap.computeIfAbsent(sessionRef.getSecurityCtx().getTenantId(), id -> ConcurrentHashMap.newKeySet());
synchronized (publicUserSessions) {
publicUserSessions.removeIf(subId -> subId.startsWith(sessionId));
}
@@ -349,7 +349,7 @@ public class DefaultWebSocketService implements WebSocketService {
}
}
- private boolean processSubscription(WebSocketSessionRef sessionRef, SubscriptionCmd cmd) {
+ boolean processSubscription(WebSocketSessionRef sessionRef, SubscriptionCmd cmd) {
var tenantProfileConfiguration = getTenantProfileConfiguration(sessionRef);
if (tenantProfileConfiguration == null) return true;
@@ -401,9 +401,11 @@ public class DefaultWebSocketService implements WebSocketService {
}
}
if (tenantProfileConfiguration.getMaxWsSubscriptionsPerPublicUser() > 0 && UserPrincipal.Type.PUBLIC_ID.equals(sessionRef.getSecurityCtx().getUserPrincipal().getType())) {
- Set publicUserSessions = publicUserSubscriptionsMap.computeIfAbsent(sessionRef.getSecurityCtx().getId(), id -> ConcurrentHashMap.newKeySet());
+ Set publicUserSessions = publicUserSubscriptionsMap.computeIfAbsent(sessionRef.getSecurityCtx().getTenantId(), id -> ConcurrentHashMap.newKeySet());
synchronized (publicUserSessions) {
- if (publicUserSessions.size() < tenantProfileConfiguration.getMaxWsSubscriptionsPerPublicUser()) {
+ if (cmd.isUnsubscribe()) {
+ publicUserSessions.remove(subId);
+ } else if (publicUserSessions.size() < tenantProfileConfiguration.getMaxWsSubscriptionsPerPublicUser()) {
publicUserSessions.add(subId);
} else {
log.info("[{}][{}][{}] Failed to start subscription. Max public user subscriptions limit reached"
diff --git a/application/src/main/resources/thingsboard.yml b/application/src/main/resources/thingsboard.yml
index d4e1a27ae7..5532d9847d 100644
--- a/application/src/main/resources/thingsboard.yml
+++ b/application/src/main/resources/thingsboard.yml
@@ -567,6 +567,18 @@ actors:
# Use this when your rule chains need to reach devices on private networks (e.g., 192.168.1.0/24).
# Example: "192.168.1.0/24,10.0.0.0/8,my-internal-service.corp"
ssrf_allowed_hosts: "${SSRF_ALLOWED_HOSTS:}"
+ http_client:
+ # Server-level ceiling for parallel in-flight HTTP requests per external HTTP rule node instance.
+ # Applied as min(nodeConfig, systemMax) when set; 0 = no system-level restriction (node config wins).
+ max_parallel_requests: "${ACTORS_RULE_EXTERNAL_HTTP_CLIENT_MAX_PARALLEL_REQUESTS:0}"
+ # Server-level ceiling for the pending-request queue depth per external HTTP rule node instance.
+ # Applied as min(nodeConfig, systemMax) when set; 0 = no system-level restriction.
+ max_pending_requests: "${ACTORS_RULE_EXTERNAL_HTTP_CLIENT_MAX_PENDING_REQUESTS:0}"
+ # Maximum number of TCP connections in the reactor-netty connection pool per external HTTP rule node instance.
+ # Defaults to reactor-netty's ConnectionProvider.DEFAULT_POOL_MAX_CONNECTIONS: max(availableProcessors, 8) * 2
+ # (e.g. 16 on an 8-core host). Increase for high-throughput nodes calling remote services that support many connections.
+ # 0 = use reactor-netty default.
+ pool_max_connections: "${ACTORS_RULE_EXTERNAL_HTTP_CLIENT_POOL_MAX_CONNECTIONS:${TB_RE_HTTP_CLIENT_POOL_MAX_CONNECTIONS:0}}"
rpc:
# Maximum number of persistent RPC call retries in case of failed request delivery.
max_retries: "${ACTORS_RPC_MAX_RETRIES:5}"
diff --git a/application/src/test/java/org/thingsboard/server/controller/AlarmCommentControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/AlarmCommentControllerTest.java
index b87473f6e3..ba86d60852 100644
--- a/application/src/test/java/org/thingsboard/server/controller/AlarmCommentControllerTest.java
+++ b/application/src/test/java/org/thingsboard/server/controller/AlarmCommentControllerTest.java
@@ -44,7 +44,9 @@ import org.thingsboard.server.dao.service.DaoSqlTest;
import java.util.LinkedList;
import java.util.List;
+import java.util.Optional;
+import static org.assertj.core.api.Assertions.assertThat;
import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.equalTo;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
@@ -223,6 +225,18 @@ public class AlarmCommentControllerTest extends AbstractControllerTest {
doDelete("/api/alarm/" + alarm.getId() + "/comment/" + alarmComment.getId())
.andExpect(status().isOk());
+ Optional systemCommentOpt = doGetTyped(
+ "/api/alarm/" + alarm.getId() + "/comment" + "?page=0&pageSize=10", new TypeReference>() {
+ }
+ ).getData().stream().filter(alarmCommentInfo -> alarmCommentInfo.getType().equals(AlarmCommentType.SYSTEM)).findFirst();
+ assertThat(systemCommentOpt).isPresent();
+ AlarmCommentInfo systemComment = systemCommentOpt.get();
+
+ assertThat(systemComment.getId()).isEqualTo(alarmComment.getId());
+ assertThat(systemComment.getType()).isEqualTo(AlarmCommentType.SYSTEM);
+ assertThat(systemComment.getComment().get("text").asText()).isEqualTo(String.format("User %s deleted his comment",
+ TENANT_ADMIN_EMAIL));
+
AlarmComment expectedAlarmComment = AlarmComment.builder()
.alarmId(alarm.getId())
.type(AlarmCommentType.SYSTEM)
@@ -356,6 +370,39 @@ public class AlarmCommentControllerTest extends AbstractControllerTest {
Assert.assertTrue("Created alarm doesn't match the found one!", equals);
}
+ @Test
+ public void testShouldNotCreateOrUpdateSystemAlarmComment() throws Exception {
+ loginTenantAdmin();
+
+ AlarmComment alarmComment = AlarmComment.builder()
+ .type(AlarmCommentType.SYSTEM)
+ .comment(JacksonUtil.newObjectNode().set("text", new TextNode("Acknowledged by tenant admin")))
+ .build();
+ AlarmComment created = doPost("/api/alarm/" + alarm.getId() + "/comment", alarmComment, AlarmComment.class);
+ assertThat(created.getType()).isEqualTo(AlarmCommentType.OTHER);
+
+ // acknowledge alarm to create system comment
+ doPost("/api/alarm/" + alarm.getId() + "/ack").andExpect(status().isOk());
+
+ Optional systemCommentOpt = doGetTyped(
+ "/api/alarm/" + alarm.getId() + "/comment" + "?page=0&pageSize=10", new TypeReference>() {
+ }
+ ).getData().stream().filter(alarmCommentInfo -> alarmCommentInfo.getType().equals(AlarmCommentType.SYSTEM)).findFirst();
+ assertThat(systemCommentOpt).isPresent();
+ AlarmCommentInfo systemComment = systemCommentOpt.get();
+
+ // system comment can't be updated with other type
+ systemComment.setType(AlarmCommentType.OTHER);
+ doPost("/api/alarm/" + alarm.getId() + "/comment", systemComment).andExpect(status().isBadRequest())
+ .andExpect(statusReason(containsString("System alarm comment can't be updated!")));
+
+ // system comment can't be updated with other text
+ systemComment.setType(AlarmCommentType.SYSTEM);
+ systemComment.setComment(JacksonUtil.newObjectNode().set("text", new TextNode("New system comment")));
+ doPost("/api/alarm/" + alarm.getId() + "/comment", systemComment).andExpect(status().isBadRequest())
+ .andExpect(statusReason(containsString("System alarm comment can't be updated!")));
+ }
+
private AlarmComment createAlarmComment(AlarmId alarmId, String text) {
AlarmComment alarmComment = AlarmComment.builder()
.comment(JacksonUtil.newObjectNode().set("text", new TextNode(text)))
diff --git a/application/src/test/java/org/thingsboard/server/controller/plugin/TbWebSocketHandlerTest.java b/application/src/test/java/org/thingsboard/server/controller/plugin/TbWebSocketHandlerTest.java
index 053cb6808f..4dc637725f 100644
--- a/application/src/test/java/org/thingsboard/server/controller/plugin/TbWebSocketHandlerTest.java
+++ b/application/src/test/java/org/thingsboard/server/controller/plugin/TbWebSocketHandlerTest.java
@@ -25,16 +25,28 @@ import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.Mockito;
+import org.springframework.test.util.ReflectionTestUtils;
import org.springframework.web.socket.CloseStatus;
+import org.springframework.web.socket.WebSocketSession;
import org.springframework.web.socket.adapter.NativeWebSocketSession;
import org.thingsboard.common.util.ThingsBoardThreadFactory;
+import org.thingsboard.server.common.data.TenantProfile;
+import org.thingsboard.server.common.data.id.CustomerId;
+import org.thingsboard.server.common.data.id.EntityId;
+import org.thingsboard.server.common.data.id.TenantId;
+import org.thingsboard.server.common.data.id.UserId;
+import org.thingsboard.server.dao.tenant.TbTenantProfileCache;
+import org.thingsboard.server.service.security.model.SecurityUser;
+import org.thingsboard.server.service.security.model.UserPrincipal;
import org.thingsboard.server.service.ws.WebSocketSessionRef;
import java.io.IOException;
+import java.lang.reflect.Method;
import java.util.Collection;
import java.util.Deque;
import java.util.List;
import java.util.Random;
+import java.util.UUID;
import java.util.concurrent.ConcurrentLinkedDeque;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.CountDownLatch;
@@ -184,4 +196,70 @@ class TbWebSocketHandlerTest {
assertThat(msgs).map(Integer::parseInt).doesNotHaveDuplicates().hasSize(100);
}
+ // Regression test for the bug where publicUserSessionsMap was keyed by UserId(NULL_UUID),
+ // making maxWsSessionsPerPublicUser a global limit shared across all tenants.
+ // The limit is now scoped per-tenant.
+ @Test
+ void checkLimits_publicUserSessions_limitIsPerTenantNotGlobal() throws Exception {
+ TbTenantProfileCache tenantProfileCache = mock(TbTenantProfileCache.class);
+ ReflectionTestUtils.setField(wsHandler, "tenantProfileCache", tenantProfileCache);
+
+ int maxPublicSessions = 2;
+
+ TenantId tenant1 = TenantId.fromUUID(UUID.randomUUID());
+ TenantProfile profile1 = new TenantProfile();
+ profile1.createDefaultTenantProfileData();
+ profile1.getDefaultProfileConfiguration().setMaxWsSessionsPerPublicUser(maxPublicSessions);
+ willReturn(profile1).given(tenantProfileCache).get(tenant1);
+
+ TenantId tenant2 = TenantId.fromUUID(UUID.randomUUID());
+ TenantProfile profile2 = new TenantProfile();
+ profile2.createDefaultTenantProfileData();
+ profile2.getDefaultProfileConfiguration().setMaxWsSessionsPerPublicUser(maxPublicSessions);
+ willReturn(profile2).given(tenantProfileCache).get(tenant2);
+
+ Method checkLimits = TbWebSocketHandler.class.getDeclaredMethod(
+ "checkLimits", WebSocketSession.class, WebSocketSessionRef.class);
+ checkLimits.setAccessible(true);
+
+ // tenant1 fills up its limit
+ for (int i = 0; i < maxPublicSessions; i++) {
+ assertThat((boolean) checkLimits.invoke(wsHandler, mockWsSession("t1-" + i), mockPublicSessionRef(tenant1))).isTrue();
+ }
+
+ // tenant2 must get its own independent quota — this was the bug: with NULL_UUID as key
+ // all tenants shared one global counter, so tenant2 would be blocked here
+ for (int i = 0; i < maxPublicSessions; i++) {
+ assertThat((boolean) checkLimits.invoke(wsHandler, mockWsSession("t2-" + i), mockPublicSessionRef(tenant2)))
+ .as("tenant2 session %d should not be affected by tenant1's sessions", i + 1)
+ .isTrue();
+ }
+
+ // tenant1's (maxPublicSessions + 1)-th session must be rejected
+ NativeWebSocketSession overLimit = mockWsSession("t1-over");
+ assertThat((boolean) checkLimits.invoke(wsHandler, overLimit, mockPublicSessionRef(tenant1))).isFalse();
+ verify(overLimit).close(CloseStatus.POLICY_VIOLATION.withReason("Max public user sessions limit reached"));
+ }
+
+ private NativeWebSocketSession mockWsSession(String id) {
+ NativeWebSocketSession s = mock(NativeWebSocketSession.class);
+ willReturn(id).given(s).getId();
+ return s;
+ }
+
+ private WebSocketSessionRef mockPublicSessionRef(TenantId tenantId) {
+ CustomerId customerId = new CustomerId(UUID.randomUUID());
+ SecurityUser securityUser = mock(SecurityUser.class);
+ willReturn(tenantId).given(securityUser).getTenantId();
+ willReturn(customerId).given(securityUser).getCustomerId();
+ willReturn(new UserId(EntityId.NULL_UUID)).given(securityUser).getId();
+ willReturn(true).given(securityUser).isCustomerUser();
+ willReturn(new UserPrincipal(UserPrincipal.Type.PUBLIC_ID, customerId.toString())).given(securityUser).getUserPrincipal();
+
+ WebSocketSessionRef ref = mock(WebSocketSessionRef.class);
+ willReturn(securityUser).given(ref).getSecurityCtx();
+ willReturn(UUID.randomUUID().toString()).given(ref).getSessionId();
+ return ref;
+ }
+
}
diff --git a/application/src/test/java/org/thingsboard/server/service/ai/Langchain4jChatModelConfigurerImplTest.java b/application/src/test/java/org/thingsboard/server/service/ai/Langchain4jChatModelConfigurerImplTest.java
new file mode 100644
index 0000000000..014e3f1b64
--- /dev/null
+++ b/application/src/test/java/org/thingsboard/server/service/ai/Langchain4jChatModelConfigurerImplTest.java
@@ -0,0 +1,66 @@
+/**
+ * Copyright © 2016-2026 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.service.ai;
+
+import com.google.cloud.vertexai.api.GenerationConfig;
+import dev.langchain4j.model.chat.ChatModel;
+import org.junit.jupiter.api.Test;
+import org.springframework.test.util.ReflectionTestUtils;
+import org.thingsboard.server.common.data.ai.model.chat.GoogleVertexAiGeminiChatModelConfig;
+import org.thingsboard.server.common.data.ai.provider.GoogleVertexAiGeminiProviderConfig;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+class Langchain4jChatModelConfigurerImplTest {
+
+ private static final String TEST_SERVICE_ACCOUNT_KEY = """
+ {
+ "type": "service_account",
+ "project_id": "test-project",
+ "private_key_id": "key-id",
+ "private_key": "-----BEGIN PRIVATE KEY-----\\nMIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDNrHph/y7zyxIg\\ncmYYeOD8mFg9KraK71n84ffTQyVrl4HzlQgRIz5m4vM2rV5zjVLFi0xlAPT/iq/5\\nbh2zA4iXI0dEsR901nVjcL182t/GRYbKen53ZiuScBxBoCZPXW16Md+Yk8nMdNUb\\n4LoIRGZq64bjsJ+vh3Aa2gdGUHpDyebIXlXbF8ehWmEhgUsL7XjL0PkJ4lt2UMG+\\nx1j2Or25rqJmfc5M9kbxvINtdvSRTPiMOIXX00fCDZjQdd18RBVHOxraGxDgQpmv\\nk4qjFEPqGr0YTsa5dI8fz+4DqJpEi3rancRiTKM/KUwYLGnPSD07XqGfiDA8npVm\\nj4N62LhnAgMBAAECggEADbFfH87DRk7YQO8XgKdCOf7oglX+0NwjjmmlkVvwjgEI\\nZqT0ObPcz9u/MSjfV2vAEs/LK773ELD1NfLQqQjiBfHpfkIZOTLynhwKOYRBjqvf\\n+p38ynUzucGbV/vSC8meuW/AQPe3Nn9MFYQ4znEYrSNLbTWRRA3idvSEtHfffqDA\\nDHRBI1eIlxh1OTIR3L+HhcNYuus1LuoKnSlmwLGhAZLt7fjuWK3PkOiFT15e0M9M\\nUhp3WwhHcRC0o6bxT+BWRYKMVX3Vjlro4sF1fq4+jePThX1bpJcPfUmsC95tXPfX\\njfNGAxHlZ+MS1V/cLlIqyz7drXBcwCDJtbPmvavmNQKBgQD1ZR/ePHcjXUGM57U8\\nbxPatNOrcicvaP2AtTA6Y/JjfbcydkVXsenDGk0h6hykpIiMwrAaJuUTjGeM6QTI\\nOhK0k1QbGitcM71d9TSdLzWUdb3yvsyaPZlPR/6u2FBb6Bf9rWOQRYYyv1Lvu0+1\\nYLnR4sHBxiAur1NGxuHfA4ZUOwKBgQDWj+fcS/x/ifbCqexr7teWU+tUeyG3eLGA\\nMmB9eCkY7djl0/LHu/IHgqrGRVgra3IB1uI7Wr3jZYvlS7qGL3KpjeIPYj7LTQC5\\nznm0875NvJELPjQK/A4EM3mC057QRvb7y52KBNKJi7/JwHU7VHmudB78e7uGlW2K\\n5Ccl0PJFxQKBgCWv5yoJXT64JsYOG95xLLptBQkSmgQE+tHWgdal3Ob8urLsSRAD\\nyePl2Sy5OLbscfA0Qjlx+cJ70LdqXgqmKJNFASi8ZyZc59tTOkZdprvrLUXnmaKi\\njTYI14tgu06yIWUbSOwyUT7f9UvOF5rChSc/zQQGepDQ6lg3WR8X+nxbAoGBAKiu\\nfAcqSfjuuuuxcWgtXpoVoaZKI2i9Xza85DTf+ddabjHJXk3+iTm0VZQIwldoYjnl\\n+PfW0ABtPf1net2xgcChBf84Ksvj3tU06WQEWDF/NLyVC48zN8W/viDHREzT7app\\nGpJ+VhLCpmXzg3bAY+Vt70pp8DTPV05hLhHB4iZNAoGBAJW+bYh7jE61+58VpjvF\\nP36BK09jEEPWVucJdghb2mb62iA6JDy3ApU+8FzckXHDewt0sqvsW4VqukgwVZx3\\npSC7mR4B+Fm6znm0Z5mBWiG5bOOgTJ0mRZv4cYgC+JRRF/E3yYR58RyAKFAFIAFH\\nng8XYP1wQp64Fzv4+rUSwM49\\n-----END PRIVATE KEY-----\\n",
+ "client_email": "test@test-project.iam.gserviceaccount.com",
+ "client_id": "123456789",
+ "auth_uri": "https://accounts.google.com/o/oauth2/auth",
+ "token_uri": "https://oauth2.googleapis.com/token"
+ }
+ """;
+
+ private final Langchain4jChatModelConfigurerImpl configurer = new Langchain4jChatModelConfigurerImpl();
+
+ @Test
+ void configureChatModel_vertexAi_setsFrequencyAndPresencePenaltyFromCorrectConfigFields() {
+ // GIVEN
+ var providerConfig = new GoogleVertexAiGeminiProviderConfig(
+ "test.json", "test-project", "us-central1", TEST_SERVICE_ACCOUNT_KEY
+ );
+ var chatModelConfig = GoogleVertexAiGeminiChatModelConfig.builder()
+ .providerConfig(providerConfig)
+ .modelId("gemini-2.0-flash")
+ .frequencyPenalty(0.3)
+ .presencePenalty(0.7)
+ .build();
+
+ // WHEN
+ ChatModel chatModel = configurer.configureChatModel(chatModelConfig);
+
+ // THEN
+ var generationConfig = (GenerationConfig) ReflectionTestUtils.getField(chatModel, "generationConfig");
+ assertThat(generationConfig.getFrequencyPenalty()).isEqualTo(0.3f);
+ assertThat(generationConfig.getPresencePenalty()).isEqualTo(0.7f);
+ }
+
+}
diff --git a/application/src/test/java/org/thingsboard/server/service/cf/DefaultCalculatedFieldCacheTest.java b/application/src/test/java/org/thingsboard/server/service/cf/DefaultCalculatedFieldCacheTest.java
new file mode 100644
index 0000000000..ee83f9df64
--- /dev/null
+++ b/application/src/test/java/org/thingsboard/server/service/cf/DefaultCalculatedFieldCacheTest.java
@@ -0,0 +1,349 @@
+/**
+ * Copyright © 2016-2026 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.service.cf;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+import org.thingsboard.server.common.data.Device;
+import org.thingsboard.server.common.data.cf.CalculatedField;
+import org.thingsboard.server.common.data.cf.CalculatedFieldLink;
+import org.thingsboard.server.common.data.cf.CalculatedFieldType;
+import org.thingsboard.server.common.data.cf.configuration.CalculatedFieldConfiguration;
+import org.thingsboard.server.common.data.id.AssetId;
+import org.thingsboard.server.common.data.id.AssetProfileId;
+import org.thingsboard.server.common.data.id.CalculatedFieldId;
+import org.thingsboard.server.common.data.id.CustomerId;
+import org.thingsboard.server.common.data.id.DeviceId;
+import org.thingsboard.server.common.data.id.DeviceProfileId;
+import org.thingsboard.server.common.data.id.EntityId;
+import org.thingsboard.server.common.data.id.TenantId;
+import org.thingsboard.server.common.data.page.PageData;
+import org.thingsboard.server.common.data.plugin.ComponentLifecycleEvent;
+import org.thingsboard.server.common.msg.plugin.ComponentLifecycleMsg;
+import org.thingsboard.server.dao.asset.AssetService;
+import org.thingsboard.server.dao.cf.CalculatedFieldService;
+import org.thingsboard.server.dao.customer.CustomerService;
+import org.thingsboard.server.dao.device.DeviceService;
+
+import java.util.Collections;
+import java.util.List;
+import java.util.UUID;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+@ExtendWith(MockitoExtension.class)
+public class DefaultCalculatedFieldCacheTest {
+
+ @Mock
+ private CalculatedFieldService calculatedFieldService;
+ @Mock
+ private DeviceService deviceService;
+ @Mock
+ private AssetService assetService;
+ @Mock
+ private CustomerService customerService;
+
+ private DefaultCalculatedFieldCache cache;
+
+ @BeforeEach
+ public void setUp() {
+ cache = new DefaultCalculatedFieldCache(calculatedFieldService, null, null);
+ }
+
+ // --- Tenant deletion tests ---
+
+ @Test
+ public void onComponentLifecycleEvent_tenantDeleted_evictsAllTenantCfsFromAllMaps() {
+ TenantId tenant1 = new TenantId(UUID.randomUUID());
+ TenantId tenant2 = new TenantId(UUID.randomUUID());
+ DeviceId device1 = new DeviceId(UUID.randomUUID());
+ DeviceId device2 = new DeviceId(UUID.randomUUID());
+
+ CalculatedField cf1 = addCfToCache(tenant1, device1);
+ CalculatedField cf2 = addCfToCache(tenant2, device2);
+
+ cache.onComponentLifecycleEvent(new ComponentLifecycleMsg(tenant1, tenant1, ComponentLifecycleEvent.DELETED));
+
+ assertThat(cache.getCalculatedField(cf1.getId())).isNull();
+ assertThat(cache.getCalculatedFieldsByEntityId(device1)).isEmpty();
+ assertThat(cache.getCalculatedField(cf2.getId())).isEqualTo(cf2);
+ assertThat(cache.getCalculatedFieldsByEntityId(device2)).containsExactly(cf2);
+ }
+
+ @Test
+ public void onComponentLifecycleEvent_tenantDeleted_removesLinksToLinkedEntities() {
+ TenantId tenant = new TenantId(UUID.randomUUID());
+ DeviceId cfEntity = new DeviceId(UUID.randomUUID());
+ DeviceId linkedDevice = new DeviceId(UUID.randomUUID());
+
+ CalculatedField cf = addCfToCache(tenant, cfEntity, linkedDevice);
+
+ cache.onComponentLifecycleEvent(new ComponentLifecycleMsg(tenant, tenant, ComponentLifecycleEvent.DELETED));
+
+ assertThat(cache.getCalculatedFieldLinksByEntityId(linkedDevice)).isEmpty();
+ assertThat(cache.getCalculatedField(cf.getId())).isNull();
+ }
+
+ @Test
+ public void onComponentLifecycleEvent_tenantUpdated_doesNotEvictCfs() {
+ TenantId tenant = new TenantId(UUID.randomUUID());
+ DeviceId device = new DeviceId(UUID.randomUUID());
+ CalculatedField cf = addCfToCache(tenant, device);
+
+ cache.onComponentLifecycleEvent(new ComponentLifecycleMsg(tenant, tenant, ComponentLifecycleEvent.UPDATED));
+
+ assertThat(cache.getCalculatedField(cf.getId())).isEqualTo(cf);
+ }
+
+ // --- Device/Asset deletion tests ---
+
+ @Test
+ public void onComponentLifecycleEvent_deviceDeleted_evictsCfsForThatDevice() {
+ TenantId tenant = new TenantId(UUID.randomUUID());
+ DeviceId device = new DeviceId(UUID.randomUUID());
+ CalculatedField cf = addCfToCache(tenant, device);
+
+ cache.onComponentLifecycleEvent(new ComponentLifecycleMsg(tenant, device, ComponentLifecycleEvent.DELETED));
+
+ assertThat(cache.getCalculatedField(cf.getId())).isNull();
+ assertThat(cache.getCalculatedFieldsByEntityId(device)).isEmpty();
+ }
+
+ @Test
+ public void onComponentLifecycleEvent_deviceDeleted_removesLinksForLinkedEntities() {
+ TenantId tenant = new TenantId(UUID.randomUUID());
+ DeviceId device = new DeviceId(UUID.randomUUID());
+ DeviceId linkedDevice = new DeviceId(UUID.randomUUID());
+ addCfToCache(tenant, device, linkedDevice);
+
+ cache.onComponentLifecycleEvent(new ComponentLifecycleMsg(tenant, device, ComponentLifecycleEvent.DELETED));
+
+ assertThat(cache.getCalculatedFieldLinksByEntityId(linkedDevice)).isEmpty();
+ }
+
+ @Test
+ public void onComponentLifecycleEvent_assetDeleted_evictsCfsForThatAsset() {
+ TenantId tenant = new TenantId(UUID.randomUUID());
+ AssetId asset = new AssetId(UUID.randomUUID());
+ CalculatedField cf = addCfToCache(tenant, asset);
+
+ cache.onComponentLifecycleEvent(new ComponentLifecycleMsg(tenant, asset, ComponentLifecycleEvent.DELETED));
+
+ assertThat(cache.getCalculatedField(cf.getId())).isNull();
+ assertThat(cache.getCalculatedFieldsByEntityId(asset)).isEmpty();
+ }
+
+ // --- DeviceProfile/AssetProfile deletion tests ---
+
+ @Test
+ public void onComponentLifecycleEvent_deviceProfileDeleted_evictsCfsForThatProfile() {
+ TenantId tenant = new TenantId(UUID.randomUUID());
+ DeviceProfileId profileId = new DeviceProfileId(UUID.randomUUID());
+ CalculatedField cf = addCfToCache(tenant, profileId);
+
+ cache.onComponentLifecycleEvent(new ComponentLifecycleMsg(tenant, profileId, ComponentLifecycleEvent.DELETED));
+
+ assertThat(cache.getCalculatedField(cf.getId())).isNull();
+ assertThat(cache.getCalculatedFieldsByEntityId(profileId)).isEmpty();
+ }
+
+ @Test
+ public void onComponentLifecycleEvent_deviceProfileDeleted_removesLinksForLinkedEntities() {
+ TenantId tenant = new TenantId(UUID.randomUUID());
+ DeviceProfileId profileId = new DeviceProfileId(UUID.randomUUID());
+ DeviceId linkedDevice = new DeviceId(UUID.randomUUID());
+ addCfToCache(tenant, profileId, linkedDevice);
+
+ cache.onComponentLifecycleEvent(new ComponentLifecycleMsg(tenant, profileId, ComponentLifecycleEvent.DELETED));
+
+ assertThat(cache.getCalculatedFieldLinksByEntityId(linkedDevice)).isEmpty();
+ }
+
+ @Test
+ public void onComponentLifecycleEvent_deviceProfileDeleted_doesNotEvictOtherProfilesCfs() {
+ TenantId tenant = new TenantId(UUID.randomUUID());
+ DeviceProfileId profile1 = new DeviceProfileId(UUID.randomUUID());
+ DeviceProfileId profile2 = new DeviceProfileId(UUID.randomUUID());
+ CalculatedField cf1 = addCfToCache(tenant, profile1);
+ CalculatedField cf2 = addCfToCache(tenant, profile2);
+
+ cache.onComponentLifecycleEvent(new ComponentLifecycleMsg(tenant, profile1, ComponentLifecycleEvent.DELETED));
+
+ assertThat(cache.getCalculatedField(cf1.getId())).isNull();
+ assertThat(cache.getCalculatedFieldsByEntityId(profile1)).isEmpty();
+ assertThat(cache.getCalculatedField(cf2.getId())).isEqualTo(cf2);
+ assertThat(cache.getCalculatedFieldsByEntityId(profile2)).containsExactly(cf2);
+ }
+
+ @Test
+ public void onComponentLifecycleEvent_deviceProfileUpdated_doesNotEvictCfs() {
+ TenantId tenant = new TenantId(UUID.randomUUID());
+ DeviceProfileId profileId = new DeviceProfileId(UUID.randomUUID());
+ CalculatedField cf = addCfToCache(tenant, profileId);
+
+ cache.onComponentLifecycleEvent(new ComponentLifecycleMsg(tenant, profileId, ComponentLifecycleEvent.UPDATED));
+
+ assertThat(cache.getCalculatedField(cf.getId())).isEqualTo(cf);
+ assertThat(cache.getCalculatedFieldsByEntityId(profileId)).containsExactly(cf);
+ }
+
+ @Test
+ public void onComponentLifecycleEvent_assetProfileDeleted_evictsCfsForThatProfile() {
+ TenantId tenant = new TenantId(UUID.randomUUID());
+ AssetProfileId profileId = new AssetProfileId(UUID.randomUUID());
+ CalculatedField cf = addCfToCache(tenant, profileId);
+
+ cache.onComponentLifecycleEvent(new ComponentLifecycleMsg(tenant, profileId, ComponentLifecycleEvent.DELETED));
+
+ assertThat(cache.getCalculatedField(cf.getId())).isNull();
+ assertThat(cache.getCalculatedFieldsByEntityId(profileId)).isEmpty();
+ }
+
+ @Test
+ public void onComponentLifecycleEvent_assetProfileDeleted_removesLinksForLinkedEntities() {
+ TenantId tenant = new TenantId(UUID.randomUUID());
+ AssetProfileId profileId = new AssetProfileId(UUID.randomUUID());
+ AssetId linkedAsset = new AssetId(UUID.randomUUID());
+ addCfToCache(tenant, profileId, linkedAsset);
+
+ cache.onComponentLifecycleEvent(new ComponentLifecycleMsg(tenant, profileId, ComponentLifecycleEvent.DELETED));
+
+ assertThat(cache.getCalculatedFieldLinksByEntityId(linkedAsset)).isEmpty();
+ }
+
+ @Test
+ public void onComponentLifecycleEvent_assetProfileDeleted_doesNotEvictOtherProfilesCfs() {
+ TenantId tenant = new TenantId(UUID.randomUUID());
+ AssetProfileId profile1 = new AssetProfileId(UUID.randomUUID());
+ AssetProfileId profile2 = new AssetProfileId(UUID.randomUUID());
+ CalculatedField cf1 = addCfToCache(tenant, profile1);
+ CalculatedField cf2 = addCfToCache(tenant, profile2);
+
+ cache.onComponentLifecycleEvent(new ComponentLifecycleMsg(tenant, profile1, ComponentLifecycleEvent.DELETED));
+
+ assertThat(cache.getCalculatedField(cf1.getId())).isNull();
+ assertThat(cache.getCalculatedFieldsByEntityId(profile1)).isEmpty();
+ assertThat(cache.getCalculatedField(cf2.getId())).isEqualTo(cf2);
+ assertThat(cache.getCalculatedFieldsByEntityId(profile2)).containsExactly(cf2);
+ }
+
+ @Test
+ public void onComponentLifecycleEvent_assetProfileUpdated_doesNotEvictCfs() {
+ TenantId tenant = new TenantId(UUID.randomUUID());
+ AssetProfileId profileId = new AssetProfileId(UUID.randomUUID());
+ CalculatedField cf = addCfToCache(tenant, profileId);
+
+ cache.onComponentLifecycleEvent(new ComponentLifecycleMsg(tenant, profileId, ComponentLifecycleEvent.UPDATED));
+
+ assertThat(cache.getCalculatedField(cf.getId())).isEqualTo(cf);
+ assertThat(cache.getCalculatedFieldsByEntityId(profileId)).containsExactly(cf);
+ }
+
+ // --- CalculatedField lifecycle tests ---
+
+ @Test
+ public void onComponentLifecycleEvent_calculatedFieldCreated_addsCfToCache() {
+ TenantId tenant = new TenantId(UUID.randomUUID());
+ DeviceId device = new DeviceId(UUID.randomUUID());
+ CalculatedFieldId cfId = new CalculatedFieldId(UUID.randomUUID());
+ CalculatedField cf = buildCalculatedField(cfId, tenant, device, simpleCfConfig());
+ when(calculatedFieldService.findById(tenant, cfId)).thenReturn(cf);
+
+ cache.onComponentLifecycleEvent(new ComponentLifecycleMsg(tenant, cfId, ComponentLifecycleEvent.CREATED));
+
+ assertThat(cache.getCalculatedField(cfId)).isEqualTo(cf);
+ assertThat(cache.getCalculatedFieldsByEntityId(device)).containsExactly(cf);
+ }
+
+ @Test
+ public void onComponentLifecycleEvent_calculatedFieldDeleted_evictsCfFromCache() {
+ TenantId tenant = new TenantId(UUID.randomUUID());
+ DeviceId device = new DeviceId(UUID.randomUUID());
+ CalculatedField cf = addCfToCache(tenant, device);
+
+ cache.onComponentLifecycleEvent(new ComponentLifecycleMsg(tenant, cf.getId(), ComponentLifecycleEvent.DELETED));
+
+ assertThat(cache.getCalculatedField(cf.getId())).isNull();
+ assertThat(cache.getCalculatedFieldsByEntityId(device)).isEmpty();
+ }
+
+ @Test
+ public void onComponentLifecycleEvent_calculatedFieldUpdated_refreshesCfInCache() {
+ TenantId tenant = new TenantId(UUID.randomUUID());
+ DeviceId device = new DeviceId(UUID.randomUUID());
+ CalculatedField cf = addCfToCache(tenant, device);
+
+ CalculatedField updatedCf = buildCalculatedField(cf.getId(), tenant, device, simpleCfConfig());
+ updatedCf.setName("updated-name");
+ when(calculatedFieldService.findById(tenant, cf.getId())).thenReturn(updatedCf);
+
+ cache.onComponentLifecycleEvent(new ComponentLifecycleMsg(tenant, cf.getId(), ComponentLifecycleEvent.UPDATED));
+
+ assertThat(cache.getCalculatedField(cf.getId())).isEqualTo(updatedCf);
+ }
+
+ private CalculatedField addCfToCache(TenantId tenantId, EntityId entityId) {
+ CalculatedFieldId cfId = new CalculatedFieldId(UUID.randomUUID());
+ CalculatedField cf = buildCalculatedField(cfId, tenantId, entityId, simpleCfConfig());
+ when(calculatedFieldService.findById(tenantId, cfId)).thenReturn(cf);
+ cache.addCalculatedField(tenantId, cfId);
+ return cf;
+ }
+
+ private CalculatedField addCfToCache(TenantId tenantId, EntityId entityId, EntityId linkedEntity) {
+ CalculatedFieldId cfId = new CalculatedFieldId(UUID.randomUUID());
+ CalculatedFieldConfiguration config = linkedEntityCfConfig(tenantId, cfId, linkedEntity);
+ CalculatedField cf = buildCalculatedField(cfId, tenantId, entityId, config);
+ when(calculatedFieldService.findById(tenantId, cfId)).thenReturn(cf);
+ cache.addCalculatedField(tenantId, cfId);
+ return cf;
+ }
+
+ private CalculatedField buildCalculatedField(CalculatedFieldId id, TenantId tenantId, EntityId entityId, CalculatedFieldConfiguration config) {
+ CalculatedField cf = new CalculatedField();
+ cf.setId(id);
+ cf.setTenantId(tenantId);
+ cf.setEntityId(entityId);
+ cf.setType(CalculatedFieldType.SIMPLE);
+ cf.setName("test-cf-" + id.getId());
+ cf.setConfiguration(config);
+ return cf;
+ }
+
+ private CalculatedFieldConfiguration simpleCfConfig() {
+ CalculatedFieldConfiguration config = mock(CalculatedFieldConfiguration.class);
+ when(config.getReferencedEntities()).thenReturn(Collections.emptyList());
+ when(config.buildCalculatedFieldLinks(any(), any(), any())).thenReturn(Collections.emptyList());
+ return config;
+ }
+
+ private CalculatedFieldConfiguration linkedEntityCfConfig(TenantId tenantId, CalculatedFieldId cfId, EntityId linkedEntity) {
+ CalculatedFieldConfiguration config = mock(CalculatedFieldConfiguration.class);
+ CalculatedFieldLink link = new CalculatedFieldLink(tenantId, linkedEntity, cfId);
+ when(config.getReferencedEntities()).thenReturn(List.of(linkedEntity));
+ when(config.buildCalculatedFieldLinks(any(), any(), any())).thenReturn(List.of(link));
+ when(config.buildCalculatedFieldLink(any(), eq(linkedEntity), any())).thenReturn(link);
+ return config;
+ }
+
+}
diff --git a/application/src/test/java/org/thingsboard/server/service/profile/DefaultTbAssetProfileCacheTest.java b/application/src/test/java/org/thingsboard/server/service/profile/DefaultTbAssetProfileCacheTest.java
new file mode 100644
index 0000000000..f9b8d428d7
--- /dev/null
+++ b/application/src/test/java/org/thingsboard/server/service/profile/DefaultTbAssetProfileCacheTest.java
@@ -0,0 +1,159 @@
+/**
+ * Copyright © 2016-2026 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.service.profile;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+import org.thingsboard.server.common.data.asset.Asset;
+import org.thingsboard.server.common.data.asset.AssetProfile;
+import org.thingsboard.server.common.data.id.AssetId;
+import org.thingsboard.server.common.data.id.AssetProfileId;
+import org.thingsboard.server.common.data.id.EntityId;
+import org.thingsboard.server.common.data.id.TenantId;
+import org.thingsboard.server.common.data.plugin.ComponentLifecycleEvent;
+import org.thingsboard.server.common.msg.plugin.ComponentLifecycleMsg;
+import org.thingsboard.server.dao.asset.AssetProfileService;
+import org.thingsboard.server.dao.asset.AssetService;
+
+import java.util.UUID;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+@ExtendWith(MockitoExtension.class)
+public class DefaultTbAssetProfileCacheTest {
+
+ @Mock
+ private AssetProfileService assetProfileService;
+ @Mock
+ private AssetService assetService;
+
+ private DefaultTbAssetProfileCache cache;
+
+ @BeforeEach
+ public void setUp() {
+ cache = new DefaultTbAssetProfileCache(assetProfileService, assetService);
+ }
+
+ @Test
+ public void onComponentLifecycleEvent_tenantDeleted_evictsAssetProfilesForThatTenant() {
+ TenantId tenant1 = new TenantId(UUID.randomUUID());
+ TenantId tenant2 = new TenantId(UUID.randomUUID());
+ AssetProfileId profileId1 = new AssetProfileId(UUID.randomUUID());
+ AssetProfileId profileId2 = new AssetProfileId(UUID.randomUUID());
+
+ loadProfileIntoCache(tenant1, profileId1);
+ loadProfileIntoCache(tenant2, profileId2);
+
+ cache.onComponentLifecycleEvent(new ComponentLifecycleMsg(tenant1, tenant1, ComponentLifecycleEvent.DELETED));
+
+ // After deletion tenant1 profile should be reloaded from service on next get
+ when(assetProfileService.findAssetProfileById(any(), any())).thenReturn(null);
+ assertThat(cache.get(tenant1, profileId1)).isNull();
+ verify(assetProfileService, times(1)).findAssetProfileById(tenant2, profileId2);
+ }
+
+ @Test
+ public void onComponentLifecycleEvent_tenantDeleted_evictsAssetMappingsForThatTenant() {
+ TenantId tenant = new TenantId(UUID.randomUUID());
+ AssetProfileId profileId = new AssetProfileId(UUID.randomUUID());
+ AssetId assetId = new AssetId(UUID.randomUUID());
+
+ loadProfileIntoCache(tenant, profileId);
+ loadAssetMappingIntoCache(tenant, assetId, profileId);
+
+ cache.onComponentLifecycleEvent(new ComponentLifecycleMsg(tenant, tenant, ComponentLifecycleEvent.DELETED));
+
+ // After tenant deletion, asset-to-profile mapping should be gone; get() should try to reload
+ when(assetService.findAssetById(any(), any())).thenReturn(null);
+ assertThat(cache.get(tenant, assetId)).isNull();
+ verify(assetService, times(2)).findAssetById(tenant, assetId); // once on load, once after eviction
+ }
+
+ @Test
+ public void onComponentLifecycleEvent_tenantDeleted_removesListenersForThatTenant() {
+ TenantId tenant = new TenantId(UUID.randomUUID());
+ EntityId listenerId = new AssetId(UUID.randomUUID());
+ AtomicInteger callCount = new AtomicInteger();
+
+ cache.addListener(tenant, listenerId, profile -> callCount.incrementAndGet(), null);
+
+ cache.onComponentLifecycleEvent(new ComponentLifecycleMsg(tenant, tenant, ComponentLifecycleEvent.DELETED));
+
+ // Evicting a profile after tenant deletion should not trigger the removed listener
+ AssetProfileId profileId = new AssetProfileId(UUID.randomUUID());
+ loadProfileIntoCache(tenant, profileId);
+ cache.evict(tenant, profileId);
+
+ assertThat(callCount.get()).isZero();
+ }
+
+ @Test
+ public void onComponentLifecycleEvent_tenantUpdated_doesNotEvictProfiles() {
+ TenantId tenant = new TenantId(UUID.randomUUID());
+ AssetProfileId profileId = new AssetProfileId(UUID.randomUUID());
+ loadProfileIntoCache(tenant, profileId);
+
+ cache.onComponentLifecycleEvent(new ComponentLifecycleMsg(tenant, tenant, ComponentLifecycleEvent.UPDATED));
+
+ // Profile should still be served from cache without hitting the service again
+ cache.get(tenant, profileId);
+ verify(assetProfileService, times(1)).findAssetProfileById(tenant, profileId);
+ }
+
+ @Test
+ public void onComponentLifecycleEvent_differentTenantDeleted_keepsOtherTenantsProfiles() {
+ TenantId tenant1 = new TenantId(UUID.randomUUID());
+ TenantId tenant2 = new TenantId(UUID.randomUUID());
+ AssetProfileId profileId1 = new AssetProfileId(UUID.randomUUID());
+ AssetProfileId profileId2 = new AssetProfileId(UUID.randomUUID());
+
+ AssetProfile profile1 = loadProfileIntoCache(tenant1, profileId1);
+ loadProfileIntoCache(tenant2, profileId2);
+
+ cache.onComponentLifecycleEvent(new ComponentLifecycleMsg(tenant2, tenant2, ComponentLifecycleEvent.DELETED));
+
+ assertThat(cache.get(tenant1, profileId1)).isEqualTo(profile1);
+ verify(assetProfileService, times(1)).findAssetProfileById(tenant1, profileId1);
+ }
+
+ // --- Helpers ---
+
+ private AssetProfile loadProfileIntoCache(TenantId tenantId, AssetProfileId profileId) {
+ AssetProfile profile = new AssetProfile();
+ profile.setId(profileId);
+ profile.setTenantId(tenantId);
+ when(assetProfileService.findAssetProfileById(tenantId, profileId)).thenReturn(profile);
+ cache.get(tenantId, profileId);
+ return profile;
+ }
+
+ private void loadAssetMappingIntoCache(TenantId tenantId, AssetId assetId, AssetProfileId profileId) {
+ Asset asset = new Asset();
+ asset.setId(assetId);
+ asset.setAssetProfileId(profileId);
+ when(assetService.findAssetById(tenantId, assetId)).thenReturn(asset);
+ cache.get(tenantId, assetId);
+ }
+
+}
diff --git a/application/src/test/java/org/thingsboard/server/service/profile/DefaultTbDeviceProfileCacheTest.java b/application/src/test/java/org/thingsboard/server/service/profile/DefaultTbDeviceProfileCacheTest.java
new file mode 100644
index 0000000000..a26413514c
--- /dev/null
+++ b/application/src/test/java/org/thingsboard/server/service/profile/DefaultTbDeviceProfileCacheTest.java
@@ -0,0 +1,160 @@
+/**
+ * Copyright © 2016-2026 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.service.profile;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+import org.thingsboard.server.common.data.Device;
+import org.thingsboard.server.common.data.DeviceProfile;
+import org.thingsboard.server.common.data.id.DeviceId;
+import org.thingsboard.server.common.data.id.DeviceProfileId;
+import org.thingsboard.server.common.data.id.EntityId;
+import org.thingsboard.server.common.data.id.TenantId;
+import org.thingsboard.server.common.data.plugin.ComponentLifecycleEvent;
+import org.thingsboard.server.common.msg.plugin.ComponentLifecycleMsg;
+import org.thingsboard.server.dao.device.DeviceProfileService;
+import org.thingsboard.server.dao.device.DeviceService;
+
+import java.util.UUID;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+@ExtendWith(MockitoExtension.class)
+public class DefaultTbDeviceProfileCacheTest {
+
+ @Mock
+ private DeviceProfileService deviceProfileService;
+ @Mock
+ private DeviceService deviceService;
+
+ private DefaultTbDeviceProfileCache cache;
+
+ @BeforeEach
+ public void setUp() {
+ cache = new DefaultTbDeviceProfileCache(deviceProfileService, deviceService);
+ }
+
+ @Test
+ public void onComponentLifecycleEvent_tenantDeleted_evictsDeviceProfilesForThatTenant() {
+ TenantId tenant1 = new TenantId(UUID.randomUUID());
+ TenantId tenant2 = new TenantId(UUID.randomUUID());
+ DeviceProfileId profileId1 = new DeviceProfileId(UUID.randomUUID());
+ DeviceProfileId profileId2 = new DeviceProfileId(UUID.randomUUID());
+
+ loadProfileIntoCache(tenant1, profileId1);
+ loadProfileIntoCache(tenant2, profileId2);
+
+ cache.onComponentLifecycleEvent(new ComponentLifecycleMsg(tenant1, tenant1, ComponentLifecycleEvent.DELETED));
+
+ // After deletion tenant1 profile should be reloaded from service on next get
+ when(deviceProfileService.findDeviceProfileById(any(), any())).thenReturn(null);
+ assertThat(cache.get(tenant1, profileId1)).isNull();
+ // tenant2 profile should still be served from cache (no extra service call)
+ verify(deviceProfileService, times(1)).findDeviceProfileById(tenant2, profileId2);
+ }
+
+ @Test
+ public void onComponentLifecycleEvent_tenantDeleted_evictsDeviceMappingsForThatTenant() {
+ TenantId tenant = new TenantId(UUID.randomUUID());
+ DeviceProfileId profileId = new DeviceProfileId(UUID.randomUUID());
+ DeviceId deviceId = new DeviceId(UUID.randomUUID());
+
+ loadProfileIntoCache(tenant, profileId);
+ loadDeviceMappingIntoCache(tenant, deviceId, profileId);
+
+ cache.onComponentLifecycleEvent(new ComponentLifecycleMsg(tenant, tenant, ComponentLifecycleEvent.DELETED));
+
+ // After tenant deletion, device-to-profile mapping should be gone; get() should try to reload
+ when(deviceService.findDeviceById(any(), any())).thenReturn(null);
+ assertThat(cache.get(tenant, deviceId)).isNull();
+ verify(deviceService, times(2)).findDeviceById(tenant, deviceId); // once on load, once after eviction
+ }
+
+ @Test
+ public void onComponentLifecycleEvent_tenantDeleted_removesListenersForThatTenant() {
+ TenantId tenant = new TenantId(UUID.randomUUID());
+ EntityId listenerId = new DeviceId(UUID.randomUUID());
+ AtomicInteger callCount = new AtomicInteger();
+
+ cache.addListener(tenant, listenerId, profile -> callCount.incrementAndGet(), null);
+
+ cache.onComponentLifecycleEvent(new ComponentLifecycleMsg(tenant, tenant, ComponentLifecycleEvent.DELETED));
+
+ // Evicting a profile after tenant deletion should not trigger the removed listener
+ DeviceProfileId profileId = new DeviceProfileId(UUID.randomUUID());
+ loadProfileIntoCache(tenant, profileId);
+ cache.evict(tenant, profileId);
+
+ assertThat(callCount.get()).isZero();
+ }
+
+ @Test
+ public void onComponentLifecycleEvent_tenantUpdated_doesNotEvictProfiles() {
+ TenantId tenant = new TenantId(UUID.randomUUID());
+ DeviceProfileId profileId = new DeviceProfileId(UUID.randomUUID());
+ loadProfileIntoCache(tenant, profileId);
+
+ cache.onComponentLifecycleEvent(new ComponentLifecycleMsg(tenant, tenant, ComponentLifecycleEvent.UPDATED));
+
+ // Profile should still be served from cache without hitting the service again
+ cache.get(tenant, profileId);
+ verify(deviceProfileService, times(1)).findDeviceProfileById(tenant, profileId);
+ }
+
+ @Test
+ public void onComponentLifecycleEvent_differentTenantDeleted_keepsOtherTenantsProfiles() {
+ TenantId tenant1 = new TenantId(UUID.randomUUID());
+ TenantId tenant2 = new TenantId(UUID.randomUUID());
+ DeviceProfileId profileId1 = new DeviceProfileId(UUID.randomUUID());
+ DeviceProfileId profileId2 = new DeviceProfileId(UUID.randomUUID());
+
+ DeviceProfile profile1 = loadProfileIntoCache(tenant1, profileId1);
+ loadProfileIntoCache(tenant2, profileId2);
+
+ cache.onComponentLifecycleEvent(new ComponentLifecycleMsg(tenant2, tenant2, ComponentLifecycleEvent.DELETED));
+
+ assertThat(cache.get(tenant1, profileId1)).isEqualTo(profile1);
+ verify(deviceProfileService, times(1)).findDeviceProfileById(tenant1, profileId1);
+ }
+
+ // --- Helpers ---
+
+ private DeviceProfile loadProfileIntoCache(TenantId tenantId, DeviceProfileId profileId) {
+ DeviceProfile profile = new DeviceProfile();
+ profile.setId(profileId);
+ profile.setTenantId(tenantId);
+ when(deviceProfileService.findDeviceProfileById(tenantId, profileId)).thenReturn(profile);
+ cache.get(tenantId, profileId);
+ return profile;
+ }
+
+ private void loadDeviceMappingIntoCache(TenantId tenantId, DeviceId deviceId, DeviceProfileId profileId) {
+ Device device = new Device();
+ device.setId(deviceId);
+ device.setDeviceProfileId(profileId);
+ when(deviceService.findDeviceById(tenantId, deviceId)).thenReturn(device);
+ cache.get(tenantId, deviceId);
+ }
+
+}
diff --git a/application/src/test/java/org/thingsboard/server/service/ws/DefaultWebSocketServiceTest.java b/application/src/test/java/org/thingsboard/server/service/ws/DefaultWebSocketServiceTest.java
new file mode 100644
index 0000000000..a533e0369f
--- /dev/null
+++ b/application/src/test/java/org/thingsboard/server/service/ws/DefaultWebSocketServiceTest.java
@@ -0,0 +1,277 @@
+/**
+ * Copyright © 2016-2026 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.service.ws;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.springframework.test.util.ReflectionTestUtils;
+import org.thingsboard.server.common.data.TenantProfile;
+import org.thingsboard.server.common.data.id.CustomerId;
+import org.thingsboard.server.common.data.id.EntityId;
+import org.thingsboard.server.common.data.id.TenantId;
+import org.thingsboard.server.common.data.id.UserId;
+import org.thingsboard.server.dao.attributes.AttributesService;
+import org.thingsboard.server.dao.tenant.TbTenantProfileCache;
+import org.thingsboard.server.dao.timeseries.TimeseriesService;
+import org.thingsboard.server.queue.discovery.TbServiceInfoProvider;
+import org.thingsboard.server.service.security.AccessValidator;
+import org.thingsboard.server.service.security.model.SecurityUser;
+import org.thingsboard.server.service.security.model.UserPrincipal;
+import org.thingsboard.server.service.subscription.TbEntityDataSubscriptionService;
+import org.thingsboard.server.service.subscription.TbLocalSubscriptionService;
+import org.thingsboard.server.service.ws.notification.NotificationCommandsHandler;
+import org.thingsboard.server.service.ws.telemetry.cmd.v1.AttributesSubscriptionCmd;
+
+import java.util.Set;
+import java.util.UUID;
+import java.util.concurrent.ConcurrentMap;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.BDDMockito.willReturn;
+import static org.mockito.Mockito.mock;
+
+class DefaultWebSocketServiceTest {
+
+ DefaultWebSocketService service;
+ TbTenantProfileCache tenantProfileCache;
+ WebSocketMsgEndpoint msgEndpoint;
+
+ @BeforeEach
+ void setUp() {
+ tenantProfileCache = mock(TbTenantProfileCache.class);
+ msgEndpoint = mock(WebSocketMsgEndpoint.class);
+
+ service = new DefaultWebSocketService(
+ mock(TbLocalSubscriptionService.class),
+ mock(TbEntityDataSubscriptionService.class),
+ mock(NotificationCommandsHandler.class),
+ msgEndpoint,
+ mock(AccessValidator.class),
+ mock(AttributesService.class),
+ mock(TimeseriesService.class),
+ mock(TbServiceInfoProvider.class),
+ tenantProfileCache
+ );
+ }
+
+ // Regression test: publicUserSubscriptionsMap must be keyed by TenantId, not UserId(NULL_UUID).
+ // With the old UserId(NULL_UUID) key, all tenants shared one global subscription counter.
+ @Test
+ void processSubscription_publicUserSubscriptionsMap_isPerTenantNotGlobal() throws Exception {
+ int maxPublicSubscriptions = 2;
+
+ TenantId tenant1 = TenantId.fromUUID(UUID.randomUUID());
+ TenantProfile profile1 = new TenantProfile();
+ profile1.createDefaultTenantProfileData();
+ profile1.getDefaultProfileConfiguration().setMaxWsSubscriptionsPerPublicUser(maxPublicSubscriptions);
+ willReturn(profile1).given(tenantProfileCache).get(tenant1);
+
+ TenantId tenant2 = TenantId.fromUUID(UUID.randomUUID());
+ TenantProfile profile2 = new TenantProfile();
+ profile2.createDefaultTenantProfileData();
+ profile2.getDefaultProfileConfiguration().setMaxWsSubscriptionsPerPublicUser(maxPublicSubscriptions);
+ willReturn(profile2).given(tenantProfileCache).get(tenant2);
+
+ // tenant1 fills up its quota
+ for (int i = 0; i < maxPublicSubscriptions; i++) {
+ assertThat(service.processSubscription(mockPublicSessionRef(tenant1, "t1-session-" + i), subscriptionCmd(i)))
+ .as("tenant1 subscription %d should be accepted", i + 1)
+ .isTrue();
+ }
+
+ // tenant2 must have its own independent quota — this was the bug:
+ // with UserId(NULL_UUID) as key all tenants shared one counter, so tenant2 would be blocked here
+ for (int i = 0; i < maxPublicSubscriptions; i++) {
+ assertThat(service.processSubscription(mockPublicSessionRef(tenant2, "t2-session-" + i), subscriptionCmd(i)))
+ .as("tenant2 subscription %d should not be affected by tenant1's subscriptions", i + 1)
+ .isTrue();
+ }
+
+ // tenant1's (maxPublicSubscriptions + 1)-th subscription must be rejected
+ assertThat(service.processSubscription(mockPublicSessionRef(tenant1, "t1-session-over"), subscriptionCmd(99)))
+ .as("tenant1 should be rejected after exceeding its limit")
+ .isFalse();
+
+ // Verify that publicUserSubscriptionsMap has separate entries per tenant
+ @SuppressWarnings("unchecked")
+ ConcurrentMap> publicUserSubscriptionsMap =
+ (ConcurrentMap>) ReflectionTestUtils.getField(service, "publicUserSubscriptionsMap");
+
+ assertThat(publicUserSubscriptionsMap).as("map should contain tenant1").containsKey(tenant1);
+ assertThat(publicUserSubscriptionsMap).as("map should contain tenant2").containsKey(tenant2);
+ assertThat(publicUserSubscriptionsMap).as("map must not have a single NULL_UUID entry for all tenants")
+ .doesNotContainKey(new TenantId(EntityId.NULL_UUID));
+
+ assertThat(publicUserSubscriptionsMap.get(tenant1))
+ .as("tenant1 should have exactly %d subscriptions", maxPublicSubscriptions)
+ .hasSize(maxPublicSubscriptions);
+ assertThat(publicUserSubscriptionsMap.get(tenant2))
+ .as("tenant2 should have exactly %d subscriptions", maxPublicSubscriptions)
+ .hasSize(maxPublicSubscriptions);
+ }
+
+ @Test
+ void processSubscription_publicUserSubscriptionsMap_subscriptionIdFormat() {
+ int maxPublicSubscriptions = 5;
+ TenantId tenantId = TenantId.fromUUID(UUID.randomUUID());
+ TenantProfile profile = new TenantProfile();
+ profile.createDefaultTenantProfileData();
+ profile.getDefaultProfileConfiguration().setMaxWsSubscriptionsPerPublicUser(maxPublicSubscriptions);
+ willReturn(profile).given(tenantProfileCache).get(tenantId);
+
+ String sessionId = "my-session-id";
+ int cmdId = 42;
+ WebSocketSessionRef sessionRef = mockPublicSessionRef(tenantId, sessionId);
+ service.processSubscription(sessionRef, subscriptionCmd(cmdId));
+
+ @SuppressWarnings("unchecked")
+ ConcurrentMap> publicUserSubscriptionsMap =
+ (ConcurrentMap>) ReflectionTestUtils.getField(service, "publicUserSubscriptionsMap");
+
+ Set subs = publicUserSubscriptionsMap.get(tenantId);
+ assertThat(subs).hasSize(1);
+ assertThat(subs.iterator().next()).isEqualTo("[" + sessionId + "]:[" + cmdId + "]");
+ }
+
+ @Test
+ void processSubscription_unsubscribe_removesEntryFromPublicUserSubscriptionsMap() {
+ int maxPublicSubscriptions = 5;
+ TenantId tenantId = TenantId.fromUUID(UUID.randomUUID());
+ TenantProfile profile = new TenantProfile();
+ profile.createDefaultTenantProfileData();
+ profile.getDefaultProfileConfiguration().setMaxWsSubscriptionsPerPublicUser(maxPublicSubscriptions);
+ willReturn(profile).given(tenantProfileCache).get(tenantId);
+
+ String sessionId = "session-1";
+ int cmdId = 1;
+ WebSocketSessionRef sessionRef = mockPublicSessionRef(tenantId, sessionId);
+
+ service.processSubscription(sessionRef, subscriptionCmd(cmdId));
+
+ @SuppressWarnings("unchecked")
+ ConcurrentMap> publicUserSubscriptionsMap =
+ (ConcurrentMap>) ReflectionTestUtils.getField(service, "publicUserSubscriptionsMap");
+ assertThat(publicUserSubscriptionsMap.get(tenantId)).hasSize(1);
+
+ AttributesSubscriptionCmd unsubCmd = subscriptionCmd(cmdId);
+ unsubCmd.setUnsubscribe(true);
+ service.processSubscription(sessionRef, unsubCmd);
+
+ assertThat(publicUserSubscriptionsMap.get(tenantId)).isEmpty();
+ }
+
+ @Test
+ void processSubscription_unsubscribe_freesSlotForNewSubscription() {
+ int maxPublicSubscriptions = 1;
+ TenantId tenantId = TenantId.fromUUID(UUID.randomUUID());
+ TenantProfile profile = new TenantProfile();
+ profile.createDefaultTenantProfileData();
+ profile.getDefaultProfileConfiguration().setMaxWsSubscriptionsPerPublicUser(maxPublicSubscriptions);
+ willReturn(profile).given(tenantProfileCache).get(tenantId);
+
+ WebSocketSessionRef sessionRef = mockPublicSessionRef(tenantId, "session-1");
+ service.processSubscription(sessionRef, subscriptionCmd(1));
+
+ // slot is full — second subscription on same session should be rejected
+ assertThat(service.processSubscription(sessionRef, subscriptionCmd(2))).isFalse();
+
+ // unsubscribe cmd 1 to free the slot
+ AttributesSubscriptionCmd unsubCmd = subscriptionCmd(1);
+ unsubCmd.setUnsubscribe(true);
+ service.processSubscription(sessionRef, unsubCmd);
+
+ // now a new subscription should succeed
+ assertThat(service.processSubscription(sessionRef, subscriptionCmd(3)))
+ .as("new subscription should succeed after unsubscribe freed the slot")
+ .isTrue();
+ }
+
+ @Test
+ void processSessionClose_removesAllSessionSubscriptionsFromPublicUserSubscriptionsMap() {
+ int maxPublicSubscriptions = 10;
+ TenantId tenantId = TenantId.fromUUID(UUID.randomUUID());
+ TenantProfile profile = new TenantProfile();
+ profile.createDefaultTenantProfileData();
+ profile.getDefaultProfileConfiguration().setMaxWsSubscriptionsPerPublicUser(maxPublicSubscriptions);
+ willReturn(profile).given(tenantProfileCache).get(tenantId);
+
+ String sessionId = "closing-session";
+ WebSocketSessionRef sessionRef = mockPublicSessionRef(tenantId, sessionId);
+
+ service.processSubscription(sessionRef, subscriptionCmd(1));
+ service.processSubscription(sessionRef, subscriptionCmd(2));
+ service.processSubscription(sessionRef, subscriptionCmd(3));
+
+ @SuppressWarnings("unchecked")
+ ConcurrentMap> publicUserSubscriptionsMap =
+ (ConcurrentMap>) ReflectionTestUtils.getField(service, "publicUserSubscriptionsMap");
+ assertThat(publicUserSubscriptionsMap.get(tenantId)).hasSize(3);
+
+ service.processSessionClose(sessionRef);
+
+ assertThat(publicUserSubscriptionsMap.get(tenantId)).isEmpty();
+ }
+
+ @Test
+ void processSessionClose_onlyRemovesClosedSessionSubscriptions() {
+ int maxPublicSubscriptions = 10;
+ TenantId tenantId = TenantId.fromUUID(UUID.randomUUID());
+ TenantProfile profile = new TenantProfile();
+ profile.createDefaultTenantProfileData();
+ profile.getDefaultProfileConfiguration().setMaxWsSubscriptionsPerPublicUser(maxPublicSubscriptions);
+ willReturn(profile).given(tenantProfileCache).get(tenantId);
+
+ WebSocketSessionRef session1 = mockPublicSessionRef(tenantId, "session-1");
+ WebSocketSessionRef session2 = mockPublicSessionRef(tenantId, "session-2");
+
+ service.processSubscription(session1, subscriptionCmd(1));
+ service.processSubscription(session1, subscriptionCmd(2));
+ service.processSubscription(session2, subscriptionCmd(1));
+
+ @SuppressWarnings("unchecked")
+ ConcurrentMap> publicUserSubscriptionsMap =
+ (ConcurrentMap>) ReflectionTestUtils.getField(service, "publicUserSubscriptionsMap");
+ assertThat(publicUserSubscriptionsMap.get(tenantId)).hasSize(3);
+
+ service.processSessionClose(session1);
+
+ Set remaining = publicUserSubscriptionsMap.get(tenantId);
+ assertThat(remaining).hasSize(1);
+ assertThat(remaining).allMatch(subId -> subId.startsWith("[session-2]"));
+ }
+
+ private WebSocketSessionRef mockPublicSessionRef(TenantId tenantId, String sessionId) {
+ CustomerId customerId = new CustomerId(UUID.randomUUID());
+ SecurityUser securityUser = mock(SecurityUser.class);
+ willReturn(tenantId).given(securityUser).getTenantId();
+ willReturn(customerId).given(securityUser).getCustomerId();
+ willReturn(new UserId(EntityId.NULL_UUID)).given(securityUser).getId();
+ willReturn(true).given(securityUser).isCustomerUser();
+ willReturn(new UserPrincipal(UserPrincipal.Type.PUBLIC_ID, customerId.toString())).given(securityUser).getUserPrincipal();
+
+ WebSocketSessionRef ref = mock(WebSocketSessionRef.class);
+ willReturn(securityUser).given(ref).getSecurityCtx();
+ willReturn(sessionId).given(ref).getSessionId();
+ return ref;
+ }
+
+ private AttributesSubscriptionCmd subscriptionCmd(int cmdId) {
+ AttributesSubscriptionCmd cmd = new AttributesSubscriptionCmd();
+ cmd.setCmdId(cmdId);
+ return cmd;
+ }
+
+}
diff --git a/application/src/test/java/org/thingsboard/server/system/SystemPatchApplierTest.java b/application/src/test/java/org/thingsboard/server/system/SystemPatchApplierTest.java
index 67e5e946c8..51e35bd999 100644
--- a/application/src/test/java/org/thingsboard/server/system/SystemPatchApplierTest.java
+++ b/application/src/test/java/org/thingsboard/server/system/SystemPatchApplierTest.java
@@ -32,12 +32,16 @@ import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.id.WidgetTypeId;
import org.thingsboard.server.common.data.widget.WidgetTypeDetails;
+import org.thingsboard.server.dao.resource.ImageService;
import org.thingsboard.server.dao.widget.WidgetTypeService;
+import org.thingsboard.server.service.install.DatabaseSchemaSettingsService;
import org.thingsboard.server.service.install.InstallScripts;
import org.thingsboard.server.service.system.SystemPatchApplier;
import java.nio.file.Files;
import java.nio.file.Path;
+import java.util.Collections;
+import java.util.Set;
import java.util.UUID;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
@@ -71,9 +75,15 @@ public class SystemPatchApplierTest {
@Mock
private InstallScripts installScripts;
+ @Mock
+ private DatabaseSchemaSettingsService schemaSettingsService;
+
@Mock
private WidgetTypeService widgetTypeService;
+ @Mock
+ private ImageService imageService;
+
@InjectMocks
private SystemPatchApplier reconciler;
@@ -373,6 +383,247 @@ public class SystemPatchApplierTest {
verify(widgetTypeService, times(1)).saveWidgetType(any());
}
+ // --- isVersionIncreased tests ---
+
+ @ParameterizedTest(name = "isVersionIncreased: {0} (package={1}, db={2}) -> {3}")
+ @MethodSource("provideVersionComparisonTestCases")
+ void testIsVersionIncreased(String testName, SystemPatchApplier.VersionInfo packageVersion,
+ SystemPatchApplier.VersionInfo dbVersion, boolean expected) {
+ Boolean result = ReflectionTestUtils.invokeMethod(reconciler, "isVersionIncreased", packageVersion, dbVersion);
+ assertEquals(expected, result, testName);
+ }
+
+ private static Stream provideVersionComparisonTestCases() {
+ return Stream.of(
+ // Maintenance digit increases within same LTS family
+ Arguments.of("maintenance increased",
+ new SystemPatchApplier.VersionInfo(4, 3, 1, 0),
+ new SystemPatchApplier.VersionInfo(4, 3, 0, 0), true),
+ Arguments.of("maintenance increased by more than one",
+ new SystemPatchApplier.VersionInfo(4, 3, 3, 0),
+ new SystemPatchApplier.VersionInfo(4, 3, 0, 0), true),
+
+ // Patch digit increases within same maintenance
+ Arguments.of("patch increased",
+ new SystemPatchApplier.VersionInfo(4, 3, 0, 1),
+ new SystemPatchApplier.VersionInfo(4, 3, 0, 0), true),
+ Arguments.of("patch increased by more than one",
+ new SystemPatchApplier.VersionInfo(4, 3, 0, 5),
+ new SystemPatchApplier.VersionInfo(4, 3, 0, 2), true),
+
+ // Both maintenance and patch increased
+ Arguments.of("maintenance and patch both increased",
+ new SystemPatchApplier.VersionInfo(4, 3, 1, 1),
+ new SystemPatchApplier.VersionInfo(4, 3, 0, 0), true),
+
+ // Maintenance increased, patch value is lower (irrelevant — maintenance wins)
+ Arguments.of("maintenance increased, patch is lower",
+ new SystemPatchApplier.VersionInfo(4, 3, 2, 0),
+ new SystemPatchApplier.VersionInfo(4, 3, 1, 5), true),
+
+ // Same version — no increase
+ Arguments.of("same version",
+ new SystemPatchApplier.VersionInfo(4, 3, 0, 0),
+ new SystemPatchApplier.VersionInfo(4, 3, 0, 0), false),
+ Arguments.of("same version with non-zero parts",
+ new SystemPatchApplier.VersionInfo(4, 3, 1, 2),
+ new SystemPatchApplier.VersionInfo(4, 3, 1, 2), false),
+
+ // Decreased versions — no increase
+ Arguments.of("maintenance decreased",
+ new SystemPatchApplier.VersionInfo(4, 3, 0, 0),
+ new SystemPatchApplier.VersionInfo(4, 3, 1, 0), false),
+ Arguments.of("patch decreased",
+ new SystemPatchApplier.VersionInfo(4, 3, 0, 0),
+ new SystemPatchApplier.VersionInfo(4, 3, 0, 1), false),
+
+ // Different major — different family, skip
+ Arguments.of("different major",
+ new SystemPatchApplier.VersionInfo(5, 3, 0, 0),
+ new SystemPatchApplier.VersionInfo(4, 3, 0, 0), false),
+ Arguments.of("major decreased",
+ new SystemPatchApplier.VersionInfo(3, 3, 0, 0),
+ new SystemPatchApplier.VersionInfo(4, 3, 0, 0), false),
+
+ // Different minor — different LTS family, skip
+ Arguments.of("minor increased (different LTS family)",
+ new SystemPatchApplier.VersionInfo(4, 4, 0, 0),
+ new SystemPatchApplier.VersionInfo(4, 3, 0, 0), false),
+ Arguments.of("minor decreased",
+ new SystemPatchApplier.VersionInfo(4, 2, 0, 0),
+ new SystemPatchApplier.VersionInfo(4, 3, 0, 0), false)
+ );
+ }
+
+ // --- isVersionChanged tests ---
+
+ @Test
+ void whenVersionIncreased_thenVersionChangedReturnsTrue() {
+ when(schemaSettingsService.getPackageSchemaVersion()).thenReturn("4.3.1.0");
+ when(schemaSettingsService.getDbSchemaVersion()).thenReturn("4.3.0.0");
+
+ Boolean result = ReflectionTestUtils.invokeMethod(reconciler, "isVersionChanged");
+
+ assertTrue(result);
+ }
+
+ @Test
+ void whenVersionNotIncreased_thenVersionChangedReturnsFalse() {
+ when(schemaSettingsService.getPackageSchemaVersion()).thenReturn("4.3.0.0");
+ when(schemaSettingsService.getDbSchemaVersion()).thenReturn("4.3.0.0");
+
+ Boolean result = ReflectionTestUtils.invokeMethod(reconciler, "isVersionChanged");
+
+ assertFalse(result);
+ }
+
+ @Test
+ void whenVersionUnparseable_thenVersionChangedReturnsFalse() {
+ when(schemaSettingsService.getPackageSchemaVersion()).thenReturn("invalid");
+ when(schemaSettingsService.getDbSchemaVersion()).thenReturn("4.3.0.0");
+
+ Boolean result = ReflectionTestUtils.invokeMethod(reconciler, "isVersionChanged");
+
+ assertFalse(result);
+ }
+
+ @Test
+ void whenDbVersionUnparseable_thenVersionChangedReturnsFalse() {
+ when(schemaSettingsService.getPackageSchemaVersion()).thenReturn("4.3.1.0");
+ when(schemaSettingsService.getDbSchemaVersion()).thenReturn("bad");
+
+ Boolean result = ReflectionTestUtils.invokeMethod(reconciler, "isVersionChanged");
+
+ assertFalse(result);
+ }
+
+ // --- updateLtsSqlSchema tests ---
+
+ @Test
+ void whenLtsSqlFileExists_thenExecutesSql() throws Exception {
+ Path dataDir = tempDir.resolve("data");
+ Path ltsDir = dataDir.resolve("upgrade").resolve("lts");
+ Files.createDirectories(ltsDir);
+ Files.writeString(ltsDir.resolve("schema_update.sql"), "ALTER TABLE device ADD COLUMN IF NOT EXISTS test_col VARCHAR(255);");
+ when(installScripts.getDataDir()).thenReturn(dataDir.toString());
+
+ ReflectionTestUtils.invokeMethod(reconciler, "updateLtsSqlSchema");
+
+ verify(jdbcTemplate).execute("ALTER TABLE device ADD COLUMN IF NOT EXISTS test_col VARCHAR(255);");
+ }
+
+ @Test
+ void whenLtsSqlFileDoesNotExist_thenSkips() {
+ Path dataDir = tempDir.resolve("data");
+ // Don't create the file
+ when(installScripts.getDataDir()).thenReturn(dataDir.toString());
+
+ ReflectionTestUtils.invokeMethod(reconciler, "updateLtsSqlSchema");
+
+ verify(jdbcTemplate, never()).execute(anyString());
+ }
+
+ @Test
+ void whenLtsSqlFileHasMultipleStatements_thenExecutesAll() throws Exception {
+ Path dataDir = tempDir.resolve("data");
+ Path ltsDir = dataDir.resolve("upgrade").resolve("lts");
+ Files.createDirectories(ltsDir);
+ String sql = "DO $$ BEGIN\n" +
+ " IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'test_type') THEN\n" +
+ " CREATE TYPE test_type AS ENUM ('A', 'B');\n" +
+ " END IF;\n" +
+ "END $$;\n" +
+ "ALTER TABLE device ADD COLUMN IF NOT EXISTS test_col VARCHAR(255);";
+ Files.writeString(ltsDir.resolve("schema_update.sql"), sql);
+ when(installScripts.getDataDir()).thenReturn(dataDir.toString());
+
+ ReflectionTestUtils.invokeMethod(reconciler, "updateLtsSqlSchema");
+
+ verify(jdbcTemplate).execute(sql);
+ }
+
+ // --- applyPatchIfNeeded flow tests ---
+
+ @Test
+ void whenVersionIncreased_thenAppliesLtsSqlBeforeViewsAndWidgets() throws Exception {
+ when(schemaSettingsService.getPackageSchemaVersion()).thenReturn("4.3.1.0");
+ when(schemaSettingsService.getDbSchemaVersion()).thenReturn("4.3.0.0");
+ when(jdbcTemplate.queryForObject(contains("pg_try_advisory_lock"), eq(Boolean.class), anyLong())).thenReturn(true);
+ when(jdbcTemplate.queryForObject(contains("pg_advisory_unlock"), eq(Boolean.class), anyLong())).thenReturn(true);
+
+ Path dataDir = tempDir.resolve("data");
+ Path ltsDir = dataDir.resolve("upgrade").resolve("lts");
+ Files.createDirectories(ltsDir);
+ Files.writeString(ltsDir.resolve("schema_update.sql"), "SELECT 1;");
+ when(installScripts.getDataDir()).thenReturn(dataDir.toString());
+
+ Path widgetTypesDir = tempDir.resolve("widget_types");
+ Files.createDirectories(widgetTypesDir);
+ when(installScripts.getWidgetTypesDir()).thenReturn(widgetTypesDir);
+
+ ReflectionTestUtils.invokeMethod(reconciler, "applyPatchIfNeeded");
+
+ // LTS SQL was executed
+ verify(jdbcTemplate).execute("SELECT 1;");
+ // Schema version was updated
+ verify(schemaSettingsService).updateSchemaVersion();
+ }
+
+ @Test
+ void whenVersionNotIncreased_thenSkipsEverything() {
+ when(schemaSettingsService.getPackageSchemaVersion()).thenReturn("4.3.0.0");
+ when(schemaSettingsService.getDbSchemaVersion()).thenReturn("4.3.0.0");
+
+ ReflectionTestUtils.invokeMethod(reconciler, "applyPatchIfNeeded");
+
+ // No lock acquired
+ verify(jdbcTemplate, never()).queryForObject(contains("pg_try_advisory_lock"), eq(Boolean.class), anyLong());
+ // No schema update
+ verify(schemaSettingsService, never()).updateSchemaVersion();
+ }
+
+ @Test
+ void whenLockNotAcquired_thenSkipsPatchApplication() {
+ when(schemaSettingsService.getPackageSchemaVersion()).thenReturn("4.3.1.0");
+ when(schemaSettingsService.getDbSchemaVersion()).thenReturn("4.3.0.0");
+ when(jdbcTemplate.queryForObject(contains("pg_try_advisory_lock"), eq(Boolean.class), anyLong())).thenReturn(false);
+
+ ReflectionTestUtils.invokeMethod(reconciler, "applyPatchIfNeeded");
+
+ verify(schemaSettingsService, never()).updateSchemaVersion();
+ verify(jdbcTemplate, never()).execute(anyString());
+ }
+
+ @Test
+ void whenMaintenanceVersionIncreased_thenAppliesPatch() throws Exception {
+ when(schemaSettingsService.getPackageSchemaVersion()).thenReturn("4.3.2.0");
+ when(schemaSettingsService.getDbSchemaVersion()).thenReturn("4.3.1.0");
+ when(jdbcTemplate.queryForObject(contains("pg_try_advisory_lock"), eq(Boolean.class), anyLong())).thenReturn(true);
+ when(jdbcTemplate.queryForObject(contains("pg_advisory_unlock"), eq(Boolean.class), anyLong())).thenReturn(true);
+
+ Path dataDir = tempDir.resolve("data");
+ when(installScripts.getDataDir()).thenReturn(dataDir.toString());
+
+ Path widgetTypesDir = tempDir.resolve("widget_types");
+ Files.createDirectories(widgetTypesDir);
+ when(installScripts.getWidgetTypesDir()).thenReturn(widgetTypesDir);
+
+ ReflectionTestUtils.invokeMethod(reconciler, "applyPatchIfNeeded");
+
+ verify(schemaSettingsService).updateSchemaVersion();
+ }
+
+ @Test
+ void whenDifferentLtsFamily_thenSkipsPatch() {
+ when(schemaSettingsService.getPackageSchemaVersion()).thenReturn("4.4.0.0");
+ when(schemaSettingsService.getDbSchemaVersion()).thenReturn("4.3.0.0");
+
+ ReflectionTestUtils.invokeMethod(reconciler, "applyPatchIfNeeded");
+
+ verify(jdbcTemplate, never()).queryForObject(contains("pg_try_advisory_lock"), eq(Boolean.class), anyLong());
+ verify(schemaSettingsService, never()).updateSchemaVersion();
+ }
+
private static Stream provideDescriptorComparisonTestCases() {
return Stream.of(
Arguments.of("Both null", null, null, true),
@@ -407,4 +658,200 @@ public class SystemPatchApplierTest {
return widget;
}
+ // --- createMissingSystemImages tests ---
+
+ @Test
+ void whenImagesDirDoesNotExist_thenReturnsZeroAndDoesNotCallImageService() {
+ Path dataDir = tempDir.resolve("data");
+ // Intentionally do not create resources/images dir
+ when(installScripts.getDataDir()).thenReturn(dataDir.toString());
+
+ Integer created = ReflectionTestUtils.invokeMethod(reconciler, "createMissingSystemImages");
+
+ assertEquals(0, created);
+ verify(imageService, never()).getAllImageKeysByTenantId(any());
+ verify(imageService, never()).createOrUpdateSystemImage(anyString(), any(byte[].class));
+ }
+
+ @Test
+ void whenImagesDirIsEmpty_thenReturnsZeroAndDoesNotCallImageService() throws Exception {
+ Path imagesDir = tempDir.resolve("data").resolve(InstallScripts.RESOURCES_DIR).resolve("images");
+ Files.createDirectories(imagesDir);
+ when(installScripts.getDataDir()).thenReturn(tempDir.resolve("data").toString());
+ when(imageService.getAllImageKeysByTenantId(TenantId.SYS_TENANT_ID)).thenReturn(Collections.emptySet());
+
+ Integer created = ReflectionTestUtils.invokeMethod(reconciler, "createMissingSystemImages");
+
+ assertEquals(0, created);
+ verify(imageService, never()).createOrUpdateSystemImage(anyString(), any(byte[].class));
+ }
+
+ @Test
+ void whenSystemImageDoesNotExistInDb_thenCreateIt() throws Exception {
+ Path imagesDir = tempDir.resolve("data").resolve(InstallScripts.RESOURCES_DIR).resolve("images");
+ Files.createDirectories(imagesDir);
+ when(installScripts.getDataDir()).thenReturn(tempDir.resolve("data").toString());
+
+ byte[] imageBytes = new byte[]{1, 2, 3, 4, 5};
+ Files.write(imagesDir.resolve("gateway.png"), imageBytes);
+
+ when(imageService.getAllImageKeysByTenantId(TenantId.SYS_TENANT_ID)).thenReturn(Collections.emptySet());
+
+ Integer created = ReflectionTestUtils.invokeMethod(reconciler, "createMissingSystemImages");
+
+ assertEquals(1, created);
+ verify(imageService).getAllImageKeysByTenantId(TenantId.SYS_TENANT_ID);
+ verify(imageService).createOrUpdateSystemImage(eq("gateway.png"), eq(imageBytes));
+ }
+
+ @Test
+ void whenSystemImageExistsInDb_thenSkipIt() throws Exception {
+ Path imagesDir = tempDir.resolve("data").resolve(InstallScripts.RESOURCES_DIR).resolve("images");
+ Files.createDirectories(imagesDir);
+ when(installScripts.getDataDir()).thenReturn(tempDir.resolve("data").toString());
+
+ Files.write(imagesDir.resolve("gateway.png"), new byte[]{1, 2, 3});
+
+ when(imageService.getAllImageKeysByTenantId(TenantId.SYS_TENANT_ID)).thenReturn(Set.of("gateway.png"));
+
+ Integer created = ReflectionTestUtils.invokeMethod(reconciler, "createMissingSystemImages");
+
+ assertEquals(0, created);
+ verify(imageService).getAllImageKeysByTenantId(TenantId.SYS_TENANT_ID);
+ verify(imageService, never()).createOrUpdateSystemImage(anyString(), any(byte[].class));
+ }
+
+ @Test
+ void whenMixOfNewAndExistingImages_thenOnlyCreateMissingOnes() throws Exception {
+ Path imagesDir = tempDir.resolve("data").resolve(InstallScripts.RESOURCES_DIR).resolve("images");
+ Files.createDirectories(imagesDir);
+ when(installScripts.getDataDir()).thenReturn(tempDir.resolve("data").toString());
+
+ byte[] newImageBytes = new byte[]{9, 9, 9};
+ byte[] existingImageBytes = new byte[]{1, 1, 1};
+ Files.write(imagesDir.resolve("new.png"), newImageBytes);
+ Files.write(imagesDir.resolve("existing.svg"), existingImageBytes);
+
+ when(imageService.getAllImageKeysByTenantId(TenantId.SYS_TENANT_ID)).thenReturn(Set.of("existing.svg"));
+
+ Integer created = ReflectionTestUtils.invokeMethod(reconciler, "createMissingSystemImages");
+
+ assertEquals(1, created);
+ verify(imageService, times(1)).getAllImageKeysByTenantId(TenantId.SYS_TENANT_ID);
+ verify(imageService).createOrUpdateSystemImage(eq("new.png"), eq(newImageBytes));
+ verify(imageService, never()).createOrUpdateSystemImage(eq("existing.svg"), any(byte[].class));
+ }
+
+ @Test
+ void whenImagesDirContainsSubdirectory_thenSubdirectoryIsIgnored() throws Exception {
+ Path imagesDir = tempDir.resolve("data").resolve(InstallScripts.RESOURCES_DIR).resolve("images");
+ Files.createDirectories(imagesDir);
+ Files.createDirectories(imagesDir.resolve("nested"));
+ when(installScripts.getDataDir()).thenReturn(tempDir.resolve("data").toString());
+
+ byte[] imageBytes = new byte[]{5, 6, 7};
+ Files.write(imagesDir.resolve("logo.png"), imageBytes);
+
+ when(imageService.getAllImageKeysByTenantId(TenantId.SYS_TENANT_ID)).thenReturn(Collections.emptySet());
+
+ Integer created = ReflectionTestUtils.invokeMethod(reconciler, "createMissingSystemImages");
+
+ assertEquals(1, created);
+ verify(imageService).createOrUpdateSystemImage(eq("logo.png"), eq(imageBytes));
+ verify(imageService, never()).createOrUpdateSystemImage(eq("nested"), any(byte[].class));
+ }
+
+ @Test
+ void whenMultipleNewImages_thenCreatesAll() throws Exception {
+ Path imagesDir = tempDir.resolve("data").resolve(InstallScripts.RESOURCES_DIR).resolve("images");
+ Files.createDirectories(imagesDir);
+ when(installScripts.getDataDir()).thenReturn(tempDir.resolve("data").toString());
+
+ Files.write(imagesDir.resolve("a.png"), new byte[]{1});
+ Files.write(imagesDir.resolve("b.svg"), new byte[]{2});
+ Files.write(imagesDir.resolve("c.jpg"), new byte[]{3});
+
+ when(imageService.getAllImageKeysByTenantId(TenantId.SYS_TENANT_ID)).thenReturn(Collections.emptySet());
+
+ Integer created = ReflectionTestUtils.invokeMethod(reconciler, "createMissingSystemImages");
+
+ assertEquals(3, created);
+ verify(imageService, times(1)).getAllImageKeysByTenantId(TenantId.SYS_TENANT_ID);
+ verify(imageService).createOrUpdateSystemImage(eq("a.png"), any(byte[].class));
+ verify(imageService).createOrUpdateSystemImage(eq("b.svg"), any(byte[].class));
+ verify(imageService).createOrUpdateSystemImage(eq("c.jpg"), any(byte[].class));
+ }
+
+ @Test
+ void whenImageServiceThrows_thenWrapsAndPropagates() throws Exception {
+ Path imagesDir = tempDir.resolve("data").resolve(InstallScripts.RESOURCES_DIR).resolve("images");
+ Files.createDirectories(imagesDir);
+ when(installScripts.getDataDir()).thenReturn(tempDir.resolve("data").toString());
+
+ Files.write(imagesDir.resolve("broken.png"), new byte[]{1, 2});
+
+ when(imageService.getAllImageKeysByTenantId(TenantId.SYS_TENANT_ID)).thenReturn(Collections.emptySet());
+ when(imageService.createOrUpdateSystemImage(eq("broken.png"), any(byte[].class)))
+ .thenThrow(new RuntimeException("DB error"));
+
+ RuntimeException thrown = assertThrows(RuntimeException.class,
+ () -> ReflectionTestUtils.invokeMethod(reconciler, "createMissingSystemImages"));
+ assertTrue(thrown.getMessage().contains("broken.png"));
+ }
+
+ @Test
+ void whenExistingKeysLookupFails_thenDoesNotCreateImage() throws Exception {
+ Path imagesDir = tempDir.resolve("data").resolve(InstallScripts.RESOURCES_DIR).resolve("images");
+ Files.createDirectories(imagesDir);
+ when(installScripts.getDataDir()).thenReturn(tempDir.resolve("data").toString());
+
+ Files.write(imagesDir.resolve("img.png"), new byte[]{1});
+
+ when(imageService.getAllImageKeysByTenantId(TenantId.SYS_TENANT_ID))
+ .thenThrow(new RuntimeException("lookup failed"));
+
+ assertThrows(RuntimeException.class,
+ () -> ReflectionTestUtils.invokeMethod(reconciler, "createMissingSystemImages"));
+ verify(imageService, never()).createOrUpdateSystemImage(anyString(), any(byte[].class));
+ }
+
+ // --- applyPatchIfNeeded integration with createMissingSystemImages ---
+
+ @Test
+ void whenApplyPatchIfNeededRuns_thenCreatesMissingImagesAfterWidgets() throws Exception {
+ when(schemaSettingsService.getPackageSchemaVersion()).thenReturn("4.3.1.0");
+ when(schemaSettingsService.getDbSchemaVersion()).thenReturn("4.3.0.0");
+ when(jdbcTemplate.queryForObject(contains("pg_try_advisory_lock"), eq(Boolean.class), anyLong())).thenReturn(true);
+ when(jdbcTemplate.queryForObject(contains("pg_advisory_unlock"), eq(Boolean.class), anyLong())).thenReturn(true);
+
+ Path dataDir = tempDir.resolve("data");
+ Path imagesDir = dataDir.resolve(InstallScripts.RESOURCES_DIR).resolve("images");
+ Files.createDirectories(imagesDir);
+ byte[] imgBytes = new byte[]{7, 7, 7};
+ Files.write(imagesDir.resolve("new_icon.svg"), imgBytes);
+ when(installScripts.getDataDir()).thenReturn(dataDir.toString());
+
+ Path widgetTypesDir = tempDir.resolve("widget_types");
+ Files.createDirectories(widgetTypesDir);
+ when(installScripts.getWidgetTypesDir()).thenReturn(widgetTypesDir);
+
+ when(imageService.getAllImageKeysByTenantId(TenantId.SYS_TENANT_ID)).thenReturn(Collections.emptySet());
+
+ ReflectionTestUtils.invokeMethod(reconciler, "applyPatchIfNeeded");
+
+ verify(imageService).createOrUpdateSystemImage(eq("new_icon.svg"), eq(imgBytes));
+ verify(schemaSettingsService).updateSchemaVersion();
+ }
+
+ @Test
+ void whenVersionNotIncreased_thenImagesAreNotTouched() {
+ when(schemaSettingsService.getPackageSchemaVersion()).thenReturn("4.3.0.0");
+ when(schemaSettingsService.getDbSchemaVersion()).thenReturn("4.3.0.0");
+
+ ReflectionTestUtils.invokeMethod(reconciler, "applyPatchIfNeeded");
+
+ verify(imageService, never()).getAllImageKeysByTenantId(any());
+ verify(imageService, never()).createOrUpdateSystemImage(anyString(), any(byte[].class));
+ }
+
}
diff --git a/common/actor/pom.xml b/common/actor/pom.xml
index e4d566a779..4d5fff8157 100644
--- a/common/actor/pom.xml
+++ b/common/actor/pom.xml
@@ -20,7 +20,7 @@
4.0.0
org.thingsboard
- 4.2.2.1-SNAPSHOT
+ 4.2.2.2-SNAPSHOT
common
org.thingsboard.common
diff --git a/common/cache/pom.xml b/common/cache/pom.xml
index 4d4ece7c83..f3f4f47bb9 100644
--- a/common/cache/pom.xml
+++ b/common/cache/pom.xml
@@ -20,7 +20,7 @@
4.0.0
org.thingsboard
- 4.2.2.1-SNAPSHOT
+ 4.2.2.2-SNAPSHOT
common
org.thingsboard.common
diff --git a/common/cache/src/main/java/org/thingsboard/server/cache/TbJsonRedisSerializer.java b/common/cache/src/main/java/org/thingsboard/server/cache/TbJsonRedisSerializer.java
index bf5fb7e448..3e8aef83a1 100644
--- a/common/cache/src/main/java/org/thingsboard/server/cache/TbJsonRedisSerializer.java
+++ b/common/cache/src/main/java/org/thingsboard/server/cache/TbJsonRedisSerializer.java
@@ -18,6 +18,8 @@ package org.thingsboard.server.cache;
import org.springframework.data.redis.serializer.SerializationException;
import org.thingsboard.common.util.JacksonUtil;
+import java.io.IOException;
+
public class TbJsonRedisSerializer implements TbRedisSerializer {
private final Class clazz;
@@ -33,6 +35,13 @@ public class TbJsonRedisSerializer implements TbRedisSerializer {
@Override
public V deserialize(K key, byte[] bytes) throws SerializationException {
- return JacksonUtil.fromBytes(bytes, clazz);
+ if (bytes == null) {
+ return null;
+ }
+ try {
+ return JacksonUtil.IGNORE_UNKNOWN_PROPERTIES_JSON_MAPPER.readValue(bytes, clazz);
+ } catch (IOException e) {
+ throw new SerializationException("Failed to deserialize cached value", e);
+ }
}
}
diff --git a/common/cluster-api/pom.xml b/common/cluster-api/pom.xml
index 4fcde696b2..bd0c1e6016 100644
--- a/common/cluster-api/pom.xml
+++ b/common/cluster-api/pom.xml
@@ -20,7 +20,7 @@
4.0.0
org.thingsboard
- 4.2.2.1-SNAPSHOT
+ 4.2.2.2-SNAPSHOT
common
org.thingsboard.common
diff --git a/common/coap-server/pom.xml b/common/coap-server/pom.xml
index d3d62ddade..55f2ee4896 100644
--- a/common/coap-server/pom.xml
+++ b/common/coap-server/pom.xml
@@ -22,7 +22,7 @@
4.0.0
org.thingsboard
- 4.2.2.1-SNAPSHOT
+ 4.2.2.2-SNAPSHOT
common
org.thingsboard.common
diff --git a/common/dao-api/pom.xml b/common/dao-api/pom.xml
index 43f441b5a2..f2a1fabfbd 100644
--- a/common/dao-api/pom.xml
+++ b/common/dao-api/pom.xml
@@ -20,7 +20,7 @@
4.0.0
org.thingsboard
- 4.2.2.1-SNAPSHOT
+ 4.2.2.2-SNAPSHOT
common
org.thingsboard.common
diff --git a/common/dao-api/src/main/java/org/thingsboard/server/dao/resource/ImageService.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/resource/ImageService.java
index f412c5488a..406263aaed 100644
--- a/common/dao-api/src/main/java/org/thingsboard/server/dao/resource/ImageService.java
+++ b/common/dao-api/src/main/java/org/thingsboard/server/dao/resource/ImageService.java
@@ -29,6 +29,7 @@ import org.thingsboard.server.common.data.page.PageLink;
import org.thingsboard.server.common.data.widget.WidgetTypeDetails;
import java.util.Collection;
+import java.util.Set;
public interface ImageService {
@@ -38,6 +39,8 @@ public interface ImageService {
TbResourceInfo getImageInfoByTenantIdAndKey(TenantId tenantId, String key);
+ Set getAllImageKeysByTenantId(TenantId tenantId);
+
TbResourceInfo getPublicImageInfoByKey(String publicResourceKey);
PageData getImagesByTenantId(TenantId tenantId, ResourceSubType imageSubType, PageLink pageLink);
diff --git a/common/data/pom.xml b/common/data/pom.xml
index 9e1d24ef26..e14e7072ea 100644
--- a/common/data/pom.xml
+++ b/common/data/pom.xml
@@ -20,7 +20,7 @@
4.0.0
org.thingsboard
- 4.2.2.1-SNAPSHOT
+ 4.2.2.2-SNAPSHOT
common
org.thingsboard.common
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/alarm/AlarmCreateOrUpdateActiveRequest.java b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/AlarmCreateOrUpdateActiveRequest.java
index c456323120..65e943c574 100644
--- a/common/data/src/main/java/org/thingsboard/server/common/data/alarm/AlarmCreateOrUpdateActiveRequest.java
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/AlarmCreateOrUpdateActiveRequest.java
@@ -39,6 +39,7 @@ public class AlarmCreateOrUpdateActiveRequest implements AlarmModificationReques
private TenantId tenantId;
@Schema(description = "JSON object with Customer Id", accessMode = Schema.AccessMode.READ_ONLY)
private CustomerId customerId;
+ @NoXss
@NotNull
@Schema(requiredMode = Schema.RequiredMode.REQUIRED, description = "representing type of the Alarm", example = "High Temperature Alarm")
@Length(fieldName = "type")
diff --git a/common/discovery-api/pom.xml b/common/discovery-api/pom.xml
index 3df2e3f204..0d75e57b9a 100644
--- a/common/discovery-api/pom.xml
+++ b/common/discovery-api/pom.xml
@@ -20,7 +20,7 @@
4.0.0
org.thingsboard
- 4.2.2.1-SNAPSHOT
+ 4.2.2.2-SNAPSHOT
common
org.thingsboard.common
diff --git a/common/edge-api/pom.xml b/common/edge-api/pom.xml
index 1310f2cd69..bb0d5cb192 100644
--- a/common/edge-api/pom.xml
+++ b/common/edge-api/pom.xml
@@ -20,7 +20,7 @@
4.0.0
org.thingsboard
- 4.2.2.1-SNAPSHOT
+ 4.2.2.2-SNAPSHOT
common
org.thingsboard.common
diff --git a/common/edge-api/src/main/proto/edge.proto b/common/edge-api/src/main/proto/edge.proto
index e2f565c7a3..70f90cc45b 100644
--- a/common/edge-api/src/main/proto/edge.proto
+++ b/common/edge-api/src/main/proto/edge.proto
@@ -47,6 +47,7 @@ enum EdgeVersion {
V_4_2_1_2 = 14;
V_4_2_2 = 4220;
V_4_2_2_1 = 4221;
+ V_4_2_2_2 = 4222;
V_LATEST = 99999;
}
diff --git a/common/edqs/pom.xml b/common/edqs/pom.xml
index 56d2f2cb0c..4b2cd36592 100644
--- a/common/edqs/pom.xml
+++ b/common/edqs/pom.xml
@@ -20,7 +20,7 @@
4.0.0
org.thingsboard
- 4.2.2.1-SNAPSHOT
+ 4.2.2.2-SNAPSHOT
common
org.thingsboard.common
diff --git a/common/message/pom.xml b/common/message/pom.xml
index 6221bd74b3..69c4e72e2a 100644
--- a/common/message/pom.xml
+++ b/common/message/pom.xml
@@ -20,7 +20,7 @@
4.0.0
org.thingsboard
- 4.2.2.1-SNAPSHOT
+ 4.2.2.2-SNAPSHOT
common
org.thingsboard.common
diff --git a/common/message/src/main/java/org/thingsboard/server/common/msg/TbMsg.java b/common/message/src/main/java/org/thingsboard/server/common/msg/TbMsg.java
index 0830e12154..79059a9a56 100644
--- a/common/message/src/main/java/org/thingsboard/server/common/msg/TbMsg.java
+++ b/common/message/src/main/java/org/thingsboard/server/common/msg/TbMsg.java
@@ -204,18 +204,6 @@ public final class TbMsg implements Serializable {
return builder.build();
}
- @Deprecated(forRemoval = true, since = "4.1") // to be removed in 4.2
- public static TbMsg fromProto(String queueName, TbMsgProto proto, ByteString data, TbMsgCallback callback) {
- try {
- if (!data.isEmpty()) {
- proto = TbMsgProto.parseFrom(data);
- }
- } catch (InvalidProtocolBufferException e) {
- throw new IllegalStateException("Could not parse protobuf for TbMsg", e);
- }
- return fromProto(queueName, proto, callback);
- }
-
public static TbMsg fromProto(String queueName, TbMsgProto proto, TbMsgCallback callback) {
TbMsgMetaData metaData = new TbMsgMetaData(proto.getMetaData().getDataMap());
EntityId entityId = EntityIdFactory.getByTypeAndUuid(proto.getEntityType(), new UUID(proto.getEntityIdMSB(), proto.getEntityIdLSB()));
diff --git a/common/pom.xml b/common/pom.xml
index f0e6936ec8..ab2388eb9d 100644
--- a/common/pom.xml
+++ b/common/pom.xml
@@ -20,7 +20,7 @@
4.0.0
org.thingsboard
- 4.2.2.1-SNAPSHOT
+ 4.2.2.2-SNAPSHOT
thingsboard
common
diff --git a/common/proto/pom.xml b/common/proto/pom.xml
index dbf6a62211..023b2bc46a 100644
--- a/common/proto/pom.xml
+++ b/common/proto/pom.xml
@@ -20,7 +20,7 @@
4.0.0
org.thingsboard
- 4.2.2.1-SNAPSHOT
+ 4.2.2.2-SNAPSHOT
common
org.thingsboard.common
diff --git a/common/proto/src/main/proto/queue.proto b/common/proto/src/main/proto/queue.proto
index 29f4777c3e..7ad4d2883d 100644
--- a/common/proto/src/main/proto/queue.proto
+++ b/common/proto/src/main/proto/queue.proto
@@ -132,7 +132,6 @@ message SessionInfoProto {
message RestApiCallResponseMsgProto {
int64 requestIdMSB = 1;
int64 requestIdLSB = 2;
- bytes response = 5 [deprecated = true];
msgqueue.TbMsgProto responseProto = 6;
}
diff --git a/common/queue/pom.xml b/common/queue/pom.xml
index 0c4364c790..a7d4d5b568 100644
--- a/common/queue/pom.xml
+++ b/common/queue/pom.xml
@@ -20,7 +20,7 @@
4.0.0
org.thingsboard
- 4.2.2.1-SNAPSHOT
+ 4.2.2.2-SNAPSHOT
common
org.thingsboard.common
diff --git a/common/script/pom.xml b/common/script/pom.xml
index 16793dc718..3527a1c695 100644
--- a/common/script/pom.xml
+++ b/common/script/pom.xml
@@ -20,7 +20,7 @@
4.0.0
org.thingsboard
- 4.2.2.1-SNAPSHOT
+ 4.2.2.2-SNAPSHOT
common
org.thingsboard.common
diff --git a/common/script/remote-js-client/pom.xml b/common/script/remote-js-client/pom.xml
index 5aa9ddbc4f..6746100ef2 100644
--- a/common/script/remote-js-client/pom.xml
+++ b/common/script/remote-js-client/pom.xml
@@ -20,7 +20,7 @@
4.0.0
org.thingsboard.common
- 4.2.2.1-SNAPSHOT
+ 4.2.2.2-SNAPSHOT
script
org.thingsboard.common.script
diff --git a/common/script/script-api/pom.xml b/common/script/script-api/pom.xml
index bc4c9192c5..6a55f3cc71 100644
--- a/common/script/script-api/pom.xml
+++ b/common/script/script-api/pom.xml
@@ -20,7 +20,7 @@
4.0.0
org.thingsboard.common
- 4.2.2.1-SNAPSHOT
+ 4.2.2.2-SNAPSHOT
script
org.thingsboard.common.script
diff --git a/common/stats/pom.xml b/common/stats/pom.xml
index 84a4dceee3..75d15a89d6 100644
--- a/common/stats/pom.xml
+++ b/common/stats/pom.xml
@@ -22,7 +22,7 @@
4.0.0
org.thingsboard
- 4.2.2.1-SNAPSHOT
+ 4.2.2.2-SNAPSHOT
common
org.thingsboard.common
diff --git a/common/transport/coap/pom.xml b/common/transport/coap/pom.xml
index 42170bdad5..3e63520ce2 100644
--- a/common/transport/coap/pom.xml
+++ b/common/transport/coap/pom.xml
@@ -20,7 +20,7 @@
4.0.0
org.thingsboard.common
- 4.2.2.1-SNAPSHOT
+ 4.2.2.2-SNAPSHOT
transport
org.thingsboard.common.transport
diff --git a/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/efento/CoapEfentoTransportResource.java b/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/efento/CoapEfentoTransportResource.java
index 708263b26a..843f857218 100644
--- a/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/efento/CoapEfentoTransportResource.java
+++ b/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/efento/CoapEfentoTransportResource.java
@@ -245,7 +245,7 @@ public class CoapEfentoTransportResource extends AbstractCoapTransportResource {
}
List getEfentoMeasurements(MeasurementsProtos.ProtoMeasurements protoMeasurements, UUID sessionId) {
- String serialNumber = CoapEfentoUtils.convertByteArrayToString(protoMeasurements.getSerialNum().toByteArray());
+ String serialNumber = CoapEfentoUtils.convertByteArrayToString(protoMeasurements.getSerialNumber().toByteArray());
boolean batteryStatus = protoMeasurements.getBatteryStatus();
int measurementPeriodBase = protoMeasurements.getMeasurementPeriodBase();
int measurementPeriodFactor = protoMeasurements.getMeasurementPeriodFactor();
@@ -258,6 +258,9 @@ public class CoapEfentoTransportResource extends AbstractCoapTransportResource {
}
Map valuesMap = new TreeMap<>();
+ // general measurements per message
+ valuesMap.put(TimeUnit.SECONDS.toMillis(channelsList.get(0).getTimestamp()), CoapEfentoUtils.setDefaultMeasurements(serialNumber, batteryStatus, nextTransmissionAtMillis, signal));
+
for (int channel = 0; channel < channelsList.size(); channel++) {
ProtoChannel protoChannel = channelsList.get(channel);
List sampleOffsetsList = protoChannel.getSampleOffsetsList();
@@ -271,6 +274,10 @@ public class CoapEfentoTransportResource extends AbstractCoapTransportResource {
long measurementPeriodMillis = TimeUnit.SECONDS.toMillis(measurementPeriod);
long startTimestampMillis = TimeUnit.SECONDS.toMillis(protoChannel.getTimestamp());
+ // measurements per channel
+ JsonObject tsValues = valuesMap.computeIfAbsent(startTimestampMillis, k -> new JsonObject());
+ tsValues.addProperty("measurement_interval_" + (channel + 1), measurementPeriod);
+
for (int i = 0; i < sampleOffsetsList.size(); i++) {
int sampleOffset = sampleOffsetsList.get(i);
if (isSensorError(sampleOffset)) {
@@ -290,22 +297,16 @@ public class CoapEfentoTransportResource extends AbstractCoapTransportResource {
}
long sampleOffsetMillis = TimeUnit.SECONDS.toMillis(sampleOffset);
long measurementTimestamp = startTimestampMillis + Math.abs(sampleOffsetMillis);
- values = valuesMap.computeIfAbsent(measurementTimestamp - 1000, k ->
- CoapEfentoUtils.setDefaultMeasurements(serialNumber, batteryStatus, measurementPeriod, nextTransmissionAtMillis, signal, k));
+ values = valuesMap.computeIfAbsent(measurementTimestamp - 1000, k -> new JsonObject());
addBinarySample(protoChannel, currentIsOk, values, channel + 1, sessionId);
} else {
long timestampMillis = startTimestampMillis + i * measurementPeriodMillis;
- values = valuesMap.computeIfAbsent(timestampMillis, k -> CoapEfentoUtils.setDefaultMeasurements(
- serialNumber, batteryStatus, measurementPeriod, nextTransmissionAtMillis, signal, k));
+ values = valuesMap.computeIfAbsent(timestampMillis, k -> new JsonObject());
addContinuesSample(protoChannel, sampleOffset, values, channel + 1, sessionId);
}
}
}
- if (CollectionUtils.isEmpty(valuesMap)) {
- throw new IllegalStateException("[" + sessionId + "]: Failed to collect Efento measurements, reason, values map is empty!");
- }
-
return valuesMap.entrySet().stream()
.map(entry -> new EfentoTelemetry(entry.getKey(), entry.getValue()))
.collect(Collectors.toList());
@@ -446,7 +447,7 @@ public class CoapEfentoTransportResource extends AbstractCoapTransportResource {
}
}
- private EfentoTelemetry getEfentoDeviceInfo(DeviceInfoProtos.ProtoDeviceInfo protoDeviceInfo) {
+ EfentoTelemetry getEfentoDeviceInfo(DeviceInfoProtos.ProtoDeviceInfo protoDeviceInfo) {
JsonObject values = new JsonObject();
values.addProperty("sw_version", protoDeviceInfo.getSwVersion());
@@ -476,41 +477,86 @@ public class CoapEfentoTransportResource extends AbstractCoapTransportResource {
//modem info
DeviceInfoProtos.ProtoModem modem = protoDeviceInfo.getModem();
- values.addProperty("modem_types", modem.getType().toString());
- values.addProperty("sc_EARNFCN_offset", modem.getParameters(0));
- values.addProperty("sc_EARFCN", modem.getParameters(1));
- values.addProperty("sc_PCI", modem.getParameters(2));
- values.addProperty("sc_Cell_id", modem.getParameters(3));
- values.addProperty("sc_RSRP", modem.getParameters(4));
- values.addProperty("sc_RSRQ", modem.getParameters(5));
- values.addProperty("sc_RSSI", modem.getParameters(6));
- values.addProperty("sc_SINR", modem.getParameters(7));
- values.addProperty("sc_Band", modem.getParameters(8));
- values.addProperty("sc_TAC", modem.getParameters(9));
- values.addProperty("sc_ECL", modem.getParameters(10));
- values.addProperty("sc_TX_PWR", modem.getParameters(11));
- values.addProperty("op_mode", modem.getParameters(12));
- values.addProperty("nc_EARFCN", modem.getParameters(13));
- values.addProperty("nc_EARNFCN_offset", modem.getParameters(14));
- values.addProperty("nc_PCI", modem.getParameters(15));
- values.addProperty("nc_RSRP", modem.getParameters(16));
- values.addProperty("RLC_UL_BLER", modem.getParameters(17));
- values.addProperty("RLC_DL_BLER", modem.getParameters(18));
- values.addProperty("MAC_UL_BLER", modem.getParameters(19));
- values.addProperty("MAC_DL_BLER", modem.getParameters(20));
- values.addProperty("MAC_UL_TOTAL_BYTES", modem.getParameters(21));
- values.addProperty("MAC_DL_TOTAL_BYTES", modem.getParameters(22));
- values.addProperty("MAC_UL_total_HARQ_Tx", modem.getParameters(23));
- values.addProperty("MAC_DL_total_HARQ_Tx", modem.getParameters(24));
- values.addProperty("MAC_UL_HARQ_re_Tx", modem.getParameters(25));
- values.addProperty("MAC_DL_HARQ_re_Tx", modem.getParameters(26));
- values.addProperty("RLC_UL_tput", modem.getParameters(27));
- values.addProperty("RLC_DL_tput", modem.getParameters(28));
- values.addProperty("MAC_UL_tput", modem.getParameters(29));
- values.addProperty("MAC_DL_tput", modem.getParameters(30));
- values.addProperty("sleep_duration", modem.getParameters(31));
- values.addProperty("rx_time", modem.getParameters(32));
- values.addProperty("tx_time", modem.getParameters(33));
+ DeviceInfoProtos.ModemType modemType = modem.getType();
+ values.addProperty("modem_types", modemType.toString());
+ values.addProperty("sim_card_identification", modem.getSimCardIdentification());
+ values.addProperty("firmware_version", modem.getFirmwareVersion().toString());
+ values.addProperty("modem_identification", modem.getModemIdentification());
+ if (modem.getModemStatisticsCount() >= 4) {
+ values.addProperty("modem_transmissions_count", modem.getModemStatistics(0));
+ values.addProperty("modem_time_since_last_devinfo", modem.getModemStatistics(1));
+ values.addProperty("modem_total_psm_time", modem.getModemStatistics(2));
+ values.addProperty("modem_total_active_time", modem.getModemStatistics(3));
+ }
+ switch (modemType) {
+ case MODEM_TYPE_BC660:
+ values.addProperty("sc_EARFCN", modem.getParameters(0));
+ values.addProperty("sc_EARNFCN_offset", modem.getParameters(1));
+ values.addProperty("sc_PCI", modem.getParameters(2));
+ values.addProperty("sc_Cell_id", modem.getParameters(3));
+ values.addProperty("sc_RSRP", modem.getParameters(4));
+ values.addProperty("sc_RSRQ", modem.getParameters(5));
+ values.addProperty("sc_RSSI", modem.getParameters(6));
+ values.addProperty("sc_SINR", modem.getParameters(7));
+ values.addProperty("sc_Band", modem.getParameters(8));
+ values.addProperty("sc_TAC", modem.getParameters(9));
+ values.addProperty("sc_ECL", modem.getParameters(10));
+ values.addProperty("sc_TX_PWR", modem.getParameters(11));
+ values.addProperty("op_mode", modem.getParameters(12));
+ values.addProperty("nc_EARFCN", modem.getParameters(13));
+ values.addProperty("nc_PCI", modem.getParameters(14));
+ values.addProperty("nc_RSRP", modem.getParameters(15));
+ values.addProperty("nc_RSRQ", modem.getParameters(16));
+ values.addProperty("sleep_duration", modem.getParameters(17));
+ values.addProperty("rx_time", modem.getParameters(18));
+ values.addProperty("tx_time", modem.getParameters(19));
+ values.addProperty("PLMN_state", modem.getParameters(20));
+ values.addProperty("select_PLMN", modem.getParameters(21));
+ break;
+ case MODEM_TYPE_SHARED_MODEM:
+ values.addProperty("RSRP", modem.getParameters(0));
+ values.addProperty("RSRQ", modem.getParameters(1));
+ values.addProperty("RSSI", modem.getParameters(2));
+ values.addProperty("SINR", modem.getParameters(3));
+ break;
+ default:
+ // MODEM_TYPE_UNSPECIFIED, MODEM_TYPE_BC66, MODEM_TYPE_BC66NA
+ values.addProperty("sc_EARNFCN_offset", modem.getParameters(0));
+ values.addProperty("sc_EARFCN", modem.getParameters(1));
+ values.addProperty("sc_PCI", modem.getParameters(2));
+ values.addProperty("sc_Cell_id", modem.getParameters(3));
+ values.addProperty("sc_RSRP", modem.getParameters(4));
+ values.addProperty("sc_RSRQ", modem.getParameters(5));
+ values.addProperty("sc_RSSI", modem.getParameters(6));
+ values.addProperty("sc_SINR", modem.getParameters(7));
+ values.addProperty("sc_Band", modem.getParameters(8));
+ values.addProperty("sc_TAC", modem.getParameters(9));
+ values.addProperty("sc_ECL", modem.getParameters(10));
+ values.addProperty("sc_TX_PWR", modem.getParameters(11));
+ values.addProperty("op_mode", modem.getParameters(12));
+ values.addProperty("nc_EARFCN", modem.getParameters(13));
+ values.addProperty("nc_EARNFCN_offset", modem.getParameters(14));
+ values.addProperty("nc_PCI", modem.getParameters(15));
+ values.addProperty("nc_RSRP", modem.getParameters(16));
+ values.addProperty("RLC_UL_BLER", modem.getParameters(17));
+ values.addProperty("RLC_DL_BLER", modem.getParameters(18));
+ values.addProperty("MAC_UL_BLER", modem.getParameters(19));
+ values.addProperty("MAC_DL_BLER", modem.getParameters(20));
+ values.addProperty("MAC_UL_TOTAL_BYTES", modem.getParameters(21));
+ values.addProperty("MAC_DL_TOTAL_BYTES", modem.getParameters(22));
+ values.addProperty("MAC_UL_total_HARQ_Tx", modem.getParameters(23));
+ values.addProperty("MAC_DL_total_HARQ_Tx", modem.getParameters(24));
+ values.addProperty("MAC_UL_HARQ_re_Tx", modem.getParameters(25));
+ values.addProperty("MAC_DL_HARQ_re_Tx", modem.getParameters(26));
+ values.addProperty("RLC_UL_tput", modem.getParameters(27));
+ values.addProperty("RLC_DL_tput", modem.getParameters(28));
+ values.addProperty("MAC_UL_tput", modem.getParameters(29));
+ values.addProperty("MAC_DL_tput", modem.getParameters(30));
+ values.addProperty("sleep_duration", modem.getParameters(31));
+ values.addProperty("rx_time", modem.getParameters(32));
+ values.addProperty("tx_time", modem.getParameters(33));
+ break;
+ }
//Runtime info
DeviceInfoProtos.ProtoRuntime runtimeInfo = protoDeviceInfo.getRuntimeInfo();
@@ -521,7 +567,7 @@ public class CoapEfentoTransportResource extends AbstractCoapTransportResource {
values.addProperty("counter_of_non_confirmable_messages_attempts", runtimeInfo.getMessageCounters(1));
values.addProperty("counter_of_succeeded_messages", runtimeInfo.getMessageCounters(2));
values.addProperty("min_battery_mcu_temp", runtimeInfo.getMinBatteryMcuTemperature());
- values.addProperty("min_battery_voltage", runtimeInfo.getMinBatteryVoltage());
+ values.addProperty("min_battery_voltage", runtimeInfo.getBatteryVoltage());
values.addProperty("min_mcu_temp", runtimeInfo.getMinMcuTemperature());
values.addProperty("runtime_errors", runtimeInfo.getRuntimeErrorsCount());
values.addProperty("up_time", runtimeInfo.getUpTime());
@@ -529,8 +575,8 @@ public class CoapEfentoTransportResource extends AbstractCoapTransportResource {
return new EfentoTelemetry(System.currentTimeMillis(), values);
}
- private JsonElement getEfentoConfiguration(byte[] bytes) throws InvalidProtocolBufferException {
- return parseString(ProtoConverter.dynamicMsgToJson(bytes, ConfigProtos.getDescriptor().getMessageTypes().get(2)));
+ JsonElement getEfentoConfiguration(byte[] bytes) throws InvalidProtocolBufferException {
+ return parseString(ProtoConverter.dynamicMsgToJson(bytes, ConfigProtos.getDescriptor().getMessageTypes().get(0)));
}
private static String getDate(long seconds) {
diff --git a/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/efento/utils/CoapEfentoUtils.java b/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/efento/utils/CoapEfentoUtils.java
index 015bf07292..1acac0ec75 100644
--- a/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/efento/utils/CoapEfentoUtils.java
+++ b/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/efento/utils/CoapEfentoUtils.java
@@ -59,14 +59,12 @@ public class CoapEfentoUtils {
return String.format("%s UTC", simpleDateFormat.format(new Date(timestampInMillis)));
}
- public static JsonObject setDefaultMeasurements(String serialNumber, boolean batteryStatus, long measurementPeriod, long nextTransmissionAtMillis, long signal, long startTimestampMillis) {
+ public static JsonObject setDefaultMeasurements(String serialNumber, boolean batteryStatus, long nextTransmissionAtMillis, long signal) {
JsonObject values = new JsonObject();
values.addProperty("serial", serialNumber);
values.addProperty("battery", batteryStatus ? "ok" : "low");
- values.addProperty("measured_at", convertTimestampToUtcString(startTimestampMillis));
values.addProperty("next_transmission_at", convertTimestampToUtcString(nextTransmissionAtMillis));
values.addProperty("signal", signal);
- values.addProperty("measurement_interval", measurementPeriod);
return values;
}
diff --git a/common/transport/coap/src/main/proto/efento/proto_config.proto b/common/transport/coap/src/main/proto/efento/proto_config.proto
index ae6d7902b3..1052d338e7 100644
--- a/common/transport/coap/src/main/proto/efento/proto_config.proto
+++ b/common/transport/coap/src/main/proto/efento/proto_config.proto
@@ -15,338 +15,471 @@
*/
syntax = "proto3";
-import "efento/proto_measurement_types.proto";
import "efento/proto_rule.proto";
+import "efento/proto_config_types.proto";
+import "efento/proto_measurement_types.proto";
option java_package = "org.thingsboard.server.gen.transport.coap";
option java_outer_classname = "ConfigProtos";
-/* Message containing optional channels control parameters */
-message ProtoOutputControlState {
-
- /* Channel index */
- uint32 channel_index = 1;
-
- /* Channel state ON/OFF. Range (1 - OFF; 2 - ON) */
- uint32 channel_state = 2;
-}
-
-/* Message containing request data for accessing calibration parameters */
-message ProtoCalibrationParameters {
-
- /* Request details. Bitmask: */
- /* - calibration_request[0:2] - requested channel number. */
- uint32 calibration_request = 1;
-
- /* Assignment of a channel. */
- uint32 channel_assignment = 2;
-
- /* Table of calibration parameters. Max size = 8. */
- repeated int32 parameters = 3;
-}
-
-enum BleAdvertisingPeriodMode {
-
- /* Invalid value */
- BLE_ADVERTISING_PERIOD_MODE_UNSPECIFIED = 0;
+message ProtoConfig {
- /* Default behavior - faster advertising when measurement period is < 15s. */
- BLE_ADVERTISING_PERIOD_MODE_DEFAULT = 1;
+ /* RESERVED FIELDS ---------------------------------------------------------------------------------------------------------- */
- /* User-configured normal interval is used. */
- BLE_ADVERTISING_PERIOD_MODE_NORMAL = 2;
+ reserved 1,48;
- /* User-configured fast interval is used. */
- BLE_ADVERTISING_PERIOD_MODE_FAST = 3;
-}
+ /* DEVICE STATUS FIELDS ----------------------------------------------------------------------------------------------------- */
-/* Message containing BLE advertising period configuration */
-message ProtoBleAdvertisingPeriod {
+ /* Serial number of the device. *
+ * Length: 6 bytes. *
+ * This field is only sent by the device. *
+ * Status: In use [06.00 - LATEST] */
+ bytes serial_number = 25;
- /* BLE advertising mode: */
- /* - 1: Default, BLE advertising interval is set to 1022.5ms or some lower value, based on continuous measurement period. */
- /* - 2: Normal, uses user-configured value from 'normal' field. */
- /* - 3: Fast, uses user-configured value from 'fast' field (must be lower than or equal to 'normal' field). */
- BleAdvertisingPeriodMode mode = 1;
+ /* Configuration payload split information: *
+ * - Values < 0 - Payload split, expect another part of the payload in the next message. *
+ * The absolute value indicates an index of the current message *
+ * - Values = 0 - Payload not split *
+ * - Values > 0 - Last part of the split payload, the value indicates the total number of the messages sent *
+ * This field is only sent by the device. *
+ * Status: In use [06.08.00 - LATEST] */
+ sint32 payload_split_info = 44;
- /* BLE advertising interval when in normal mode, configured in 0.625ms steps. */
- /* Range: [32:16384] */
- uint32 normal = 2;
+ /* Identifier of the current configuration. *
+ * The value of this field changes with every configuration change. *
+ * This field is only sent by the device. *
+ * Status: In use [07.00.00 - LATEST] / Previously as hash [06.00 - 06.xx.xx] */
+ uint32 configuration_hash = 21;
+
+ /* Timestamp when the new configuration was set. *
+ * This field is only sent by the device. *
+ * Status: In use [07.00.00 - LATEST] / Previously as hash_timestamp [06.02 - 06.xx.xx] */
+ uint32 configuration_hash_timestamp = 39;
+
+ /* Configuration errors. *
+ * Up to 20 error codes supported. *
+ * This field is only sent by the device. *
+ * Status: In use [07.00.00 - LATEST] / Previously as errors [06.00 - 06.xx.xx] */
+ repeated uint32 configuration_errors = 20;
+
+ /* Timestamp when a new configuration error was reported. *
+ * This field is only sent by the device. *
+ * Status: In use [07.00.00 - LATEST] / Previously as timestamp [06.02 - 06.xx.xx] */
+ uint32 configuration_error_timestamp = 38;
+
+ /* Measurement channel types. *
+ * This field is only sent by the device. *
+ * Status: In use [06.00 - LATEST] */
+ repeated MeasurementType channel_types = 27;
- /* BLE advertising interval when in fast mode, configured in 0.625ms steps. */
- /* Range: [32:16384] */
- uint32 fast = 3;
-}
+ /* NvM status: *
+ * - 1 - Defaults restored on CRC error: default sensor configuration restored due to the non-volatile memory configuration *
+ * data integrity failure *
+ * - 2 - NvM initialization error: changes in the sensor configuration won't be stored in the non-volatile memory, a power *
+ * reset of the sensor is required *
+ * When empty field is sent by the sensor NvM status is OK. *
+ * NvM status can be cleared by sending to the sensor value 0x7F. *
+ * Status: In use [07.00.00 - LATEST] */
+ uint32 nvm_status = 62;
+
+ /* SERVER STATUS FIELDS ----------------------------------------------------------------------------------------------------- */
+
+ /* Current time in seconds since 1st of January 1970 (epoch time). *
+ * This field is only sent by the server. *
+ * Status: In use [06.00 - LATEST] */
+ uint32 current_time = 8;
-/* Main message sent in the payload. Each field in this message is independent of the others - only parameters that should be */
-/* changed need to be sent in the payload. */
-/* If the value of a selected parameter shall not be changed, do not include it in the payload */
-message ProtoConfig {
+ /* REQUEST FIELDS ----------------------------------------------------------------------------------------------------------- */
+
+ /* Specifies whether the device should accept the configuration without functional testing (e.g., network connection). *
+ * This field is only sent by the user/server. *
+ * Status: In use [07.00.00 - LATEST] / Previously as accept_without_testing [06.00 - 06.xx.xx] */
+ bool accept_without_testing_request = 22;
+
+ /* Specifies whether to send the configuration from the sensor to the configuration endpoint. *
+ * This field is only sent by the user/server. *
+ * Status: In use [07.00.00 - LATEST] / Previously as request_configuration [06.00 - 06.xx.xx] */
+ bool configuration_request = 19;
+
+ /* Specifies whether to send the device information from the sensor to the device information endpoint. *
+ * This field is only sent by the user/server. *
+ * Status: In use [07.00.00 - LATEST] / Previously as request_device_info [06.00 - 06.xx.xx] */
+ bool device_info_request = 6;
+
+ /* Specifies whether to send the extended configuration from the sensor to the extended configuration endpoint. *
+ * This field is only sent by the user/server. *
+ * Status: In use [07.00.00 - LATEST] */
+ bool extended_configuration_request = 63;
+
+ /* Specifies, if software update is available. *
+ * This field is only sent by the user/server. *
+ * Status: In use [07.00.00 - LATEST] / Previously as request_fw_update [06.00 - 06.xx.xx] */
+ bool update_software_request = 7;
+
+ /* Device will clear all runtime errors. *
+ * This field is only sent by the user/server. *
+ * Status: In use [07.00.00 - LATEST] / Previously as request_runtime_errors_clear [06.02 - 06.xx.xx] */
+ bool clear_runtime_errors_request = 37;
+
+ /* Device will power off its cellular modem for requested number of seconds. *
+ * Range: [60:604800] (1 minute : 7 days) *
+ * This field is only sent by the user/server. *
+ * Status: In use [06.00 - LATEST] */
+ uint32 disable_modem_request = 18;
- /* DEPRECATED - Used for backward compatibility with fw versions 5.x */
- /* repeated Threshold thresholds = 1; */
-
- /* 'Measurement_period_base' and 'measurement_period_factor' define how often the measurements are taken. */
- /* Sensors of 'Continuous' type take measurement each Measurement_period_base * measurement_period_factor. */
- /* Sensors of 'Binary' type take measurement each Measurement_period_base. */
- /* For backward compatibility with versions 5.x in case of binary/mixed sensors, if the 'measurement_period_factor' is */
- /* not sent (equal to 0), then the default value '14' shall be used for period calculation. */
- /* For backward compatibility with versions 5.x in case of continues sensors, if the measurement_period_factor is */
- /* not sent (equal to 0), then the default value '1' shall be used for period calculation. */
- /* measurement period base in seconds */
- /* Range [1:65535] - minimum value can vary depends on installed sensors */
+ /* Specifies, if the modem firmware update is available. *
+ * String up to 48 characters: *
+ * - DFOTA URL - For use by BC66/BC660 modem. *
+ * This field is only sent by the user/server. *
+ * Status: In use [07.00.00 - LATEST] / Previously as modem_update_request [06.08.00 - 06.xx.xx] */
+ string update_modem_request = 45;
+
+ /* Device will erase all measurements from memory. *
+ * This field is only sent by the user/server. *
+ * Status: In use [07.00.00 - LATEST] / Previously as memory_reset_request [06.00 - 06.xx.xx] */
+ bool reset_memory_request = 30;
+
+ /* Device will restart the collection of memory statistics. *
+ * This field is only sent by the user/server. *
+ * Status: In use [07.00.00 - LATEST] */
+ bool restart_memory_stats_request = 64;
+
+ /* Specifies whether to send measurements from the sensor starting at the specified timestamp. *
+ * Tiemstamp in seconds since 1st of January 1970 (epoch time). *
+ * All previous measurements will be marked as sent. *
+ * This field is only sent by the user/server. *
+ * Status: In use [07.00.00 - LATEST] */
+ uint32 data_transfer_start_timestamp_request = 65;
+
+ /* Device will set new calibration parameters or will send the current set of parameters to the configuration endpoint. *
+ * Status: Deprecated [06.10.00 - 06.xx.xx] */
+ ProtoCalibrationParametersRequest calibration_parameters_request = 49;
+
+ /* Device will set the new output state of the output control channel pins. *
+ * Up to 3 channels supported in the one request. *
+ * This field is only sent by the user/server. *
+ * Status: In use [07.00.00 - LATEST] / Previously as output_control_state_request [06.13.00/06.21.00 - 06.xx.xx] */
+ repeated ProtoOutputControlState set_output_control_state_request = 58;
+
+ /* MEASUREMENTS CONFIGURATION ----------------------------------------------------------------------------------------------- */
+
+ /* Measurement period defines how often the measurements are to be taken. *
+ * Sensors of 'Continuous' type take measurement each 'measurement_period_base' * 'measurement_period_factor'. *
+ * Sensors of 'Binary' type take measurement each 'measurement_period_base'. *
+ * For backward compatibility with versions 5.xx in case of 'Binary/Mixed' sensors, if the 'measurement_period_factor' is *
+ * not sent (equal to 0), then the default value '14' shall be used for the period calculation. *
+ * For backward compatibility with versions 5.xx in case of 'Continuous' sensors, if the 'measurement_period_factor' is *
+ * not sent (equal to 0), then the default value '1' shall be used for the period calculation. */
+
+ /* Measurement period base in seconds. *
+ * Range: [1:65535] (minimum value may vary depending on sensors installed) *
+ * Group: Measurement Period *
+ * Status: In use [06.00 - LATEST] */
uint32 measurement_period_base = 2;
- /* Measurement period factor */
- /* Range [1:65535] - minimum value can vary depends on installed sensors */
+ /* Measurement period factor. *
+ * Range: [1:65535] (minimum value may vary depending on sensors installed) *
+ * Group: Measurement Period *
+ * Status: In use [06.00 - LATEST] */
uint32 measurement_period_factor = 26;
- /* Transmission interval in seconds. Range: [60:604800] */
- uint32 transmission_interval = 3;
+ /* BLUETOOTH CONFIGURATION -------------------------------------------------------------------------------------------------- */
- /* BLE turnoff time in seconds. Once receiving this setting, BLE will be switched off after the set number of seconds. */
- /* If BLE is already switched off, it will switch on for the set number of seconds and switch off afterwards. */
- /* Range [60:604800] and 0xFFFFFFFF */
- /* 0xFFFFFFFF - always on */
+ /* Bluetooth turn-off time: *
+ * - [60:604800] - Time in seconds after which Bluetooth is turned off *
+ * - 0xFFFFFFFF - Bluetooth is always on *
+ * When this setting is received, Bluetooth is turned off after the set number of seconds. *
+ * If Bluetooth is already off, it will turn on for the set number of seconds and then turn off. *
+ * Group: Bluetooth Turn-Off *
+ * Status: In use [06.00 - LATEST] */
uint32 ble_turnoff_time = 4;
- /* ACK interval in seconds */
- /* Range [180:2592000] and 0xFFFFFFFF */
- /* 0xFFFFFFFF - always request ACK */
- uint32 ack_interval = 5;
-
- /* Specifies, if the additional device info is requested. If true, sensor will send a message to endpoint '/i' with the */
- /* device info. This field is only sent by server */
- bool request_device_info = 6;
-
- /* Specifies, if software update is available. This field is only sent by server */
- bool request_fw_update = 7;
-
- /* Current time in seconds sine 1st of January 1970 (epoch time). */
- uint32 current_time = 8;
-
- /* NB-IoT transfer limit */
- /* Range: [1:65535] */
- /* 65535 - disable transfer limit function */
- uint32 transfer_limit = 9;
-
- /* NB-IoT transfer limit timer in seconds */
- /* Range: [1:65535] */
- /* 65535 - disable transfer limit function */
- uint32 transfer_limit_timer = 10;
-
- /* For firmware >= 6.07.00: */
- /* IP or URL address of the data (measurements) server */
- /* The IP or URL of the data server, provided as string with a maximum length of 31 characters */
- /* For example, use "18.184.24.239" for an IP address or "efento.test.io" for a URL */
- /* For firmware < 6.07.00: */
- /* IP address of the data (measurements) server */
- /* For example, use "18.184.24.239" */
- string data_server_ip = 11;
-
- /* Data (measurements) server port */
- /* Range: [1:65535] */
- uint32 data_server_port = 12;
-
- /* For firmware >= 6.07.00: */
- /* IP or URL address of the update server */
- /* The IP or URL of the update server, provided as string with a maximum length of 31 characters */
- /* For example, use "18.184.24.239" for an IP address or "efento.test.io" for a URL */
- /* For firmware < 6.07.00: */
- /* IP address of the update server */
- /* For example, use "18.184.24.239" */
- string update_server_ip = 13;
-
- /* Update server port for UDP transmission */
- /* Range: [1:65535] */
- uint32 update_server_port_udp = 14;
-
- /* Update server port for CoAP transmission */
- /* Range: [1:65535] */
- uint32 update_server_port_coap = 15;
-
- /* APN as string. Max length 49 */
- /* String with special character 0x7F (DEL) only indicates that automatic apn is turn on */
- string apn = 16;
+ /* Bluetooth Tx power level. *
+ * Value is the index of the absolute value of the Tx power, which depends on the BLE module. *
+ * Range: [1:4] *
+ * Group: Bluetooth Tx Power *
+ * Status: In use [06.02 - LATEST] */
+ uint32 ble_tx_power_level = 36;
- /* PLMN selection */
- /* Range: [100:999999] */
- /* 0xFFFFFFFF or 1000000 - automatic selection */
- uint32 plmn_selection = 17;
+ /* Encryption key. *
+ * Max length: 16 bytes. *
+ * A one-element array containing only one 0x7F (DEL) byte disables encryption. *
+ * Device sends two last bytes of SHA256 hash of current key in this field. *
+ * When the encryption key is disabled, the device sends a 0x7F (DEL) byte. *
+ * Group: Encryption *
+ * Status: In use [06.11.00 - LATEST] */
+ bytes encryption_key = 54;
- /* Device will power off its cellular modem for requested number of seconds. */
- /* Range: [60:604800] (1 minute : 7 days) */
- /* This field is only sent by server */
- uint32 disable_modem_request = 18;
+ /* Bluetooth advertising period. *
+ * Group: Bluetooth Advertising Period *
+ * Status: In use [06.13.00/06.21.00 - LATEST] */
+ ProtoBleAdvertisingPeriod ble_advertising_period = 59;
- /* If set, the device will send its configuration to the endpoint '/c' as a confirmable message */
- /* This field is only sent by server */
- bool request_configuration = 19;
+ /* Advertisement manufacturer specific data format. *
+ * Group: Advertisement Manufacturer Data Format *
+ * Status: In use [07.00.00 - LATEST] */
+ AdvertisementManufacturerDataFormat advertisement_manufacturer_data_format = 60;
- /* Device's error codes. */
- /* This field is only sent by device */
- repeated uint32 errors = 20;
+ /* EDGE LOGIC CONFIGURATION ------------------------------------------------------------------------------------------------- */
- /* Identifier of current configuration - Every change of the configuration results in change of the value of this field */
- /* This field is only sent by device */
- uint32 hash = 21;
+ /* Edge logic rules set on the device. *
+ * Up to 16 rules supported (previously 12 rules [06.00 - 07.01.xx]). *
+ * Group: Edge Logic Rule No. # *
+ * Status: In use [06.00 - LATEST] */
+ repeated ProtoRule rules = 28;
- /* If true, the device will accept the configuration without functional testing (eg. network connection) */
- bool accept_without_testing = 22;
+ /* Calendars set on the device. *
+ * Up to 6 calendars supported. *
+ * Group: Calendar No. # *
+ * Status: In use [06.08.00 - LATEST] */
+ repeated ProtoCalendar calendars = 47;
- /* Cloud token configuration: */
- /* - 1: cloud token set to the value of cloud_token field */
- /* - 2: cloud token set to IMEI of the cellular module */
- /* - 255: do not send cloud_token field */
- uint32 cloud_token_config = 23;
+ /* SERVER COMMUNICATION CONFIGURATION --------------------------------------------------------------------------------------- */
- /* Cloud token that should be sent with each measurement frame */
- string cloud_token = 24;
+ /* Transmission interval in seconds. *
+ * Range: [60:604800] *
+ * Group: Server Intervals *
+ * Status: In use [06.00 - LATEST] */
+ uint32 transmission_interval = 3;
- /* Serial number of the device */
- /* This field is only sent by device */
- bytes serial_number = 25;
+ /* ACK interval: *
+ * - [180:2592000] - Time in seconds after which the device will request an ACK *
+ * - 0xFFFFFFFF - Always request ACK *
+ * Group: Server Intervals *
+ * Status: In use [06.00 - LATEST] */
+ uint32 ack_interval = 5;
- /* Type of channel */
- /* This field is only sent by device */
- repeated MeasurementType channel_types = 27;
+ /* Server transfer limit: *
+ * - [1:65534] - Number of transfers *
+ * - 65535 - Transfer limit disabled *
+ * Group: Server Transfer Limit *
+ * Status: In use [06.00 - LATEST] */
+ uint32 transfer_limit = 9;
- /* Edge logic rules set on the device. Up to 12 rules are supported */
- repeated ProtoRule rules = 28;
+ /* Server transfer limit timer: *
+ * - [1:65534] - Time in seconds after which the transfer is renewed *
+ * - 65535 - Transfer limit disabled *
+ * Group: Server Transfer Limit *
+ * Status: In use [06.00 - LATEST] */
+ uint32 transfer_limit_timer = 10;
- /* Supervision period */
- /* Range: [180:604800] */
- /* 0xFFFFFFFF - Functionality disabled */
+ /* Server supervision period: *
+ * - [180:604800] - Time in seconds after which the device resets itself if there is no communication with the server *
+ * - 0xFFFFFFFF - Server supervision disabled *
+ * Group: Server Supervision *
+ * Status: In use [06.00 - LATEST] */
uint32 supervision_period = 29;
- /* If true, sensor's measurement memory will be erased */
- bool memory_reset_request = 30;
+ /* DATA SERVER CONFIGURATION ------------------------------------------------------------------------------------------------ */
- /* Bytes 0-4 - Band selection mask. Mask = 1 << position */
- /* Band | 1 | 2 | 3 | 4 | 5 | 8 | 12 | 13 | 17 | 18 | 19 | 20 | 25 | 26 | 28 | 66 | 71 | 85 | */
- /* Position: | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | */
- /* example: To enable band 3, 8 and 20 set to (1 << 2) + (1 << 5) + (1 << 11) = 2084 */
- uint32 modem_bands_mask = 31;
+ /* Data server address. *
+ * String up to 31 characters: *
+ * - IPv4 address (examples: "18.184.24.239") [06.00 - 06.06.xx] *
+ * - IPv4/URL address (examples: "18.184.24.239", "efento.test.io") [06.07.00 - LATEST] *
+ * Group: Data Server *
+ * Status: In use [06.00 - LATEST] */
+ string data_server_ip = 11;
+
+ /* Data server port. *
+ * Range: [1:65535] *
+ * Group: Data Server *
+ * Status: In use [06.00 - LATEST] */
+ uint32 data_server_port = 12;
- /* Data endpoint (string - max length 16) */
+ /* Data endpoint. *
+ * String up to 11 characters. *
+ * Group: Data Server *
+ * Status: In use [06.02 - LATEST] */
string data_endpoint = 32;
- /* Configuration endpoint (string - max length 16) */
+ /* Configuration endpoint. *
+ * String up to 11 characters. *
+ * Group: Data Server *
+ * Status: In use [06.02 - LATEST] */
string configuration_endpoint = 33;
- /* Device info endpoint (string - max length 16) */
+ /* Extended configuration endpoint. *
+ * String up to 11 characters. *
+ * Group: Data Server *
+ * Status: In use [07.00.00 - LATEST] */
+ string extended_configuration_endpoint = 61;
+
+ /* Device information endpoint. *
+ * String up to 11 characters. *
+ * Group: Device Information Endpoint *
+ * Status: In use [06.02 - LATEST] */
string device_info_endpoint = 34;
- /* Time endpoint (string - max length 16) */
+ /* Time endpoint. *
+ * String up to 11 characters. *
+ * Group: Time Endpoint *
+ * Status: In use [06.02 - LATEST] */
string time_endpoint = 35;
- /* Bluetooth TX power level. Value is the index of the absolute value of TX power, that depends on the BLE module */
- /* Range: [1:4] */
- uint32 ble_tx_power_level = 36;
+ /* UPDATE SERVER CONFIGURATION ---------------------------------------------------------------------------------------------- */
- /* Deprecated field */
- /* If true, the sensor's runtime errors will be cleared */
- bool request_runtime_errors_clear = 37;
+ /* Update server address. *
+ * String up to 31 characters: *
+ * - IPv4 address (examples: "18.184.24.239") [06.00 - 06.06.xx] *
+ * - IPv4/URL address (examples: "18.184.24.239", "efento.test.io") [06.07.00 - LATEST] *
+ * Group: Update Server *
+ * Status: In use [06.00 - LATEST] */
+ string update_server_ip = 13;
- /* Timestamp when a new error code was reported */
- uint32 error_timestamp = 38;
+ /* Update server port for UDP transfer. *
+ * Range: [1:65535] *
+ * Group: Update Server *
+ * Status: In use [06.00 - LATEST] */
+ uint32 update_server_port_udp = 14;
- /* Timestamp when the new configuration was set */
- uint32 hash_timestamp = 39;
+ /* Update server port for CoAP transfer. *
+ * Range: [1:65535] *
+ * Group: Update Server *
+ * Status: In use [06.00 - LATEST] */
+ uint32 update_server_port_coap = 15;
- /* Cloud token CoAP option ID: */
- /* - [1:64999] - CoAP option ID containing cloud token */
- /* - 65000 - cloud token sent in the payload */
- uint32 cloud_token_coap_option = 40;
+ /* CLOUD CONFIGURATION ------------------------------------------------------------------------------------------------------ */
- /* ECDSA payload signature CoAP option ID: */
- /* - [1:64999] - CoAP option ID containing ECDSA payload signature */
- /* - 65000 - no payload signature in CoAP option */
- uint32 payload_signature_coap_option = 41;
+ /* Cloud token configuration: *
+ * - 1 - Cloud token set to the value of the 'cloud_token' field *
+ * - 2 - Cloud token set to the modem identification (IMEI for cellular modems) *
+ * - 255 - Do not send 'cloud_token' field *
+ * Group: Cloud Token *
+ * Status: In use [06.00 - LATEST] */
+ uint32 cloud_token_config = 23;
- /* DNS server IP address grouped in the array as four octets. Set 255.255.255.255 to use a network DNS server */
- /* Note: when setting less than four octets the remaining will be filled with zeros. */
- repeated uint32 dns_server_ip = 42;
+ /* Cloud token that should be sent with each measurement frame. *
+ * String up to 36 characters. *
+ * Group: Cloud Token *
+ * Status: In use [06.00 - LATEST] */
+ string cloud_token = 24;
- /* DNS TTL configuration: */
- /* - [1:864000] - custom TTL in seconds (additionally, the DNS request when communication has failed) */
- /* - 864001 - accept TTL from the DNS server (additionally, the DNS request when communication has failed) */
- /* - 864002 - DNS request is only after communication failed */
- uint32 dns_ttl_config = 43;
+ /* Cloud token CoAP option ID: *
+ * - [1:64999] - CoAP option ID with cloud token *
+ * - 65000 - Cloud token sent in the payload *
+ * Group: Cloud Token *
+ * Status: In use [06.07.00 - LATEST] */
+ uint32 cloud_token_coap_option = 40;
- /* Configuration payload split information. Information about dividing the payload into parts */
- /* values < 0 - payload has been split, expect another part of the payload in the next message. */
- /* The absolute value indicates an index of the current message. */
- /* value = 0 - payload has not been splitted */
- /* values > 0 - last part of the split payload, the value indicates the total number of the messages sent */
- sint32 payload_split_info = 44;
+ /* ECDSA payload signature CoAP option ID: *
+ * - [1:64999] - CoAP option ID with payload signature *
+ * - 65000 - Payload signature is not sent *
+ * Group: Cloud Token *
+ * Status: In use [06.07.00 - LATEST] */
+ uint32 payload_signature_coap_option = 41;
- /* Modem update request (string - max length 48) */
- /* This field is only sent by server */
- /* For BC66 module, this field is a DFOTA URL */
- string modem_update_request = 45;
+ /* Modem identification CoAP option ID: *
+ * - [1:64999] - CoAP option ID with modem identification *
+ * - 65000 - Modem identification is not sent *
+ * Group: Cloud Token *
+ * Status: In use [07.00.00 - LATEST] */
+ uint32 modem_identification_coap_option = 52;
- /* Cellular configuration parameters. */
- /* 1st item - Number of used cellular parameters */
- /* 2nd - 12th items - Cellular parameters */
- repeated uint32 cellular_config_params = 46;
+ /* NETWORK CONFIGURATION ---------------------------------------------------------------------------------------------------- */
- /* Calendar configuration. Up to 6 calendars are supported */
- repeated ProtoCalendar calendars = 47;
+ /* DNS server IP address. *
+ * Grouped in the array as four octets. Set 255.255.255.255 to use a cellular network DNS server. *
+ * Note: when setting less than four octets the remaining will be filled with zeros. *
+ * Group: Data Server *
+ * Status: In use [06.07.00 - LATEST] */
+ repeated uint32 dns_server_ip = 42;
- /* DEPRECATED - Used for backward compatibility */
- reserved 48;
-
- /* Set/get calibration parameters for single channel. */
- ProtoCalibrationParameters calibration_parameters_request = 49;
-
- /* LED behaviour configuration: */
- /* Period of LEDs flashing (5-600 seconds in 5 seconds resolution): */
- /* - led_config[0] - green LED */
- /* - led_config[1] - red LED */
- /* Time from entering the normal state, after which the LED indication is turned off */
- /* (0-240 minutes in 1 minute resolution, or 255 for always turned on): */
- /* - led_config[2] - flashing red led on communication problem */
- /* - led_config[3] - flashing red led on a sensor problem */
- /* - led_config[4] - flashing red led on a low power */
- /* - led_config[5] - flashing green led on measurement */
- /* - led_config[6] - flashing green led on transmission */
- /* - led_config[7] - flashing green led to indicate sensor's proper operation */
- /* - led_config[8] - Blink duration (20-1000ms in 5 ms resolution) */
- repeated uint32 led_config = 50;
+ /* DNS TTL configuration: *
+ * - [1:864000] - Custom TTL in seconds (if communication fails, DNS is also queried) *
+ * - 864001 - Accept TTL from the DNS server (if communication fails, DNS is also queried) *
+ * - 864002 - DNS query only after communication failure *
+ * Group: DNS *
+ * Status: In use [06.07.00 - LATEST] */
+ uint32 dns_ttl_config = 43;
- /* Network troubleshooting configuration, if bluetooth is turned off and communication with the server is faulty, */
- /* bluetooth will be automatically turned on until the connection is stabilized */
- /* - 1: network troubleshooting disabled */
- /* - 2: network troubleshooting enabled */
+ /* Network troubleshooting:
+ * - 1 - Network troubleshooting disabled *
+ * - 2 - Network troubleshooting enabled *
+ * If Bluetooth is turned off and communication with the server is faulty, Bluetooth will be automatically turned on until *
+ * the connection is stabilized. *
+ * Group: Network *
+ * Status: In use [06.10.00 - LATEST] */
uint32 network_troubleshooting = 51;
- /* Reserved by gateway client */
- reserved 52, 53;
-
- /* Encryption key configuration. Sensor sends in this field two last bytes of SHA256 hash calculated from its current */
- /* encryption_key configuration. When encryption key is disabled one byte 0x7F (DEL) is sent. */
- /* Max length: 16 bytes. */
- /* 0x7F - encryption key disabled. */
- bytes encryption_key = 54;
-
- /* User name as string. Max length 31 */
- /* String with special character 0x7F (DEL) only indicates that automatic user name is turn on */
- /* User name can only be set to custom value if apn has been configured (is not automatic) */
- string apn_user_name = 55;
+ /* Network key. *
+ * Max length: 16 bytes. *
+ * A one-element array containing only one 0x7F (DEL) byte disables network key. *
+ * Device sends two last bytes of SHA256 hash of current key in this field. *
+ * When the network key is disabled, the device sends a 0x7F (DEL) byte. *
+ * Group: Local Network *
+ * Status: In use [07.00.00 - LATEST] */
+ bytes network_key = 53;
+
+ /* MODEM CONFIGURATION ------------------------------------------------------------------------------------------------------ */
+
+ /* APN (Access Point Name). *
+ * String up to 49 characters. *
+ * A string containing only the special character 0x7F (DEL) indicates that automatic APN is enabled. *
+ * Group: Data Server *
+ * Status: In use [06.00 - LATEST] */
+ string apn = 16;
- /* Password as string. Max length 31 */
- /* String with special character 0x7F (DEL) only indicates that automatic password is turn on */
- /* Password can only be set to custom value if apn_user_name has been configured (is not automatic) */
+ /* APN username. *
+ * String up to 31 characters. *
+ * A string containing only the special character 0x7F (DEL) indicates that the username is not being used. *
+ * The username can only be set to a custom value if APN has been configured. *
+ * Group: Data Server *
+ * Status: In use [07.00.00 - LATEST] / Previously as apn_user_name [06.11.00 - 06.xx.xx] */
+ string apn_username = 55;
+
+ /* APN password. *
+ * String up to 31 characters. *
+ * A string containing only the special character 0x7F (DEL) indicates that the password is not being used. *
+ * The password can only be set to a custom value if APN username has been configured. *
+ * Group: Data Server *
+ * Status: In use [06.11.00 - LATEST] */
string apn_password = 56;
- /* Reserved by versions above 06.20.00 */
- reserved 57;
+ /* PLMN selection: *
+ * - [100:999999] - Selected operator code *
+ * - 1000000 - Automatic selection *
+ * - 0xFFFFFFFF - Automatic selection (legacy) *
+ * Group: Data Server *
+ * Status: In use [06.00 - LATEST] */
+ uint32 plmn_selection = 17;
- /* Control output state on channel pin. Maximal number of requests equals 3 */
- /* This field is only sent by server */
- repeated ProtoOutputControlState output_control_state_request = 58;
+ /* Modem bands mask. *
+ * 4-byte bit mask, where each bit represents a specific band: *
+ * Band: | 1 | 2 | 3 | 4 | 5 | 8 | 12 | 13 | 17 | 18 | 19 | 20 | 25 | 26 | 28 | 66 | 71 | 85 | 70 | *
+ * Bit: | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | *
+ * Example: To enable band 3, 8 and 20 set to 2084 = (1 << 2) + (1 << 5) + (1 << 11) *
+ * Group: Data Server *
+ * Status: In use [06.00 - LATEST] */
+ uint32 modem_bands_mask = 31;
- /* BLE advertising period configuration. */
- ProtoBleAdvertisingPeriod ble_advertising_period = 59;
-}
+ /* Modem configuration parameters. *
+ * 1st item - The number of cellular parameters that are used (depending on the modem in use). *
+ * 2nd - 16th item - Cellular parameters. *
+ * Group: Cellular Configuration *
+ * Status: In use [06.08.00 - LATEST] */
+ repeated uint32 cellular_config_params = 46;
+
+ /* Network search schema. *
+ * Group: Network Search *
+ * Status: In use [06.20.00 - LATEST] */
+ ProtoNetworkSearch network_search = 57;
+
+ /* USER INTERFACE CONFIGURATION --------------------------------------------------------------------------------------------- */
+
+ /* LEDs behavior. *
+ * Period of LED blinking in 5-second unit (value is multiplied by 5). Range: [1:120]: *
+ * - 1st item - Period of GREEN LED blinking *
+ * - 2nd item - Period of RED LED blinking *
+ * Time elapsing from the entry into the normal state, after which the LED indicator is switched off, in minutes. *
+ * Range: [0:240; 255]. Value of 255 means always on: *
+ * - 3rd item - Blinking RED LED on communication problem *
+ * - 4th item - Blinking RED LED on a sensor problem *
+ * - 5th item - Blinking RED LED on a low power *
+ * - 6th item - Blinking GREEN LED on measurement *
+ * - 7th item - Blinking GREEN LED on transmission *
+ * - 8th item - Blinking GREEN LED to indicate sensor's proper operation *
+ * 9th item - Duration of LED blinking in 5-millisecond unit. Range: [4:200] *
+ * Group: LED *
+ * Status: In use [06.10.00 - LATEST] */
+ repeated uint32 led_config = 50;
+}
\ No newline at end of file
diff --git a/common/transport/coap/src/main/proto/efento/proto_config_types.proto b/common/transport/coap/src/main/proto/efento/proto_config_types.proto
new file mode 100644
index 0000000000..44fb9197a4
--- /dev/null
+++ b/common/transport/coap/src/main/proto/efento/proto_config_types.proto
@@ -0,0 +1,125 @@
+/**
+ * Copyright © 2016-2026 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.
+ */
+syntax = "proto3";
+
+option java_package = "org.thingsboard.server.gen.transport.coap";
+option java_outer_classname = "ConfigTypesProtos";
+
+message ProtoCalibrationParametersRequest {
+
+ /* Request details. *
+ * Bitmask: *
+ * - Bit 0:2 - Requested channel number, range: [1:6] *
+ * Status: Deprecated [06.10.00 - 06.xx.xx] */
+ uint32 calibration_request = 1;
+
+ /* Channel assignment - the sensor code. *
+ * Status: Deprecated [06.10.00 - 06.xx.xx] */
+ uint32 channel_assignment = 2;
+
+ /* Channel calibration parameters. *
+ * Up to 8 parameters supported. *
+ * If this field is empty, the sensor will send the current set of parameters. *
+ * Status: Deprecated [06.10.00 - 06.xx.xx] */
+ repeated int32 parameters = 3;
+}
+
+message ProtoOutputControlState {
+
+ /* Channel index. *
+ * Range: [0:5] *
+ * Status: In use [06.13.00/06.21.00 - LATEST] */
+ uint32 channel_index = 1;
+
+ /* Channel output state: *
+ * - 1 - OFF *
+ * - 2 - ON *
+ * Status: In use [06.13.00/06.21.00 - LATEST] */
+ uint32 channel_state = 2;
+}
+
+enum BleAdvertisingPeriodMode {
+
+ /* Invalid value. */
+ BLE_ADVERTISING_PERIOD_MODE_UNSPECIFIED = 0;
+
+ /* Default mode. *
+ * Bluetooth advertising interval is set to 1022.5ms or a lower value, based on the continuous measurement period. */
+ BLE_ADVERTISING_PERIOD_MODE_DEFAULT = 1;
+
+ /* Normal mode. *
+ * Uses the value configured by the user from the 'normal' field. */
+ BLE_ADVERTISING_PERIOD_MODE_NORMAL = 2;
+
+ /* Fast mode. *
+ * Uses the value configured by the user from the 'fast' field. */
+ BLE_ADVERTISING_PERIOD_MODE_FAST = 3;
+}
+
+message ProtoBleAdvertisingPeriod {
+
+ /* Bluetooth advertising mode. *
+ * Status: In use [06.13.00/06.21.00 - LATEST] */
+ BleAdvertisingPeriodMode mode = 1;
+
+ /* Bluetooth advertising interval in normal mode, configured in steps of 0.625 ms. *
+ * Range: [32:16384] *
+ * Status: In use [06.13.00/06.21.00 - LATEST] */
+ uint32 normal = 2;
+
+ /* Bluetooth advertising interval in fast mode, configured in steps of 0.625 ms. *
+ * Range: [32:16384] *
+ * Status: In use [06.13.00/06.21.00 - LATEST] */
+ uint32 fast = 3;
+}
+
+enum AdvertisementManufacturerDataFormat {
+
+ /* Invalid value. */
+ ADVERTISEMENT_MANUFACTURER_DATA_FORMAT_UNSPECIFIED = 0;
+
+ /* Advertisement manufacturer specific data format 3. */
+ ADVERTISEMENT_MANUFACTURER_DATA_FORMAT_V3 = 1;
+
+ /* Advertisement manufacturer specific data format 5. */
+ ADVERTISEMENT_MANUFACTURER_DATA_FORMAT_V5 = 2;
+}
+
+message ProtoNetworkSearch {
+
+ /* Timing schema, if successful registration since the last reset. *
+ * Length: 6 items. *
+ * 1st - 6th item - Time in minutes. Range: [1:255]. *
+ * Status: In use [06.20.00 - LATEST] */
+ repeated uint32 time_schema_last_registration_ok = 1;
+
+ /* Timing schema, if no successful registration since the last reset. *
+ * Length: 6 items. *
+ * 1st - 6th item - Time in minutes. Range: [1:255]. *
+ * Status: In use [06.20.00 - LATEST] */
+ repeated uint32 time_schema_last_registration_not_ok = 2;
+
+ /* Disable base period in minutes. *
+ * Disable time = 'disable_period_base' * counter (from 1 to 'counter_max'). *
+ * Range: [1:255] *
+ * Status: In use [06.20.00 - LATEST] */
+ uint32 disable_period_base = 3;
+
+ /* Disable counter maximum. *
+ * Range: [1:255] *
+ * Status: In use [06.20.00 - LATEST] */
+ uint32 counter_max = 4;
+}
\ No newline at end of file
diff --git a/common/transport/coap/src/main/proto/efento/proto_device_info.proto b/common/transport/coap/src/main/proto/efento/proto_device_info.proto
index 3e1f066c59..f3908826ad 100644
--- a/common/transport/coap/src/main/proto/efento/proto_device_info.proto
+++ b/common/transport/coap/src/main/proto/efento/proto_device_info.proto
@@ -18,185 +18,330 @@ syntax = "proto3";
option java_package = "org.thingsboard.server.gen.transport.coap";
option java_outer_classname = "DeviceInfoProtos";
+message ProtoRuntime
+{
+
+ /* Up-time in seconds (since reset). *
+ * Status: In use [06.00 - LATEST] */
+ uint32 up_time = 1;
+
+ /* Message counters (since reset). *
+ * 1st item - Confirmable message attempts counter. *
+ * 2nd item - Non-confirmable message attempts counter. *
+ * 3rd item - Successful message counter. *
+ * Status: In use [06.00 - LATEST] */
+ repeated uint32 message_counters = 2;
+
+ /* MCU temperature in Celsius. *
+ * Status: In use [06.00 - LATEST] */
+ sint32 mcu_temperature = 3;
+
+ /* Battery voltage in mV. *
+ * - [0:65534] - Battery voltage in millivolts *
+ * - 65535 - No measurement *
+ * Status: In use [07.00.00 - LATEST] / Previously as min_battery_voltage [06.00 - 06.xx.xx] */
+ uint32 battery_voltage = 4;
+
+ /* MCU temperature in Celsius when minimum battery voltage was reached. *
+ * Status: Deprecated [06.00 - 06.xx.xx] */
+ sint32 min_battery_mcu_temperature = 5;
+
+ /* Battery reset timestamp in seconds since 1st of January 1970 (epoch time). *
+ * Status: Deprecated [06.00 - 06.xx.xx] */
+ uint32 battery_reset_timestamp = 6;
+
+ /* Maximum MCU temperature in Celsius. *
+ * Status: In use [06.00 - LATEST] */
+ sint32 max_mcu_temperature = 7;
+
+ /* Minimum MCU temperature in Celsius *
+ * Status: In use [06.00 - LATEST] */
+ sint32 min_mcu_temperature = 8;
+
+ /* Device's runtime errors. *
+ * Up to 20 error items supported. *
+ * Status: In use [06.02 - LATEST] */
+ repeated uint32 runtime_errors = 9;
+
+ /* Number of sensor resets (since power-up). *
+ * Status: In use [06.21.00 - LATEST] */
+ uint32 reset_counter = 10;
+}
+
+message ProtoUpdateInfo
+{
+
+ /* Timestamp of the last update check in seconds since 1st of January 1970 (epoch time). *
+ * Status: In use [06.00 - LATEST] */
+ uint32 timestamp = 1;
+
+ /* Status of the last update check: *
+ * - 1 - No update check *
+ * - 2 - No error *
+ * - 3 - UDP socekt error *
+ * - 4 - Invalid hash *
+ * - 5 - Missing packet *
+ * - 6 - Invalid data *
+ * - 7 - Sending timeout *
+ * - 8 - No software to update *
+ * - 9 - Sending unexpected error *
+ * - 10 - Unexpected error *
+ * Status: In use [06.00 - LATEST] */
+ uint32 status = 2;
+}
+
enum ModemType
{
- /* Invalid value */
+ /* Invalid value. */
MODEM_TYPE_UNSPECIFIED = 0;
- /* Quectel BC66 modem */
+ /* Quectel BC66 modem. */
MODEM_TYPE_BC66 = 1;
- /* Quectel BC66-NA modem */
+ /* Quectel BC66-NA modem. */
MODEM_TYPE_BC66NA = 2;
+
+ /* Uses a shared modem from another sensor. */
+ MODEM_TYPE_SHARED_MODEM = 3;
+
+ /* Quectel BC660 modem. */
+ MODEM_TYPE_BC660 = 4;
}
-message ProtoRuntime
+enum ModemFirmwareVersion
{
+ /* Invalid value. */
+ MODEM_FIRMWARE_VERSION_UNSPECIFIED = 0;
- /* Up-time in seconds (since reset) */
- uint32 up_time = 1;
+ /* Unable to read firmware version from device. */
+ MODEM_FIRMWARE_VERSION_READING_ERROR = 1;
- /* Message counters (since reset). There are 3 counters: */
- /* message_counters[0] - Counter of confirmable messages attempts */
- /* message_counters[1] - Counter of non-confirmable messages attempts */
- /* message_counters[2] - Counter of succeeded messages */
- repeated uint32 message_counters = 2;
+ /* Unknown firmware version. */
+ MODEM_FIRMWARE_VERSION_UNKNOWN = 2;
- /* MCU temperature in Celsius */
- sint32 mcu_temperature = 3;
+ /* BC660KGLAAR01A05_01.002.01.002. */
+ MODEM_FIRMWARE_VERSION_BC660_V1 = 3;
- /* Minimum battery voltage in mV */
- uint32 min_battery_voltage = 4;
+ /* BC660KGLAAR01A05_01.200.01.200. */
+ MODEM_FIRMWARE_VERSION_BC660_V2 = 4;
- /* MCU temperature in Celsius, while the minimum battery voltage was reached */
- sint32 min_battery_mcu_temperature = 5;
+ /* BC660KGLAAR01A05_01.202.01.202. */
+ MODEM_FIRMWARE_VERSION_BC660_V3 = 5;
- /* Battery reset timestamp (Unix timestamp) */
- uint32 battery_reset_timestamp = 6;
+ /* BC660KGLAAR01A05_01.203.01.203. */
+ MODEM_FIRMWARE_VERSION_BC660_V4 = 6;
- /* Max MCU temperature in Celsius */
- sint32 max_mcu_temperature = 7;
+ /* BC660KGLAAR01A05_01.204.01.204. */
+ MODEM_FIRMWARE_VERSION_BC660_V5 = 7;
- /* Min MCU temperature in Celsius */
- sint32 min_mcu_temperature = 8;
+ /* BC660KGLAAR01A05_01.205.01.205. */
+ MODEM_FIRMWARE_VERSION_BC660_V6 = 8;
- /* Table of runtime errors. Max length: 20 */
- repeated uint32 runtime_errors = 9;
+ /* BC660KGLAAR01A05_01.301.01.301. */
+ MODEM_FIRMWARE_VERSION_BC660_V7 = 9;
+
+ /* BC660KGLAAR01A05_01.303.01.303. */
+ MODEM_FIRMWARE_VERSION_BC660_V8 = 10;
}
message ProtoModem
{
+ /* Modem type. *
+ * Status: In use [06.00 - LATEST] */
ModemType type = 1;
- /* Parameters for BC66 modem: */
- /* parameters[0] - sc_EARFCN - Range: [0:262143]. Unknown value: -1 */
- /* parameters[1] - sc_EARNFCN_offset - Range: [0:4] mapped to [-2, -1, -0.5, 0, 1]. Unknown value: -1 */
- /* parameters[2] - sc_PCI - Range: [0:502]. Unknown value: -1 */
- /* parameters[3] - sc_Cell id - Range: [1:268435456]. Unknown value: 0 */
- /* parameters[4] - sc_RSRP - [dBm] - Range: [-140:-44]. Unknown value: 0 */
- /* parameters[5] - sc_RSRQ - [dB] - Range: [-20:-3]. Unknown value: 0 */
- /* parameters[6] - sc_RSSI - [dBm] - Range: [-110:-3] Unknown value: 0 */
- /* parameters[7] - sc_SINR - [dB] - Range: [-10:30]. Unknown value: 31 */
- /* parameters[8] - sc_Band - Range: [see module supported bands]. The current serving cell band. Unknown value: -1 */
- /* parameters[9] - sc_TAC - Range: [0:65536]. Unknown value: -1 */
- /* parameters[10] - sc_ECL - Range: [0:2]. Unknown value: -1 */
- /* parameters[11] - sc_TX_PWR - [0.1cBm] - Range [-440:230]. Unknown value: -1000 */
- /* parameters[12] - OP_MODE - Range: [0:3]. Unknown value: -1 */
- /* parameters[13] - nc_EARFCN - Range: [0:262143]. Unknown value: -1 */
- /* parameters[14] - nc_EARNFCN_offset - Range: [0:4] mapped to [-2, -1, -0.5, 0, 1]. Unknown value: -1 */
- /* parameters[15] - nc_PCI - Range: [0:502]. Unknown value: -1 */
- /* parameters[16] - nc_RSRP - [dBm] - Range: [-140:-44]. Unknown value: 0 */
- /* parameters[17] - RLC_UL_BLER - Range: [0:100]. Unknown value: -1 */
- /* parameters[18] - RLC_DL_BLER - Range: [0:100]. Unknown value: -1 */
- /* parameters[19] - MAC_UL_BLER - Range: [0:100]. Unknown value: -1 */
- /* parameters[20] - MAC_DL_BLER - Range: [0:100]. Unknown value: -1 */
- /* parameters[21] - MAC_UL_TOTAL_BYTES - Range: [0:2147483647]. Unknown value: -1 */
- /* parameters[22] - MAC_DL_TOTAL_BYTES - Range: [0:2147483647]. Unknown value: -1 */
- /* parameters[23] - MAC_UL_total_HARQ_Tx - Range: [0:2147483647]. Unknown value: -1 */
- /* parameters[24] - MAC_DL_total_HARQ_Tx - Range: [0:2147483647]. Unknown value: -1 */
- /* parameters[25] - MAC_UL_HARQ_re_Tx - Range: [0:2147483647]. Unknown value: -1 */
- /* parameters[26] - MAC_DL_HARQ_re_Tx - Range: [0:2147483647]. Unknown value: -1 */
- /* parameters[27] - RLC_UL_tput - Range: [0:2147483647]. Unknown value: -1 */
- /* parameters[28] - RLC_DL_tput - Range: [0:2147483647]. Unknown value: -1 */
- /* parameters[29] - MAC_UL_tput - Range: [0:2147483647]. Unknown value: -1 */
- /* parameters[30] - MAC_DL_tput - Range: [0:2147483647]. Unknown value: -1 */
- /* parameters[31] - sleep_duration - [0.1s] - Range: [0:2147483647]. Unknown value: -1 */
- /* parameters[32] - Rx_time - [0.1s] - Range: [0:2147483647]. Unknown value: -1 */
- /* parameters[33] - Tx_time - [0.1s] - Range: [0:2147483647]. Unknown value: -1 */
+ /* Modem runtime parameters. *
+ * For BC66 modem (34 parameters): *
+ * - 1st item - sc_EARFCN. Range: [0:262143]. Unknown value: -1 *
+ * - 2nd item - sc_EARNFCN_offset. Range: [0:4] mapped to [-2, -1, -0.5, 0, 1]. Unknown value: -1 *
+ * - 3rd item - sc_PCI. Range: [0:502]. Unknown value: -1 *
+ * - 4th item - sc_Cell_Id. Range: [1:268435456]. Unknown value: 0 *
+ * - 5th item - sc_RSRP [dBm]. Range: [-140:-44]. Unknown value: 0 *
+ * - 6th item - sc_RSRQ [dB]. Range: [-20:-3]. Unknown value: 0 *
+ * - 7th item - sc_RSSI [dBm]. Range: [-110:-3] Unknown value: 0 *
+ * - 8th item - sc_SINR [dB]. Range: [-10:30]. Unknown value: 31 *
+ * - 9th item - sc_Band. Range: [see module supported bands]. The current serving cell band. Unknown value: -1 *
+ * - 10th item - sc_TAC. Range: [0:65536]. Unknown value: -1 *
+ * - 11th item - sc_ECL. Range: [0:2]. Unknown value: -1 *
+ * - 12th item - sc_TX_PWR [0.1dBm]. Range [-440:230]. Unknown value: -1000 *
+ * - 13th item - OP_MODE. Range: [0:3]. Unknown value: -1 *
+ * - 14th item - nc_EARFCN. Range: [0:262143]. Unknown value: -1 *
+ * - 15th item - nc_EARNFCN_offset. Range: [0:4] mapped to [-2, -1, -0.5, 0, 1]. Unknown value: -1 *
+ * - 16th item - nc_PCI. Range: [0:502]. Unknown value: -1 *
+ * - 17th item - nc_RSRP [dBm]. Range: [-140:-44]. Unknown value: 0 *
+ * - 18th item - RLC_UL_BLER. Range: [0:100]. Unknown value: -1 *
+ * - 19th item - RLC_DL_BLER. Range: [0:100]. Unknown value: -1 *
+ * - 20th item - MAC_UL_BLER. Range: [0:100]. Unknown value: -1 *
+ * - 21th item - MAC_DL_BLER. Range: [0:100]. Unknown value: -1 *
+ * - 22th item - MAC_UL_TOTAL_BYTES. Range: [0:2147483647]. Unknown value: -1 *
+ * - 23th item - MAC_DL_TOTAL_BYTES. Range: [0:2147483647]. Unknown value: -1 *
+ * - 24th item - MAC_UL_total_HARQ_Tx. Range: [0:2147483647]. Unknown value: -1 *
+ * - 25th item - MAC_DL_total_HARQ_Tx. Range: [0:2147483647]. Unknown value: -1 *
+ * - 26th item - MAC_UL_HARQ_re_Tx. Range: [0:2147483647]. Unknown value: -1 *
+ * - 27th item - MAC_DL_HARQ_re_Tx. Range: [0:2147483647]. Unknown value: -1 *
+ * - 28th item - RLC_UL_tput. Range: [0:2147483647]. Unknown value: -1 *
+ * - 29th item - RLC_DL_tput. Range: [0:2147483647]. Unknown value: -1 *
+ * - 30th item - MAC_UL_tput. Range: [0:2147483647]. Unknown value: -1 *
+ * - 31th item - MAC_DL_tput. Range: [0:2147483647]. Unknown value: -1 *
+ * - 32th item - sleep_duration [0.1s]. Range: [0:2147483647]. Unknown value: -1 *
+ * - 33th item - Rx_time [0.1s]. Range: [0:2147483647]. Unknown value: -1 *
+ * - 34th item - Tx_time [0.1s]. Range: [0:2147483647]. Unknown value: -1 *
+ * For BC660 modem (22 parameters): *
+ * - 1st item - sc_EARFCN. Range: [0:262143]. Unknown value: -1 *
+ * - 2nd item - sc_EARNFCN_offset. Range: [0:21] mapped to *
+ * [Invalid, -10, -9, -8, -7, -6, -5, -4, -3, -2, -1, -0.5, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9]. *
+ * Unknown value: -1 *
+ * - 3rd item - sc_PCI. Range: [0:503]. Unknown value: -1 *
+ * - 4th item - sc_Cell_Id. Range: [1:268435456]. Unknown value: 0 *
+ * - 5th item - sc_RSRP [dBm]. Range: [-140:-44]. Unknown value: 0 *
+ * - 6th item - sc_RSRQ [dB]. Range: [-20:-3]. Unknown value: 0 *
+ * - 7th item - sc_RSSI [dBm]. Range: [-110:-3] Unknown value: 0 *
+ * - 8th item - sc_SINR [dB]. Range: [-10:30]. Unknown value: 31 *
+ * - 9th item - sc_Band. Range: [see module supported bands]. The current serving cell band. Unknown value: -1 *
+ * - 10th item - sc_TAC. Range: [0:65536]. Unknown value: -1 *
+ * - 11th item - sc_ECL. Range: [0:2]. Unknown value: -1 *
+ * - 12th item - sc_TX_PWR [dBm]. Range [-45:23]. Unknown value: 128 *
+ * - 13th item - OP_MODE. Range: [0:3] mapped to [In-band same PCI, In-band different PCI, Guard band, Stand alone]. *
+ * Unknown value: -1 *
+ * - 14th item - nc_EARFCN. Range: [0:262143]. Unknown value: -1 *
+ * - 15th item - nc_PCI. Range: [0:503]. Unknown value: -1 *
+ * - 16th item - nc_RSRP [dBm]. Range: [-140:-44]. Unknown value: 0 *
+ * - 17th item - nc_RSRQ [dBm]. Range: [-140:-44]. Unknown value: 0 *
+ * - 18th item - sleep_duration [0.1s]. Range: [0:2147483647]. Unknown value: -1 *
+ * - 19th item - Rx_time [0.1s]. Range: [0:2147483647]. Unknown value: -1 *
+ * - 20th item - Tx_time [0.1s]. Range: [0:2147483647]. Unknown value: -1 *
+ * - 21st item - PLMN_state. Range: [0:3] mapped to [No PLMN, Searching, Selected, Unknown]. Unknown value: 3 *
+ * - 22nd item - select_PLMN. Range: [100:999999]. Unknown value: -1 *
+ * For shared modem client (4 parameters): *
+ * - 1st item - RSRP [dBm]. Range: [-140:-44]. Unknown value: 0 *
+ * - 2nd item - RSRQ [dB]. Range: [-20:-3]. Unknown value: 0 *
+ * - 3rd item - RSSI [dBm]. Range: [-110:-3] Unknown value: 0 *
+ * - 4th item - SINR [dB]. Range: [-10:30]. Unknown value: 31 *
+ * Status: In use [06.00 - LATEST] */
repeated sint32 parameters = 2;
- /* ICCID of inserted/soldered sim card. String up to 22 characters long. */
- /* 0x7F if sim card is not detected, empty (not sent) if device does not have modem. */
- /* This field is only sent by device */
+ /* Integrated Circuit Card Identifier (ICCID) of the inserted/soldered SIM card. *
+ * String up to 22 characters: *
+ * A string containing only the special character 0x7F (DEL) indicates that the SIM card is not detected. *
+ * Status: In use [06.07.04/06.09.09/06.10.10/06.11.09/06.12.06/06.13.00/06.21.00 - LATEST] */
string sim_card_identification = 3;
+
+ /* Modem firmware version. *
+ * Status: In use [06.20.05/06.21.00 - LATEST] */
+ ModemFirmwareVersion firmware_version = 4;
+
+ /* Modem identification: *
+ * - IMEI (International Mobile Equipment Identity) of the modem *
+ * - Serial number of the most recently used modem-sharing sensor *
+ * String up to 15 characters. *
+ * Status: In use [07.00.00 - LATEST] */
+ string modem_identification = 5;
+
+ /* Modem statistics. *
+ * 1st item - Number of transmissions since the last device information message. Undefined value: 0. *
+ * 2nd item - Time in seconds since the last device information message. Undefined value: 0. *
+ * 3rd item - Total time in the power saving mode, in seconds. Undefined value: 0. *
+ * 4th item - Total time in the active state in seconds. Undefined value: 0. *
+ * Status: In use [07.00.00 - LATEST] */
+ repeated uint32 modem_statistics = 6;
}
-message ProtoUpdateInfo
+message ProtoDeviceInfo
{
- /* Timestamp of update (Unix timestamp) */
- uint32 timestamp = 1;
+ /* RESERVED FIELDS ---------------------------------------------------------------------------------------------------------- */
- /* Update status, possible values: */
- /* - 1 - No update yet */
- /* - 2 - No error */
- /* - 3 - UDP socekt error */
- /* - 4 - Hash error */
- /* - 5 - Missing packet error */
- /* - 6 - Invalid data error */
- /* - 7 - Sending timeout error */
- /* - 8 - No SW to update error */
- /* - 9 - Sending unexpected error */
- /* - 10 - Unexpected error */
- uint32 status = 2;
-}
+ reserved 2,4,5,6,7,8,9,10,11,12;
-message ProtoDeviceInfo
-{
+ /* DEVICE STATUS FIELDS ----------------------------------------------------------------------------------------------------- */
- /* Serial number of device */
- bytes serial_num = 1;
+ /* Serial number of the device. *
+ * Length: 6 bytes. *
+ * Status: In use [07.00.00 - LATEST] / Previously as serial_num [06.00 - 06.xx.xx] */
+ bytes serial_number = 1;
- /* Deprecated field */
- reserved 2;
+ /* DEVICE INFORMATION FIELDS ------------------------------------------------------------------------------------------------ */
- /* Software version e.g ver 06.10 -> 0x060A -> 1546 */
+ /* Software version (excluding LTS). *
+ * Example: 1546 -> 0x060A -> SW version 06.10 *
+ * Status: In use [06.00 - LATEST] */
uint32 sw_version = 3;
- /* Deprecated fields */
- reserved 4,5,6,7,8,9,10,11,12;
+ /* Software commit ID and LTS version. *
+ * String of 7 characters: *
+ * - Commit ID (examples: "fa02cd0" means the beginning of the commit ID "fa02cd0") [06.00 - 06.06.xx] *
+ * - LTS version and commit ID (examples: "0bdd23f" means LTS version 11 and the beginning of the commit ID "dd23f") *
+ [06.07.00 - LATEST] *
+ * Status: In use [06.00 - LATEST] */
+ string commit_id = 15;
- /* Structure with battery and temperature information */
+ /* Device runtime information. *
+ * Status: In use [06.00 - LATEST] */
ProtoRuntime runtime_info = 13;
- /* Structure with modem specific runtime information */
+ /* Memory statistics: *
+ * 1st item - Status of the non-volatile storage: *
+ * - 0 - Non-volatile storage works properly *
+ * - 1 - Non-volatile storage has some corrupt packets. Memory is read-only *
+ * - 2 - Non-volatile storage is corrupted. Memory is unavailable *
+ * 2nd item - Timestamp of the end of collecting statistics in seconds since 1st of January 1970 (epoch time). *
+ Undefined value: 4294967295. *
+ * 3rd item - Capacity of the memory in bytes. *
+ * 4th item - Used space in bytes. *
+ * 5th item - Size of invalid (outdated) packets in bytes. *
+ * 6th item - Size of corrupt packets in bytes. *
+ * 7th item - Number of valid packets. *
+ * 8th item - Number of invalid (outdated) packets. *
+ * 9th item - Number of corrupt packets. *
+ * 10th item - Number of all samples for channel 1 (valid packets). *
+ * 11th item - Number of all samples for channel 2 (valid packets). *
+ * 12th item - Number of all samples for channel 3 (valid packets). *
+ * 13th item - Number of all samples for channel 4 (valid packets). *
+ * 14th item - Number of all samples for channel 5 (valid packets). *
+ * 15th item - Number of all samples for channel 6 (valid packets). *
+ * 16th item - Timestamp of the first binary measurement in seconds since 1st of January 1970 (epoch time). *
+ * Undefined value: 4294967295. *
+ * 17th item - Timestamp of the last binary measurement in seconds since 1st of January 1970 (epoch time). *
+ * Undefined value: 4294967295. *
+ * 18th item - Timestamp of the last binary measurement marked as sent, in seconds since 1st of January 1970 (epoch time). *
+ * Undefined value: 4294967295. *
+ * 19th item - Timestamp of the first continuous measurement in seconds since 1st of January 1970 (epoch time). *
+ * Undefined value: 4294967295. *
+ * 20th item - Timestamp of the last continuous measurement in seconds since 1st of January 1970 (epoch time). *
+ * Undefined value: 4294967295. *
+ * 21th item - Timestamp of the last continuous measurement marked as sent, in seconds since 1st of January 1970 (epoch time).*
+ * Undefined value: 4294967295. *
+ * 22th item - NvM write counter. *
+ * Status: In use [06.02 - LATEST] */
+ repeated uint32 memory_statistics = 17;
+
+ /* Last update check information. *
+ * Status: In use [06.08.00 - LATEST] */
+ ProtoUpdateInfo last_update_info = 18;
+
+ /* Modem information. *
+ * Only used if the sensor has a modem. *
+ * Status: In use [06.00 - LATEST] */
ProtoModem modem = 14;
- /* String up to 7 bytes long. Software commit id e.g. "e0e8556" */
- /* From version 06.07 the first two characters indicate the LTS version. */
- /* For example: the value "0bdd23f" means LTS version 11 and the beginning of the commit ID "dd23f" */
- string commit_id = 15;
+ /* SERVER COMMUNICATION FIELDS ---------------------------------------------------------------------------------------------- */
- /* Optional string up to 36 bytes long. Can be set to any user define value or hold device's IMEI */
+ /* Cloud token. *
+ * Can be empty, set to any user-defined value, or hold device IMEI. *
+ * String up to 36 characters. *
+ * This field is only sent during server communication. *
+ * Status: In use [06.00 - LATEST] */
string cloud_token = 16;
- /* Memory statistics: */
- /* memory_statistics[0] - Status of Nv storage: */
- /* - 0 - Nv storage hasn't errors */
- /* - 1 - Nv storage has some corrupted packet. Memory is read-only */
- /* - 2 - Nv storage is corrupted. Memory is unavailable */
- /* memory_statistics[1] - Timestamp of the end of collecting statistics. */
- /* Value in seconds since UNIX EPOCH 01-01-1970. Undefined value: 4294967295 */
- /* memory_statistics[2] - Capacity of memory in bytes */
- /* memory_statistics[3] - Used space in bytes */
- /* memory_statistics[4] - Size of invalid (outdated) packets in bytes */
- /* memory_statistics[5] - Size of corrupted packets in bytes */
- /* memory_statistics[6] - Number of valid packets */
- /* memory_statistics[7] - Number of invalid (outdated) packets */
- /* memory_statistics[8] - Number of corrupted packets */
- /* memory_statistics[9] - Number of all samples for channel 1 (valid packets) */
- /* memory_statistics[10] - Number of all samples for channel 2 (valid packets) */
- /* memory_statistics[11] - Number of all samples for channel 3 (valid packets) */
- /* memory_statistics[12] - Number of all samples for channel 4 (valid packets) */
- /* memory_statistics[13] - Number of all samples for channel 5 (valid packets) */
- /* memory_statistics[14] - Number of all samples for channel 6 (valid packets) */
- /* memory_statistics[15] - Timestamp of the first binary measurement. */
- /* Value in seconds since UNIX EPOCH 01-01-1970. Undefined value: 4294967295 */
- /* memory_statistics[16] - Timestamp of the last binary measurement. */
- /* Value in seconds since UNIX EPOCH 01-01-1970. Undefined value: 4294967295 */
- /* memory_statistics[17] - Timestamp of the last binary measurement, that marked as sent. */
- /* Value in seconds since UNIX EPOCH 01-01-1970. Undefined value: 4294967295 */
- /* memory_statistics[18] - Timestamp of the first continuous measurement. */
- /* Value in seconds since UNIX EPOCH 01-01-1970. Undefined value: 4294967295 */
- /* memory_statistics[19] - Timestamp of the last continuous measurement. */
- /* Value in seconds since UNIX EPOCH 01-01-1970. Undefined value: 4294967295 */
- /* memory_statistics[20] - Timestamp of the last continuous measurement, that marked as sent. */
- /* Value in seconds since UNIX EPOCH 01-01-1970. Undefined value: 4294967295 */
- /* memory_statistics[21] - NVM write counter */
- repeated uint32 memory_statistics = 17;
-
- /* Information about last sensor SW update */
- ProtoUpdateInfo last_update_info = 18;
+ /* Enclosure tamper alert: *
+ * - 0 - Inactive *
+ * - 1 - Active (enclosure opened) *
+ * - 2 - Inactive, was active (enclosure closed) *
+ * The transition from 'Inactive, was active' to 'Inactive' occurs after the alert has been successfully sent to the server *
+ * or the user has reset the alert via Bluetooth. *
+ * This field is only sent during server communication. *
+ * Status: In use [07.00.01 - LATEST] */
+ uint32 enclosure_tamper_alert = 19;
}
\ No newline at end of file
diff --git a/common/transport/coap/src/main/proto/efento/proto_measurement_types.proto b/common/transport/coap/src/main/proto/efento/proto_measurement_types.proto
index fc72cc10e3..7e7e908e16 100644
--- a/common/transport/coap/src/main/proto/efento/proto_measurement_types.proto
+++ b/common/transport/coap/src/main/proto/efento/proto_measurement_types.proto
@@ -19,160 +19,201 @@ option java_package = "org.thingsboard.server.gen.transport.coap";
option java_outer_classname = "MeasurementTypeProtos";
enum MeasurementType {
- /* [] - No sensor on the channel */
+
+ /* [] - No sensor on the channel. */
MEASUREMENT_TYPE_NO_SENSOR = 0;
- /* [°C] - Celsius degree. Resolution 0.1°C. Range [-273.2:4000.0]. Type: Continuous */
+ /* [°C] - Celsius degree. Temperature. Resolution: 0.1°C. Range: [-273.2:4000.0]. Type: Continuous. */
MEASUREMENT_TYPE_TEMPERATURE = 1;
- /* [% RH] - Relative humidity. Resolution 1%. Range [0:100]. Type: Continuous */
+ /* [% RH] - Percentage. Relative humidity. Resolution: 1%. Range: [0:100]. Type: Continuous. */
MEASUREMENT_TYPE_HUMIDITY = 2;
- /* [hPa] - Hectopascal (1hPa = 100Pa). Resolution 0.1hPa. Range: [1.0:2000.0]. Atmospheric pressure. Type: Continuous */
+ /* [hPa] - Hectopascal. Atmospheric pressure. Resolution: 0.1hPa. Range: [1.0:2000.0]. Type: Continuous. */
MEASUREMENT_TYPE_ATMOSPHERIC_PRESSURE = 3;
- /* [Pa] - Pascal. Resolution 1Pa. Range [-10000:10000]. Differential pressure. Type: Continuous */
+ /* [Pa] - Pascal. Differential pressure. Resolution: 1Pa. Range: [-10000:10000]. Type: Continuous. */
MEASUREMENT_TYPE_DIFFERENTIAL_PRESSURE = 4;
- /* Sign indicates state: (+) ALARM, (-) OK. Type: Binary */
+ /* Sign indicates state: (+) Alarm, (-) OK. Type: Binary. */
MEASUREMENT_TYPE_OK_ALARM = 5;
- /* [IAQ] - IAQ index. Resolution 1IAQ. Range [0:500]. To get IAQ index the value should be divided by 3. */
- /* Sensor return also calibration status as metadata (is the remainder when the absolute value is divided by 3): */
- /* - 0: Calibration required (sensor returns not accurate values) */
- /* - 1: Calibration on-going (sensor returns not accurate values) */
- /* - 2: Calibration done (best accuracy of IAQ sensor) */
- /* Type: Continuous */
+ /* [IAQ] - IAQ index. Air quality. Resolution: 1IAQ. Range: [0:500]. Type: Continuous. *
+ * To obtain the IAQ index, the measurement value should be divided by 3. *
+ * Measurement also includes calibration status as metadata (is the remainder of the absolute value divided by 3): *
+ * - 0 - Calibration required (sensor returns not accurate values) *
+ * - 1 - Calibration on-going (sensor returns not accurate values) *
+ * - 2 - Calibration done (best accuracy of IAQ sensor) */
MEASUREMENT_TYPE_IAQ = 6;
- /* Sign indicates water presence: (+) water not detected, (-) water detected. Type: Binary */
+ /* Sign indicates state: (+) Water detected, (-) Water not detected. Type: Binary. */
MEASUREMENT_TYPE_FLOODING = 7;
- /* [NB] Number of pulses. Resolution 1 pulse. Range [0:8000000]. Type: Continuous */
+ /* [NB] - Number of pulses. Number of pulses in a single period. Resolution: 1 pulse. Range: [0:8000000]. Type: Continuous. */
MEASUREMENT_TYPE_PULSE_CNT = 8;
- /* [Wh] - Watthour; Resolution 1Wh. Range [0:8000000]. Number of Watthours in a single period. Type: Continuous */
+ /* [Wh] - Watt-hour. Number of watt-hours in a single period. Resolution: 1Wh. Range: [0:8000000]. Type: Continuous. */
MEASUREMENT_TYPE_ELECTRICITY_METER = 9;
- /* [l] - Liter. Resolution 1l. Range [0:8000000]. Number of litres in a single period. Type: Continuous */
+ /* [l] - Liter. Number of litres in a single period. Resolution: 1l. Range: [0:8000000]. Type: Continuous. */
MEASUREMENT_TYPE_WATER_METER = 10;
- /* [kPa] - Kilopascal (1kPa = 1000Pa); Resolution 1kPa. Range [-1000:0]. Soil moisture (tension). Type: Continuous */
+ /* [kPa] - Kilopascal. Soil moisture (tension). Resolution: 1kPa. Range: [-1000:0]. Type: Continuous. */
MEASUREMENT_TYPE_SOIL_MOISTURE = 11;
- /* [ppm] - Parts per million. Resolution 1ppm. Range [0:1000000]. Carbon monoxide concentration. Type: Continuous */
+ /* [ppm] - Parts per million. Carbon monoxide concentration. Resolution: 1ppm. Range: [0:1000000]. Type: Continuous. */
MEASUREMENT_TYPE_CO_GAS = 12;
- /* [ppm] - Parts per million. Resolution 1.0ppm. Range [0:1000000]. Nitrogen dioxide concentration. Type: Continuous */
+ /* [ppm] - Parts per million. Nitrogen dioxide concentration. Resolution: 1ppm. Range: [0:1000000]. Type: Continuous. */
MEASUREMENT_TYPE_NO2_GAS = 13;
- /* [ppm] - Parts per million. Resolution 0.01ppm. Range [0.00:80000.00]. Hydrogen sulfide concentration. Type: Continuous */
+ /* [ppm] - Parts per million. Hydrogen sulfide concentration. Resolution: 0.01ppm. Range: [0.00:80000.00]. Type: Continuous. */
MEASUREMENT_TYPE_H2S_GAS = 14;
- /* [lx] - Lux. Resolution 0.1lx. Range [0.0:100000.0]. Illuminance. Type: Continuous */
+ /* [lx] - Lux. Illuminance. Resolution: 0.1lx. Range: [0.0:100000.0]. Type: Continuous. */
MEASUREMENT_TYPE_AMBIENT_LIGHT = 15;
- /* [µg/m^3] - Micro gram per cubic meter. Resolution 1µg/m^3. Range [0:1000]. */
- /* Particles with an aerodynamic diameter less than 1 micrometer. Type: Continuous */
+ /* [µg/m^3] - Microgram per cubic meter. Particles with an aerodynamic diameter of less than 1 micrometers. *
+ * Resolution: 1µg/m^3. Range: [0:1000]. Type: Continuous. */
MEASUREMENT_TYPE_PM_1_0 = 16;
- /* [µg/m^3] - Micro gram per cubic meter. Resolution 1µg/m^3. Range [0:1000]. */
- /* Particles with an aerodynamic diameter less than 2.5 micrometers. Type: Continuous */
+ /* [µg/m^3] - Microgram per cubic meter. Particles with an aerodynamic diameter of less than 2.5 micrometers. *
+ * Resolution: 1µg/m^3. Range: [0:1000]. Type: Continuous. */
MEASUREMENT_TYPE_PM_2_5 = 17;
- /* [µg/m^3] - Micro gram per cubic meter. Resolution 1µg/m^3. Range [0:1000]. */
- /* Particles with an aerodynamic diameter less than 10 micrometers. Type: Continuous */
+ /* [µg/m^3] - Microgram per cubic meter. Particles with an aerodynamic diameter of less than 10 micrometers. *
+ * Resolution: 1µg/m^3. Range: [0:1000]. Type: Continuous. */
MEASUREMENT_TYPE_PM_10_0 = 18;
- /* [dB] - Decibels. Resolution 0.1 dB. Range: [0.0:200.0]. Noise level. Type: Continuous */
+ /* [dB] - Decibel. Noise level. Resolution: 0.1 dB. Range: [0.0:200.0]. Type: Continuous. */
MEASUREMENT_TYPE_NOISE_LEVEL = 19;
- /* [ppm] - Parts per million. Resolution 1ppm. Range [0:1000000]. Ammonia concentration. Type: Continuous */
+ /* [ppm] - Parts per million. Ammonia concentration. Resolution: 1ppm. Range: [0:1000000]. Type: Continuous. */
MEASUREMENT_TYPE_NH3_GAS = 20;
- /* [ppm] - Parts per million. Resolution 1ppm. Range [0:1000000]. Methane concentration. Type: Continuous */
+ /* [ppm] - Parts per million. Methane concentration. Resolution: 1ppm. Range: [0:1000000]. Type: Continuous. */
MEASUREMENT_TYPE_CH4_GAS = 21;
- /* [kPa] - Kilopascal (1kPa = 1000Pa, 100kPa = 1bar). Resolution 1kPa. Range [0:200000]. Pressure. Type: Continuous */
+ /* [kPa] - Kilopascal (100kPa = 1bar). Pressure. Resolution: 1kPa. Range: [0:200000]. Type: Continuous. */
MEASUREMENT_TYPE_HIGH_PRESSURE = 22;
- /* [mm] - Millimeter. Resolution 1mm. Range [0:100000]. Distance. Type: Continuous */
+ /* [mm] - Millimeter. Distance. Resolution: 1mm. Range: [0:100000]. Type: Continuous. */
MEASUREMENT_TYPE_DISTANCE_MM = 23;
- /* [l] - Liter. Resolution 1l. Range [0:1000000]. Accumulative water meter (minor). Type: Continuous */
+ /* [l] - Liter. Water meter (minor). Resolution: 1l. Range: [0:99]. Type: Continuous. *
+ * To obtain the liters, the measurement value should be divided by 6. *
+ * Measurement also includes a major channel index related with the minor channel as metadata *
+ * (is the remainder of the absolute value divided by 6). */
MEASUREMENT_TYPE_WATER_METER_ACC_MINOR = 24;
- /* [hl] - Hectoliter. Resolution 1hl. Range [0:1000000]. Accumulative water meter (major). Type: Continuous */
+ /* [hl] - Hectoliter. Water meter (major). Resolution: 1hl. Range: [0:999999]. Type: Continuous. *
+ * To obtain the hectoliters, the measurement value should be divided by 4. *
+ * Measurement also includes a measurement status as metadata (is the remainder of the absolute value divided by 4): *
+ * - 0 - Counter works properly *
+ * - 1 - Error occurred (the counted value may be underestimated) *
+ * - 2 - Sensor reset occurred (the counted value may be underestimated) *
+ * - 3 - Sensor reset and error occurred (the counted value may be underestimated) */
MEASUREMENT_TYPE_WATER_METER_ACC_MAJOR = 25;
- /* [ppm] - Parts per million. Resolution 1ppm. Range [0:1000000]. Carbon dioxide concentration. Type: Continuous */
+ /* [ppm] - Parts per million. Carbon dioxide concentration. Resolution: 1ppm. Range: [0:1000000]. Type: Continuous. *
+ * To obtain the carbon dioxide concentration, the measurement value should be divided by 3. *
+ * Measurement also includes calibration status as metadata (is the remainder of the absolute value divided by 3): *
+ * - 0 - Auto calibration has not yet been performed *
+ * - 1 - The last auto calibration was successful *
+ * - 2 - Last auto calibration failed */
MEASUREMENT_TYPE_CO2_GAS = 26;
- /* [% RH] - Relative humidity. Resolution 0.1%. Range [0.0:100.0]. Type: Continuous */
+ /* [% RH] - Percentage. Relative humidity (accurate). Resolution: 0.1%. Range: [0.0:100.0]. Type: Continuous. */
MEASUREMENT_TYPE_HUMIDITY_ACCURATE = 27;
- /* [sIAQ] - Static IAQ index. Resolution 1IAQ. Range [0:10000]. To get static IAQ index the value should be divided by 3. */
- /* Sensor return also calibration status as metadata (is the remainder when the absolute value is divided by 3): */
- /* - 0: Calibration required (sensor returns not accurate values) */
- /* - 1: Calibration on-going (sensor returns not accurate values) */
- /* - 2: Calibration done (best accuracy of IAQ sensor) */
- /* Type: Continuous */
+ /* [sIAQ] - Static IAQ index. Air quality. Resolution: 1sIAQ. Range: [0:10000]. Type: Continuous. *
+ * To obtain the static IAQ index, the measurement value should be divided by 3. *
+ * Measurement also includes calibration status as metadata (is the remainder of the absolute value divided by 3): *
+ * - 0 - Calibration required (sensor returns not accurate values) *
+ * - 1 - Calibration on-going (sensor returns not accurate values) *
+ * - 2 - Calibration done (best accuracy of IAQ sensor) */
MEASUREMENT_TYPE_STATIC_IAQ = 28;
- /* [ppm] - Parts per million. Resolution 1ppm. Range [0:1000000]. CO2 equivalent. */
- /* To get CO2 equivalent the value should be divided by 3. */
- /* Sensor return also calibration status as metadata (is the remainder when the absolute value is divided by 3): */
- /* - 0: Calibration required (sensor returns not accurate values) */
- /* - 1: Calibration on-going (sensor returns not accurate values) */
- /* - 2: Calibration done (best accuracy of IAQ sensor) */
- /* Type: Continuous */
+ /* [ppm] - Parts per million. CO2 equivalent. Resolution: 1ppm. Range: [0:1000000]. Type: Continuous. *
+ * To obtain the CO2 equivalent, the measurement value should be divided by 3. *
+ * Measurement also includes calibration status as metadata (is the remainder of the absolute value divided by 3): *
+ * - 0 - Calibration required (sensor returns not accurate values) *
+ * - 1 - Calibration on-going (sensor returns not accurate values) *
+ * - 2 - Calibration done (best accuracy of IAQ sensor) */
MEASUREMENT_TYPE_CO2_EQUIVALENT = 29;
- /* [ppm] - Parts per million. Resolution 1ppm. Range [0:100000]. Breath VOC estimate. */
- /* To get breath VOC estimate the value should be divided by 3. */
- /* Sensor return also calibration status as metadata (is the remainder when the absolute value is divided by 3): */
- /* - 0: Calibration required (sensor returns not accurate values) */
- /* - 1: Calibration on-going (sensor returns not accurate values) */
- /* - 2: Calibration done (best accuracy of IAQ sensor) */
- /* Type: Continuous */
+ /* [ppm] - Parts per million. Breath VOC estimate. Resolution: 1ppm. Range: [0:100000]. Type: Continuous. *
+ * To obtain the breath VOC estimate, the measurement value should be divided by 3. *
+ * Measurement also includes calibration status as metadata (is the remainder of the absolute value divided by 3): *
+ * - 0 - Calibration required (sensor returns not accurate values) *
+ * - 1 - Calibration on-going (sensor returns not accurate values) *
+ * - 2 - Calibration done (best accuracy of IAQ sensor) */
MEASUREMENT_TYPE_BREATH_VOC = 30;
- /* Special measurement type reserved for cellular gateway. */
- /* Type: Continuous */
- MEASUREMENT_TYPE_CELLULAR_GATEWAY = 31;
+ /* Reserved for the Shared Modem functionality. Type: Continuous. */
+ MEASUREMENT_TYPE_SHARED_MODEM = 31;
- /* [%] - Percentage. Resolution 0.01%. Range [0.00:100.00]. Type: Continuous */
+ /* [%] - Percentage. Generic type. Resolution: 0.01%. Range: [0.00:100.00]. Type: Continuous. */
MEASUREMENT_TYPE_PERCENTAGE = 32;
- /* [mV] - Milivolt. Resolution 0.1mV. Range [0.0:100000.0]. Type: Continuous */
+ /* [mV] - Millivolt. Voltage. Resolution: 0.1mV. Range: [0.0:100000.0]. Type: Continuous. */
MEASUREMENT_TYPE_VOLTAGE = 33;
- /* [mA] - Milliampere. Resolution 0.01mA. Range [0.0:10000.00]. Type: Continuous */
+ /* [mA] - Milliampere. Current. Resolution: 0.01mA. Range: [0.00:10000.00]. Type: Continuous. */
MEASUREMENT_TYPE_CURRENT = 34;
- /* [NB] Number of pulses. Resolution 1 pulse. Range [0:1000000]. Type: Continuous */
+ /* [NB] - Number of pulses. Pulse counter (minor). Resolution: 1 pulse. Range: [0:999]. Type: Continuous. *
+ * To obtain the number of pulses, the measurement value should be divided by 6. *
+ * Measurement also includes a major channel index related with the minor channel as metadata *
+ * (is the remainder of the absolute value divided by 6). */
MEASUREMENT_TYPE_PULSE_CNT_ACC_MINOR = 35;
- /* [kNB] Number of kilopulses. Resolution 1 kilopulse. Range [0:1000000]. Type: Continuous */
+ /* [kNB] - Number of kilopulses. Pulse counter (major). Resolution: 1 kilopulse. Range: [0:999999]. Type: Continuous. *
+ * To obtain the number of kilopulses, the measurement value should be divided by 4. *
+ * Measurement also includes a measurement status as metadata (is the remainder of the absolute value divided by 4): *
+ * - 0 - Counter works properly *
+ * - 1 - Error occurred (the counted value may be underestimated) *
+ * - 2 - Sensor reset occurred (the counted value may be underestimated) *
+ * - 3 - Sensor reset and error occurred (the counted value may be underestimated) */
MEASUREMENT_TYPE_PULSE_CNT_ACC_MAJOR = 36;
- /* [Wh] - Watt-hour; Resolution 1Wh. Range [0:1000000]. Number of watt-hours in a single period. Type: Continuous */
+ /* [Wh] - Watt-hour. Electricity meter (minor). Resolution: 1Wh. Range: [0:999]. Type: Continuous. *
+ * To obtain the watt-hours, the measurement value should be divided by 6. *
+ * Measurement also includes a major channel index related with the minor channel as metadata *
+ * (is the remainder of the absolute value divided by 6). */
MEASUREMENT_TYPE_ELEC_METER_ACC_MINOR = 37;
- /* [kWh] - Kilowatt-hour; Resolution 1kWh. Range [0:1000000]. Number of kilowatt-hours in a single period. Type: Continuous */
+ /* [kWh] - Kilowatt-hour. Electricity meter (major). Resolution: 1kWh. Range: [0:999999]. Type: Continuous. *
+ * To obtain the kilowatt-hours, the measurement value should be divided by 4. *
+ * Measurement also includes a measurement status as metadata (is the remainder of the absolute value divided by 4): *
+ * - 0 - Counter works properly *
+ * - 1 - Error occurred (the counted value may be underestimated) *
+ * - 2 - Sensor reset occurred (the counted value may be underestimated) *
+ * - 3 - Sensor reset and error occurred (the counted value may be underestimated) */
MEASUREMENT_TYPE_ELEC_METER_ACC_MAJOR = 38;
- /* [NB] Number of pulses (wide range). Resolution 1 pulse. Range [0:999999]. Type: Continuous */
+ /* [NB] - Number of pulses. Wide-range pulse counter (minor). Resolution: 1 pulse. Range: [0:999999]. Type: Continuous. *
+ * To obtain the number of pulses, the measurement value should be divided by 6. *
+ * Measurement also includes a major channel index related with the minor channel as metadata *
+ * (is the remainder of the absolute value divided by 6). */
MEASUREMENT_TYPE_PULSE_CNT_ACC_WIDE_MINOR = 39;
- /* [MNB] Number of megapulses (wide range). Resolution 1 megapulse. Range [0:999999]. Type: Continuous */
+ /* [MNB] - Number of megapulses. Wide-range pulse counter (major). Resolution: 1 megapulse. Range: [0:999999]. *
+ * Type: Continuous. *
+ * To obtain the number of megapulses, the measurement value should be divided by 4. *
+ * Measurement also includes a measurement status as metadata (is the remainder of the absolute value divided by 4): *
+ * - 0 - Counter works properly *
+ * - 1 - Error occurred (the counted value may be underestimated) *
+ * - 2 - Sensor reset occurred (the counted value may be underestimated) *
+ * - 3 - Sensor reset and error occurred (the counted value may be underestimated) */
MEASUREMENT_TYPE_PULSE_CNT_ACC_WIDE_MAJOR = 40;
- /* [mA] - Milliampere. Resolution 0.001mA. Range [-4 000.000:4 000.000]. Type: Continuous */
+ /* [mA] - Milliampere. Current (precise). Resolution: 0.001mA. Range: [-4000.000:4000.000]. Type: Continuous. */
MEASUREMENT_TYPE_CURRENT_PRECISE = 41;
- /* Sign indicates state: (+) ON, (-) OFF. Type: Binary */
+ /* Sign indicates state: (+) ON, (-) OFF. Type: Binary. */
MEASUREMENT_TYPE_OUTPUT_CONTROL = 42;
-}
-
+ /* [Ω] - Ohm. Resolution: 1Ω. Range: [0:1000000]. Type: Continuous. */
+ MEASUREMENT_TYPE_RESISTANCE = 43;
+}
\ No newline at end of file
diff --git a/common/transport/coap/src/main/proto/efento/proto_measurements.proto b/common/transport/coap/src/main/proto/efento/proto_measurements.proto
index 650487ff29..12626d4834 100644
--- a/common/transport/coap/src/main/proto/efento/proto_measurements.proto
+++ b/common/transport/coap/src/main/proto/efento/proto_measurements.proto
@@ -14,6 +14,7 @@
* limitations under the License.
*/
syntax = "proto3";
+
import "efento/proto_measurement_types.proto";
option java_package = "org.thingsboard.server.gen.transport.coap";
@@ -21,106 +22,166 @@ option java_outer_classname = "MeasurementsProtos";
message ProtoChannel {
- /* Type of channel */
+ /* Reserved fields. */
+ reserved 6,7,8;
+
+ /* Type of measurement. *
+ * Status: In use [06.00 - LATEST] */
MeasurementType type = 1;
- /* Timestamp of the first sample (the oldest one) in seconds since UNIX EPOCH 01-01-1970 */
+ /* Timestamp of the first (oldest) sample in seconds since 1st of January 1970 (epoch time). *
+ * Status: In use [06.00 - LATEST] */
int32 timestamp = 2;
- /* Only used for 'Continuous' sensor types. Value used as the starting point for calculating the values of all */
- /* measurements in the package. */
- /* Format defined by 'MeasurementType' field */
+ /* Start point of measurement values. *
+ * This is used as the base value for calculating the values of all channel measurements in the message. *
+ * Used for 'Continuous' sensor types only. *
+ * Format is defined by 'MeasurementType' field. *
+ * Status: In use [06.00 - LATEST] */
sint32 start_point = 4;
- /* 'Continuous' sensor types */
- /* Value of the offset from the 'start_point' for each measurement in the package. The oldest sample first ([0]). */
- /* 'sample_offsets' format defined by 'MeasurementType' field. */
- /* If the 'sample_offset' has a value from the range [8355840: 8388607], it should be interpreted as a sensor error code. */
- /* In that case value of the 'start_point' field should not be added to this 'sample_offset'. See ES6-264 for error codes. */
- /* Example: MeasurementType = 1 (temperature), start_point = 100, sample_offsets[0] = 15, sample_offsets[1] = 20, */
- /* sample_offset[2] = 8388605 */
- /* 1st sample in the package temperature value = 11.5 °C, 2nd sample in the package temperature value = 12 °C */
- /* 3rd sample in the package has no temperature value. It has information about failure of MCP9808 (temperature) sensor. */
- /* Calculating timestamps of the measurements: timestamp = 1606391700, measurement_period_base = 60, */
- /* measurement_period_factor = 1. Timestamp of the 1st sample = 1606391700, timestamp of the 2nd sample = 1606391760, */
- /* timestamp of the 3rd sample 1606391820 */
-
- /* 'Binary' sensor types: */
- /* Absolute value of the 'sample_offsets' field indicates the offset in seconds from 'timestamp' field. */
- /* Sign (- or +) indicates the state of measurements depending on the sensor type. */
- /* Value of this field equals to '1' or '-1' indicates the state at the 'timestamp'. Other values */
- /* indicate the state of the relay at the time (in seconds) equal to 'timestamp' + absolute value -1. */
- /* Values of this field are incremented starting from 1 (1->0: state at the time */
- /* of 'timestamp', 2->1: state at the time equal to 'timestamp' + 1 s, 3->2 : */
- /* state at the time equal to 'timestamp' + 2 s, etc.). The first and the last sample define the time range of the */
- /* measurements. Only state changes in the time range are included in the 'sample_offsets' field */
- /* Examples: if 'timestamp' value is 1553518060 and 'sample_offsets' equals '1', it means that at 1553518060 the state */
- /* was high, if 'timestamp' value is 1553518060 and 'sample_offsets' equals '-9', it means at 1553518068 the state was low */
+ /* Measurement value offsets. *
+ * For 'Continuous' sensor types: *
+ * These are the values of the offsets from the 'start_point' for each channel measurement in the message. *
+ * Format is defined by 'MeasurementType' field. The first sample is the oldest. *
+ * If the sample offset has a value in the range [8355840:8388607], it should be interpreted as a sensor error code. *
+ * In this case, the value of the 'start_point' field should not be added to this sample offset. *
+ * Example: type = 1 (temperature) *
+ * start_point = 100 *
+ * sample_offsets[0] = 15, sample_offsets[1] = 20, sample_offsets[2] = 8388557 *
+ * timestamp = 1606391700 *
+ * measurement_period_base = 60 *
+ * measurement_period_factor = 1 *
+ * 1st measurement is the temperature value = 11.5°C, measured at 1606391700 *
+ * 2nd measurement is the temperature value = 12°C, measured at 1606391760 *
+ * 3rd measurement is the error code (failure of the SHT4x temperature sensor), measured at 1606391820 *
+ * For 'Binary' sensor types: *
+ * The absolute value of the sample offset minus 1 is the offset in seconds from the 'timestamp' field. *
+ * Sign (- or +) indicates the state of the measurement depending on the sensor type. *
+ * The first and the last samples define the time range of the measurements. *
+ * Only state changes within the time range are included in the 'sample_offsets' field. *
+ * Example: type = 5 (OK/Alarm) *
+ * sample_offsets[0] = 1, sample_offsets[1] = -9, sample_offsets[2] = 13 *
+ * timestamp = 1606391700 *
+ * 1st measurement is the state at 1606391700, which is Alarm (+) *
+ * 2nd measurement is the state at 1606391708, which is OK (-) *
+ * 3rd measurement is the state at 1606391712, which is Alarm (+) *
+ * Status: In use [06.00 - LATEST] */
repeated sint32 sample_offsets = 5 [packed=true];
-
- /* Deprecated - configuration is sent to endpoint 'c' */
- /* int32 lo_threshold = 6; */
- reserved 6;
-
- /* Deprecated - configuration is sent to endpoint 'c' */
- /* int32 hi_threshold = 7; */
- reserved 7;
-
- /* Deprecated - configurations sent to endpoint 'c' */
- /* int32 diff_threshold = 8; */
- reserved 8;
}
message ProtoMeasurements {
- /* Serial number of the device */
- bytes serial_num = 1;
-
- /* Battery status: true - battery ok, false - battery low */
- bool battery_status = 2;
-
- /* 'Measurement_period_base' and 'measurement_period_factor' define how often the measurements are taken. */
- /* Sensors of 'Continuous' type take measurement each Measurement_period_base * measurement_period_factor. */
- /* Sensors of 'Binary' type take measurement each Measurement_period_base. */
- /* For backward compatibility with versions 5.x in case of binary/mixed sensors, if the 'measurement_period_factor' is */
- /* not sent (equal to 0), then the default value '14' shall be used for period calculation. */
- /* For backward compatibility with versions 5.x in case of continues sensors, if the measurement_period_factor is */
- /* not sent (equal to 0), then the default value '1' shall be used for period calculation. */
- /* measurement period base in seconds */
+ /* RESERVED FIELDS ---------------------------------------------------------------------------------------------------------- */
+
+ reserved 10,11,12,13,14,15;
+
+ /* DEVICE STATUS FIELDS ----------------------------------------------------------------------------------------------------- */
+
+ /* Serial number of the device. *
+ * Length: 6 bytes. *
+ * Status: In use [07.00.00 - LATEST] / Previously as serial_num [06.00 - 06.xx.xx] */
+ bytes serial_number = 1;
+
+ /* Identifier of the current configuration. *
+ * The value of this field changes with every configuration change. *
+ * Status: In use [07.00.00 - LATEST] / Previously as hash [06.00 - 06.xx.xx] */
+ uint32 configuration_hash = 9;
+
+ /* Identifier of the current extended configuration. *
+ * The value of this field changes with every extended configuration change. *
+ * Status: In use [07.00.00 - LATEST] */
+ uint32 extended_configuration_hash = 17;
+
+ /* Battery level and status: *
+ * - [1:65525] - Battery voltage measurement and a battery status *
+ * - [65526:65531] - No battery voltage measurement and a battery status *
+ * To obtain the battery voltage, the battery_level value should be divided by 6 and the remainder of the division discarded. *
+ * The result is in hundredths of a volt or 10921 (no battery voltage measurement). *
+ * Battery level also includes a battery status as metadata (the remainder of the value divided by 6): *
+ * - 0 - Device is powered by a battery *
+ * - 1 - Device is powered by a rechargeable battery *
+ * - 2 - Device is powered by an external source and a battery is charging *
+ * - 3 - Reserved for the future use *
+ * - 4 - Reserved for the future use *
+ * - 5 - Reserved for the future use *
+ * Example: battery_level = 2474 *
+ * Battery voltage = 2474/6 = 412 = 4.12[V] *
+ * Battery status = 2474 % 6 = 2 *
+ * Status: In use [07.00.00 - LATEST] */
+ uint32 battery_level = 18;
+
+ /* MEASUREMENT FIELDS ------------------------------------------------------------------------------------------------------- */
+
+ /* Measurement period defines how often the measurements are to be taken. *
+ * Sensors of 'Continuous' type take measurement each 'measurement_period_base' * 'measurement_period_factor'. *
+ * Sensors of 'Binary' type take measurement each 'measurement_period_base'. *
+ * For backward compatibility with versions 5.xx in case of 'Binary/Mixed' sensors, if the 'measurement_period_factor' is *
+ * not sent (equal to 0), then the default value '14' shall be used for the period calculation. *
+ * For backward compatibility with versions 5.xx in case of 'Continuous' sensors, if the 'measurement_period_factor' is *
+ * not sent (equal to 0), then the default value '1' shall be used for the period calculation. */
+
+ /* Measurement period base in seconds. *
+ * Status: In use [06.00 - LATEST] */
uint32 measurement_period_base = 3;
- /* Measurement period factor */
+ /* Measurement period factor. *
+ * Status: In use [06.00 - LATEST] */
uint32 measurement_period_factor = 8;
+ /* Measurements grouped by channel. *
+ * Status: In use [06.00 - LATEST] */
repeated ProtoChannel channels = 4;
- /* Timestamp of the next scheduled transmission. If the device will not send data until this time, */
- /* it should be considered as 'lost' */
+ /* SERVER COMMUNICATION FIELDS ---------------------------------------------------------------------------------------------- */
+
+ /* Battery status: *
+ * - True - Battery OK *
+ * - False - Battery discharged *
+ * Status: In use [06.00 - LATEST] */
+ bool battery_status = 2;
+
+ /* Timestamp of the next scheduled transmission. *
+ * If the device has not sent any data by this time, it should be considered 'lost'. *
+ * This field is only sent during server communication. *
+ * Status: In use [06.00 - LATEST] */
uint32 next_transmission_at = 5;
- /* Reason of transmission - unsigned integer where each bit indicates different possible communication reason. */
- /* Can be more than one: */
- /* - bit 0: first message after sensor reset */
- /* - bit 1: user button triggered */
- /* - bit 2: user BLE triggered */
- /* - bit 3-7: number of retries -> incremented after each unsuccessful transmission. Max value 31. */
- /* Set to 0 after a successful transmission. */
- /* - bit 8...19: rule 1...12 was met */
- /* - bit 20: triggered after the end of the limit */
+ /* Reason for transmission. *
+ * Bitmask, where each bit represents a specific reason for initiating communication (multiple reasons can be active *
+ * simultaneously): *
+ * - Bit 0 - First message after sensor reset *
+ * - Bit 1 - User-triggered (button press) *
+ * - Bit 2 - User-triggered (Bluetooth) *
+ * - Bit 3:7 - Number of retries - incremented after each failed transmission and reset upon success. Range: [0:31] *
+ * - Bit 8:19 - Rule-based triggers (12 bits): *
+ * - [06.00 - 07.01.xx]: Bitmask indicating which of the rules 1-12 were met *
+ * - [07.02.00 - LATEST]: The transfer bit 'i' (where i = 0 to 11) is set if either rule 'i' (0-11) is set *
+ * or if rule 'i + 12' (12-15) is set. This aggregation applies only to bits 0-3 (rules 0-3 and 12-15). *
+ * The mask aggregates 16 rules (0-15) onto 12 bits. *
+ * - Bit 20 - Triggered at the end of the defined limit *
+ * - Bit 21 - Triggered after a PIN tamper alert *
+ * - Bit 22 - Triggered after an enclosure tamper alert *
+ * - Bit 23 - Triggered before shutdown *
+ * This field is only sent during server communication. *
+ * Status: In use [06.00 - LATEST] */
uint32 transfer_reason = 6;
- /* Signal strength level mapped from RSSI: */
- /* - 0: RSSI < -110 dBm */
- /* - 1: -110 dBm <= RSSI < -109 dBm */
- /* - 2...61: -109 <= RSSI < -108 dBm ... -50 dBm <= RSSI < -49 dBm */
- /* - 62: -49 dBm <= RSSI < -48 dBm */
- /* - 63: RSSI >= -48 dBm */
- /* - 99: Not known or not detectable */
+ /* Signal strength (RSSI - Received Signal Strength Level): *
+ * - 0 - RSSI < -110dBm *
+ * - 1 - -110dBm <= RSSI < -109dBm *
+ * - 2:61 - -109dBm <= RSSI < -108dBm ... -50dBm <= RSSI < -49dBm *
+ * - 62 - -49dBm <= RSSI < -48dBm *
+ * - 63 - RSSI >= -48dBm *
+ * - 99 - Unknown or undetectable *
+ * This field is only sent during server communication. *
+ * Status: Deprecated [06.00 - 06.xx.xx] */
uint32 signal = 7;
- /* Hash of the current configuration. Hash value changes each time a device receives a new configuration */
- uint32 hash = 9;
-
- /* Optional string up to 36 bytes long. Can be set to any user define value or hold device's IMEI */
+ /* Cloud token. *
+ * Can be empty, set to any user-defined value, or hold device IMEI. *
+ * String up to 36 characters. *
+ * This field is only sent during server communication. *
+ * Status: In use [06.00 - LATEST] */
string cloud_token = 16;
}
\ No newline at end of file
diff --git a/common/transport/coap/src/main/proto/efento/proto_rule.proto b/common/transport/coap/src/main/proto/efento/proto_rule.proto
index 7473d9b52a..5d91712864 100644
--- a/common/transport/coap/src/main/proto/efento/proto_rule.proto
+++ b/common/transport/coap/src/main/proto/efento/proto_rule.proto
@@ -18,270 +18,287 @@ syntax = "proto3";
option java_package = "org.thingsboard.server.gen.transport.coap";
option java_outer_classname = "ProtoRuleProtos";
-/* Encoding A: used to set absolute values in the Rules (e.g. upper and lower threshold values) */
-/* - TEMPERATURE - [°C] - Celsius degree. Resolution 0.1°C. Range [-273.2:4000.0]. */
-/* - HUMIDITY - [% RH] - Relative humidity. Resolution 1%. Range [0:100]. */
-/* - ATMOSPHERIC_PRESSURE - [hPa] - Hectopascal (1hPa = 100Pa). Resolution 0.1hPa. Range: [1.0:2000.0]. */
-/* - DIFERENTIAL_PRESSURE - [Pa] - Pascal. Resolution 1Pa. Range [-10000:10000] */
-/* - OK/ALARM - Not applicable */
-/* - IAQ - [IAQ] - IAQ index. Resolution 1IAQ. Range [0:500]. */
-/* - FLOODING - Not applicable */
-/* - PULSE_CNT - [NB] Number of pulses. Resolution 1 pulse. Range [0:8000000]. */
-/* - ELECTRICITY_METER - [W] - Watt; Resolution 1W. Range [0:8000000]. Average power consumption in period */
-/* - WATER_METER [l/min] - Liter per minute. Resolution 1l/min. Range [0:8000000]. Average water flow in period. */
-/* - SOIL_MOISTURE - [kPa] - Kilopascal (1kPa = 1000Pa); Resolution 1kPa. Range [-1000:0]. Soil moisture (tension). */
-/* - CO_GAS - [ppm] - Parts per million. Resolution 1ppm. Range [0:1000000]. Carbon monoxide concentration. */
-/* - NO2_GAS - [ppm] - Parts per million. Resolution 1ppm. Range [0:1000000]. Nitrogen dioxide concentration. */
-/* - H2S_GAS - [ppm] - Parts per million. Resolution 0.01ppm. Range [0.00:80000.00]. Hydrogen sulfide concentration. */
-/* - AMBIENT_LIGHT -[lx] - Lux. Resolution 0.1lx. Range [0.0:100000.0]. Illuminance. */
-/* - PM_1_0 - [µg/m^3] - Micro gram per cubic meter. Resolution 1µg/m^3 Range [0:1000]. */
-/* - PM_2_5 - [µg/m^3] - Micro gram per cubic meter. Resolution 1µg/m^3 Range [0:1000]. */
-/* - PM_10_0 - [µg/m^3] - Micro gram per cubic meter. Resolution 1µg/m^3 Range [0:1000]. */
-/* - NOISE_LEVEL - [dB] - Decibels. Resolution 0.1 dB. Range: [0.0:200.0]. Noise level. */
-/* - NH3_GAS - [ppm] - Parts per million. Resolution 1ppm. Range [0:1000000]. Ammonia concentration. */
-/* - CH4_GAS - [ppm] - Parts per million. Resolution 1ppm. Range [0:1000000]. Methane concentration. */
-/* - HIGH_PRESSURE - [kPa] - Kilopascal (1kPa = 1000Pa, 100kPa = 1bar). Resolution 1kPa. Range [0:200000]. Pressure. */
-/* - DISTANCE_MM - [mm] - Millimeter. Resolution 1mm. Range [0:100000]. Distance. */
-/* - WATER_METER_ACC_MINOR - [l] - Liter. Resolution 1l. Range [0:1000000]. Accumulative water meter (minor). */
-/* - WATER_METER_ACC_MAJOR - [hl] - Hectoliter. Resolution 1hl. Range [0:1000000]. Accumulative water meter (major). */
-/* - CO2_GAS - [ppm] - Parts per million. Resolution 1ppm. Range [0:1000000]. Carbon dioxide concentration. */
-/* - HUMIDITY ACCURATE - [% RH] - Relative humidity. Resolution 0.1%. Range [0.0:100.0]. */
-/* - STATIC_IAQ - [sIAQ] - Static IAQ index. Resolution 1sIAQ. Range [0:10000]. */
-/* - CO2_EQUIVALENT - [ppm] - Parts per million. Resolution 1ppm. Range [0:1000000]. Carbon dioxide equivalent. */
-/* - BREATH_VOC - [ppm] - Parts per million. Resolution 1ppm. Range [0:100000]. Breath VOC estimate. */
-/* - PERCENTAGE - [%] - Percentage. Resolution 0.01%. Range [0.00:100.00]. */
-/* - VOLTAGE - [mV] - Milivolt. Resolution 0.1mV. Range [0.0:100000.0]. */
-/* - CURRENT - [mA] - Miliampere. Resolution 0.01mA. Range [0.00:10000.00]. */
-/* - PULSE_CNT_ACC_MINOR - [NB] - Number of pulses. Resolution 1 pulse. Range [0:1000000]. Accumulative pulse counter (minor). */
-/* - PULSE_CNT_ACC_MAJOR - [kNB] - Number of kilopulses. Resolution 1 kilopulse. Range [0:1000000]. */
-/* Accumulative pulse counter (major). */
-/* - ELEC_METER_ACC_MINOR - [Wh] - Watt-hour. Resolution 1Wh. Range [0:1000000]. Accumulative electricity meter (minor). */
-/* - ELEC_METER_ACC_MAJOR - [kWh] - Kilowatt-hour. Resolution 1kWh. Range [0:1000000]. Accumulative electricity meter (major). */
-/* - PULSE_CNT_ACC_WIDE_MINOR - [NB] - Number of pulses. Resolution 1 pulse. Range [0:999999]. */
-/* Accumulative pulse counter wide range (minor). */
-/* - PULSE_CNT_ACC_WIDE_MAJOR - [MNB] - Number of megapulses. Resolution 1 megapulse. Range [0:999999]. */
-/* Accumulative pulse counter wide range (major). */
-/* - CURRENT_PRECISE - [mA] - Miliampere. Resolution 0.001mA. Range [-4 000.000:4 000.000]. */
-/* - OUTPUT_CONTROL - Not applicable */
-
-/* Encoding R: used to set relative values in the Rules (e.g. differential threshold and hysteresis) */
-/* - TEMPERATURE - [°C] - Celsius degree. Resolution 0.1°C. Range [0.1:4273.2]. */
-/* - HUMIDITY - [% RH] - Relative humidity. Resolution 1%. Range [1:100]. */
-/* - ATMOSPHERIC_PRESSURE - [hPa] - Hectopascal (1hPa = 100Pa). Resolution 0.1hPa. Range: [0.1:1999.0]. */
-/* - DIFERENTIAL_PRESSURE - [Pa] - Pascal. Resolution 1Pa. Range [1:20000] */
-/* - OK/ALARM - Not applicable */
-/* - VOC - [IAQ] - Iaq index. Resolution 1IAQ. Range [1:500]. */
-/* - FLOODING - Not applicable */
-/* - PULSE_CNT - [NB] Number of pulses. Resolution 1 pulse. Range [1:8000000]. */
-/* - ELECTRICITY_METER - [W] - Watt; Resolution 1W. Range [1:8000000]. Average power consumption in period */
-/* - WATER_METER [l/min] - Liter per minute. Resolution 1l/min. Range [1:8000000]. Average water flow in period. */
-/* - SOIL_MOISTURE - [kPa] - Kilopascal (1kPa = 1000Pa); Resolution 1kPa. Range [1:1000]. Soil moisture (tension). */
-/* - CO_GAS - [ppm] - Parts per million. Resolution 1ppm. Range [1:1000000]. Carbon monoxide concentration. */
-/* - NO2_GAS - [ppm] - Parts per million. Resolution 1ppm. Range [1:1000000]. Nitrogen dioxide concentration. */
-/* - H2S_GAS - [ppm] - Parts per million. Resolution 0.01ppm. Range [0.01:80000.00]. Hydrogen sulfide concentration. */
-/* - AMBIENT_LIGHT -[lx] - Lux. Resolution 0.1lx. Range [0.1:100000.0]. Illuminance. */
-/* - PM_1_0 - [µg/m^3] - Micro gram per cubic meter. Resolution 1µg/m^3 Range [1:1000]. */
-/* - PM_2_5 - [µg/m^3] - Micro gram per cubic meter. Resolution 1µg/m^3 Range [1:1000]. */
-/* - PM_10_0 - [µg/m^3] - Micro gram per cubic meter. Resolution 1µg/m^3 Range [1:1000]. */
-/* - NOISE_LEVEL - [dB] - Decibels. Resolution 0.1 dB. Range: [0.1:200.0]. Noise level. */
-/* - NH3_GAS - [ppm] - Parts per million. Resolution 1ppm. Range [1:1000000]. Ammonia concentration. */
-/* - CH4_GAS - [ppm] - Parts per million. Resolution 1ppm. Range [1:1000000]. Methane concentration. */
-/* - HIGH_PRESSURE - [kPa] - Kilopascal (1kPa = 1000Pa, 100kPa = 1bar). Resolution 1kPa. Range [1:200000]. Pressure. */
-/* - DISTANCE_MM - [mm] - Millimeter. Resolution 1mm. Range [1:100000]. Distance. */
-/* - WATER_METER_ACC_MINOR - [l] - Liter. Resolution 1l. Range [1:1000000]. Accumulative water meter (minor). */
-/* - WATER_METER_ACC_MAJOR - [hl] - Hectoliter. Resolution 1hl. Range [1:1000000]. Accumulative water meter (major). */
-/* - CO2_GAS - [ppm] - Parts per million. Resolution 1ppm. Range [1:1000000]. Carbon dioxide concentration. */
-/* - HUMIDITY ACCURATE - [% RH] - Relative humidity. Resolution 0.1%. Range [0.1:100.0]. */
-/* - STATIC_IAQ - [sIAQ] - Static IAQ index. Resolution 1sIAQ. Range [1:10000]. */
-/* - CO2_EQUIVALENT - [ppm] - Parts per million. Resolution 1ppm. Range [1:1000000]. Carbon dioxide equivalent. */
-/* - BREATH_VOC - [ppm] - Parts per million. Resolution 1ppm. Range [1:100000]. Breath VOC estimate. */
-/* - PERCENTAGE - [%] - Percentage. Resolution 0.01%. Range [0.01:100.00]. */
-/* - VOLTAGE - [mV] - Milivolt. Resolution 0.1mV. Range [0.1:100000.0]. */
-/* - CURRENT - [mA] - Miliampere. Resolution 0.01mA. Range [0.01:10000.00]. */
-/* - PULSE_CNT_ACC_MINOR - [NB] - Number of pulses. Resolution 1 pulse. Range [1:1000000]. Accumulative pulse counter (minor). */
-/* - PULSE_CNT_ACC_MAJOR - [kNB] - Number of kilopulses. Resolution 1 kilopulse. Range [1:1000000]. */
-/* Accumulative pulse counter (major). */
-/* - ELEC_METER_ACC_MINOR - [Wh] - Watt-hour. Resolution 1Wh. Range [1:1000000]. Accumulative electricity meter (minor). */
-/* - ELEC_METER_ACC_MAJOR - [kWh] - Kilowatt-hour. Resolution 1kWh. Range [1:1000000]. Accumulative electricity meter (major). */
-/* - PULSE_CNT_ACC_WIDE_MINOR - [NB] - Number of pulses. Resolution 1 pulse. Range [1:999999]. */
-/* Accumulative pulse counter wide range (minor). */
-/* - PULSE_CNT_ACC_WIDE_MAJOR - [MNB] - Number of megapulses. Resolution 1 megapulse. Range [1:999999]. */
-/* Accumulative pulse counter wide range (major). */
-/* - CURRENT_PRECISE - [mA] - Miliampere. Resolution 0.001mA. Range [0.001:8 000.000]. */
-/* - OUTPUT_CONTROL - Not applicable */
-
-/* Condition to be checked by the device. If the condition is true, an action is triggered */
+/* Encoding A: used to set absolute values in rules (e.g., upper and lower thresholds): *
+ * - TEMPERATURE - [°C] Celsius degree. Resolution: 0.1°C. Range: [-273.2:4000.0] *
+ * - HUMIDITY - [% RH] Percentage (Relative humidity). Resolution: 1%. Range: [0:100] *
+ * - ATMOSPHERIC_PRESSURE - [hPa] Hectopascal. Resolution: 0.1hPa. Range: [1.0:2000.0] *
+ * - DIFERENTIAL_PRESSURE - [Pa] Pascal. Resolution: 1Pa. Range: [-10000:10000] *
+ * - OK_ALARM - Not applicable *
+ * - IAQ - [IAQ] IAQ index. Resolution: 1IAQ. Range: [0:500] *
+ * - FLOODING - Not applicable *
+ * - PULSE_CNT - [NB] Number of pulses. Resolution: 1 pulse. Range: [0:8000000]. Period number of pulses *
+ * - ELECTRICITY_METER - [W] Watt. Resolution: 1W. Range: [0:8000000]. Period average power consumption *
+ * - WATER_METER - [l/min] Liter per minute. Resolution: 1l/min. Range: [0:8000000]. Period average water *
+ * - SOIL_MOISTURE - [kPa] Kilopascal. Resolution: 1kPa. Range: [-1000:0] *
+ * - CO_GAS - [ppm] Parts per million. Resolution: 1ppm. Range: [0:1000000] *
+ * - NO2_GAS - [ppm] Parts per million. Resolution: 1ppm. Range: [0:1000000] *
+ * - H2S_GAS - [ppm] Parts per million. Resolution: 0.01ppm. Range: [0.00:80000.00] *
+ * - AMBIENT_LIGHT - [lx] Lux. Resolution: 0.1lx. Range: [0.0:100000.0] *
+ * - PM_1_0 - [µg/m^3] Microgram per cubic meter. Resolution: 1µg/m^3. Range: [0:1000] *
+ * - PM_2_5 - [µg/m^3] Microgram per cubic meter. Resolution: 1µg/m^3. Range: [0:1000] *
+ * - PM_10_0 - [µg/m^3] Microgram per cubic meter. Resolution: 1µg/m^3. Range: [0:1000] *
+ * - NOISE_LEVEL - [dB] Decibel. Resolution: 0.1 dB. Range: [0.0:200.0] *
+ * - NH3_GAS - [ppm] Parts per million. Resolution: 1ppm. Range: [0:1000000] *
+ * - CH4_GAS - [ppm] Parts per million. Resolution: 1ppm. Range: [0:1000000] *
+ * - HIGH_PRESSURE - [kPa] Kilopascal (100kPa = 1bar). Resolution: 1kPa. Range: [0:200000] *
+ * - DISTANCE_MM - [mm] Millimeter. Resolution: 1mm. Range: [0:100000] *
+ * - WATER_METER_ACC_MINOR - [l] Liter. Resolution: 1l. Range: [0:99] *
+ * - WATER_METER_ACC_MAJOR - [hl] Hectoliter. Resolution: 1hl. Range: [0:999999] *
+ * - CO2_GAS - [ppm] Parts per million. Resolution: 1ppm. Range: [0:1000000] *
+ * - HUMIDITY_ACCURATE - [% RH] Percentage (Relative humidity). Resolution: 0.1%. Range: [0.0:100.0] *
+ * - STATIC_IAQ - [sIAQ] Static IAQ index. Resolution: 1sIAQ. Range: [0:10000] *
+ * - CO2_EQUIVALENT - [ppm] Parts per million. Resolution: 1ppm. Range: [0:1000000] *
+ * - BREATH_VOC - [ppm] Parts per million. Resolution: 1ppm. Range: [0:100000] *
+ * - CELLULAR_GATEWAY - Not applicable *
+ * - PERCENTAGE - [%] Percentage. Resolution: 0.01%. Range: [0.00:100.00] *
+ * - VOLTAGE - [mV] Millivolt. Resolution: 0.1mV. Range: [0.0:100000.0] *
+ * - CURRENT - [mA] Milliampere. Resolution: 0.01mA. Range: [0.00:10000.00] *
+ * - PULSE_CNT_ACC_MINOR - [NB] Number of pulses. Resolution: 1 pulse. Range: [0:999] *
+ * - PULSE_CNT_ACC_MAJOR - [kNB] Number of kilopulses. Resolution: 1 kilopulse. Range: [0:999999] *
+ * - ELEC_METER_ACC_MINOR - [Wh] Watt-hour. Resolution: 1Wh. Range: [0:999] *
+ * - ELEC_METER_ACC_MAJOR - [kWh] Kilowatt-hour. Resolution: 1kWh. Range: [0:999999] *
+ * - PULSE_CNT_ACC_WIDE_MINOR - [NB] Number of pulses. Resolution: 1 pulse. Range: [0:999999] *
+ * - PULSE_CNT_ACC_WIDE_MAJOR - [MNB] Number of megapulses. Resolution: 1 megapulse. Range: [0:999999] *
+ * - CURRENT_PRECISE - [mA] Milliampere. Resolution: 0.001mA. Range: [-4000.000:4000.000] *
+ * - OUTPUT_CONTROL - Not applicable *
+ * - RESISTANCE - [Ω] Ohm. Resolution: 1Ω. Range: [0:1000000] */
+
+/* Encoding R: used to set relative values in rules (e.g., differential threshold and hysteresis): *
+ * - TEMPERATURE - [°C] Celsius degree. Resolution: 0.1°C. Range: [0.1:4273.2] *
+ * - HUMIDITY - [% RH] Percentage (Relative humidity). Resolution: 1%. Range: [1:100] *
+ * - ATMOSPHERIC_PRESSURE - [hPa] Hectopascal. Resolution: 0.1hPa. Range: [0.1:1999.0] *
+ * - DIFERENTIAL_PRESSURE - [Pa] Pascal. Resolution: 1Pa. Range: [1:20000] *
+ * - OK_ALARM - Not applicable *
+ * - IAQ - [IAQ] IAQ index. Resolution: 1IAQ. Range: [1:500] *
+ * - FLOODING - Not applicable *
+ * - PULSE_CNT - [NB] Number of pulses. Resolution: 1 pulse. Range: [1:8000000]. Period number of pulses *
+ * - ELECTRICITY_METER - [W] Watt. Resolution: 1W. Range: [1:8000000]. Period average power consumption *
+ * - WATER_METER - [l/min] Liter per minute. Resolution: 1l/min. Range: [1:8000000]. Period average water *
+ * - SOIL_MOISTURE - [kPa] Kilopascal. Resolution: 1kPa. Range: [1:1000] *
+ * - CO_GAS - [ppm] Parts per million. Resolution: 1ppm. Range: [1:1000000] *
+ * - NO2_GAS - [ppm] Parts per million. Resolution: 1ppm. Range: [1:1000000] *
+ * - H2S_GAS - [ppm] Parts per million. Resolution: 0.01ppm. Range: [0.01:80000.00] *
+ * - AMBIENT_LIGHT - [lx] Lux. Resolution: 0.1lx. Range: [0.1:100000.0] *
+ * - PM_1_0 - [µg/m^3] Microgram per cubic meter. Resolution: 1µg/m^3. Range: [1:1000] *
+ * - PM_2_5 - [µg/m^3] Microgram per cubic meter. Resolution: 1µg/m^3. Range: [1:1000] *
+ * - PM_10_0 - [µg/m^3] Microgram per cubic meter. Resolution: 1µg/m^3. Range: [1:1000] *
+ * - NOISE_LEVEL - [dB] Decibel. Resolution: 0.1 dB. Range: [0.1:200.0] *
+ * - NH3_GAS - [ppm] Parts per million. Resolution: 1ppm. Range: [1:1000000] *
+ * - CH4_GAS - [ppm] Parts per million. Resolution: 1ppm. Range: [1:1000000] *
+ * - HIGH_PRESSURE - [kPa] Kilopascal (100kPa = 1bar). Resolution: 1kPa. Range: [1:200000] *
+ * - DISTANCE_MM - [mm] Millimeter. Resolution: 1mm. Range: [1:100000] *
+ * - WATER_METER_ACC_MINOR - [l] Liter. Resolution: 1l. Range: [1:99] *
+ * - WATER_METER_ACC_MAJOR - [hl] Hectoliter. Resolution: 1hl. Range: [1:999999] *
+ * - CO2_GAS - [ppm] Parts per million. Resolution: 1ppm. Range: [1:1000000] *
+ * - HUMIDITY_ACCURATE - [% RH] Percentage (Relative humidity). Resolution: 0.1%. Range: [0.1:100.0] *
+ * - STATIC_IAQ - [sIAQ] Static IAQ index. Resolution: 1sIAQ. Range: [1:10000] *
+ * - CO2_EQUIVALENT - [ppm] Parts per million. Resolution: 1ppm. Range: [1:1000000] *
+ * - BREATH_VOC - [ppm] Parts per million. Resolution: 1ppm. Range: [1:100000] *
+ * - CELLULAR_GATEWAY - Not applicable *
+ * - PERCENTAGE - [%] Percentage. Resolution: 0.01%. Range: [0.01:100.00] *
+ * - VOLTAGE - [mV] Millivolt. Resolution: 0.1mV. Range: [0.1:100000.0] *
+ * - CURRENT - [mA] Milliampere. Resolution: 0.01mA. Range: [0.01:10000.00] *
+ * - PULSE_CNT_ACC_MINOR - [NB] Number of pulses. Resolution: 1 pulse. Range: [1:999] *
+ * - PULSE_CNT_ACC_MAJOR - [kNB] Number of kilopulses. Resolution: 1 kilopulse. Range: [1:999999] *
+ * - ELEC_METER_ACC_MINOR - [Wh] Watt-hour. Resolution: 1Wh. Range: [1:999] *
+ * - ELEC_METER_ACC_MAJOR - [kWh] Kilowatt-hour. Resolution: 1kWh. Range: [1:999999] *
+ * - PULSE_CNT_ACC_WIDE_MINOR - [NB] Number of pulses. Resolution: 1 pulse. Range: [1:999999] *
+ * - PULSE_CNT_ACC_WIDE_MAJOR - [MNB] Number of megapulses. Resolution: 1 megapulse. Range: [1:999999] *
+ * - CURRENT_PRECISE - [mA] Milliampere. Resolution: 0.001mA. Range: [0.001:8000.000] *
+ * - OUTPUT_CONTROL - Not applicable *
+ * - RESISTANCE - [Ω] Ohm. Resolution: 1Ω. Range: [1:1000000] */
+
+/* Condition to be checked by the device. If the condition is true, an action is triggered. */
enum Condition {
- /* Invalid value */
+ /* Invalid value. */
CONDITION_UNSPECIFIED = 0;
- /* Threshold function for given rule_id is disabled */
+ /* The rule is disabled. */
CONDITION_DISABLED = 1;
- /* Upper threshold. Continuous sensors only. If the measurement (or average from a few measurements) is over the threshold, */
- /* an action is triggered. */
- /* parameter[0] - Threshold value in "Encoding A" format. Must match channel type */
- /* parameter[1] - Hysteresis value in "Encoding R" format. Must much channel type. Set to "0" to disable */
- /* parameter[2] - Triggering mode: */
- /* - 1 - moving average (a1=(n1+n2+n3)/3, a2=(n2+n3+n4)/3, etc.) */
- /* - 2 - window average (a1=(n1+n2+n3)/3, a2=(n4+n5+n6)/3, etc.) */
- /* - 3 - consecutive samples (number of consecutive samples above threshold) */
- /* parameter[3] - Number of measurements for trigger determination. E.g parameter[3] equals 3, average value from three */
- /* samples will be calculated and compared to the threshold value in average mode or the third consecutive */
- /* sample above threshold will trigger action in consecutive mode. Range: [1:10]. */
- /* parameter[4] - Type of measurement (as described in MeasurementType). */
+ /* Upper threshold. 'Continuous' sensors only. *
+ * If the measurement (or average from a few measurements) is over the threshold, an action is triggered. *
+ * parameter[0] - Threshold value in "Encoding A" format. Must match channel type. *
+ * parameter[1] - Hysteresis value in "Encoding R" format. Must match channel type. Set to "0" to disable. *
+ * parameter[2] - Triggering mode: *
+ * - 1 - Moving average (a1=(n1+n2+n3)/3, a2=(n2+n3+n4)/3, etc.) *
+ * - 2 - Window average (a1=(n1+n2+n3)/3, a2=(n4+n5+n6)/3, etc.) *
+ * - 3 - Consecutive samples (number of consecutive samples above threshold) *
+ * parameter[3] - Number of measurements for trigger determination. E.g., parameter[3] equals 3, average value from three *
+ * samples will be calculated and compared to the threshold value in average mode or the third consecutive *
+ * sample above threshold will trigger action in consecutive mode. Range: [1:10]. *
+ * parameter[4] - Type of measurement (as described in MeasurementType). */
CONDITION_HIGH_THRESHOLD = 2;
- /* Lower threshold. Continuous sensors only. If the measurement (or average from a few measurements) is below the threshold, */
- /* an action is triggered. */
- /* parameter[0] - Threshold value in "Encoding A" format. Must match channel type */
- /* parameter[1] - Hysteresis value in "Encoding R" format. Must much channel type. Set to "0" to disable */
- /* parameter[2] - Triggering mode: */
- /* - 1 - moving average (a1=(n1+n2+n3)/3, a2=(n2+n3+n4)/3, etc.) */
- /* - 2 - window average (a1=(n1+n2+n3)/3, a2=(n4+n5+n6)/3, etc.) */
- /* - 3 - consecutive samples (number of consecutive samples above threshold) */
- /* parameter[3] - Number of measurements for trigger determination. E.g parameter[3] equals 3, average value from three */
- /* samples will be calculated and compared to the threshold value in average mode or the third consecutive */
- /* sample below threshold will trigger action in consecutive mode. Range: [1:10]. */
- /* parameter[4] - Type of measurement (as described in MeasurementType). */
+ /* Lower threshold. 'Continuous' sensors only. *
+ * If the measurement (or average from a few measurements) is below the threshold, an action is triggered. *
+ * parameter[0] - Threshold value in "Encoding A" format. Must match channel type. *
+ * parameter[1] - Hysteresis value in "Encoding R" format. Must match channel type. Set to "0" to disable. *
+ * parameter[2] - Triggering mode: *
+ * - 1 - Moving average (a1=(n1+n2+n3)/3, a2=(n2+n3+n4)/3, etc.) *
+ * - 2 - Window average (a1=(n1+n2+n3)/3, a2=(n4+n5+n6)/3, etc.) *
+ * - 3 - Consecutive samples (number of consecutive samples above threshold) *
+ * parameter[3] - Number of measurements for trigger determination. E.g., parameter[3] equals 3, average value from three *
+ * samples will be calculated and compared to the threshold value in average mode or the third consecutive *
+ * sample below threshold will trigger action in consecutive mode. Range: [1:10]. *
+ * parameter[4] - Type of measurement (as described in MeasurementType). */
CONDITION_LOW_THRESHOLD = 3;
- /* Differential threshold. Continuous sensors only. If the absolute value of the difference between the last value sent to */
- /* the server and the measurement value (or average from a few measurements) is greater or equal to the value of */
- /* the threshold set, an action is triggered. */
- /* parameter[0] - Threshold value in "Encoding R" format. Must match channel type */
- /* parameter[1] - Triggering mode: */
- /* - 1 - moving average (a1=(n1+n2+n3)/3, a2=(n2+n3+n4)/3, etc.) */
- /* - 2 - window average (a1=(n1+n2+n3)/3, a2=(n4+n5+n6)/3, etc.) */
- /* - 3 - consecutive samples (number of consecutive samples above threshold) */
- /* parameter[2] - Number of measurements for trigger determination. E.g parameter[3] equals 3, average value from three */
- /* samples will be calculated and compared to the threshold value in average mode or the third consecutive */
- /* sample exceeding threshold will trigger action in consecutive mode. Range: [1:10]. */
- /* parameter[3] - Type of measurement (as described in MeasurementType). */
+ /* Differential threshold. 'Continuous' sensors only. *
+ * If the absolute value of the difference between the last value sent to the server and the measurement value *
+ * (or average from a few measurements) is greater or equal to the value of the threshold set, an action is triggered. *
+ * parameter[0] - Threshold value in "Encoding R" format. Must match channel type. *
+ * parameter[1] - Triggering mode: *
+ * - 1 - Moving average (a1=(n1+n2+n3)/3, a2=(n2+n3+n4)/3, etc.) *
+ * - 2 - Window average (a1=(n1+n2+n3)/3, a2=(n4+n5+n6)/3, etc.) *
+ * - 3 - Consecutive samples (number of consecutive samples above threshold) *
+ * parameter[2] - Number of measurements for trigger determination. E.g., parameter[3] equals 3, average value from three *
+ * samples will be calculated and compared to the threshold value in average mode or the third consecutive *
+ * sample exceeding threshold will trigger action in consecutive mode. Range: [1:10]. *
+ * parameter[3] - Type of measurement (as described in MeasurementType). */
CONDITION_DIFF_THRESHOLD = 4;
- /* Change of binary sensor's state. Binary sensors only. Each change of the binary's sensor state will trigger an action. */
+ /* Change of binary sensor's state. 'Binary' sensors only. *
+ * Each change of the binary sensor's state will trigger an action. */
CONDITION_BINARY_CHANGE_STATE = 5;
- /* Logic operator. Used for combining multiple rules into more complex conditions. If the logic condition specified by */
- /* parameters (logic operator and selected rules) is met, an action is triggered. */
- /* parameter[0] - Logic operator (as described in LogicOperation). */
- /* parameter[1] - Rule selector (bit mask). Specifies which rules should be taken into account while determining */
- /* rules outcome. */
- /* parameter[2] - Rule negation (bit mask). Specifies which of chosen in parameter[1] rules should be negated */
- /* before determining rules outcome. */
- /* parameter[3] - Rule action delay [s]. Specifies time delay between the rule activation and rule action being triggered. */
- /* Range: [0:864000]. */
- /* parameter[4] - Rule return delay [s]. Specifies time delay between the rule deactivation and rule action being triggered. */
- /* Range: [0:864001]. Max parameter value disables action triggering on rule deactivation. */
+ /* Logic operator. Used for combining multiple rules into more complex conditions. *
+ * If the logic condition specified by parameters (logic operator and selected rules) is met, an action is triggered. *
+ * parameter[0] - Logic operator (as described in LogicOperator). *
+ * parameter[1] - Rule selector (bitmask). Specifies which rules should be taken into account while determining *
+ * rules outcome. *
+ * parameter[2] - Rule negation (bitmask). Specifies which of chosen in parameter[1] rules should be negated *
+ * before determining rules outcome. *
+ * parameter[3] - Rule action delay [s]. Specifies time delay between the rule activation and rule action being triggered. *
+ * Range: [0:864000]. *
+ * parameter[4] - Rule return delay [s]. Specifies time delay between the rule deactivation and rule action being triggered. *
+ * Range: [0:864001]. Max parameter value disables action triggering on rule deactivation. */
CONDITION_LOGIC_OPERATOR = 6;
- /* On measurement. Continous sensors only. The basic function is to trigger communication after measurement if at least 60s */
- /* have passed since the last one. Transmission may occur every x measurement. Optionally dependency on the other rule can */
- /* be configured, then, when all conditions are met, transmission is triggered. */
- /* parameter[0] - Send every n measurement. This parameter specifies every which measurement transmission will be triggered */
- /* if all other conditions are met. Range: [1:500]. If parameter[0] equals 1, transmission will occur after */
- /* every measurement. */
- /* parameter[1] - Optional. Rule selector (bit mask). Specifies which rule should be taken into account while determining */
- /* the measurement rule outcome. */
- /* parameter[2] - Optional. Rule negation (bit mask). Specifies which of chosen in parameter[1] rule should be negated */
- /* before determining the measurement rule outcome. */
+ /* On measurement. 'Continous' sensors only.
+ * The basic function is to trigger communication after measurement if at least 60s have passed since the last one. *
+ * Transmission may occur every n-th measurement. Optionally dependency on the other rule can be configured, then, when all *
+ * conditions are met, transmission is triggered. *
+ * parameter[0] - Send every n-th measurement. This parameter specifies every which measurement transmission will be *
+ * triggered if all other conditions are met. Range: [1:500]. If parameter[0] equals 1, transmission will *
+ * occur after every measurement. *
+ * parameter[1] - Optional. Rule selector (bitmask). Specifies which rule should be taken into account while determining *
+ * the measurement rule outcome. *
+ * parameter[2] - Optional. Rule negation (bitmask). Specifies which of the rules selected in parameter[1] should be negated *
+ * before determining the measurement rule outcome. */
CONDITION_ON_MEASUREMENT = 7;
+
+ /* On sensor error. 'Continous' sensors only. *
+ * If the sensor returns an error code, an action will be triggered. *
+ * The rule becomes inactive when a correct measurement appears. */
+ CONDITION_ON_SENSOR_ERROR = 8;
}
/* Logic operators to be used for determining the outcome of rules with logic operator condition. */
enum LogicOperator {
- /* Invalid use */
+ /* Invalid value. */
LOGIC_OPERATOR_UNSPECIFIED = 0;
- /* Logic AND */
+ /* Logic AND. */
LOGIC_OPERATOR_AND = 1;
- /* Logic OR */
+ /* Logic OR. */
LOGIC_OPERATOR_OR = 2;
}
-/* Action to be triggered. Currently the only possible action is to trigger the transmission. */
-/* Other actions will be available in next SW releases. */
+/* Action to be triggered. */
enum Action {
- /* Invalid value */
+ /* Invalid value. */
ACTION_UNSPECIFIED = 0;
- /* To trigger the transmission */
+ /* To trigger the transmission. */
ACTION_TRIGGER_TRANSMISSION = 1;
- /* To take no action. Possible for logic operator components */
+ /* To take no action. Possible for logic operator components. */
ACTION_NO_ACTION = 2;
- /* To trigger the transmission with ACK */
+ /* To trigger the transmission with ACK. */
ACTION_TRIGGER_TRANSMISSION_WITH_ACK = 3;
- /* To change BLE advertising period mode to fast (with lower user-configured advertising interval). */
- /* Once the rule is deactived avertising period mode returns to previously configured value. */
+ /* To change BLE advertising period mode to fast (with lower user-configured advertising interval). *
+ * Once the rule is deactived avertising period mode returns to previously configured value. */
ACTION_FAST_ADVERTISING_MODE = 4;
}
/* Type of a rule calendars. */
enum CalendarType {
- /* Invalid value */
+ /* Invalid value. */
CALENDAR_TYPE_UNSPECIFIED = 0;
- /* Type for inactive calendars */
+ /* Calendar is inactive. */
CALENDAR_TYPE_DISABLED = 1;
- /* Week type. Enables selcted rules on specified days of the week in specified time periods. */
- /* parameter[0] - Week day mask. Bitmask of days when selected rules are enabled */
- /* - Bit 0 - Sunday */
- /* - Bit 1 - Monday */
- /* ... */
- /* - Bit 6 - Saturday */
- /* parameter[1] - 'From time' - point in time from which selected rules will be enabled (in minutes from midnight). */
- /* parameter[2] - 'To time' - point in time from which selected rules will be disabled (in minutes from midnight). */
- /* Note: if 'From time' is bigger than 'To time' there are two periods when rules are enabled - from 00:00 to 'To time' */
- /* and from 'From time' to 23:59. */
- /* parameter[3] - Timezone - desired timezone for date comparison. Encoded as number (N) of 15 minutes offsets */
- /* - example - if N = 4, then offset = 4 * 15min = 1h. I.e. timezone is UTC+1. */
+ /* Week calendar. Enables selcted rules on specified days of the week in specified time periods. *
+ * parameter[0] - Week day mask. Bitmask of days when selected rules are enabled: *
+ * - Bit 0 - Sunday *
+ * - Bit 1 - Monday *
+ * ... *
+ * - Bit 6 - Saturday *
+ * parameter[1] - 'From time' - point in time from which selected rules will be enabled (in minutes from midnight). *
+ * parameter[2] - 'To time' - point in time from which selected rules will be disabled (in minutes from midnight). *
+ * parameter[3] - Timezone - desired timezone for date comparison. Encoded as number (N) of 15 minutes offsets, *
+ * for example: if N = 4, then offset = 4 * 15min = 1h. I.e. timezone is UTC+1. *
+ * Note: If 'From time' is bigger than 'To time' there are two periods when rules are enabled - from 00:00 to 'To time' *
+ * and from 'From time' to 23:59. */
CALENDAR_TYPE_WEEK = 2;
}
/* Rules calendars. Used for enabling/disabling rules based on date/time. */
-/* It is possible to configure up to 6 calendars. Each of them can affect any number of rules. */
message ProtoCalendar {
- /* Bit mask of selected rules. Mask on bits [0:11] */
- /* - Bit 0 - Rule ID 0 */
- /* - Bit 1 - Rule ID 1 */
- /* ... */
- /* - Bit 11 - Rule ID 11 */
+ /* Selected rules. *
+ * Bitmask - up to 16 rules supported (previously 12 rules [06.00 - 07.01.xx]): *
+ * - Bit 0 - Rule 1 *
+ * - Bit 1 - Rule 2 *
+ * ... *
+ * - Bit 15 - Rule 16 *
+ * Status: In use [06.08.00 - LATEST] */
uint32 rule_mask = 1;
- /* Calendars's parameters. Described in Type. */
+ /* Calendar parameters (as described in CalendarType). *
+ * Status: In use [06.08.00 - LATEST] */
repeated sint32 parameters = 2;
- /* Calendar's type. Described in Type. */
+ /* Calendar type (as described in CalendarType). *
+ * Status: In use [06.08.00 - LATEST] */
CalendarType type = 3;
}
-/* Rules used to define edge logic on the device. Rules are defined by conditions and actions: */
-/* If Condition is true, trigger Action. It is possible to configure up to 12 rules and assign them to different channels. */
-/* One rule can be assigned to any number of channels. For instance rule "If temperature is over 10 C, trigger the transmission"*/
-/* can be assigned to channels 1 and 2. No matter to how many channels a rule is assigned, it's still counted as one rule. */
+/* Rules are used to define edge logic on the device. Rules are defined by conditions and actions. *
+ * If 'condition' is true, trigger 'action'. One rule can be assigned to any number of channels. *
+ * For instance rule "If temperature is over 10 C, trigger the transmission" can be assigned to channels 1 and 2. *
+ * No matter to how many channels a rule is assigned, it's still counted as one rule. */
message ProtoRule {
- /* Channels to which the rule is assigned. One rule can be assigned to multiple channels as long as those are of the same type*/
- /* Bit mask on bits [0:5]. E.g. To assign the rule for channel 1: "000001", to assign rule to channels 2 and 4: "001010" */
+ /* Channels to which the rule is assigned. *
+ * One rule can be assigned to multiple channels as long as those are of the same type. *
+ * Bitmask: *
+ * - Bit 0 - Channel 1 *
+ * - Bit 1 - Channel 2 *
+ * ... *
+ * - Bit 5 - Channel 6 *
+ * Status: In use [06.00 - LATEST] */
uint32 channel_mask = 1;
- /* Rule's condition (as described in Condition). */
+ /* Rule condition (as described in Condition). *
+ * Status: In use [06.00 - LATEST] */
Condition condition = 2;
- /* Condition's parameters (as described in Condition). For binary sensors there are no parameters */
+ /* Condition parameters (as described in Condition). *
+ * For 'Binary' sensors there are no parameters. *
+ * Status: In use [06.00 - LATEST] */
repeated sint32 parameters = 3;
- /* Action to be triggered. */
+ /* Action to be triggered. *
+ * Status: In use [06.00 - LATEST] */
Action action = 4;
}
\ No newline at end of file
diff --git a/common/transport/coap/src/test/java/org/thingsboard/server/transport/coap/efento/CoapEfentoTransportResourceTest.java b/common/transport/coap/src/test/java/org/thingsboard/server/transport/coap/efento/CoapEfentoTransportResourceTest.java
index eb0a764f5e..f6eaa9976f 100644
--- a/common/transport/coap/src/test/java/org/thingsboard/server/transport/coap/efento/CoapEfentoTransportResourceTest.java
+++ b/common/transport/coap/src/test/java/org/thingsboard/server/transport/coap/efento/CoapEfentoTransportResourceTest.java
@@ -16,23 +16,32 @@
package org.thingsboard.server.transport.coap.efento;
import com.google.protobuf.ByteString;
+import com.google.protobuf.InvalidProtocolBufferException;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;
+import org.thingsboard.server.gen.transport.coap.ConfigProtos;
+import org.thingsboard.server.gen.transport.coap.ConfigTypesProtos;
+import org.thingsboard.server.gen.transport.coap.DeviceInfoProtos;
import org.thingsboard.server.gen.transport.coap.MeasurementTypeProtos.MeasurementType;
import org.thingsboard.server.gen.transport.coap.MeasurementsProtos;
import org.thingsboard.server.gen.transport.coap.MeasurementsProtos.ProtoMeasurements;
+import org.thingsboard.server.gen.transport.coap.ProtoRuleProtos;
import org.thingsboard.server.transport.coap.CoapTransportContext;
import org.thingsboard.server.transport.coap.efento.utils.CoapEfentoUtils;
import java.nio.ByteBuffer;
+import java.text.SimpleDateFormat;
import java.time.Instant;
import java.util.Arrays;
+import java.util.Comparator;
+import java.util.Date;
import java.util.List;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
+import java.util.stream.Collectors;
import java.util.stream.Stream;
import static org.assertj.core.api.Assertions.assertThat;
@@ -85,7 +94,7 @@ class CoapEfentoTransportResourceTest {
void checkContinuousSensorWithSomeMeasurements() {
long tsInSec = Instant.now().getEpochSecond();
ProtoMeasurements measurements = ProtoMeasurements.newBuilder()
- .setSerialNum(integerToByteString(1234))
+ .setSerialNumber(integerToByteString(1234))
.setCloudToken("test_token")
.setMeasurementPeriodBase(180)
.setMeasurementPeriodFactor(5)
@@ -93,7 +102,7 @@ class CoapEfentoTransportResourceTest {
.setSignal(0)
.setNextTransmissionAt(1000)
.setTransferReason(0)
- .setHash(0)
+ .setConfigurationHash(0)
.addAllChannels(List.of(MeasurementsProtos.ProtoChannel.newBuilder()
.setType(MeasurementType.MEASUREMENT_TYPE_TEMPERATURE)
.setTimestamp(Math.toIntExact(tsInSec))
@@ -114,7 +123,7 @@ class CoapEfentoTransportResourceTest {
assertThat(efentoMeasurements.get(1).getTs()).isEqualTo((tsInSec + 180 * 5) * 1000);
assertThat(efentoMeasurements.get(1).getValues().getAsJsonObject().get("temperature_1").getAsDouble()).isEqualTo(22.4);
assertThat(efentoMeasurements.get(1).getValues().getAsJsonObject().get("humidity_2").getAsDouble()).isEqualTo(30);
- checkDefaultMeasurements(measurements, efentoMeasurements, 180 * 5, false);
+ checkDefaultMeasurements(measurements, efentoMeasurements, 180 * 5);
}
@ParameterizedTest
@@ -122,7 +131,7 @@ class CoapEfentoTransportResourceTest {
void checkContinuousSensor(MeasurementType measurementType, List sampleOffsets, String property, double expectedValue) {
long tsInSec = Instant.now().getEpochSecond();
ProtoMeasurements measurements = ProtoMeasurements.newBuilder()
- .setSerialNum(integerToByteString(1234))
+ .setSerialNumber(integerToByteString(1234))
.setCloudToken("test_token")
.setMeasurementPeriodBase(180)
.setMeasurementPeriodFactor(0)
@@ -130,7 +139,7 @@ class CoapEfentoTransportResourceTest {
.setSignal(0)
.setNextTransmissionAt(1000)
.setTransferReason(0)
- .setHash(0)
+ .setConfigurationHash(0)
.addAllChannels(List.of(MeasurementsProtos.ProtoChannel.newBuilder()
.setType(measurementType)
.setTimestamp(Math.toIntExact(tsInSec))
@@ -142,7 +151,7 @@ class CoapEfentoTransportResourceTest {
assertThat(efentoMeasurements).hasSize(1);
assertThat(efentoMeasurements.get(0).getTs()).isEqualTo(tsInSec * 1000);
assertThat(efentoMeasurements.get(0).getValues().getAsJsonObject().get(property).getAsDouble()).isEqualTo(expectedValue);
- checkDefaultMeasurements(measurements, efentoMeasurements, 180, false);
+ checkDefaultMeasurements(measurements, efentoMeasurements, 180);
}
private static Stream checkContinuousSensor() {
@@ -176,7 +185,7 @@ class CoapEfentoTransportResourceTest {
String totalPropertyName, double expectedTotalValue) {
long tsInSec = Instant.now().getEpochSecond();
ProtoMeasurements measurements = ProtoMeasurements.newBuilder()
- .setSerialNum(integerToByteString(1234))
+ .setSerialNumber(integerToByteString(1234))
.setCloudToken("test_token")
.setMeasurementPeriodBase(180)
.setMeasurementPeriodFactor(0)
@@ -184,7 +193,7 @@ class CoapEfentoTransportResourceTest {
.setSignal(0)
.setNextTransmissionAt(1000)
.setTransferReason(0)
- .setHash(0)
+ .setConfigurationHash(0)
.addAllChannels(Arrays.asList(MeasurementsProtos.ProtoChannel.newBuilder()
.setType(majorType)
.setTimestamp(Math.toIntExact(tsInSec))
@@ -200,7 +209,7 @@ class CoapEfentoTransportResourceTest {
assertThat(efentoMeasurements).hasSize(1);
assertThat(efentoMeasurements.get(0).getTs()).isEqualTo(tsInSec * 1000);
assertThat(efentoMeasurements.get(0).getValues().getAsJsonObject().get(totalPropertyName + "_2").getAsDouble()).isEqualTo(expectedTotalValue);
- checkDefaultMeasurements(measurements, efentoMeasurements, 180, false);
+ checkDefaultMeasurements(measurements, efentoMeasurements, 180);
}
private static Stream checkPulseCounterSensors() {
@@ -220,7 +229,7 @@ class CoapEfentoTransportResourceTest {
void checkBinarySensor() {
long tsInSec = Instant.now().getEpochSecond();
ProtoMeasurements measurements = ProtoMeasurements.newBuilder()
- .setSerialNum(integerToByteString(1234))
+ .setSerialNumber(integerToByteString(1234))
.setCloudToken("test_token")
.setMeasurementPeriodBase(180)
.setMeasurementPeriodFactor(0)
@@ -228,7 +237,7 @@ class CoapEfentoTransportResourceTest {
.setSignal(0)
.setNextTransmissionAt(1000)
.setTransferReason(0)
- .setHash(0)
+ .setConfigurationHash(0)
.addChannels(MeasurementsProtos.ProtoChannel.newBuilder()
.setType(MEASUREMENT_TYPE_OK_ALARM)
.setTimestamp(Math.toIntExact(tsInSec))
@@ -239,7 +248,7 @@ class CoapEfentoTransportResourceTest {
assertThat(efentoMeasurements).hasSize(1);
assertThat(efentoMeasurements.get(0).getTs()).isEqualTo(tsInSec * 1000);
assertThat(efentoMeasurements.get(0).getValues().getAsJsonObject().get("ok_alarm_1").getAsString()).isEqualTo("ALARM");
- checkDefaultMeasurements(measurements, efentoMeasurements, 180 * 14, true);
+ checkDefaultMeasurements(measurements, efentoMeasurements, 180 * 14);
}
@ParameterizedTest
@@ -247,7 +256,7 @@ class CoapEfentoTransportResourceTest {
void checkBinarySensorWhenValueIsVarying(MeasurementType measurementType, String property, String expectedValueWhenOffsetNotOk, String expectedValueWhenOffsetOk) {
long tsInSec = Instant.now().getEpochSecond();
ProtoMeasurements measurements = ProtoMeasurements.newBuilder()
- .setSerialNum(integerToByteString(1234))
+ .setSerialNumber(integerToByteString(1234))
.setCloudToken("test_token")
.setMeasurementPeriodBase(180)
.setMeasurementPeriodFactor(1)
@@ -255,7 +264,7 @@ class CoapEfentoTransportResourceTest {
.setSignal(0)
.setNextTransmissionAt(1000)
.setTransferReason(0)
- .setHash(0)
+ .setConfigurationHash(0)
.addChannels(MeasurementsProtos.ProtoChannel.newBuilder()
.setType(measurementType)
.setTimestamp(Math.toIntExact(tsInSec))
@@ -268,7 +277,7 @@ class CoapEfentoTransportResourceTest {
assertThat(efentoMeasurements.get(0).getValues().getAsJsonObject().get(property).getAsString()).isEqualTo(expectedValueWhenOffsetNotOk);
assertThat(efentoMeasurements.get(1).getTs()).isEqualTo((tsInSec + 9) * 1000);
assertThat(efentoMeasurements.get(1).getValues().getAsJsonObject().get(property).getAsString()).isEqualTo(expectedValueWhenOffsetOk);
- checkDefaultMeasurements(measurements, efentoMeasurements, 180, true);
+ checkDefaultMeasurements(measurements, efentoMeasurements, 180);
}
private static Stream checkBinarySensorWhenValueIsVarying() {
@@ -282,7 +291,7 @@ class CoapEfentoTransportResourceTest {
@Test
void checkExceptionWhenChannelsListIsEmpty() {
ProtoMeasurements measurements = ProtoMeasurements.newBuilder()
- .setSerialNum(integerToByteString(1234))
+ .setSerialNumber(integerToByteString(1234))
.setCloudToken("test_token")
.setMeasurementPeriodBase(180)
.setMeasurementPeriodFactor(1)
@@ -290,7 +299,7 @@ class CoapEfentoTransportResourceTest {
.setSignal(0)
.setNextTransmissionAt(1000)
.setTransferReason(0)
- .setHash(0)
+ .setConfigurationHash(0)
.build();
UUID sessionId = UUID.randomUUID();
@@ -299,29 +308,587 @@ class CoapEfentoTransportResourceTest {
.hasMessage("[" + sessionId + "]: Failed to get Efento measurements, reason: channels list is empty!");
}
+ // -------------------------------------------------------------------------
+ // ProtoDeviceInfo parsing tests
+ // -------------------------------------------------------------------------
+
@Test
- void checkExceptionWhenValuesMapIsEmpty() {
- long tsInSec = Instant.now().getEpochSecond();
- ProtoMeasurements measurements = ProtoMeasurements.newBuilder()
- .setSerialNum(integerToByteString(1234))
- .setCloudToken("test_token")
- .setMeasurementPeriodBase(180)
- .setMeasurementPeriodFactor(1)
- .setBatteryStatus(true)
- .setSignal(0)
- .setNextTransmissionAt(1000)
- .setTransferReason(0)
- .setHash(0)
- .addChannels(MeasurementsProtos.ProtoChannel.newBuilder()
- .setType(MEASUREMENT_TYPE_TEMPERATURE)
- .setTimestamp(Math.toIntExact(tsInSec))
+ void getEfentoDeviceInfo_parsesSwVersion() {
+ DeviceInfoProtos.ProtoDeviceInfo deviceInfo = minimalDeviceInfo()
+ .setSwVersion(1546) // ver 06.10 => 0x060A => 1546
+ .build();
+
+ CoapEfentoTransportResource.EfentoTelemetry result = coapEfentoTransportResource.getEfentoDeviceInfo(deviceInfo);
+
+ assertThat(result.getValues().getAsJsonObject().get("sw_version").getAsInt()).isEqualTo(1546);
+ }
+
+ @Test
+ void getEfentoDeviceInfo_parsesAllMemoryStatistics() {
+ // proto uint32 maps to Java int; the "undefined" sentinel is 0xFFFFFFFF = -1 in signed int
+ int undefinedTs = -1;
+ int knownTs = 1_700_000_000; // arbitrary known Unix timestamp fitting in uint32
+ // clearMemoryStatistics() is needed because minimalDeviceInfo() pre-populates 22 zeros;
+ // addAllMemoryStatistics() appends in proto, so without clear the test values land at indices 22-43
+ DeviceInfoProtos.ProtoDeviceInfo.Builder builder = minimalDeviceInfo().clearMemoryStatistics();
+ builder.addAllMemoryStatistics(List.of(
+ 0, // [0] nv_storage_status: 0 = no errors
+ knownTs, // [1] timestamp_of_the_end_of_collecting_statistics
+ 1048576, // [2] capacity_of_memory_in_bytes
+ 512000, // [3] used_space_in_bytes
+ 1024, // [4] size_of_invalid_packets_in_bytes
+ 256, // [5] size_of_corrupted_packets_in_bytes
+ 100, // [6] number_of_valid_packets
+ 10, // [7] number_of_invalid_packets
+ 2, // [8] number_of_corrupted_packets
+ 500, // [9] number_of_all_samples_for_channel_1
+ 400, // [10] number_of_all_samples_for_channel_2
+ 300, // [11] number_of_all_samples_for_channel_3
+ 200, // [12] number_of_all_samples_for_channel_4
+ 100, // [13] number_of_all_samples_for_channel_5
+ 50, // [14] number_of_all_samples_for_channel_6
+ undefinedTs, // [15] timestamp_of_the_first_binary_measurement (undefined)
+ undefinedTs, // [16] timestamp_of_the_last_binary_measurement (undefined)
+ undefinedTs, // [17] timestamp_of_the_first_binary_measurement_sent (undefined)
+ knownTs, // [18] timestamp_of_the_first_continuous_measurement
+ knownTs, // [19] timestamp_of_the_last_continuous_measurement
+ knownTs, // [20] timestamp_of_the_last_continuous_measurement_sent
+ 42 // [21] nvm_write_counter
+ ));
+
+ CoapEfentoTransportResource.EfentoTelemetry result = coapEfentoTransportResource.getEfentoDeviceInfo(builder.build());
+ var json = result.getValues().getAsJsonObject();
+
+ assertThat(json.get("nv_storage_status").getAsLong()).isEqualTo(0);
+ assertThat(json.get("timestamp_of_the_end_of_collecting_statistics").getAsString()).isEqualTo(formatDate(knownTs));
+ assertThat(json.get("capacity_of_memory_in_bytes").getAsLong()).isEqualTo(1048576L);
+ assertThat(json.get("used_space_in_bytes").getAsLong()).isEqualTo(512000L);
+ assertThat(json.get("size_of_invalid_packets_in_bytes").getAsLong()).isEqualTo(1024L);
+ assertThat(json.get("size_of_corrupted_packets_in_bytes").getAsLong()).isEqualTo(256L);
+ assertThat(json.get("number_of_valid_packets").getAsLong()).isEqualTo(100L);
+ assertThat(json.get("number_of_invalid_packets").getAsLong()).isEqualTo(10L);
+ assertThat(json.get("number_of_corrupted_packets").getAsLong()).isEqualTo(2L);
+ assertThat(json.get("number_of_all_samples_for_channel_1").getAsLong()).isEqualTo(500L);
+ assertThat(json.get("number_of_all_samples_for_channel_2").getAsLong()).isEqualTo(400L);
+ assertThat(json.get("number_of_all_samples_for_channel_3").getAsLong()).isEqualTo(300L);
+ assertThat(json.get("number_of_all_samples_for_channel_4").getAsLong()).isEqualTo(200L);
+ assertThat(json.get("number_of_all_samples_for_channel_5").getAsLong()).isEqualTo(100L);
+ assertThat(json.get("number_of_all_samples_for_channel_6").getAsLong()).isEqualTo(50L);
+ assertThat(json.get("timestamp_of_the_first_binary_measurement").getAsString()).isEqualTo("Undefined");
+ assertThat(json.get("timestamp_of_the_last_binary_measurement").getAsString()).isEqualTo("Undefined");
+ assertThat(json.get("timestamp_of_the_first_binary_measurement_sent").getAsString()).isEqualTo("Undefined");
+ assertThat(json.get("timestamp_of_the_first_continuous_measurement").getAsString()).isEqualTo(formatDate(knownTs));
+ assertThat(json.get("timestamp_of_the_last_continuous_measurement").getAsString()).isEqualTo(formatDate(knownTs));
+ assertThat(json.get("timestamp_of_the_last_continuous_measurement_sent").getAsString()).isEqualTo(formatDate(knownTs));
+ assertThat(json.get("nvm_write_counter").getAsLong()).isEqualTo(42L);
+ }
+
+ @Test
+ void getEfentoDeviceInfo_parsesModemInfo() {
+ // 34 modem parameters (indices 0-33) as defined in proto_device_info.proto
+ List params = List.of(
+ 0, // [0] sc_EARNFCN_offset
+ 1000, // [1] sc_EARFCN
+ 42, // [2] sc_PCI
+ 123456, // [3] sc_Cell_id
+ -90, // [4] sc_RSRP
+ -10, // [5] sc_RSRQ
+ -80, // [6] sc_RSSI
+ 15, // [7] sc_SINR
+ 3, // [8] sc_Band
+ 1234, // [9] sc_TAC
+ 1, // [10] sc_ECL
+ -30, // [11] sc_TX_PWR
+ 2, // [12] op_mode
+ 999, // [13] nc_EARFCN
+ 1, // [14] nc_EARNFCN_offset
+ 100, // [15] nc_PCI
+ -95, // [16] nc_RSRP
+ 5, // [17] RLC_UL_BLER
+ 3, // [18] RLC_DL_BLER
+ 4, // [19] MAC_UL_BLER
+ 2, // [20] MAC_DL_BLER
+ 50000,// [21] MAC_UL_TOTAL_BYTES
+ 60000,// [22] MAC_DL_TOTAL_BYTES
+ 200, // [23] MAC_UL_total_HARQ_Tx
+ 150, // [24] MAC_DL_total_HARQ_Tx
+ 10, // [25] MAC_UL_HARQ_re_Tx
+ 8, // [26] MAC_DL_HARQ_re_Tx
+ 1000, // [27] RLC_UL_tput
+ 1200, // [28] RLC_DL_tput
+ 900, // [29] MAC_UL_tput
+ 1100, // [30] MAC_DL_tput
+ 5000, // [31] sleep_duration
+ 300, // [32] rx_time
+ 100 // [33] tx_time
+ );
+ DeviceInfoProtos.ProtoDeviceInfo deviceInfo = minimalDeviceInfo()
+ .setModem(DeviceInfoProtos.ProtoModem.newBuilder()
+ .setType(DeviceInfoProtos.ModemType.MODEM_TYPE_BC66)
+ .addAllParameters(params)
.build())
.build();
- UUID sessionId = UUID.randomUUID();
- assertThatThrownBy(() -> coapEfentoTransportResource.getEfentoMeasurements(measurements, sessionId))
- .isInstanceOf(IllegalStateException.class)
- .hasMessage("[" + sessionId + "]: Failed to collect Efento measurements, reason, values map is empty!");
+ CoapEfentoTransportResource.EfentoTelemetry result = coapEfentoTransportResource.getEfentoDeviceInfo(deviceInfo);
+ var json = result.getValues().getAsJsonObject();
+
+ assertThat(json.get("modem_types").getAsString()).isEqualTo("MODEM_TYPE_BC66");
+ assertThat(json.get("sc_EARNFCN_offset").getAsInt()).isEqualTo(0);
+ assertThat(json.get("sc_EARFCN").getAsInt()).isEqualTo(1000);
+ assertThat(json.get("sc_PCI").getAsInt()).isEqualTo(42);
+ assertThat(json.get("sc_Cell_id").getAsInt()).isEqualTo(123456);
+ assertThat(json.get("sc_RSRP").getAsInt()).isEqualTo(-90);
+ assertThat(json.get("sc_RSRQ").getAsInt()).isEqualTo(-10);
+ assertThat(json.get("sc_RSSI").getAsInt()).isEqualTo(-80);
+ assertThat(json.get("sc_SINR").getAsInt()).isEqualTo(15);
+ assertThat(json.get("sc_Band").getAsInt()).isEqualTo(3);
+ assertThat(json.get("sc_TAC").getAsInt()).isEqualTo(1234);
+ assertThat(json.get("sc_ECL").getAsInt()).isEqualTo(1);
+ assertThat(json.get("sc_TX_PWR").getAsInt()).isEqualTo(-30);
+ assertThat(json.get("op_mode").getAsInt()).isEqualTo(2);
+ assertThat(json.get("nc_EARFCN").getAsInt()).isEqualTo(999);
+ assertThat(json.get("nc_EARNFCN_offset").getAsInt()).isEqualTo(1);
+ assertThat(json.get("nc_PCI").getAsInt()).isEqualTo(100);
+ assertThat(json.get("nc_RSRP").getAsInt()).isEqualTo(-95);
+ assertThat(json.get("RLC_UL_BLER").getAsInt()).isEqualTo(5);
+ assertThat(json.get("RLC_DL_BLER").getAsInt()).isEqualTo(3);
+ assertThat(json.get("MAC_UL_BLER").getAsInt()).isEqualTo(4);
+ assertThat(json.get("MAC_DL_BLER").getAsInt()).isEqualTo(2);
+ assertThat(json.get("MAC_UL_TOTAL_BYTES").getAsInt()).isEqualTo(50000);
+ assertThat(json.get("MAC_DL_TOTAL_BYTES").getAsInt()).isEqualTo(60000);
+ assertThat(json.get("MAC_UL_total_HARQ_Tx").getAsInt()).isEqualTo(200);
+ assertThat(json.get("MAC_DL_total_HARQ_Tx").getAsInt()).isEqualTo(150);
+ assertThat(json.get("MAC_UL_HARQ_re_Tx").getAsInt()).isEqualTo(10);
+ assertThat(json.get("MAC_DL_HARQ_re_Tx").getAsInt()).isEqualTo(8);
+ assertThat(json.get("RLC_UL_tput").getAsInt()).isEqualTo(1000);
+ assertThat(json.get("RLC_DL_tput").getAsInt()).isEqualTo(1200);
+ assertThat(json.get("MAC_UL_tput").getAsInt()).isEqualTo(900);
+ assertThat(json.get("MAC_DL_tput").getAsInt()).isEqualTo(1100);
+ assertThat(json.get("sleep_duration").getAsInt()).isEqualTo(5000);
+ assertThat(json.get("rx_time").getAsInt()).isEqualTo(300);
+ assertThat(json.get("tx_time").getAsInt()).isEqualTo(100);
+ }
+
+ @Test
+ void getEfentoDeviceInfo_parsesModemInfoBC660() {
+ // 22 modem parameters for MODEM_TYPE_BC660 as defined in proto_device_info.proto
+ List params = List.of(
+ 1000, // [0] sc_EARFCN
+ 5, // [1] sc_EARNFCN_offset
+ 42, // [2] sc_PCI
+ 123456, // [3] sc_Cell_id
+ -90, // [4] sc_RSRP
+ -10, // [5] sc_RSRQ
+ -80, // [6] sc_RSSI
+ 15, // [7] sc_SINR
+ 3, // [8] sc_Band
+ 1234, // [9] sc_TAC
+ 1, // [10] sc_ECL
+ -30, // [11] sc_TX_PWR
+ 2, // [12] op_mode
+ 999, // [13] nc_EARFCN
+ 100, // [14] nc_PCI
+ -95, // [15] nc_RSRP
+ -12, // [16] nc_RSRQ
+ 5000, // [17] sleep_duration
+ 300, // [18] rx_time
+ 100, // [19] tx_time
+ 1, // [20] PLMN_state
+ 26201 // [21] select_PLMN
+ );
+ DeviceInfoProtos.ProtoDeviceInfo deviceInfo = minimalDeviceInfo()
+ .setModem(DeviceInfoProtos.ProtoModem.newBuilder()
+ .setType(DeviceInfoProtos.ModemType.MODEM_TYPE_BC660)
+ .addAllParameters(params)
+ .setSimCardIdentification("89001012012341234120")
+ .setFirmwareVersion(DeviceInfoProtos.ModemFirmwareVersion.MODEM_FIRMWARE_VERSION_BC660_V2)
+ .setModemIdentification("123456789012345")
+ .addAllModemStatistics(List.of(10, 3600, 7200, 600))
+ .build())
+ .build();
+
+ CoapEfentoTransportResource.EfentoTelemetry result = coapEfentoTransportResource.getEfentoDeviceInfo(deviceInfo);
+ var json = result.getValues().getAsJsonObject();
+
+ assertThat(json.get("modem_types").getAsString()).isEqualTo("MODEM_TYPE_BC660");
+ assertThat(json.get("sim_card_identification").getAsString()).isEqualTo("89001012012341234120");
+ assertThat(json.get("firmware_version").getAsString()).isEqualTo("MODEM_FIRMWARE_VERSION_BC660_V2");
+ assertThat(json.get("modem_identification").getAsString()).isEqualTo("123456789012345");
+ assertThat(json.get("modem_transmissions_count").getAsInt()).isEqualTo(10);
+ assertThat(json.get("modem_time_since_last_devinfo").getAsInt()).isEqualTo(3600);
+ assertThat(json.get("modem_total_psm_time").getAsInt()).isEqualTo(7200);
+ assertThat(json.get("modem_total_active_time").getAsInt()).isEqualTo(600);
+ assertThat(json.get("sc_EARFCN").getAsInt()).isEqualTo(1000);
+ assertThat(json.get("sc_EARNFCN_offset").getAsInt()).isEqualTo(5);
+ assertThat(json.get("sc_PCI").getAsInt()).isEqualTo(42);
+ assertThat(json.get("sc_Cell_id").getAsInt()).isEqualTo(123456);
+ assertThat(json.get("sc_RSRP").getAsInt()).isEqualTo(-90);
+ assertThat(json.get("sc_RSRQ").getAsInt()).isEqualTo(-10);
+ assertThat(json.get("sc_RSSI").getAsInt()).isEqualTo(-80);
+ assertThat(json.get("sc_SINR").getAsInt()).isEqualTo(15);
+ assertThat(json.get("sc_Band").getAsInt()).isEqualTo(3);
+ assertThat(json.get("sc_TAC").getAsInt()).isEqualTo(1234);
+ assertThat(json.get("sc_ECL").getAsInt()).isEqualTo(1);
+ assertThat(json.get("sc_TX_PWR").getAsInt()).isEqualTo(-30);
+ assertThat(json.get("op_mode").getAsInt()).isEqualTo(2);
+ assertThat(json.get("nc_EARFCN").getAsInt()).isEqualTo(999);
+ assertThat(json.get("nc_PCI").getAsInt()).isEqualTo(100);
+ assertThat(json.get("nc_RSRP").getAsInt()).isEqualTo(-95);
+ assertThat(json.get("nc_RSRQ").getAsInt()).isEqualTo(-12);
+ assertThat(json.get("sleep_duration").getAsInt()).isEqualTo(5000);
+ assertThat(json.get("rx_time").getAsInt()).isEqualTo(300);
+ assertThat(json.get("tx_time").getAsInt()).isEqualTo(100);
+ assertThat(json.get("PLMN_state").getAsInt()).isEqualTo(1);
+ assertThat(json.get("select_PLMN").getAsInt()).isEqualTo(26201);
+ // BC66-specific fields must not be present
+ assertThat(json.has("nc_EARNFCN_offset")).isFalse();
+ assertThat(json.has("RLC_UL_BLER")).isFalse();
+ }
+
+ @Test
+ void getEfentoDeviceInfo_parsesModemInfoSharedModem() {
+ // 4 modem parameters for MODEM_TYPE_SHARED_MODEM as defined in proto_device_info.proto
+ List params = List.of(
+ -90, // [0] RSRP
+ -10, // [1] RSRQ
+ -80, // [2] RSSI
+ 15 // [3] SINR
+ );
+ DeviceInfoProtos.ProtoDeviceInfo deviceInfo = minimalDeviceInfo()
+ .setModem(DeviceInfoProtos.ProtoModem.newBuilder()
+ .setType(DeviceInfoProtos.ModemType.MODEM_TYPE_SHARED_MODEM)
+ .addAllParameters(params)
+ .setModemIdentification("SN-ABCDEF")
+ .build())
+ .build();
+
+ CoapEfentoTransportResource.EfentoTelemetry result = coapEfentoTransportResource.getEfentoDeviceInfo(deviceInfo);
+ var json = result.getValues().getAsJsonObject();
+
+ assertThat(json.get("modem_types").getAsString()).isEqualTo("MODEM_TYPE_SHARED_MODEM");
+ assertThat(json.get("modem_identification").getAsString()).isEqualTo("SN-ABCDEF");
+ assertThat(json.get("RSRP").getAsInt()).isEqualTo(-90);
+ assertThat(json.get("RSRQ").getAsInt()).isEqualTo(-10);
+ assertThat(json.get("RSSI").getAsInt()).isEqualTo(-80);
+ assertThat(json.get("SINR").getAsInt()).isEqualTo(15);
+ // BC66/BC660-specific fields must not be present
+ assertThat(json.has("sc_EARFCN")).isFalse();
+ assertThat(json.has("sc_EARNFCN_offset")).isFalse();
+ }
+
+ @Test
+ void getEfentoDeviceInfo_parsesNewModemFields() {
+ // New ProtoModem fields: sim_card_identification, firmware_version, modem_identification, modem_statistics
+ DeviceInfoProtos.ProtoDeviceInfo deviceInfo = minimalDeviceInfo()
+ .setModem(DeviceInfoProtos.ProtoModem.newBuilder()
+ .setType(DeviceInfoProtos.ModemType.MODEM_TYPE_BC66)
+ .addAllParameters(java.util.Collections.nCopies(34, 0))
+ .setSimCardIdentification("89012345678901234567")
+ .setFirmwareVersion(DeviceInfoProtos.ModemFirmwareVersion.MODEM_FIRMWARE_VERSION_READING_ERROR)
+ .setModemIdentification("352519100417272")
+ .addAllModemStatistics(List.of(5, 1800, 3600, 120))
+ .build())
+ .build();
+
+ CoapEfentoTransportResource.EfentoTelemetry result = coapEfentoTransportResource.getEfentoDeviceInfo(deviceInfo);
+ var json = result.getValues().getAsJsonObject();
+
+ assertThat(json.get("sim_card_identification").getAsString()).isEqualTo("89012345678901234567");
+ assertThat(json.get("firmware_version").getAsString()).isEqualTo("MODEM_FIRMWARE_VERSION_READING_ERROR");
+ assertThat(json.get("modem_identification").getAsString()).isEqualTo("352519100417272");
+ assertThat(json.get("modem_transmissions_count").getAsInt()).isEqualTo(5);
+ assertThat(json.get("modem_time_since_last_devinfo").getAsInt()).isEqualTo(1800);
+ assertThat(json.get("modem_total_psm_time").getAsInt()).isEqualTo(3600);
+ assertThat(json.get("modem_total_active_time").getAsInt()).isEqualTo(120);
+ }
+
+ @Test
+ void getEfentoDeviceInfo_parsesRuntimeInfo() {
+ long batteryResetTs = 1_700_000_000L;
+ DeviceInfoProtos.ProtoDeviceInfo deviceInfo = minimalDeviceInfo()
+ .setRuntimeInfo(DeviceInfoProtos.ProtoRuntime.newBuilder()
+ .setUpTime(3600)
+ .addAllMessageCounters(List.of(10, 5, 9))
+ .setMcuTemperature(25)
+ .setBatteryVoltage(3200)
+ .setMinBatteryMcuTemperature(20)
+ .setBatteryResetTimestamp((int) batteryResetTs)
+ .setMaxMcuTemperature(40)
+ .setMinMcuTemperature(10)
+ .addAllRuntimeErrors(List.of(0, 1))
+ .build())
+ .build();
+
+ CoapEfentoTransportResource.EfentoTelemetry result = coapEfentoTransportResource.getEfentoDeviceInfo(deviceInfo);
+ var json = result.getValues().getAsJsonObject();
+
+ assertThat(json.get("up_time").getAsLong()).isEqualTo(3600);
+ assertThat(json.get("mcu_temp").getAsInt()).isEqualTo(25);
+ assertThat(json.get("min_battery_voltage").getAsLong()).isEqualTo(3200);
+ assertThat(json.get("min_battery_mcu_temp").getAsInt()).isEqualTo(20);
+ assertThat(json.get("battery_reset_timestamp").getAsString()).isEqualTo(formatDate(batteryResetTs));
+ assertThat(json.get("max_mcu_temp").getAsInt()).isEqualTo(40);
+ assertThat(json.get("min_mcu_temp").getAsInt()).isEqualTo(10);
+ assertThat(json.get("counter_of_confirmable_messages_attempts").getAsInt()).isEqualTo(10);
+ assertThat(json.get("counter_of_non_confirmable_messages_attempts").getAsInt()).isEqualTo(5);
+ assertThat(json.get("counter_of_succeeded_messages").getAsInt()).isEqualTo(9);
+ assertThat(json.get("runtime_errors").getAsInt()).isEqualTo(2); // count of errors, not values
+ }
+
+ @Test
+ void getEfentoDeviceInfo_undefinedTimestampRenderedAsUndefinedString() {
+ // -1 as signed int == 0xFFFFFFFF == uint32 max (4294967295), the "Undefined" sentinel
+ DeviceInfoProtos.ProtoDeviceInfo deviceInfo = minimalDeviceInfo().clearMemoryStatistics()
+ .addAllMemoryStatistics(List.of(
+ 0, -1, 0, 0, 0, 0, 0, 0, 0,
+ 0, 0, 0, 0, 0, 0,
+ -1, -1, -1,
+ -1, -1, -1,
+ 0))
+ .build();
+
+ CoapEfentoTransportResource.EfentoTelemetry result = coapEfentoTransportResource.getEfentoDeviceInfo(deviceInfo);
+ var json = result.getValues().getAsJsonObject();
+
+ assertThat(json.get("timestamp_of_the_end_of_collecting_statistics").getAsString()).isEqualTo("Undefined");
+ assertThat(json.get("timestamp_of_the_first_binary_measurement").getAsString()).isEqualTo("Undefined");
+ assertThat(json.get("timestamp_of_the_last_binary_measurement").getAsString()).isEqualTo("Undefined");
+ assertThat(json.get("timestamp_of_the_first_binary_measurement_sent").getAsString()).isEqualTo("Undefined");
+ assertThat(json.get("timestamp_of_the_first_continuous_measurement").getAsString()).isEqualTo("Undefined");
+ assertThat(json.get("timestamp_of_the_last_continuous_measurement").getAsString()).isEqualTo("Undefined");
+ assertThat(json.get("timestamp_of_the_last_continuous_measurement_sent").getAsString()).isEqualTo("Undefined");
+ }
+
+ @Test
+ void getEfentoDeviceInfo_tsIsCurrentTimeMillis() {
+ long before = System.currentTimeMillis();
+ CoapEfentoTransportResource.EfentoTelemetry result =
+ coapEfentoTransportResource.getEfentoDeviceInfo(minimalDeviceInfo().build());
+ long after = System.currentTimeMillis();
+
+ assertThat(result.getTs()).isBetween(before, after);
+ }
+
+ // -------------------------------------------------------------------------
+ // ProtoConfig / getEfentoConfiguration parsing tests
+ // -------------------------------------------------------------------------
+
+ @Test
+ void getEfentoConfiguration_parsesServerCommunicationFields() throws InvalidProtocolBufferException {
+ byte[] bytes = ConfigProtos.ProtoConfig.newBuilder()
+ .setDataServerIp("18.184.24.239")
+ .setDataServerPort(5683)
+ .setUpdateServerIp("efento.update.io")
+ .setUpdateServerPortCoap(5684)
+ .setUpdateServerPortUdp(5685)
+ .setTransmissionInterval(300)
+ .setAckInterval(600)
+ .setSupervisionPeriod(3600)
+ .build()
+ .toByteArray();
+
+ var json = coapEfentoTransportResource.getEfentoConfiguration(bytes).getAsJsonObject();
+
+ assertThat(json.get("dataServerIp").getAsString()).isEqualTo("18.184.24.239");
+ assertThat(json.get("dataServerPort").getAsLong()).isEqualTo(5683);
+ assertThat(json.get("updateServerIp").getAsString()).isEqualTo("efento.update.io");
+ assertThat(json.get("updateServerPortCoap").getAsLong()).isEqualTo(5684);
+ assertThat(json.get("updateServerPortUdp").getAsLong()).isEqualTo(5685);
+ assertThat(json.get("transmissionInterval").getAsLong()).isEqualTo(300);
+ assertThat(json.get("ackInterval").getAsLong()).isEqualTo(600);
+ assertThat(json.get("supervisionPeriod").getAsLong()).isEqualTo(3600);
+ }
+
+ @Test
+ void getEfentoConfiguration_parsesMeasurementPeriod() throws InvalidProtocolBufferException {
+ byte[] bytes = ConfigProtos.ProtoConfig.newBuilder()
+ .setMeasurementPeriodBase(60)
+ .setMeasurementPeriodFactor(5)
+ .build()
+ .toByteArray();
+
+ var json = coapEfentoTransportResource.getEfentoConfiguration(bytes).getAsJsonObject();
+
+ assertThat(json.get("measurementPeriodBase").getAsLong()).isEqualTo(60);
+ assertThat(json.get("measurementPeriodFactor").getAsLong()).isEqualTo(5);
+ }
+
+ @Test
+ void getEfentoConfiguration_parsesBooleanRequestFields() throws InvalidProtocolBufferException {
+ byte[] bytes = ConfigProtos.ProtoConfig.newBuilder()
+ .setDeviceInfoRequest(true)
+ .setUpdateSoftwareRequest(true)
+ .setConfigurationRequest(true)
+ .setAcceptWithoutTestingRequest(true)
+ .setResetMemoryRequest(true)
+ .build()
+ .toByteArray();
+
+ var json = coapEfentoTransportResource.getEfentoConfiguration(bytes).getAsJsonObject();
+
+ assertThat(json.get("deviceInfoRequest").getAsBoolean()).isTrue();
+ assertThat(json.get("updateSoftwareRequest").getAsBoolean()).isTrue();
+ assertThat(json.get("configurationRequest").getAsBoolean()).isTrue();
+ assertThat(json.get("acceptWithoutTestingRequest").getAsBoolean()).isTrue();
+ assertThat(json.get("resetMemoryRequest").getAsBoolean()).isTrue();
+ }
+
+ @Test
+ void getEfentoConfiguration_parsesModemAndNetworkFields() throws InvalidProtocolBufferException {
+ byte[] bytes = ConfigProtos.ProtoConfig.newBuilder()
+ .setApn("internet.example.com")
+ .setApnUsername("user")
+ .setApnPassword("pass")
+ .setPlmnSelection(26001)
+ .setModemBandsMask(2084) // bands 3, 8, 20
+ .setNetworkTroubleshooting(2)
+ .build()
+ .toByteArray();
+
+ var json = coapEfentoTransportResource.getEfentoConfiguration(bytes).getAsJsonObject();
+
+ assertThat(json.get("apn").getAsString()).isEqualTo("internet.example.com");
+ assertThat(json.get("apnUsername").getAsString()).isEqualTo("user");
+ assertThat(json.get("apnPassword").getAsString()).isEqualTo("pass");
+ assertThat(json.get("plmnSelection").getAsLong()).isEqualTo(26001);
+ assertThat(json.get("modemBandsMask").getAsLong()).isEqualTo(2084);
+ assertThat(json.get("networkTroubleshooting").getAsLong()).isEqualTo(2);
+ }
+
+ @Test
+ void getEfentoConfiguration_parsesCloudTokenFields() throws InvalidProtocolBufferException {
+ byte[] bytes = ConfigProtos.ProtoConfig.newBuilder()
+ .setCloudToken("my-device-token")
+ .setCloudTokenConfig(1)
+ .setCloudTokenCoapOption(65000)
+ .build()
+ .toByteArray();
+
+ var json = coapEfentoTransportResource.getEfentoConfiguration(bytes).getAsJsonObject();
+
+ assertThat(json.get("cloudToken").getAsString()).isEqualTo("my-device-token");
+ assertThat(json.get("cloudTokenConfig").getAsLong()).isEqualTo(1);
+ assertThat(json.get("cloudTokenCoapOption").getAsLong()).isEqualTo(65000);
+ }
+
+ @Test
+ void getEfentoConfiguration_parsesEndpointFields() throws InvalidProtocolBufferException {
+ byte[] bytes = ConfigProtos.ProtoConfig.newBuilder()
+ .setDataEndpoint("/m")
+ .setConfigurationEndpoint("/c")
+ .setDeviceInfoEndpoint("/i")
+ .setTimeEndpoint("/t")
+ .build()
+ .toByteArray();
+
+ var json = coapEfentoTransportResource.getEfentoConfiguration(bytes).getAsJsonObject();
+
+ assertThat(json.get("dataEndpoint").getAsString()).isEqualTo("/m");
+ assertThat(json.get("configurationEndpoint").getAsString()).isEqualTo("/c");
+ assertThat(json.get("deviceInfoEndpoint").getAsString()).isEqualTo("/i");
+ assertThat(json.get("timeEndpoint").getAsString()).isEqualTo("/t");
+ }
+
+ @Test
+ void getEfentoConfiguration_parsesBleAdvertisingPeriod() throws InvalidProtocolBufferException {
+ byte[] bytes = ConfigProtos.ProtoConfig.newBuilder()
+ .setBleAdvertisingPeriod(ConfigTypesProtos.ProtoBleAdvertisingPeriod.newBuilder()
+ .setMode(ConfigTypesProtos.BleAdvertisingPeriodMode.BLE_ADVERTISING_PERIOD_MODE_NORMAL)
+ .setNormal(1600)
+ .setFast(320)
+ .build())
+ .build()
+ .toByteArray();
+
+ var json = coapEfentoTransportResource.getEfentoConfiguration(bytes).getAsJsonObject();
+ var ble = json.get("bleAdvertisingPeriod").getAsJsonObject();
+
+ assertThat(ble.get("mode").getAsString()).isEqualTo("BLE_ADVERTISING_PERIOD_MODE_NORMAL");
+ assertThat(ble.get("normal").getAsLong()).isEqualTo(1600);
+ assertThat(ble.get("fast").getAsLong()).isEqualTo(320);
+ }
+
+ @Test
+ void getEfentoConfiguration_parsesRepeatedChannelTypes() throws InvalidProtocolBufferException {
+ byte[] bytes = ConfigProtos.ProtoConfig.newBuilder()
+ .addChannelTypes(MeasurementType.MEASUREMENT_TYPE_TEMPERATURE)
+ .addChannelTypes(MeasurementType.MEASUREMENT_TYPE_HUMIDITY)
+ .addChannelTypes(MeasurementType.MEASUREMENT_TYPE_ATMOSPHERIC_PRESSURE)
+ .build()
+ .toByteArray();
+
+ var json = coapEfentoTransportResource.getEfentoConfiguration(bytes).getAsJsonObject();
+ var channelTypes = json.get("channelTypes").getAsJsonArray();
+
+ assertThat(channelTypes).hasSize(3);
+ assertThat(channelTypes.get(0).getAsString()).isEqualTo("MEASUREMENT_TYPE_TEMPERATURE");
+ assertThat(channelTypes.get(1).getAsString()).isEqualTo("MEASUREMENT_TYPE_HUMIDITY");
+ assertThat(channelTypes.get(2).getAsString()).isEqualTo("MEASUREMENT_TYPE_ATMOSPHERIC_PRESSURE");
+ }
+
+ @Test
+ void getEfentoConfiguration_parsesEdgeLogicRules() throws InvalidProtocolBufferException {
+ // ProtoRule uses channel_mask (bit mask), condition, parameters, action
+ byte[] bytes = ConfigProtos.ProtoConfig.newBuilder()
+ .addRules(ProtoRuleProtos.ProtoRule.newBuilder()
+ .setChannelMask(1) // channel 1 → bit mask 0b000001
+ .setCondition(ProtoRuleProtos.Condition.CONDITION_HIGH_THRESHOLD)
+ .addParameters(500) // threshold value
+ .setAction(ProtoRuleProtos.Action.ACTION_TRIGGER_TRANSMISSION)
+ .build())
+ .build()
+ .toByteArray();
+
+ var json = coapEfentoTransportResource.getEfentoConfiguration(bytes).getAsJsonObject();
+ var rules = json.get("rules").getAsJsonArray();
+
+ assertThat(rules).hasSize(1);
+ var rule = rules.get(0).getAsJsonObject();
+ assertThat(rule.get("channelMask").getAsInt()).isEqualTo(1);
+ assertThat(rule.get("condition").getAsString()).isEqualTo("CONDITION_HIGH_THRESHOLD");
+ assertThat(rule.get("parameters").getAsJsonArray().get(0).getAsInt()).isEqualTo(500);
+ assertThat(rule.get("action").getAsString()).isEqualTo("ACTION_TRIGGER_TRANSMISSION");
+ }
+
+ @Test
+ void getEfentoConfiguration_emptyConfigProducesJsonWithDefaultValues() throws InvalidProtocolBufferException {
+ byte[] bytes = ConfigProtos.ProtoConfig.newBuilder().build().toByteArray();
+
+ var json = coapEfentoTransportResource.getEfentoConfiguration(bytes).getAsJsonObject();
+
+ // JsonFormat with includingDefaultValueFields prints all fields — verify a representative subset
+ assertThat(json.get("measurementPeriodBase").getAsLong()).isEqualTo(0);
+ assertThat(json.get("transmissionInterval").getAsLong()).isEqualTo(0);
+ assertThat(json.get("dataServerIp").getAsString()).isEmpty();
+ assertThat(json.get("deviceInfoRequest").getAsBoolean()).isFalse();
+ assertThat(json.get("cloudToken").getAsString()).isEmpty();
+ }
+
+ /**
+ * Builds a ProtoDeviceInfo with the minimum set of repeated fields required by getEfentoDeviceInfo:
+ * 22 memory_statistics, 34 modem parameters and 3 message_counters — all set to 0.
+ */
+ private static DeviceInfoProtos.ProtoDeviceInfo.Builder minimalDeviceInfo() {
+ List zeroMemStats = java.util.Collections.nCopies(22, 0);
+ List zeroModemParams = java.util.Collections.nCopies(34, 0);
+ return DeviceInfoProtos.ProtoDeviceInfo.newBuilder()
+ .setSerialNumber(integerToByteString(1234))
+ .setCloudToken("test_token")
+ .setSwVersion(0)
+ .addAllMemoryStatistics(zeroMemStats)
+ .setModem(DeviceInfoProtos.ProtoModem.newBuilder()
+ .setType(DeviceInfoProtos.ModemType.MODEM_TYPE_UNSPECIFIED)
+ .addAllParameters(zeroModemParams)
+ .build())
+ .setRuntimeInfo(DeviceInfoProtos.ProtoRuntime.newBuilder()
+ .setUpTime(0)
+ .addAllMessageCounters(List.of(0, 0, 0))
+ .build());
+ }
+
+ private static String formatDate(long seconds) {
+ SimpleDateFormat sdf = new SimpleDateFormat("dd MMM yyyy HH:mm:ss Z");
+ return sdf.format(new Date(TimeUnit.SECONDS.toMillis(seconds)));
}
public static ByteString integerToByteString(Integer intValue) {
@@ -340,20 +907,17 @@ class CoapEfentoTransportResourceTest {
private void checkDefaultMeasurements(ProtoMeasurements incomingMeasurements,
List actualEfentoMeasurements,
- long expectedMeasurementInterval,
- boolean isBinarySensor) {
- for (int i = 0; i < actualEfentoMeasurements.size(); i++) {
- CoapEfentoTransportResource.EfentoTelemetry actualEfentoMeasurement = actualEfentoMeasurements.get(i);
- assertThat(actualEfentoMeasurement.getValues().getAsJsonObject().get("serial").getAsString()).isEqualTo(CoapEfentoUtils.convertByteArrayToString(incomingMeasurements.getSerialNum().toByteArray()));
- assertThat(actualEfentoMeasurement.getValues().getAsJsonObject().get("battery").getAsString()).isEqualTo(incomingMeasurements.getBatteryStatus() ? "ok" : "low");
- MeasurementsProtos.ProtoChannel protoChannel = incomingMeasurements.getChannelsList().get(0);
- long measuredAt = isBinarySensor ?
- TimeUnit.SECONDS.toMillis(protoChannel.getTimestamp()) + Math.abs(TimeUnit.SECONDS.toMillis(protoChannel.getSampleOffsetsList().get(i))) - 1000 :
- TimeUnit.SECONDS.toMillis(protoChannel.getTimestamp() + i * expectedMeasurementInterval);
- assertThat(actualEfentoMeasurement.getValues().getAsJsonObject().get("measured_at").getAsString()).isEqualTo(convertTimestampToUtcString(measuredAt));
- assertThat(actualEfentoMeasurement.getValues().getAsJsonObject().get("next_transmission_at").getAsString()).isEqualTo(convertTimestampToUtcString(TimeUnit.SECONDS.toMillis(incomingMeasurements.getNextTransmissionAt())));
- assertThat(actualEfentoMeasurement.getValues().getAsJsonObject().get("signal").getAsLong()).isEqualTo(incomingMeasurements.getSignal());
- assertThat(actualEfentoMeasurement.getValues().getAsJsonObject().get("measurement_interval").getAsDouble()).isEqualTo(expectedMeasurementInterval);
+ long expectedMeasurementInterval) {
+ CoapEfentoTransportResource.EfentoTelemetry efentoTelemetry = actualEfentoMeasurements.stream()
+ .sorted(Comparator.comparing(CoapEfentoTransportResource.EfentoTelemetry::getTs))
+ .toList().get(0);
+
+ assertThat(efentoTelemetry.getValues().getAsJsonObject().get("serial").getAsString()).isEqualTo(CoapEfentoUtils.convertByteArrayToString(incomingMeasurements.getSerialNumber().toByteArray()));
+ assertThat(efentoTelemetry.getValues().getAsJsonObject().get("battery").getAsString()).isEqualTo(incomingMeasurements.getBatteryStatus() ? "ok" : "low");
+ assertThat(efentoTelemetry.getValues().getAsJsonObject().get("next_transmission_at").getAsString()).isEqualTo(convertTimestampToUtcString(TimeUnit.SECONDS.toMillis(incomingMeasurements.getNextTransmissionAt())));
+ assertThat(efentoTelemetry.getValues().getAsJsonObject().get("signal").getAsLong()).isEqualTo(incomingMeasurements.getSignal());
+ for (int i = 1; i < incomingMeasurements.getChannelsCount() + 1; i++) {
+ assertThat(efentoTelemetry.getValues().getAsJsonObject().get("measurement_interval_" + i).getAsDouble()).isEqualTo(expectedMeasurementInterval);
}
}
diff --git a/common/transport/http/pom.xml b/common/transport/http/pom.xml
index e832433cb3..3a06449bd9 100644
--- a/common/transport/http/pom.xml
+++ b/common/transport/http/pom.xml
@@ -20,7 +20,7 @@
4.0.0
org.thingsboard.common
- 4.2.2.1-SNAPSHOT
+ 4.2.2.2-SNAPSHOT
transport
org.thingsboard.common.transport
diff --git a/common/transport/lwm2m/pom.xml b/common/transport/lwm2m/pom.xml
index b1594c1c3d..9267d74dd2 100644
--- a/common/transport/lwm2m/pom.xml
+++ b/common/transport/lwm2m/pom.xml
@@ -20,7 +20,7 @@
4.0.0
org.thingsboard.common
- 4.2.2.1-SNAPSHOT
+ 4.2.2.2-SNAPSHOT
transport
org.thingsboard.common.transport
diff --git a/common/transport/mqtt/pom.xml b/common/transport/mqtt/pom.xml
index 5b947c806f..b63ad8a1ac 100644
--- a/common/transport/mqtt/pom.xml
+++ b/common/transport/mqtt/pom.xml
@@ -20,7 +20,7 @@
4.0.0
org.thingsboard.common
- 4.2.2.1-SNAPSHOT
+ 4.2.2.2-SNAPSHOT
transport
org.thingsboard.common.transport
diff --git a/common/transport/pom.xml b/common/transport/pom.xml
index ac89c33953..d77303b40c 100644
--- a/common/transport/pom.xml
+++ b/common/transport/pom.xml
@@ -20,7 +20,7 @@
4.0.0
org.thingsboard
- 4.2.2.1-SNAPSHOT
+ 4.2.2.2-SNAPSHOT
common
org.thingsboard.common
diff --git a/common/transport/snmp/pom.xml b/common/transport/snmp/pom.xml
index 5c711fdc90..f746b58b7d 100644
--- a/common/transport/snmp/pom.xml
+++ b/common/transport/snmp/pom.xml
@@ -21,7 +21,7 @@
org.thingsboard.common
- 4.2.2.1-SNAPSHOT
+ 4.2.2.2-SNAPSHOT
transport
diff --git a/common/transport/transport-api/pom.xml b/common/transport/transport-api/pom.xml
index 8fda8f4daf..10ce5ede63 100644
--- a/common/transport/transport-api/pom.xml
+++ b/common/transport/transport-api/pom.xml
@@ -20,7 +20,7 @@
4.0.0
org.thingsboard.common
- 4.2.2.1-SNAPSHOT
+ 4.2.2.2-SNAPSHOT
transport
org.thingsboard.common.transport
diff --git a/common/util/pom.xml b/common/util/pom.xml
index f529772166..4748d75d92 100644
--- a/common/util/pom.xml
+++ b/common/util/pom.xml
@@ -20,7 +20,7 @@
4.0.0
org.thingsboard
- 4.2.2.1-SNAPSHOT
+ 4.2.2.2-SNAPSHOT
common
org.thingsboard.common
diff --git a/common/version-control/pom.xml b/common/version-control/pom.xml
index 3ea8054bd5..66c87eb5d1 100644
--- a/common/version-control/pom.xml
+++ b/common/version-control/pom.xml
@@ -20,7 +20,7 @@
4.0.0
org.thingsboard
- 4.2.2.1-SNAPSHOT
+ 4.2.2.2-SNAPSHOT
common
org.thingsboard.common
diff --git a/dao/pom.xml b/dao/pom.xml
index 6e67ea5d27..bc172eeb45 100644
--- a/dao/pom.xml
+++ b/dao/pom.xml
@@ -20,7 +20,7 @@
4.0.0
org.thingsboard
- 4.2.2.1-SNAPSHOT
+ 4.2.2.2-SNAPSHOT
thingsboard
dao
diff --git a/dao/src/main/java/org/thingsboard/server/dao/alarm/BaseAlarmCommentService.java b/dao/src/main/java/org/thingsboard/server/dao/alarm/BaseAlarmCommentService.java
index 505a319baa..1dec2bbe04 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/alarm/BaseAlarmCommentService.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/alarm/BaseAlarmCommentService.java
@@ -48,13 +48,13 @@ public class BaseAlarmCommentService extends AbstractEntityService implements Al
@Override
public AlarmComment createOrUpdateAlarmComment(TenantId tenantId, AlarmComment alarmComment) {
- alarmCommentDataValidator.validate(alarmComment, c -> tenantId);
+ AlarmComment oldAlarmComment = alarmCommentDataValidator.validate(alarmComment, c -> tenantId);
boolean isCreated = alarmComment.getId() == null;
AlarmComment result;
if (isCreated) {
result = createAlarmComment(tenantId, alarmComment);
} else {
- result = updateAlarmComment(tenantId, alarmComment);
+ result = updateAlarmComment(tenantId, alarmComment, oldAlarmComment);
}
if (result != null) {
eventPublisher.publishEvent(SaveEntityEvent.builder().tenantId(tenantId).entity(result)
@@ -101,18 +101,17 @@ public class BaseAlarmCommentService extends AbstractEntityService implements Al
return alarmCommentDao.save(tenantId, alarmComment);
}
- private AlarmComment updateAlarmComment(TenantId tenantId, AlarmComment newAlarmComment) {
+ private AlarmComment updateAlarmComment(TenantId tenantId, AlarmComment newAlarmComment, AlarmComment oldAlarmComment) {
log.debug("Update Alarm comment : {}", newAlarmComment);
- AlarmComment existing = alarmCommentDao.findAlarmCommentById(tenantId, newAlarmComment.getId().getId());
- if (existing != null) {
+ if (oldAlarmComment != null) {
if (newAlarmComment.getComment() != null) {
JsonNode comment = newAlarmComment.getComment();
((ObjectNode) comment).put("edited", "true");
((ObjectNode) comment).put("editedOn", System.currentTimeMillis());
- existing.setComment(comment);
+ oldAlarmComment.setComment(comment);
}
- return alarmCommentDao.save(tenantId, existing);
+ return alarmCommentDao.save(tenantId, oldAlarmComment);
}
return null;
}
diff --git a/dao/src/main/java/org/thingsboard/server/dao/resource/BaseImageService.java b/dao/src/main/java/org/thingsboard/server/dao/resource/BaseImageService.java
index 639c60e1d6..11bbe797cf 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/resource/BaseImageService.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/resource/BaseImageService.java
@@ -188,6 +188,12 @@ public class BaseImageService extends BaseResourceService implements ImageServic
return findResourceInfoByTenantIdAndKey(tenantId, ResourceType.IMAGE, key);
}
+ @Override
+ public Set getAllImageKeysByTenantId(TenantId tenantId) {
+ log.trace("Executing getAllImageKeysByTenantId [{}]", tenantId);
+ return resourceInfoDao.findKeysByTenantIdAndResourceTypeAndResourceKeyPrefix(tenantId, ResourceType.IMAGE, "");
+ }
+
@Override
public TbResourceInfo getPublicImageInfoByKey(String publicResourceKey) {
return resourceInfoDao.findPublicResourceByKey(ResourceType.IMAGE, publicResourceKey);
diff --git a/dao/src/main/java/org/thingsboard/server/dao/service/validator/AlarmCommentDataValidator.java b/dao/src/main/java/org/thingsboard/server/dao/service/validator/AlarmCommentDataValidator.java
index 006661536c..86a6e5224b 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/service/validator/AlarmCommentDataValidator.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/service/validator/AlarmCommentDataValidator.java
@@ -18,7 +18,9 @@ package org.thingsboard.server.dao.service.validator;
import lombok.AllArgsConstructor;
import org.springframework.stereotype.Component;
import org.thingsboard.server.common.data.alarm.AlarmComment;
+import org.thingsboard.server.common.data.alarm.AlarmCommentType;
import org.thingsboard.server.common.data.id.TenantId;
+import org.thingsboard.server.dao.alarm.AlarmCommentDao;
import org.thingsboard.server.dao.exception.DataValidationException;
import org.thingsboard.server.dao.service.DataValidator;
@@ -26,6 +28,8 @@ import org.thingsboard.server.dao.service.DataValidator;
@AllArgsConstructor
public class AlarmCommentDataValidator extends DataValidator {
+ private final AlarmCommentDao alarmCommentDao;
+
@Override
protected void validateDataImpl(TenantId tenantId, AlarmComment alarmComment) {
if (alarmComment.getComment() == null) {
@@ -35,4 +39,19 @@ public class AlarmCommentDataValidator extends DataValidator {
throw new DataValidationException("Alarm id should be specified!");
}
}
+
+ @Override
+ protected AlarmComment validateUpdate(TenantId tenantId, AlarmComment alarmComment) {
+ AlarmComment oldAlarmComment = null;
+ if (alarmComment.getId() != null) {
+ oldAlarmComment = alarmCommentDao.findAlarmCommentById(tenantId, alarmComment.getId().getId());
+ if (oldAlarmComment == null) {
+ throw new DataValidationException("Can't update non existing alarm comment!");
+ }
+ if (oldAlarmComment.getType() == AlarmCommentType.SYSTEM) {
+ throw new DataValidationException("System alarm comment can't be updated!");
+ }
+ }
+ return oldAlarmComment;
+ }
}
diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/AlarmServiceTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/AlarmServiceTest.java
index e1093e4f46..2329111e22 100644
--- a/dao/src/test/java/org/thingsboard/server/dao/service/AlarmServiceTest.java
+++ b/dao/src/test/java/org/thingsboard/server/dao/service/AlarmServiceTest.java
@@ -18,6 +18,7 @@ package org.thingsboard.server.dao.service;
import com.datastax.oss.driver.api.core.uuid.Uuids;
import org.junit.Assert;
import org.junit.Test;
+import org.junit.jupiter.api.Assertions;
import org.springframework.beans.factory.annotation.Autowired;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.server.common.data.Customer;
@@ -57,6 +58,7 @@ import org.thingsboard.server.dao.alarm.AlarmService;
import org.thingsboard.server.dao.asset.AssetService;
import org.thingsboard.server.dao.customer.CustomerService;
import org.thingsboard.server.dao.device.DeviceService;
+import org.thingsboard.server.dao.exception.DataValidationException;
import org.thingsboard.server.dao.relation.RelationService;
import org.thingsboard.server.dao.user.UserService;
@@ -64,6 +66,8 @@ import java.util.Collections;
import java.util.List;
import java.util.concurrent.ExecutionException;
+import static org.assertj.core.api.Assertions.assertThat;
+
@DaoSqlTest
public class AlarmServiceTest extends AbstractServiceTest {
@@ -987,4 +991,25 @@ public class AlarmServiceTest extends AbstractServiceTest {
Assert.assertEquals(1, alarmsCount);
}
+ @Test
+ public void testShouldFailToCreateAlarmWithBadType() {
+ AssetId originatorId = new AssetId(Uuids.timeBased());
+
+ long ts = System.currentTimeMillis();
+ AlarmCreateOrUpdateActiveRequest request = AlarmCreateOrUpdateActiveRequest.builder()
+ .tenantId(tenantId)
+ .originator(originatorId)
+ .type("
")
+ .severity(AlarmSeverity.CRITICAL)
+ .startTs(ts).build();
+
+ Assertions.assertThrows(DataValidationException.class, () -> {
+ alarmService.createAlarm(request);
+ });
+
+ request.setType(TEST_ALARM);
+ AlarmApiCallResult result = alarmService.createAlarm(request);
+ assertThat(result.getAlarm().getId()).isNotNull();
+ }
+
}
diff --git a/edqs/pom.xml b/edqs/pom.xml
index c1c471ef62..9bf82c7a65 100644
--- a/edqs/pom.xml
+++ b/edqs/pom.xml
@@ -20,7 +20,7 @@
4.0.0
org.thingsboard
- 4.2.2.1-SNAPSHOT
+ 4.2.2.2-SNAPSHOT
thingsboard
edqs
diff --git a/monitoring/pom.xml b/monitoring/pom.xml
index e15b603449..2601ac47fe 100644
--- a/monitoring/pom.xml
+++ b/monitoring/pom.xml
@@ -21,7 +21,7 @@
4.0.0
org.thingsboard
- 4.2.2.1-SNAPSHOT
+ 4.2.2.2-SNAPSHOT
thingsboard
diff --git a/msa/black-box-tests/pom.xml b/msa/black-box-tests/pom.xml
index 8e4dcdc6c4..c06e98b45e 100644
--- a/msa/black-box-tests/pom.xml
+++ b/msa/black-box-tests/pom.xml
@@ -21,7 +21,7 @@
org.thingsboard
- 4.2.2.1-SNAPSHOT
+ 4.2.2.2-SNAPSHOT
msa
org.thingsboard.msa
diff --git a/msa/edqs/pom.xml b/msa/edqs/pom.xml
index 3377cff6c6..f85e9b7691 100644
--- a/msa/edqs/pom.xml
+++ b/msa/edqs/pom.xml
@@ -20,7 +20,7 @@
4.0.0
org.thingsboard
- 4.2.2.1-SNAPSHOT
+ 4.2.2.2-SNAPSHOT
msa
org.thingsboard.msa
diff --git a/msa/js-executor/docker/Dockerfile b/msa/js-executor/docker/Dockerfile
index 1ef157fdd9..736bd56529 100644
--- a/msa/js-executor/docker/Dockerfile
+++ b/msa/js-executor/docker/Dockerfile
@@ -14,7 +14,7 @@
# limitations under the License.
#
-FROM thingsboard/node:22.18.0-bookworm-slim
+FROM thingsboard/node:22.22.2-bookworm-slim
ENV NODE_ENV production
ENV DOCKER_MODE true
diff --git a/msa/js-executor/package.json b/msa/js-executor/package.json
index a09596e6ac..b096a3ae99 100644
--- a/msa/js-executor/package.json
+++ b/msa/js-executor/package.json
@@ -1,7 +1,7 @@
{
"name": "thingsboard-js-executor",
"private": true,
- "version": "4.2.2.1",
+ "version": "4.2.2.2",
"description": "ThingsBoard JavaScript Executor Microservice",
"main": "server.ts",
"bin": "server.js",
diff --git a/msa/js-executor/pom.xml b/msa/js-executor/pom.xml
index 87b3657d4a..f5ea04fac3 100644
--- a/msa/js-executor/pom.xml
+++ b/msa/js-executor/pom.xml
@@ -20,7 +20,7 @@
4.0.0
org.thingsboard
- 4.2.2.1-SNAPSHOT
+ 4.2.2.2-SNAPSHOT
msa
org.thingsboard.msa
@@ -70,7 +70,7 @@
install-node-and-yarn
- v22.18.0
+ v22.22.2
v1.22.22
diff --git a/msa/js-executor/yarn.lock b/msa/js-executor/yarn.lock
index 08a789e9b5..7e2fe58a13 100644
--- a/msa/js-executor/yarn.lock
+++ b/msa/js-executor/yarn.lock
@@ -363,9 +363,9 @@ body-parser@^2.2.0:
type-is "^2.0.0"
brace-expansion@^1.1.7:
- version "1.1.12"
- resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.12.tgz#ab9b454466e5a8cc3a187beaad580412a9c5b843"
- integrity sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==
+ version "1.1.13"
+ resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.13.tgz#d37875c01dc9eff988dd49d112a57cb67b54efe6"
+ integrity sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==
dependencies:
balanced-match "^1.0.0"
concat-map "0.0.1"
@@ -1179,9 +1179,9 @@ path-parse@^1.0.7:
integrity sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==
path-to-regexp@^8.0.0:
- version "8.2.0"
- resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-8.2.0.tgz#73990cc29e57a3ff2a0d914095156df5db79e8b4"
- integrity sha512-TdrF7fW9Rphjq4RjrW0Kp2AW0Ahwu9sRGTkS6bvDi0SCwZlEZYmcfDbEsTz8RVk0EHIS/Vd1bv3JhG+1xZuAyQ==
+ version "8.4.0"
+ resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-8.4.0.tgz#8e98fcd94826aff01a90c544ef74ffbaca3a78ed"
+ integrity sha512-PuseHIvAnz3bjrM2rGJtSgo1zjgxapTLZ7x2pjhzWwlp4SJQgK3f3iZIQwkpEnBaKz6seKBADpM4B4ySkuYypg==
picocolors@^1.1.0:
version "1.1.1"
@@ -1189,14 +1189,14 @@ picocolors@^1.1.0:
integrity sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==
picomatch@^2.0.4, picomatch@^2.2.1:
- version "2.3.1"
- resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42"
- integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==
+ version "2.3.2"
+ resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.2.tgz#5a942915e26b372dc0f0e6753149a16e6b1c5601"
+ integrity sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==
picomatch@^4.0.2:
- version "4.0.3"
- resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-4.0.3.tgz#796c76136d1eead715db1e7bad785dedd695a042"
- integrity sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==
+ version "4.0.4"
+ resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-4.0.4.tgz#fd6f5e00a143086e074dffe4c924b8fb293b0589"
+ integrity sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==
prebuild-install@^7.1.1:
version "7.1.3"
diff --git a/msa/monitoring/pom.xml b/msa/monitoring/pom.xml
index aded630179..ab484e7df8 100644
--- a/msa/monitoring/pom.xml
+++ b/msa/monitoring/pom.xml
@@ -22,7 +22,7 @@
4.0.0
org.thingsboard
- 4.2.2.1-SNAPSHOT
+ 4.2.2.2-SNAPSHOT
msa
diff --git a/msa/pom.xml b/msa/pom.xml
index d56a7aa265..ac7b7b70c3 100644
--- a/msa/pom.xml
+++ b/msa/pom.xml
@@ -20,7 +20,7 @@
4.0.0
org.thingsboard
- 4.2.2.1-SNAPSHOT
+ 4.2.2.2-SNAPSHOT
thingsboard
msa
diff --git a/msa/tb-node/pom.xml b/msa/tb-node/pom.xml
index 8272815640..3dd84135cb 100644
--- a/msa/tb-node/pom.xml
+++ b/msa/tb-node/pom.xml
@@ -20,7 +20,7 @@
4.0.0
org.thingsboard
- 4.2.2.1-SNAPSHOT
+ 4.2.2.2-SNAPSHOT
msa
org.thingsboard.msa
diff --git a/msa/tb/pom.xml b/msa/tb/pom.xml
index 33e7abeabf..5c10adf67e 100644
--- a/msa/tb/pom.xml
+++ b/msa/tb/pom.xml
@@ -20,7 +20,7 @@
4.0.0
org.thingsboard
- 4.2.2.1-SNAPSHOT
+ 4.2.2.2-SNAPSHOT
msa
org.thingsboard.msa
diff --git a/msa/transport/coap/pom.xml b/msa/transport/coap/pom.xml
index 6517d8b466..f081e6fd46 100644
--- a/msa/transport/coap/pom.xml
+++ b/msa/transport/coap/pom.xml
@@ -20,7 +20,7 @@
4.0.0
org.thingsboard.msa
- 4.2.2.1-SNAPSHOT
+ 4.2.2.2-SNAPSHOT
transport
org.thingsboard.msa.transport
diff --git a/msa/transport/http/pom.xml b/msa/transport/http/pom.xml
index 4c3445c4fc..78e445fda8 100644
--- a/msa/transport/http/pom.xml
+++ b/msa/transport/http/pom.xml
@@ -20,7 +20,7 @@
4.0.0
org.thingsboard.msa
- 4.2.2.1-SNAPSHOT
+ 4.2.2.2-SNAPSHOT
transport
org.thingsboard.msa.transport
diff --git a/msa/transport/lwm2m/pom.xml b/msa/transport/lwm2m/pom.xml
index e106d81641..d65d07f9a9 100644
--- a/msa/transport/lwm2m/pom.xml
+++ b/msa/transport/lwm2m/pom.xml
@@ -20,7 +20,7 @@
4.0.0
org.thingsboard.msa
- 4.2.2.1-SNAPSHOT
+ 4.2.2.2-SNAPSHOT
transport
org.thingsboard.msa.transport
diff --git a/msa/transport/mqtt/pom.xml b/msa/transport/mqtt/pom.xml
index f2f4f1e531..8dfe14b3a6 100644
--- a/msa/transport/mqtt/pom.xml
+++ b/msa/transport/mqtt/pom.xml
@@ -20,7 +20,7 @@
4.0.0
org.thingsboard.msa
- 4.2.2.1-SNAPSHOT
+ 4.2.2.2-SNAPSHOT
transport
org.thingsboard.msa.transport
diff --git a/msa/transport/pom.xml b/msa/transport/pom.xml
index ae50472021..aa10ff1184 100644
--- a/msa/transport/pom.xml
+++ b/msa/transport/pom.xml
@@ -20,7 +20,7 @@
4.0.0
org.thingsboard
- 4.2.2.1-SNAPSHOT
+ 4.2.2.2-SNAPSHOT
msa
org.thingsboard.msa
diff --git a/msa/transport/snmp/pom.xml b/msa/transport/snmp/pom.xml
index a1e024c735..fb344e3242 100644
--- a/msa/transport/snmp/pom.xml
+++ b/msa/transport/snmp/pom.xml
@@ -21,7 +21,7 @@
org.thingsboard.msa
transport
- 4.2.2.1-SNAPSHOT
+ 4.2.2.2-SNAPSHOT
org.thingsboard.msa.transport
diff --git a/msa/vc-executor-docker/pom.xml b/msa/vc-executor-docker/pom.xml
index 5713d9a142..0bc90c6ac4 100644
--- a/msa/vc-executor-docker/pom.xml
+++ b/msa/vc-executor-docker/pom.xml
@@ -20,7 +20,7 @@
4.0.0
org.thingsboard
- 4.2.2.1-SNAPSHOT
+ 4.2.2.2-SNAPSHOT
msa
org.thingsboard.msa
diff --git a/msa/vc-executor/pom.xml b/msa/vc-executor/pom.xml
index c476d1725c..a0c023b5f8 100644
--- a/msa/vc-executor/pom.xml
+++ b/msa/vc-executor/pom.xml
@@ -21,7 +21,7 @@
org.thingsboard
- 4.2.2.1-SNAPSHOT
+ 4.2.2.2-SNAPSHOT
msa
org.thingsboard.msa
diff --git a/msa/web-ui/docker/Dockerfile b/msa/web-ui/docker/Dockerfile
index 063374a478..b87885b45d 100644
--- a/msa/web-ui/docker/Dockerfile
+++ b/msa/web-ui/docker/Dockerfile
@@ -14,7 +14,7 @@
# limitations under the License.
#
-FROM thingsboard/node:22.18.0-bookworm-slim
+FROM thingsboard/node:22.22.2-bookworm-slim
ENV NODE_ENV production
ENV DOCKER_MODE true
diff --git a/msa/web-ui/package.json b/msa/web-ui/package.json
index 1a956617cd..c51aa6ed05 100644
--- a/msa/web-ui/package.json
+++ b/msa/web-ui/package.json
@@ -1,7 +1,7 @@
{
"name": "thingsboard-web-ui",
"private": true,
- "version": "4.2.2.1",
+ "version": "4.2.2.2",
"description": "ThingsBoard Web UI Microservice",
"main": "server.ts",
"bin": "server.js",
diff --git a/msa/web-ui/pom.xml b/msa/web-ui/pom.xml
index 2307818944..00c1703dcd 100644
--- a/msa/web-ui/pom.xml
+++ b/msa/web-ui/pom.xml
@@ -20,7 +20,7 @@
4.0.0
org.thingsboard
- 4.2.2.1-SNAPSHOT
+ 4.2.2.2-SNAPSHOT
msa
org.thingsboard.msa
@@ -79,7 +79,7 @@
install-node-and-yarn
- v22.18.0
+ v22.22.2
v1.22.22
diff --git a/msa/web-ui/yarn.lock b/msa/web-ui/yarn.lock
index 52dd2a03f3..f421a62684 100644
--- a/msa/web-ui/yarn.lock
+++ b/msa/web-ui/yarn.lock
@@ -379,9 +379,9 @@ body-parser@^2.2.0:
type-is "^2.0.0"
brace-expansion@^1.1.7:
- version "1.1.12"
- resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.12.tgz#ab9b454466e5a8cc3a187beaad580412a9c5b843"
- integrity sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==
+ version "1.1.13"
+ resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.13.tgz#d37875c01dc9eff988dd49d112a57cb67b54efe6"
+ integrity sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==
dependencies:
balanced-match "^1.0.0"
concat-map "0.0.1"
@@ -1256,9 +1256,9 @@ path-parse@^1.0.7:
integrity sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==
path-to-regexp@^8.0.0:
- version "8.2.0"
- resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-8.2.0.tgz#73990cc29e57a3ff2a0d914095156df5db79e8b4"
- integrity sha512-TdrF7fW9Rphjq4RjrW0Kp2AW0Ahwu9sRGTkS6bvDi0SCwZlEZYmcfDbEsTz8RVk0EHIS/Vd1bv3JhG+1xZuAyQ==
+ version "8.4.0"
+ resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-8.4.0.tgz#8e98fcd94826aff01a90c544ef74ffbaca3a78ed"
+ integrity sha512-PuseHIvAnz3bjrM2rGJtSgo1zjgxapTLZ7x2pjhzWwlp4SJQgK3f3iZIQwkpEnBaKz6seKBADpM4B4ySkuYypg==
picocolors@^1.1.0:
version "1.1.1"
@@ -1266,14 +1266,14 @@ picocolors@^1.1.0:
integrity sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==
picomatch@^2.0.4, picomatch@^2.2.1:
- version "2.3.1"
- resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42"
- integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==
+ version "2.3.2"
+ resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.2.tgz#5a942915e26b372dc0f0e6753149a16e6b1c5601"
+ integrity sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==
picomatch@^4.0.2:
- version "4.0.3"
- resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-4.0.3.tgz#796c76136d1eead715db1e7bad785dedd695a042"
- integrity sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==
+ version "4.0.4"
+ resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-4.0.4.tgz#fd6f5e00a143086e074dffe4c924b8fb293b0589"
+ integrity sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==
prebuild-install@^7.1.1:
version "7.1.3"
diff --git a/netty-mqtt/pom.xml b/netty-mqtt/pom.xml
index 615a0ed728..4b9b9fa21e 100644
--- a/netty-mqtt/pom.xml
+++ b/netty-mqtt/pom.xml
@@ -19,11 +19,11 @@
4.0.0
org.thingsboard
- 4.2.2.1-SNAPSHOT
+ 4.2.2.2-SNAPSHOT
thingsboard
netty-mqtt
- 4.2.2.1-SNAPSHOT
+ 4.2.2.2-SNAPSHOT
jar
Netty MQTT Client
diff --git a/pom.xml b/pom.xml
index 9dd86011be..5604ca39d7 100755
--- a/pom.xml
+++ b/pom.xml
@@ -20,7 +20,7 @@
4.0.0
org.thingsboard
thingsboard
- 4.2.2.1-SNAPSHOT
+ 4.2.2.2-SNAPSHOT
pom
Thingsboard
@@ -62,9 +62,7 @@
${project.name}
/var/log/${pkg.name}
/usr/share/${pkg.name}
- 3.5.12
- 2.21.1
- 4.1.132.Final
+ 3.5.13
2.4.0-b180830.0359
0.12.5
0.10
@@ -72,6 +70,7 @@
4.2.25
5.0.4
33.1.0-jre
+ 10.1.54
3.18.0
2.16.1
1.3.1
@@ -98,6 +97,7 @@
3.5.4
3.1.4
3.1.4
+ 3.5.0
2.8.8TB
2.2.30
0.8
@@ -916,6 +916,12 @@
**/resources/lwm2m/models/**
src/main/data/resources/**
.claude/**
+ **/lombok.config
+ **/eslint.config.mjs
+ **/config.monitoring
+ **/valkey-certs/**
+ **/data/certs/**
+ **/*.otf
JAVADOC_STYLE
@@ -968,6 +974,7 @@
org.apache.maven.plugins
maven-clean-plugin
+ ${maven-clean-plugin.version}
false
@@ -993,24 +1000,25 @@
-
+
- com.fasterxml.jackson
- jackson-bom
- ${jackson-bom.version}
- pom
- import
+ org.apache.tomcat.embed
+ tomcat-embed-core
+ ${tomcat.version}
-
-
- io.netty
- netty-bom
- ${netty.version}
- pom
- import
+ org.apache.tomcat.embed
+ tomcat-embed-el
+ ${tomcat.version}
+
+
+ org.apache.tomcat.embed
+ tomcat-embed-websocket
+ ${tomcat.version}
-
+
org.springframework.boot
spring-boot-dependencies
diff --git a/rest-client/pom.xml b/rest-client/pom.xml
index 787a28a497..d108c49cda 100644
--- a/rest-client/pom.xml
+++ b/rest-client/pom.xml
@@ -20,7 +20,7 @@
4.0.0
org.thingsboard
- 4.2.2.1-SNAPSHOT
+ 4.2.2.2-SNAPSHOT
thingsboard
rest-client
diff --git a/rule-engine/pom.xml b/rule-engine/pom.xml
index ff443deb61..de69670863 100644
--- a/rule-engine/pom.xml
+++ b/rule-engine/pom.xml
@@ -20,7 +20,7 @@
4.0.0
org.thingsboard
- 4.2.2.1-SNAPSHOT
+ 4.2.2.2-SNAPSHOT
thingsboard
rule-engine
diff --git a/rule-engine/rule-engine-api/pom.xml b/rule-engine/rule-engine-api/pom.xml
index bcc68f2049..3810c46a99 100644
--- a/rule-engine/rule-engine-api/pom.xml
+++ b/rule-engine/rule-engine-api/pom.xml
@@ -22,7 +22,7 @@
4.0.0
org.thingsboard
- 4.2.2.1-SNAPSHOT
+ 4.2.2.2-SNAPSHOT
rule-engine
org.thingsboard.rule-engine
diff --git a/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/TbContext.java b/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/TbContext.java
index dc27f79ee2..464c2df532 100644
--- a/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/TbContext.java
+++ b/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/TbContext.java
@@ -438,4 +438,10 @@ public interface TbContext {
MqttClientSettings getMqttClientSettings();
+ // Server-level safety caps for the HTTP client used by the REST API Call rule node (read from thingsboard.yml)
+
+ default TbHttpClientSettings getTbHttpClientSettings() {
+ return TbHttpClientSettings.DEFAULT;
+ }
+
}
diff --git a/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/TbHttpClientSettings.java b/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/TbHttpClientSettings.java
new file mode 100644
index 0000000000..658f82c0d7
--- /dev/null
+++ b/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/TbHttpClientSettings.java
@@ -0,0 +1,49 @@
+/**
+ * Copyright © 2016-2026 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.rule.engine.api;
+
+/**
+ * Server-level safety caps for the HTTP client used by the REST API Call rule node.
+ * Values are read from {@code thingsboard.yml} (or the corresponding environment variables)
+ * and applied as hard ceilings on top of the per-node tenant configuration.
+ * A value of {@code 0} means no system-level restriction.
+ */
+public interface TbHttpClientSettings {
+
+ /** System ceiling for {@code maxParallelRequestsCount}. 0 = no system limit. */
+ int getMaxParallelRequests();
+
+ /** System ceiling for the pending-request queue depth. 0 = no system limit. */
+ int getMaxPendingRequests();
+
+ /**
+ * Maximum number of TCP connections in the reactor-netty pool per node instance.
+ * 0 = use reactor-netty's default: {@code max(availableProcessors, 8) * 2}.
+ */
+ int getPoolMaxConnections();
+
+ TbHttpClientSettings DEFAULT = new TbHttpClientSettings() {
+ @Override
+ public int getMaxParallelRequests() { return 0; }
+
+ @Override
+ public int getMaxPendingRequests() { return 0; }
+
+ @Override
+ public int getPoolMaxConnections() { return 0; }
+ };
+
+}
diff --git a/rule-engine/rule-engine-components/pom.xml b/rule-engine/rule-engine-components/pom.xml
index c326b93b7c..a156bb22a2 100644
--- a/rule-engine/rule-engine-components/pom.xml
+++ b/rule-engine/rule-engine-components/pom.xml
@@ -22,7 +22,7 @@
4.0.0
org.thingsboard
- 4.2.2.1-SNAPSHOT
+ 4.2.2.2-SNAPSHOT
rule-engine
org.thingsboard.rule-engine
diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rest/TbHttpClient.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rest/TbHttpClient.java
index df9ce0194b..de1e73cb66 100644
--- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rest/TbHttpClient.java
+++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rest/TbHttpClient.java
@@ -33,6 +33,7 @@ import org.springframework.web.reactive.function.client.WebClientResponseExcepti
import org.springframework.web.util.UriComponentsBuilder;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.common.util.SsrfProtectionValidator;
+import org.thingsboard.rule.engine.api.TbHttpClientSettings;
import org.thingsboard.rule.engine.api.TbContext;
import org.thingsboard.rule.engine.api.TbNodeException;
import org.thingsboard.rule.engine.api.util.TbNodeUtils;
@@ -42,6 +43,7 @@ import org.thingsboard.rule.engine.credentials.CredentialsType;
import org.thingsboard.server.common.data.StringUtils;
import org.thingsboard.server.common.msg.TbMsg;
import org.thingsboard.server.common.msg.TbMsgMetaData;
+import reactor.core.scheduler.Schedulers;
import reactor.netty.http.client.HttpClient;
import reactor.netty.resources.ConnectionProvider;
import reactor.netty.transport.ProxyProvider;
@@ -53,8 +55,11 @@ import java.util.Base64;
import java.util.List;
import java.util.Map;
import java.util.Properties;
+import java.util.concurrent.BlockingQueue;
+import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.Semaphore;
import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicLong;
import java.util.function.BiConsumer;
import java.util.function.Consumer;
@@ -84,17 +89,48 @@ public class TbHttpClient {
public static final String MAX_IN_MEMORY_BUFFER_SIZE_IN_KB = "tb.http.maxInMemoryBufferSizeInKb";
+ private static final long ANOMALY_REPORT_INTERVAL_MS = 60_000;
+
private final TbRestApiCallNodeConfiguration config;
+ private final String tenantId;
+ private final String nodeId;
+ private final TbHttpClientSettings settings;
private EventLoopGroup eventLoopGroup;
private WebClient webClient;
private Semaphore semaphore;
+ private BlockingQueue pendingQueue;
+
+ private final AtomicLong dispatchedCount = new AtomicLong();
+ private final AtomicLong successCount = new AtomicLong();
+ private final AtomicLong failureCount = new AtomicLong();
+ private final AtomicLong droppedFullCount = new AtomicLong();
+ private final AtomicLong droppedStaleCount = new AtomicLong();
+ private volatile long lastAnomalyReportAt = 0;
+
+ private record PendingTask(
+ TbContext ctx,
+ TbMsg msg,
+ Consumer onSuccess,
+ BiConsumer onFailure,
+ long enqueuedNanos) {}
TbHttpClient(TbRestApiCallNodeConfiguration config, EventLoopGroup eventLoopGroupShared) throws TbNodeException {
+ this(config, eventLoopGroupShared, "n/a", "n/a", TbHttpClientSettings.DEFAULT);
+ }
+
+ TbHttpClient(TbRestApiCallNodeConfiguration config, EventLoopGroup eventLoopGroupShared,
+ String tenantId, String nodeId, TbHttpClientSettings settings) throws TbNodeException {
try {
this.config = config;
- if (config.getMaxParallelRequestsCount() > 0) {
- semaphore = new Semaphore(config.getMaxParallelRequestsCount());
+ this.tenantId = tenantId;
+ this.nodeId = nodeId;
+ this.settings = settings;
+ int effectiveParallel = effectiveMax(config.getMaxParallelRequestsCount(), settings.getMaxParallelRequests());
+ if (effectiveParallel > 0) {
+ semaphore = new Semaphore(effectiveParallel);
+ int effectivePending = effectiveMax(0, settings.getMaxPendingRequests());
+ pendingQueue = effectivePending > 0 ? new LinkedBlockingQueue<>(effectivePending) : new LinkedBlockingQueue<>();
}
ConnectionProvider connectionProvider = ConnectionProvider
@@ -155,16 +191,20 @@ public class TbHttpClient {
}
}
- private int getPoolMaxConnections() {
- String poolMaxConnectionsEnv = System.getenv("TB_RE_HTTP_CLIENT_POOL_MAX_CONNECTIONS");
+ /**
+ * Returns the effective limit: {@code min(userMax, systemMax)} when both are positive,
+ * {@code systemMax} when only the system ceiling is set, or {@code userMax} otherwise.
+ * A value of {@code 0} means unlimited.
+ */
+ private static int effectiveMax(int userMax, int systemMax) {
+ if (systemMax <= 0) return userMax;
+ if (userMax <= 0) return systemMax;
+ return Math.min(userMax, systemMax);
+ }
- int poolMaxConnections;
- if (poolMaxConnectionsEnv != null) {
- poolMaxConnections = Integer.parseInt(poolMaxConnectionsEnv);
- } else {
- poolMaxConnections = ConnectionProvider.DEFAULT_POOL_MAX_CONNECTIONS;
- }
- return poolMaxConnections;
+ private int getPoolMaxConnections() {
+ int configured = settings.getPoolMaxConnections();
+ return configured > 0 ? configured : ConnectionProvider.DEFAULT_POOL_MAX_CONNECTIONS;
}
private void validateMaxInMemoryBufferSize(TbRestApiCallNodeConfiguration config) throws TbNodeException {
@@ -207,54 +247,150 @@ public class TbHttpClient {
if (this.eventLoopGroup != null) {
this.eventLoopGroup.shutdownGracefully(0, 5, TimeUnit.SECONDS);
}
+ long full = droppedFullCount.get();
+ long stale = droppedStaleCount.get();
+ int availablePermits = semaphore != null ? semaphore.availablePermits() : -1;
+ if (full > 0 || stale > 0) {
+ log.warn("[{}][{}] REST API call node destroyed with anomalies: " +
+ "droppedQueueFull={}, droppedStale={}, dispatched={}, success={}, failure={}, semaphorePermits={}.",
+ tenantId, nodeId, full, stale,
+ dispatchedCount.get(), successCount.get(), failureCount.get(), availablePermits);
+ } else {
+ log.debug("[{}][{}] REST API call node destroyed. dispatched={}, success={}, failure={}, semaphorePermits={}.",
+ tenantId, nodeId, dispatchedCount.get(), successCount.get(), failureCount.get(), availablePermits);
+ }
}
public void processMessage(TbContext ctx, TbMsg msg,
Consumer onSuccess,
BiConsumer onFailure) {
- try {
- if (semaphore != null && !semaphore.tryAcquire(config.getReadTimeoutMs(), TimeUnit.MILLISECONDS)) {
- onFailure.accept(msg, new RuntimeException("Timeout during waiting for reply!"));
- return;
+ if (semaphore == null) {
+ doHttpCall(new PendingTask(ctx, msg, onSuccess, onFailure, 0L));
+ return;
+ }
+ if (!pendingQueue.offer(new PendingTask(ctx, msg, onSuccess, onFailure, System.nanoTime()))) {
+ droppedFullCount.incrementAndGet();
+ log.debug("[{}][{}] REST API call queue full, dropping msg {}.", tenantId, nodeId, msg.getId());
+ maybeReportAnomalies();
+ onFailure.accept(msg, new RuntimeException("Max pending requests limit exceeded!"));
+ return;
+ }
+ tryProcess();
+ }
+
+ /**
+ * Tries to acquire one concurrency slot and fire the next queued task.
+ * Stale messages (whose message pack has expired) are silently dropped.
+ * Safe to call from any thread under high concurrency.
+ */
+ private void tryProcess() {
+ while (true) {
+ if (!semaphore.tryAcquire()) {
+ return; // all slots are in use; a callback will call tryProcess() when one frees up
+ }
+ PendingTask next = pendingQueue.poll();
+ if (next == null) {
+ semaphore.release();
+ return; // queue is empty; slot released
}
+ if (!next.msg().isValid()) {
+ semaphore.release();
+ droppedStaleCount.incrementAndGet();
+ log.debug("[{}][{}] Dropping stale msg {} from REST API call queue (queueDepth={}).",
+ tenantId, nodeId, next.msg().getId(), pendingQueue.size());
+ next.onFailure().accept(next.msg(), new RuntimeException("Message is no longer valid. Dropped from queue."));
+ maybeReportAnomalies();
+ continue; // slot released — loop to check if there's a valid next item
+ }
+ dispatchedCount.incrementAndGet();
+ if (doHttpCall(next)) {
+ return; // async HTTP call started — its callback will call tryProcess()
+ }
+ // synchronous failure — semaphore already released in doHttpCall, loop to try next task
+ }
+ }
- String endpointUrl = TbNodeUtils.processPattern(config.getRestEndpointUrlPattern(), msg);
+ private void maybeReportAnomalies() {
+ long now = System.currentTimeMillis();
+ if (now - lastAnomalyReportAt < ANOMALY_REPORT_INTERVAL_MS) {
+ return;
+ }
+ lastAnomalyReportAt = now;
+ int queueSize = pendingQueue != null ? pendingQueue.size() : 0;
+ int queueRemaining = pendingQueue != null ? pendingQueue.remainingCapacity() : Integer.MAX_VALUE;
+ int availablePermits = semaphore != null ? semaphore.availablePermits() : -1;
+ log.warn("[{}][{}] REST API call node anomalies: droppedQueueFull={}, droppedStale={} " +
+ "(dispatched={}, success={}, failure={}, queueDepth={}, queueRemaining={}, semaphorePermits={}).",
+ tenantId, nodeId,
+ droppedFullCount.get(), droppedStaleCount.get(),
+ dispatchedCount.get(), successCount.get(), failureCount.get(),
+ queueSize, queueRemaining, availablePermits);
+ }
+
+ /**
+ * Initiates an async HTTP call for the given task.
+ *
+ * @return {@code true} if the async subscription was started and the semaphore slot is now
+ * owned by the callback (which will release it and call {@link #tryProcess()}).
+ * {@code false} if a synchronous exception occurred before the subscription was
+ * registered; the semaphore slot has already been released and the caller should
+ * loop rather than recurse to avoid stack overflow when many queued tasks fail
+ * synchronously (e.g. misconfigured URL pattern).
+ */
+ private boolean doHttpCall(PendingTask task) {
+ boolean asyncStarted = false;
+ try {
+ String endpointUrl = TbNodeUtils.processPattern(config.getRestEndpointUrlPattern(), task.msg());
HttpMethod method = HttpMethod.valueOf(config.getRequestMethod());
URI uri = buildEncodedUri(endpointUrl);
- RequestBodySpec request = webClient
+ RequestBodySpec req = webClient
.method(method)
.uri(uri)
- .headers(headers -> prepareHeaders(headers, msg));
+ .headers(headers -> prepareHeaders(headers, task.msg()));
if ((HttpMethod.POST.equals(method) || HttpMethod.PUT.equals(method) ||
HttpMethod.PATCH.equals(method) || HttpMethod.DELETE.equals(method)) &&
!config.isIgnoreRequestBody()) {
- request.body(BodyInserters.fromValue(getData(msg, config.isParseToPlainText())));
+ req.body(BodyInserters.fromValue(getData(task.msg(), config.isParseToPlainText())));
}
- request
- .retrieve()
+ req.retrieve()
.toEntity(String.class)
- .subscribe(responseEntity -> {
+ .publishOn(Schedulers.fromExecutor(task.ctx().getExternalCallExecutor()))
+ .doFinally(signalType -> {
+ // Runs exactly once after onComplete, onError, or cancel — the only
+ // place that releases the permit for the async path.
if (semaphore != null) {
semaphore.release();
+ tryProcess();
}
-
+ })
+ .subscribe(responseEntity -> {
if (responseEntity.getStatusCode().is2xxSuccessful()) {
- onSuccess.accept(processResponse(ctx, msg, responseEntity));
+ successCount.incrementAndGet();
+ task.onSuccess().accept(processResponse(task.ctx(), task.msg(), responseEntity));
} else {
- onFailure.accept(processFailureResponse(msg, responseEntity), null);
+ failureCount.incrementAndGet();
+ task.onFailure().accept(processFailureResponse(task.msg(), responseEntity), null);
}
}, throwable -> {
- if (semaphore != null) {
- semaphore.release();
- }
-
- onFailure.accept(processException(msg, throwable), processThrowable(throwable));
+ failureCount.incrementAndGet();
+ task.onFailure().accept(processException(task.msg(), throwable), processThrowable(throwable));
});
- } catch (InterruptedException e) {
- log.warn("Timeout during waiting for reply!", e);
+ asyncStarted = true;
+ return true;
+ } catch (Exception e) {
+ failureCount.incrementAndGet();
+ task.onFailure().accept(processException(task.msg(), e), processThrowable(e));
+ return false;
+ } finally {
+ // Synchronous permit release: only when the async pipeline was never registered
+ // (asyncStarted=false). If it was, doFinally owns the release.
+ // tryProcess() is intentionally not called here — the caller loops iteratively.
+ if (!asyncStarted && semaphore != null) {
+ semaphore.release();
+ }
}
}
diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rest/TbRestApiCallNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rest/TbRestApiCallNode.java
index 0217edf5e0..02ed33138d 100644
--- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rest/TbRestApiCallNode.java
+++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rest/TbRestApiCallNode.java
@@ -17,6 +17,7 @@ package org.thingsboard.rule.engine.rest;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
+import org.thingsboard.rule.engine.api.TbHttpClientSettings;
import org.thingsboard.rule.engine.api.RuleNode;
import org.thingsboard.rule.engine.api.TbContext;
import org.thingsboard.rule.engine.api.TbNodeConfiguration;
@@ -58,7 +59,11 @@ public class TbRestApiCallNode extends TbAbstractExternalNode {
public void init(TbContext ctx, TbNodeConfiguration configuration) throws TbNodeException {
super.init(ctx);
TbRestApiCallNodeConfiguration config = TbNodeUtils.convert(configuration, TbRestApiCallNodeConfiguration.class);
- httpClient = new TbHttpClient(config, ctx.getSharedEventLoop());
+ TbHttpClientSettings httpClientSettings = ctx.getTbHttpClientSettings();
+ httpClient = new TbHttpClient(config, ctx.getSharedEventLoop(),
+ ctx.getTenantId() != null ? ctx.getTenantId().getId().toString() : "n/a",
+ ctx.getSelfId() != null ? ctx.getSelfId().getId().toString() : "n/a",
+ httpClientSettings != null ? httpClientSettings : TbHttpClientSettings.DEFAULT);
}
@Override
diff --git a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/rest/TbHttpClientTest.java b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/rest/TbHttpClientTest.java
index a33d25b038..8b31c5a5ac 100644
--- a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/rest/TbHttpClientTest.java
+++ b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/rest/TbHttpClientTest.java
@@ -27,6 +27,7 @@ import org.mockito.ArgumentCaptor;
import org.mockito.Mockito;
import org.mockserver.integration.ClientAndServer;
import org.springframework.util.LinkedMultiValueMap;
+import org.thingsboard.common.util.DirectListeningExecutor;
import org.thingsboard.rule.engine.api.TbContext;
import org.thingsboard.server.common.data.id.DeviceId;
import org.thingsboard.server.common.data.id.EntityId;
@@ -156,6 +157,7 @@ public class TbHttpClientTest {
.build();
var ctx = mock(TbContext.class);
+ when(ctx.getExternalCallExecutor()).thenReturn(DirectListeningExecutor.INSTANCE);
when(ctx.transformMsg(
eq(msg),
eq(msg.getMetaData()),
diff --git a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/rest/TbRestApiCallNodeTest.java b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/rest/TbRestApiCallNodeTest.java
index a93417275f..cc2daa2421 100644
--- a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/rest/TbRestApiCallNodeTest.java
+++ b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/rest/TbRestApiCallNodeTest.java
@@ -27,13 +27,18 @@ import org.apache.http.protocol.HttpRequestHandler;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
+import org.junit.jupiter.api.parallel.ResourceLock;
+import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.ValueSource;
import org.mockito.ArgumentCaptor;
import org.mockito.Mock;
import org.mockito.Spy;
import org.mockito.junit.jupiter.MockitoExtension;
+import org.thingsboard.common.util.DirectListeningExecutor;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.rule.engine.AbstractRuleNodeUpgradeTest;
+import org.thingsboard.rule.engine.api.TbHttpClientSettings;
import org.thingsboard.rule.engine.api.TbContext;
import org.thingsboard.rule.engine.api.TbNode;
import org.thingsboard.rule.engine.api.TbNodeConfiguration;
@@ -43,6 +48,7 @@ import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.RuleChainId;
import org.thingsboard.server.common.data.id.RuleNodeId;
import org.thingsboard.server.common.data.msg.TbMsgType;
+import org.thingsboard.server.common.data.msg.TbNodeConnectionType;
import org.thingsboard.server.common.msg.TbMsg;
import org.thingsboard.server.common.msg.TbMsgDataType;
import org.thingsboard.server.common.msg.TbMsgMetaData;
@@ -53,15 +59,23 @@ import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.stream.Stream;
+import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotSame;
import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.BDDMockito.given;
+import static org.mockito.Mockito.never;
import static org.mockito.Mockito.timeout;
import static org.mockito.Mockito.verify;
@ExtendWith(MockitoExtension.class)
+@ResourceLock("SsrfProtectionValidator") // to avoid race conditions when modifying SsrfProtectionValidator's static configuration
public class TbRestApiCallNodeTest extends AbstractRuleNodeUpgradeTest {
+ static final long TIMEOUT = TimeUnit.SECONDS.toMillis(30);
+
@Spy
private TbRestApiCallNode restNode;
@@ -125,6 +139,8 @@ public class TbRestApiCallNodeTest extends AbstractRuleNodeUpgradeTest {
}
});
+ given(ctx.getExternalCallExecutor()).willReturn(DirectListeningExecutor.INSTANCE);
+
TbRestApiCallNodeConfiguration config = new TbRestApiCallNodeConfiguration().defaultConfiguration();
config.setRequestMethod("DELETE");
config.setHeaders(Collections.singletonMap("Foo", "Bar"));
@@ -148,7 +164,7 @@ public class TbRestApiCallNodeTest extends AbstractRuleNodeUpgradeTest {
ArgumentCaptor msgCaptor = ArgumentCaptor.forClass(TbMsg.class);
ArgumentCaptor metadataCaptor = ArgumentCaptor.forClass(TbMsgMetaData.class);
ArgumentCaptor dataCaptor = ArgumentCaptor.forClass(String.class);
- verify(ctx, timeout(10_000)).transformMsg(msgCaptor.capture(), metadataCaptor.capture(), dataCaptor.capture());
+ verify(ctx, timeout(TIMEOUT)).transformMsg(msgCaptor.capture(), metadataCaptor.capture(), dataCaptor.capture());
assertNotSame(metaData, metadataCaptor.getValue());
assertEquals(TbMsg.EMPTY_JSON_OBJECT, dataCaptor.getValue());
@@ -183,6 +199,8 @@ public class TbRestApiCallNodeTest extends AbstractRuleNodeUpgradeTest {
}
});
+ given(ctx.getExternalCallExecutor()).willReturn(DirectListeningExecutor.INSTANCE);
+
TbRestApiCallNodeConfiguration config = new TbRestApiCallNodeConfiguration().defaultConfiguration();
config.setRequestMethod("DELETE");
config.setHeaders(Collections.singletonMap("Foo", "Bar"));
@@ -206,12 +224,143 @@ public class TbRestApiCallNodeTest extends AbstractRuleNodeUpgradeTest {
ArgumentCaptor msgCaptor = ArgumentCaptor.forClass(TbMsg.class);
ArgumentCaptor metadataCaptor = ArgumentCaptor.forClass(TbMsgMetaData.class);
ArgumentCaptor dataCaptor = ArgumentCaptor.forClass(String.class);
- verify(ctx, timeout(10_000)).transformMsg(msgCaptor.capture(), metadataCaptor.capture(), dataCaptor.capture());
+ verify(ctx, timeout(TIMEOUT)).transformMsg(msgCaptor.capture(), metadataCaptor.capture(), dataCaptor.capture());
assertNotSame(metaData, metadataCaptor.getValue());
assertEquals(TbMsg.EMPTY_JSON_OBJECT, dataCaptor.getValue());
}
+ @Test
+ public void givenForceAckTrue_whenOnMsgAndServerReturns200_thenAckedImmediatelyAndEnqueuedForTellNext() throws IOException {
+ final String path = "/path/to/get";
+ setupServer("*", new HttpRequestHandler() {
+ @Override
+ public void handle(HttpRequest request, HttpResponse response, HttpContext context)
+ throws HttpException, IOException {
+ response.setStatusCode(200);
+ }
+ });
+
+ TbMsg transformedMsg = TbMsg.newMsg()
+ .type(TbMsgType.POST_TELEMETRY_REQUEST)
+ .originator(originator)
+ .copyMetaData(metaData)
+ .dataType(TbMsgDataType.JSON)
+ .data(TbMsg.EMPTY_JSON_OBJECT)
+ .ruleChainId(ruleChainId)
+ .ruleNodeId(ruleNodeId)
+ .build();
+
+ given(ctx.isExternalNodeForceAck()).willReturn(true);
+ given(ctx.getExternalCallExecutor()).willReturn(DirectListeningExecutor.INSTANCE);
+ given(ctx.transformMsg(any(), any(), any())).willReturn(transformedMsg);
+
+ TbRestApiCallNodeConfiguration config = new TbRestApiCallNodeConfiguration().defaultConfiguration();
+ config.setRequestMethod("GET");
+ config.setIgnoreRequestBody(true);
+ config.setRestEndpointUrlPattern(String.format("http://localhost:%d%s", server.getLocalPort(), path));
+ initWithConfig(config);
+
+ TbMsg msg = TbMsg.newMsg()
+ .type(TbMsgType.POST_TELEMETRY_REQUEST)
+ .originator(originator)
+ .copyMetaData(metaData)
+ .dataType(TbMsgDataType.JSON)
+ .data(TbMsg.EMPTY_JSON_OBJECT)
+ .ruleChainId(ruleChainId)
+ .ruleNodeId(ruleNodeId)
+ .build();
+ restNode.onMsg(ctx, msg);
+
+ verify(ctx).ack(msg);
+ verify(ctx, timeout(TIMEOUT)).enqueueForTellNext(any(), eq(TbNodeConnectionType.SUCCESS));
+ verify(ctx, never()).tellSuccess(any());
+ }
+
+ @ParameterizedTest
+ @ValueSource(booleans = {true, false})
+ public void givenMaxParallelRequestsCountAndBadUrl_whenOnMsg_thenSemaphoreIsReleasedAndFailureReported(boolean forceAck) throws IOException {
+ given(ctx.isExternalNodeForceAck()).willReturn(forceAck);
+
+ TbRestApiCallNodeConfiguration config = new TbRestApiCallNodeConfiguration().defaultConfiguration();
+ config.setMaxParallelRequestsCount(1);
+ config.setRestEndpointUrlPattern("");
+ initWithConfig(config);
+
+ TbMsg msg = TbMsg.newMsg()
+ .type(TbMsgType.POST_TELEMETRY_REQUEST)
+ .originator(originator)
+ .copyMetaData(metaData)
+ .dataType(TbMsgDataType.JSON)
+ .data(TbMsg.EMPTY_JSON_OBJECT)
+ .ruleChainId(ruleChainId)
+ .ruleNodeId(ruleNodeId)
+ .build();
+ restNode.onMsg(ctx, msg);
+
+ assertThat(restNode.httpClient.getSemaphore().availablePermits()).isEqualTo(1);
+ if (forceAck) {
+ verify(ctx).enqueueForTellFailure(any(), any(Throwable.class));
+ } else {
+ verify(ctx).tellFailure(any(), any());
+ }
+ }
+
+ @Test
+ public void givenMaxPendingRequestsExceeded_whenOnMsg_thenFailsImmediatelyAndQueuedRequestFiresAfterSlotOpens() throws IOException, InterruptedException {
+ CountDownLatch releaseResponse = new CountDownLatch(1);
+ setupServer("*", (request, response, context) -> {
+ try {
+ releaseResponse.await();
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ }
+ response.setStatusCode(200);
+ });
+
+ given(ctx.isExternalNodeForceAck()).willReturn(false);
+ given(ctx.getExternalCallExecutor()).willReturn(DirectListeningExecutor.INSTANCE);
+ // Simulate server-level cap: maxPendingRequests=1 via TbHttpClientSettings
+ given(ctx.getTbHttpClientSettings()).willReturn(new TbHttpClientSettings() {
+ @Override public int getMaxParallelRequests() { return 0; }
+ @Override public int getMaxPendingRequests() { return 1; }
+ @Override public int getPoolMaxConnections() { return 0; }
+ });
+ TbMsg transformedMsg = TbMsg.newMsg()
+ .type(TbMsgType.POST_TELEMETRY_REQUEST)
+ .originator(originator)
+ .copyMetaData(metaData)
+ .data(TbMsg.EMPTY_JSON_OBJECT)
+ .build();
+ given(ctx.transformMsg(any(), any(), any())).willReturn(transformedMsg);
+
+ TbRestApiCallNodeConfiguration config = new TbRestApiCallNodeConfiguration().defaultConfiguration();
+ config.setMaxParallelRequestsCount(1);
+ config.setRequestMethod("GET");
+ config.setIgnoreRequestBody(true);
+ config.setRestEndpointUrlPattern(String.format("http://localhost:%d/path", server.getLocalPort()));
+ initWithConfig(config);
+
+ TbMsg msg1 = TbMsg.newMsg().type(TbMsgType.POST_TELEMETRY_REQUEST).originator(originator)
+ .copyMetaData(metaData).dataType(TbMsgDataType.JSON).data(TbMsg.EMPTY_JSON_OBJECT)
+ .ruleChainId(ruleChainId).ruleNodeId(ruleNodeId).build();
+ TbMsg msg2 = TbMsg.newMsg().type(TbMsgType.POST_TELEMETRY_REQUEST).originator(originator)
+ .copyMetaData(metaData).dataType(TbMsgDataType.JSON).data(TbMsg.EMPTY_JSON_OBJECT)
+ .ruleChainId(ruleChainId).ruleNodeId(ruleNodeId).build();
+ TbMsg msg3 = TbMsg.newMsg().type(TbMsgType.POST_TELEMETRY_REQUEST).originator(originator)
+ .copyMetaData(metaData).dataType(TbMsgDataType.JSON).data(TbMsg.EMPTY_JSON_OBJECT)
+ .ruleChainId(ruleChainId).ruleNodeId(ruleNodeId).build();
+
+ restNode.onMsg(ctx, msg1); // fires immediately (semaphore acquired)
+ restNode.onMsg(ctx, msg2); // queues (semaphore exhausted, queue has room)
+ restNode.onMsg(ctx, msg3); // fails immediately (queue full — server-level maxPendingRequests=1)
+
+ verify(ctx, timeout(TIMEOUT)).tellFailure(any(), any());
+
+ releaseResponse.countDown();
+ verify(ctx, timeout(TIMEOUT).times(2)).tellSuccess(any());
+ }
+
private static Stream givenFromVersionAndConfig_whenUpgrade_thenVerifyHasChangesAndConfig() {
return Stream.of(
// config for version 2 with upgrade from version 0
diff --git a/tools/pom.xml b/tools/pom.xml
index 339be85998..46c56c634b 100644
--- a/tools/pom.xml
+++ b/tools/pom.xml
@@ -20,7 +20,7 @@
4.0.0
org.thingsboard
- 4.2.2.1-SNAPSHOT
+ 4.2.2.2-SNAPSHOT
thingsboard
tools
@@ -54,6 +54,24 @@
org.apache.cassandra
cassandra-all
+
+
+
+ org.perfkit.sjk.parsers
+ sjk-jfr5
+
+
+ org.perfkit.sjk.parsers
+ sjk-jfr6
+
+
+ org.perfkit.sjk.parsers
+ sjk-nps
+
+
at.yawk.lz4
diff --git a/transport/coap/pom.xml b/transport/coap/pom.xml
index 6e67ff417e..723f9a1dd2 100644
--- a/transport/coap/pom.xml
+++ b/transport/coap/pom.xml
@@ -20,7 +20,7 @@
4.0.0
org.thingsboard
- 4.2.2.1-SNAPSHOT
+ 4.2.2.2-SNAPSHOT
transport
org.thingsboard.transport
diff --git a/transport/http/pom.xml b/transport/http/pom.xml
index 39b57240fb..d4e19705c9 100644
--- a/transport/http/pom.xml
+++ b/transport/http/pom.xml
@@ -20,7 +20,7 @@
4.0.0
org.thingsboard
- 4.2.2.1-SNAPSHOT
+ 4.2.2.2-SNAPSHOT
transport
org.thingsboard.transport
diff --git a/transport/lwm2m/pom.xml b/transport/lwm2m/pom.xml
index ebe1e6bcca..fac60f7edc 100644
--- a/transport/lwm2m/pom.xml
+++ b/transport/lwm2m/pom.xml
@@ -20,7 +20,7 @@
4.0.0
org.thingsboard
- 4.2.2.1-SNAPSHOT
+ 4.2.2.2-SNAPSHOT
transport
org.thingsboard.transport
diff --git a/transport/mqtt/pom.xml b/transport/mqtt/pom.xml
index 856df50aa0..7b61c03200 100644
--- a/transport/mqtt/pom.xml
+++ b/transport/mqtt/pom.xml
@@ -20,7 +20,7 @@
4.0.0
org.thingsboard
- 4.2.2.1-SNAPSHOT
+ 4.2.2.2-SNAPSHOT
transport
org.thingsboard.transport
diff --git a/transport/pom.xml b/transport/pom.xml
index 96ddd89a8f..3645cf7d73 100644
--- a/transport/pom.xml
+++ b/transport/pom.xml
@@ -20,7 +20,7 @@
4.0.0
org.thingsboard
- 4.2.2.1-SNAPSHOT
+ 4.2.2.2-SNAPSHOT
thingsboard
transport
diff --git a/transport/snmp/pom.xml b/transport/snmp/pom.xml
index f57e99b113..683c0e3e2d 100644
--- a/transport/snmp/pom.xml
+++ b/transport/snmp/pom.xml
@@ -21,7 +21,7 @@
org.thingsboard
- 4.2.2.1-SNAPSHOT
+ 4.2.2.2-SNAPSHOT
transport
diff --git a/ui-ngx/package.json b/ui-ngx/package.json
index d479a75a5d..b88a0583e2 100644
--- a/ui-ngx/package.json
+++ b/ui-ngx/package.json
@@ -1,6 +1,6 @@
{
"name": "thingsboard",
- "version": "4.2.2.1",
+ "version": "4.2.2.2",
"scripts": {
"ng": "ng",
"start": "node --max_old_space_size=8048 ./node_modules/@angular/cli/bin/ng serve --configuration development --host 0.0.0.0 --open",
@@ -94,11 +94,11 @@
},
"devDependencies": {
"@angular-builders/custom-esbuild": "20.0.0",
- "@angular-devkit/build-angular": "20.3.20",
- "@angular-devkit/core": "20.3.20",
- "@angular-devkit/schematics": "20.3.20",
- "@angular/build": "20.3.20",
- "@angular/cli": "20.3.20",
+ "@angular-devkit/build-angular": "20.3.22",
+ "@angular-devkit/core": "20.3.22",
+ "@angular-devkit/schematics": "20.3.22",
+ "@angular/build": "20.3.22",
+ "@angular/cli": "20.3.22",
"@angular/compiler-cli": "20.3.18",
"@angular/language-service": "20.3.18",
"@types/ace-diff": "^2.1.4",
diff --git a/ui-ngx/patches/@angular+build+20.3.20.patch b/ui-ngx/patches/@angular+build+20.3.22.patch
similarity index 100%
rename from ui-ngx/patches/@angular+build+20.3.20.patch
rename to ui-ngx/patches/@angular+build+20.3.22.patch
diff --git a/ui-ngx/pom.xml b/ui-ngx/pom.xml
index 7cbd38e648..cc58f728a0 100644
--- a/ui-ngx/pom.xml
+++ b/ui-ngx/pom.xml
@@ -20,7 +20,7 @@
4.0.0
org.thingsboard
- 4.2.2.1-SNAPSHOT
+ 4.2.2.2-SNAPSHOT
thingsboard
org.thingsboard
@@ -56,7 +56,7 @@
install-node-and-yarn
- v22.18.0
+ v22.22.2
v1.22.22
diff --git a/ui-ngx/src/app/modules/common/modules-map.ts b/ui-ngx/src/app/modules/common/modules-map.ts
index 55014406d6..f8ce277f2a 100644
--- a/ui-ngx/src/app/modules/common/modules-map.ts
+++ b/ui-ngx/src/app/modules/common/modules-map.ts
@@ -16,6 +16,7 @@
import * as AngularAnimations from '@angular/animations';
import * as AngularCore from '@angular/core';
+import * as AngularCoreRxjsInterop from '@angular/core/rxjs-interop';
import * as AngularCommon from '@angular/common';
import * as AngularForms from '@angular/forms';
import * as AngularPlatformBrowser from '@angular/platform-browser';
@@ -350,6 +351,7 @@ class ModulesMap implements IModulesMap {
private modulesMap: {[key: string]: any} = {
'@angular/animations': AngularAnimations,
'@angular/core': this.angularCoreModule20to18Patch(AngularCore),
+ '@angular/core/rxjs-interop': AngularCoreRxjsInterop,
'@angular/common': AngularCommon,
'@angular/common/http': HttpClientModule,
'@angular/forms': AngularForms,
diff --git a/ui-ngx/src/app/modules/home/components/alarm/alarm-assignee-panel.component.html b/ui-ngx/src/app/modules/home/components/alarm/alarm-assignee-panel.component.html
index c0f983e6d0..fbbce2a469 100644
--- a/ui-ngx/src/app/modules/home/components/alarm/alarm-assignee-panel.component.html
+++ b/ui-ngx/src/app/modules/home/components/alarm/alarm-assignee-panel.component.html
@@ -42,7 +42,7 @@
-
+
diff --git a/ui-ngx/src/app/modules/home/components/alarm/alarm-assignee.component.scss b/ui-ngx/src/app/modules/home/components/alarm/alarm-assignee.component.scss
index c8de20ad36..1aeded1767 100644
--- a/ui-ngx/src/app/modules/home/components/alarm/alarm-assignee.component.scss
+++ b/ui-ngx/src/app/modules/home/components/alarm/alarm-assignee.component.scss
@@ -40,7 +40,11 @@
margin-left: 12px;
margin-right: 20px;
&.inline-icon {
- margin-left: 0;
+ margin-left: 8px;
margin-right: 8px;
+ padding: 6px 0 !important;
+ }
+ &.mat-icon {
+ font-size: 28px !important;
}
}
diff --git a/ui-ngx/src/app/modules/home/components/attribute/attribute-table.component.html b/ui-ngx/src/app/modules/home/components/attribute/attribute-table.component.html
index 6d758b8c7b..c7d204884b 100644
--- a/ui-ngx/src/app/modules/home/components/attribute/attribute-table.component.html
+++ b/ui-ngx/src/app/modules/home/components/attribute/attribute-table.component.html
@@ -34,7 +34,7 @@
+ @if (!inlineField && subscriptSizing === 'dynamic') {
+
+ }
{{ 'entity.type-required' | translate }}
diff --git a/ui-ngx/src/app/shared/components/entity/entity-type-select.component.ts b/ui-ngx/src/app/shared/components/entity/entity-type-select.component.ts
index 4e6d6d4347..ea24c373d6 100644
--- a/ui-ngx/src/app/shared/components/entity/entity-type-select.component.ts
+++ b/ui-ngx/src/app/shared/components/entity/entity-type-select.component.ts
@@ -21,7 +21,7 @@ import { AliasEntityType, EntityType, entityTypeTranslations } from '@app/shared
import { EntityService } from '@core/http/entity.service';
import { coerceBoolean } from '@shared/decorators/coercion';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
-import { MatFormFieldAppearance } from '@angular/material/form-field';
+import { MatFormFieldAppearance, SubscriptSizing } from '@angular/material/form-field';
@Component({
selector: 'tb-entity-type-select',
@@ -69,6 +69,9 @@ export class EntityTypeSelectComponent implements ControlValueAccessor, OnInit,
@Input()
appearance: MatFormFieldAppearance = 'fill';
+ @Input()
+ subscriptSizing: SubscriptSizing = 'fixed';
+
@Input()
@coerceBoolean()
inlineField: boolean;
diff --git a/ui-ngx/src/app/shared/models/ai-model.models.ts b/ui-ngx/src/app/shared/models/ai-model.models.ts
index c861b33225..504e4d2659 100644
--- a/ui-ngx/src/app/shared/models/ai-model.models.ts
+++ b/ui-ngx/src/app/shared/models/ai-model.models.ts
@@ -110,6 +110,10 @@ export const AiModelMap = new Map= 0.2000.0 < 0.2100.0", "@angular-devkit/architect@>=0.2000.0 < 0.2100.0":
- version "0.2003.20"
- resolved "https://registry.yarnpkg.com/@angular-devkit/architect/-/architect-0.2003.20.tgz#152edd3078be72b72c814fc625222e5ff5c90916"
- integrity sha512-1g7q37Aq4dvDdQDW0PtWXfiX5hBV78K74QUtFkAXGIXU3DkguwOQaqHILCnIRAVr0wFlWDckWVuO5OT6Cl9HeQ==
+"@angular-devkit/architect@0.2003.22", "@angular-devkit/architect@>= 0.2000.0 < 0.2100.0", "@angular-devkit/architect@>=0.2000.0 < 0.2100.0":
+ version "0.2003.22"
+ resolved "https://registry.yarnpkg.com/@angular-devkit/architect/-/architect-0.2003.22.tgz#cb660579890be1d0622339bc74a85b6f1697ae5f"
+ integrity sha512-gxVOslVweD+Co6gpRVlByHus/3HVAnsl99MobS9PBh8vh2g6bJ011PBgl0TKsP/pqBGawZOkJXYrRPeMKnobYA==
dependencies:
- "@angular-devkit/core" "20.3.20"
+ "@angular-devkit/core" "20.3.22"
rxjs "7.8.2"
-"@angular-devkit/build-angular@20.3.20":
- version "20.3.20"
- resolved "https://registry.yarnpkg.com/@angular-devkit/build-angular/-/build-angular-20.3.20.tgz#452fdb36b0f931086107ee0c3af34c7882017037"
- integrity sha512-ihBD8zsRoMsuwS3KkwqwwlJGw5jcLerG8zy1kdSEsdJwIYjITgMdV+iK02uSWptLTDd3ILOdgcoZzipcEwJX4w==
+"@angular-devkit/build-angular@20.3.22":
+ version "20.3.22"
+ resolved "https://registry.yarnpkg.com/@angular-devkit/build-angular/-/build-angular-20.3.22.tgz#a96ba0d41a708ed3402f2568db343b1e85fe0e4e"
+ integrity sha512-PnKIRue/j30sWLWC9q2T3WwE5+GJekDX/zCXTYgjW6u0y/kt+7G/sR0uxXGkX2NwSVGnIz0ucVOBsbSl9PKd6A==
dependencies:
"@ampproject/remapping" "2.3.0"
- "@angular-devkit/architect" "0.2003.20"
- "@angular-devkit/build-webpack" "0.2003.20"
- "@angular-devkit/core" "20.3.20"
- "@angular/build" "20.3.20"
+ "@angular-devkit/architect" "0.2003.22"
+ "@angular-devkit/build-webpack" "0.2003.22"
+ "@angular-devkit/core" "20.3.22"
+ "@angular/build" "20.3.22"
"@babel/core" "7.28.3"
"@babel/generator" "7.28.3"
"@babel/helper-annotate-as-pure" "7.27.3"
@@ -188,7 +188,7 @@
"@babel/preset-env" "7.28.3"
"@babel/runtime" "7.28.3"
"@discoveryjs/json-ext" "0.6.3"
- "@ngtools/webpack" "20.3.20"
+ "@ngtools/webpack" "20.3.22"
ansi-colors "4.1.3"
autoprefixer "10.4.21"
babel-loader "10.0.0"
@@ -208,7 +208,7 @@
mini-css-extract-plugin "2.9.4"
open "10.2.0"
ora "8.2.0"
- picomatch "4.0.3"
+ picomatch "4.0.4"
piscina "5.1.3"
postcss "8.5.6"
postcss-loader "8.1.1"
@@ -230,32 +230,32 @@
optionalDependencies:
esbuild "0.25.9"
-"@angular-devkit/build-webpack@0.2003.20":
- version "0.2003.20"
- resolved "https://registry.yarnpkg.com/@angular-devkit/build-webpack/-/build-webpack-0.2003.20.tgz#e6fc4b16b448f2c45b46611ed602a9c34be7d67d"
- integrity sha512-WLeYLFxRnEYwCrPUx8qciWs+PrJKDp71ZsIRqeFCcpB8qFQOgR/CjPZYxZFqAnuTJtM4MqCtczvmn57ulKTPcA==
+"@angular-devkit/build-webpack@0.2003.22":
+ version "0.2003.22"
+ resolved "https://registry.yarnpkg.com/@angular-devkit/build-webpack/-/build-webpack-0.2003.22.tgz#9769c91cfd0203b1136cdd6b6ed4d205e17da70b"
+ integrity sha512-ad4iW5CDGDYFXR9ZpdY8O2n4mx+tJtjx4mfC9Z+0ikwA4ir2IW2BOUhWvJ6IDq3f7kulFxP8S4zWN9r7jsZw3A==
dependencies:
- "@angular-devkit/architect" "0.2003.20"
+ "@angular-devkit/architect" "0.2003.22"
rxjs "7.8.2"
-"@angular-devkit/core@20.3.20", "@angular-devkit/core@>= 20.0.0 < 21.0.0", "@angular-devkit/core@^20.0.0":
- version "20.3.20"
- resolved "https://registry.yarnpkg.com/@angular-devkit/core/-/core-20.3.20.tgz#62278795afb05ff1c2f442387e5c3e996ac304bf"
- integrity sha512-Iobw7He3yJVR2aQ6JN9Kq/2ldD8+uHzJZwd41SQ91A+TzPrBRSV0t80WHHrANZ7xnAjtHDc7zSSGp/i7DzUc9g==
+"@angular-devkit/core@20.3.22", "@angular-devkit/core@>= 20.0.0 < 21.0.0", "@angular-devkit/core@^20.0.0":
+ version "20.3.22"
+ resolved "https://registry.yarnpkg.com/@angular-devkit/core/-/core-20.3.22.tgz#6040c5673c4f4bb2eaaaac0bb9801ea6fa0886ea"
+ integrity sha512-1vZnZTAjGcCM+86v2al+2eiROiSw0uAWeVllfHSQe0KsKOP1FE8UUUiWChhxVn7vIxypphlfGunkeeIn1C/ZFw==
dependencies:
ajv "8.18.0"
ajv-formats "3.0.1"
jsonc-parser "3.3.1"
- picomatch "4.0.3"
+ picomatch "4.0.4"
rxjs "7.8.2"
source-map "0.7.6"
-"@angular-devkit/schematics@20.3.20", "@angular-devkit/schematics@>= 20.0.0 < 21.0.0":
- version "20.3.20"
- resolved "https://registry.yarnpkg.com/@angular-devkit/schematics/-/schematics-20.3.20.tgz#d7d2cc74134823305b9451a1920130daecdf8c61"
- integrity sha512-+NNHQhQHcgQWZopStZ6os30YuP99lRzNS4wOnkJmoROy40SZct8lPnl2QW50a9Vc0AtaHx1a1NUZ+ohbf6fXqw==
+"@angular-devkit/schematics@20.3.22", "@angular-devkit/schematics@>= 20.0.0 < 21.0.0":
+ version "20.3.22"
+ resolved "https://registry.yarnpkg.com/@angular-devkit/schematics/-/schematics-20.3.22.tgz#e1aab6eaa431f323db2425af05276dbe45c63fc2"
+ integrity sha512-gN2XSXRn3eErGEJlH0iSfQZZ7NdxVZNdjSxuVEGBEFhe3cVeC21LzM3GTWW6xwtBb4pxHglFyc7BUFiYtZiYtg==
dependencies:
- "@angular-devkit/core" "20.3.20"
+ "@angular-devkit/core" "20.3.22"
jsonc-parser "3.3.1"
magic-string "0.30.17"
ora "8.2.0"
@@ -328,13 +328,13 @@
dependencies:
tslib "^2.3.0"
-"@angular/build@20.3.20", "@angular/build@^20.0.0":
- version "20.3.20"
- resolved "https://registry.yarnpkg.com/@angular/build/-/build-20.3.20.tgz#8692af778824d43cd4e363330494144a70fa1950"
- integrity sha512-+uWqGU+Qyso2uJKL1xNjAm+E3m3ncv5InMUG5BC/UhFnXTsL1o7oySGc6hHa+8rQunuifUhdy20HxZjU6QlqTw==
+"@angular/build@20.3.22", "@angular/build@^20.0.0":
+ version "20.3.22"
+ resolved "https://registry.yarnpkg.com/@angular/build/-/build-20.3.22.tgz#c29b7980a96f6353b2d1c5cc2d7bcaf82c46d181"
+ integrity sha512-sxjVZU6AZHXyKRHJUMawXOj4qMf3vm8XK6wUejr01UKj6BqW2YWaQO26RpRJssXD2ITTqn6+UBwL7pEwe2a4Jg==
dependencies:
"@ampproject/remapping" "2.3.0"
- "@angular-devkit/architect" "0.2003.20"
+ "@angular-devkit/architect" "0.2003.22"
"@babel/core" "7.28.3"
"@babel/helper-annotate-as-pure" "7.27.3"
"@babel/helper-split-export-declaration" "7.24.7"
@@ -350,7 +350,7 @@
magic-string "0.30.17"
mrmime "2.0.1"
parse5-html-rewriting-stream "8.0.0"
- picomatch "4.0.3"
+ picomatch "4.0.4"
piscina "5.1.3"
rollup "4.59.0"
sass "1.90.0"
@@ -370,18 +370,18 @@
parse5 "^8.0.0"
tslib "^2.3.0"
-"@angular/cli@20.3.20":
- version "20.3.20"
- resolved "https://registry.yarnpkg.com/@angular/cli/-/cli-20.3.20.tgz#adbffbbcaf5ffda133e2f86dcb83dbfb86671ca4"
- integrity sha512-wHMo0BWhoBMXcsZ1U+yJ0eOH2B5NgCWW7PTrJ1P/QbocbCSh7eqw8wKnEthIKyhrEWtAo67Jw7i8CgbeIZgmMw==
+"@angular/cli@20.3.22":
+ version "20.3.22"
+ resolved "https://registry.yarnpkg.com/@angular/cli/-/cli-20.3.22.tgz#ce00d20d55f458de39c80b080a749d4d03e4f9b6"
+ integrity sha512-0uyQPF0gGuzioWJKNyOzWSQrrC5GiidR+8gz1lODoJTnJZZdsP5n3nvccbcRmhy55B1WByHvQBE+6eDBbh06/g==
dependencies:
- "@angular-devkit/architect" "0.2003.20"
- "@angular-devkit/core" "20.3.20"
- "@angular-devkit/schematics" "20.3.20"
+ "@angular-devkit/architect" "0.2003.22"
+ "@angular-devkit/core" "20.3.22"
+ "@angular-devkit/schematics" "20.3.22"
"@inquirer/prompts" "7.8.2"
"@listr2/prompt-adapter-inquirer" "3.0.1"
"@modelcontextprotocol/sdk" "1.26.0"
- "@schematics/angular" "20.3.20"
+ "@schematics/angular" "20.3.22"
"@yarnpkg/lockfile" "1.1.0"
algoliasearch "5.35.0"
ini "5.0.0"
@@ -2397,10 +2397,10 @@
dependencies:
tslib "^2.0.0"
-"@ngtools/webpack@20.3.20":
- version "20.3.20"
- resolved "https://registry.yarnpkg.com/@ngtools/webpack/-/webpack-20.3.20.tgz#4d45b14ee374349352b90b2db6049b9901f80b9d"
- integrity sha512-5azJ9+W/aMFR4C38ShoWib6xW5ou5Q5yeup+6skpvrud5mAAj3e36S5diYYXqbzVfo3DM+sWueDRNP3DV7IHhg==
+"@ngtools/webpack@20.3.22":
+ version "20.3.22"
+ resolved "https://registry.yarnpkg.com/@ngtools/webpack/-/webpack-20.3.22.tgz#65c7eefb187516f00d2268c60613294cf8d55028"
+ integrity sha512-EgOiRjYpNG5Mu/WAhMwQrAB1BBwWrApbYm2hTU9KjaxOZvtWHjeFfsiULgd1T76GpiF4t3Iw1GtNljzgD6fT/A==
"@ngx-translate/core@^17.0.0":
version "17.0.0"
@@ -2751,13 +2751,13 @@
resolved "https://registry.yarnpkg.com/@sanity/diff-match-patch/-/diff-match-patch-3.2.0.tgz#7ce587273f7372a146308cb1075ba26177d42cdb"
integrity sha512-4hPADs0qUThFZkBK/crnfKKHg71qkRowfktBljH2UIxGHHTxIzt8g8fBiXItyCjxkuNy+zpYOdRMifQNv8+Yww==
-"@schematics/angular@20.3.20":
- version "20.3.20"
- resolved "https://registry.yarnpkg.com/@schematics/angular/-/angular-20.3.20.tgz#22b0e87bab0aeca0ddbaef7014eac5033f813972"
- integrity sha512-LyoHwenNi8lZZO5zafAjcN8DcaOdjFrkbZdrCkvdpmOFz5wy8ZGchY6XSZEDD6kdHvR8oU7WWnGTMgCfExBKCg==
+"@schematics/angular@20.3.22":
+ version "20.3.22"
+ resolved "https://registry.yarnpkg.com/@schematics/angular/-/angular-20.3.22.tgz#acd7a990148a99f04793ee2e92a56694667790a4"
+ integrity sha512-wXTdFaPIBnSSNj/m0kclvPCYQOc2EGTQN1+Q3j9RIghS9gKgPxI1unSfgieJldZWKzcl8+WdB2zUuDzE7tEshQ==
dependencies:
- "@angular-devkit/core" "20.3.20"
- "@angular-devkit/schematics" "20.3.20"
+ "@angular-devkit/core" "20.3.22"
+ "@angular-devkit/schematics" "20.3.22"
jsonc-parser "3.3.1"
"@sigstore/bundle@^4.0.0":
@@ -4156,17 +4156,17 @@ boolbase@^1.0.0:
integrity sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==
brace-expansion@^1.1.7:
- version "1.1.12"
- resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.12.tgz#ab9b454466e5a8cc3a187beaad580412a9c5b843"
- integrity sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==
+ version "1.1.13"
+ resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.13.tgz#d37875c01dc9eff988dd49d112a57cb67b54efe6"
+ integrity sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==
dependencies:
balanced-match "^1.0.0"
concat-map "0.0.1"
brace-expansion@^5.0.2:
- version "5.0.3"
- resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-5.0.3.tgz#6a9c6c268f85b53959ec527aeafe0f7300258eef"
- integrity sha512-fy6KJm2RawA5RcHkLa1z/ScpBeA762UF9KmZQxwIbDtRJrgLzM10depAiEQ+CXYcoiqW1/m96OAAoke2nE9EeA==
+ version "5.0.5"
+ resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-5.0.5.tgz#dcc3a37116b79f3e1b46db994ced5d570e930fdb"
+ integrity sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==
dependencies:
balanced-match "^4.0.2"
@@ -8053,9 +8053,9 @@ node-fetch@^3.3.2:
formdata-polyfill "^4.0.10"
node-forge@^1:
- version "1.3.3"
- resolved "https://registry.yarnpkg.com/node-forge/-/node-forge-1.3.3.tgz#0ad80f6333b3a0045e827ac20b7f735f93716751"
- integrity sha512-rLvcdSyRCyouf6jcOIPe/BgwG/d7hKjzMKOas33/pHEr6gbq18IK9zV7DiPvzsz0oBJPme6qr6H6kGZuI9/DZg==
+ version "1.4.0"
+ resolved "https://registry.yarnpkg.com/node-forge/-/node-forge-1.4.0.tgz#1c7b7d8bdc2d078739f58287d589d903a11b2fc2"
+ integrity sha512-LarFH0+6VfriEhqMMcLX2F7SwSXeWwnEAJEsYm5QKWchiVYVvJyV9v7UDvUv+w5HO23ZpQTXDv/GxdDdMyOuoQ==
node-gyp-build-optional-packages@5.2.2:
version "5.2.2"
@@ -8552,14 +8552,14 @@ path-scurry@^2.0.0:
minipass "^7.1.2"
path-to-regexp@^8.0.0:
- version "8.3.0"
- resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-8.3.0.tgz#aa818a6981f99321003a08987d3cec9c3474cd1f"
- integrity sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA==
+ version "8.4.0"
+ resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-8.4.0.tgz#8e98fcd94826aff01a90c544ef74ffbaca3a78ed"
+ integrity sha512-PuseHIvAnz3bjrM2rGJtSgo1zjgxapTLZ7x2pjhzWwlp4SJQgK3f3iZIQwkpEnBaKz6seKBADpM4B4ySkuYypg==
path-to-regexp@~0.1.12:
- version "0.1.12"
- resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-0.1.12.tgz#d5e1a12e478a976d432ef3c58d534b9923164bb7"
- integrity sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==
+ version "0.1.13"
+ resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-0.1.13.tgz#9b22ec16bc3ab88d05a0c7e369869421401ab17d"
+ integrity sha512-A/AGNMFN3c8bOlvV9RreMdrv7jsmF9XIfDeCd87+I8RNg6s78BhJxMu69NEMHBSJFxKidViTEdruRwEk/WIKqA==
pathe@^1.1.2:
version "1.1.2"
@@ -8579,15 +8579,15 @@ picocolors@^1.0.0, picocolors@^1.1.1:
resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.1.1.tgz#3d321af3eab939b083c8f929a1d12cda81c26b6b"
integrity sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==
-picomatch@4.0.3, picomatch@^4.0.2, picomatch@^4.0.3:
- version "4.0.3"
- resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-4.0.3.tgz#796c76136d1eead715db1e7bad785dedd695a042"
- integrity sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==
+picomatch@4.0.4, picomatch@^4.0.2, picomatch@^4.0.3:
+ version "4.0.4"
+ resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-4.0.4.tgz#fd6f5e00a143086e074dffe4c924b8fb293b0589"
+ integrity sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==
picomatch@^2.0.4, picomatch@^2.2.1, picomatch@^2.3.1:
- version "2.3.1"
- resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42"
- integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==
+ version "2.3.2"
+ resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.2.tgz#5a942915e26b372dc0f0e6753149a16e6b1c5601"
+ integrity sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==
pify@^2.3.0:
version "2.3.0"