Browse Source

Merge branch 'feature/iot-hub' of github.com:thingsboard/thingsboard into feature/iot-hub

pull/15539/head
Andrii Shvaika 3 months ago
parent
commit
b2449f0da5
  1. 2
      application/pom.xml
  2. 3
      application/src/main/data/json/system/widget_bundles/home_page_widgets.json
  3. 39
      application/src/main/data/json/system/widget_types/iot_hub.json
  4. 19
      application/src/main/data/upgrade/lts/schema_update.sql
  5. 5
      application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java
  6. 6
      application/src/main/java/org/thingsboard/server/actors/ruleChain/DefaultTbContext.java
  7. 51
      application/src/main/java/org/thingsboard/server/config/TbHttpClientSettingsComponent.java
  8. 2
      application/src/main/java/org/thingsboard/server/controller/AlarmCommentController.java
  9. 6
      application/src/main/java/org/thingsboard/server/controller/plugin/TbWebSocketHandler.java
  10. 4
      application/src/main/java/org/thingsboard/server/service/ai/Langchain4jChatModelConfigurerImpl.java
  11. 85
      application/src/main/java/org/thingsboard/server/service/cf/DefaultCalculatedFieldCache.java
  12. 3
      application/src/main/java/org/thingsboard/server/service/entitiy/cf/DefaultTbCalculatedFieldService.java
  13. 3
      application/src/main/java/org/thingsboard/server/service/entitiy/cf/TbCalculatedFieldService.java
  14. 1
      application/src/main/java/org/thingsboard/server/service/install/SqlDatabaseUpgradeService.java
  15. 24
      application/src/main/java/org/thingsboard/server/service/profile/DefaultTbAssetProfileCache.java
  16. 24
      application/src/main/java/org/thingsboard/server/service/profile/DefaultTbDeviceProfileCache.java
  17. 3
      application/src/main/java/org/thingsboard/server/service/queue/DefaultTbCalculatedFieldConsumerService.java
  18. 3
      application/src/main/java/org/thingsboard/server/service/queue/DefaultTbCoreConsumerService.java
  19. 2
      application/src/main/java/org/thingsboard/server/service/queue/DefaultTbEdgeConsumerService.java
  20. 5
      application/src/main/java/org/thingsboard/server/service/queue/DefaultTbRuleEngineConsumerService.java
  21. 13
      application/src/main/java/org/thingsboard/server/service/queue/processing/AbstractConsumerService.java
  22. 3
      application/src/main/java/org/thingsboard/server/service/queue/processing/AbstractPartitionBasedConsumerService.java
  23. 2
      application/src/main/java/org/thingsboard/server/service/ruleengine/DefaultRuleEngineCallService.java
  24. 1437
      application/src/main/java/org/thingsboard/server/service/solutions/DefaultSolutionService.java
  25. 40
      application/src/main/java/org/thingsboard/server/service/solutions/data/CreatedRuleChainInfo.java
  26. 9
      application/src/main/java/org/thingsboard/server/service/solutions/data/SolutionInstallContext.java
  27. 74
      application/src/main/java/org/thingsboard/server/service/system/SystemPatchApplier.java
  28. 14
      application/src/main/java/org/thingsboard/server/service/ws/DefaultWebSocketService.java
  29. 12
      application/src/main/resources/thingsboard.yml
  30. 47
      application/src/test/java/org/thingsboard/server/controller/AlarmCommentControllerTest.java
  31. 78
      application/src/test/java/org/thingsboard/server/controller/plugin/TbWebSocketHandlerTest.java
  32. 66
      application/src/test/java/org/thingsboard/server/service/ai/Langchain4jChatModelConfigurerImplTest.java
  33. 349
      application/src/test/java/org/thingsboard/server/service/cf/DefaultCalculatedFieldCacheTest.java
  34. 159
      application/src/test/java/org/thingsboard/server/service/profile/DefaultTbAssetProfileCacheTest.java
  35. 160
      application/src/test/java/org/thingsboard/server/service/profile/DefaultTbDeviceProfileCacheTest.java
  36. 277
      application/src/test/java/org/thingsboard/server/service/ws/DefaultWebSocketServiceTest.java
  37. 447
      application/src/test/java/org/thingsboard/server/system/SystemPatchApplierTest.java
  38. 2
      common/actor/pom.xml
  39. 2
      common/cache/pom.xml
  40. 11
      common/cache/src/main/java/org/thingsboard/server/cache/TbJsonRedisSerializer.java
  41. 2
      common/cluster-api/pom.xml
  42. 2
      common/coap-server/pom.xml
  43. 2
      common/dao-api/pom.xml
  44. 3
      common/dao-api/src/main/java/org/thingsboard/server/dao/resource/ImageService.java
  45. 2
      common/data/pom.xml
  46. 1
      common/data/src/main/java/org/thingsboard/server/common/data/alarm/AlarmCreateOrUpdateActiveRequest.java
  47. 2
      common/discovery-api/pom.xml
  48. 2
      common/edge-api/pom.xml
  49. 1
      common/edge-api/src/main/proto/edge.proto
  50. 2
      common/edqs/pom.xml
  51. 2
      common/message/pom.xml
  52. 12
      common/message/src/main/java/org/thingsboard/server/common/msg/TbMsg.java
  53. 2
      common/pom.xml
  54. 2
      common/proto/pom.xml
  55. 1
      common/proto/src/main/proto/queue.proto
  56. 2
      common/queue/pom.xml
  57. 2
      common/script/pom.xml
  58. 2
      common/script/remote-js-client/pom.xml
  59. 2
      common/script/script-api/pom.xml
  60. 2
      common/stats/pom.xml
  61. 2
      common/transport/coap/pom.xml
  62. 142
      common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/efento/CoapEfentoTransportResource.java
  63. 4
      common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/efento/utils/CoapEfentoUtils.java
  64. 673
      common/transport/coap/src/main/proto/efento/proto_config.proto
  65. 125
      common/transport/coap/src/main/proto/efento/proto_config_types.proto
  66. 409
      common/transport/coap/src/main/proto/efento/proto_device_info.proto
  67. 185
      common/transport/coap/src/main/proto/efento/proto_measurement_types.proto
  68. 217
      common/transport/coap/src/main/proto/efento/proto_measurements.proto
  69. 407
      common/transport/coap/src/main/proto/efento/proto_rule.proto
  70. 664
      common/transport/coap/src/test/java/org/thingsboard/server/transport/coap/efento/CoapEfentoTransportResourceTest.java
  71. 2
      common/transport/http/pom.xml
  72. 2
      common/transport/lwm2m/pom.xml
  73. 2
      common/transport/mqtt/pom.xml
  74. 2
      common/transport/pom.xml
  75. 2
      common/transport/snmp/pom.xml
  76. 2
      common/transport/transport-api/pom.xml
  77. 2
      common/util/pom.xml
  78. 2
      common/version-control/pom.xml
  79. 2
      dao/pom.xml
  80. 13
      dao/src/main/java/org/thingsboard/server/dao/alarm/BaseAlarmCommentService.java
  81. 6
      dao/src/main/java/org/thingsboard/server/dao/resource/BaseImageService.java
  82. 19
      dao/src/main/java/org/thingsboard/server/dao/service/validator/AlarmCommentDataValidator.java
  83. 25
      dao/src/test/java/org/thingsboard/server/dao/service/AlarmServiceTest.java
  84. 2
      edqs/pom.xml
  85. 2
      monitoring/pom.xml
  86. 2
      msa/black-box-tests/pom.xml
  87. 2
      msa/edqs/pom.xml
  88. 2
      msa/js-executor/docker/Dockerfile
  89. 2
      msa/js-executor/package.json
  90. 4
      msa/js-executor/pom.xml
  91. 24
      msa/js-executor/yarn.lock
  92. 2
      msa/monitoring/pom.xml
  93. 2
      msa/pom.xml
  94. 2
      msa/tb-node/pom.xml
  95. 2
      msa/tb/pom.xml
  96. 2
      msa/transport/coap/pom.xml
  97. 2
      msa/transport/http/pom.xml
  98. 2
      msa/transport/lwm2m/pom.xml
  99. 2
      msa/transport/mqtt/pom.xml
  100. 2
      msa/transport/pom.xml

2
application/pom.xml

@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.thingsboard</groupId>
<version>4.2.2.1-SNAPSHOT</version>
<version>4.2.2.2-SNAPSHOT</version>
<artifactId>thingsboard</artifactId>
</parent>
<artifactId>application</artifactId>

3
application/src/main/data/json/system/widget_bundles/home_page_widgets.json

@ -13,6 +13,7 @@
"home_page_widgets.quick_links",
"home_page_widgets.documentation_links",
"home_page_widgets.dashboards",
"home_page_widgets.usage_info"
"home_page_widgets.usage_info",
"home_page_widgets.iot_hub"
]
}

39
application/src/main/data/json/system/widget_types/iot_hub.json

File diff suppressed because one or more lines are too long

19
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.

5
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;

6
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());

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

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

6
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<TenantId, Set<String>> tenantSessionsMap = new ConcurrentHashMap<>();
private final ConcurrentMap<CustomerId, Set<String>> customerSessionsMap = new ConcurrentHashMap<>();
private final ConcurrentMap<UserId, Set<String>> regularUserSessionsMap = new ConcurrentHashMap<>();
private final ConcurrentMap<UserId, Set<String>> publicUserSessionsMap = new ConcurrentHashMap<>();
private final ConcurrentMap<TenantId, Set<String>> publicUserSessionsMap = new ConcurrentHashMap<>();
private Cache<String, SessionMetaData> 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<String> publicUserSessions = publicUserSessionsMap.computeIfAbsent(sessionRef.getSecurityCtx().getId(), id -> ConcurrentHashMap.newKeySet());
Set<String> 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<String> publicUserSessions = publicUserSessionsMap.computeIfAbsent(sessionRef.getSecurityCtx().getId(), id -> ConcurrentHashMap.newKeySet());
Set<String> publicUserSessions = publicUserSessionsMap.computeIfAbsent(sessionRef.getSecurityCtx().getTenantId(), id -> ConcurrentHashMap.newKeySet());
synchronized (publicUserSessions) {
publicUserSessions.remove(sessionId);
}

4
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());

85
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<EntityId>();
var removedLinkEntityIds = new HashSet<EntityId>();
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<CalculatedFieldLink> 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<CalculatedField> cfs = entityIdCalculatedFields.remove(entityId);
if (cfs != null) {
var cfIds = new HashSet<CalculatedFieldId>();
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());
}

3
application/src/main/java/org/thingsboard/server/service/entitiy/cf/DefaultTbCalculatedFieldService.java

@ -20,6 +20,7 @@ import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.User;
import org.thingsboard.server.common.data.audit.ActionType;
import org.thingsboard.server.common.data.cf.CalculatedField;
import org.thingsboard.server.common.data.exception.ThingsboardException;
@ -76,7 +77,7 @@ public class DefaultTbCalculatedFieldService extends AbstractTbEntityService imp
@Override
@Transactional
public void delete(CalculatedField calculatedField, SecurityUser user) {
public void delete(CalculatedField calculatedField, User user) {
ActionType actionType = ActionType.DELETED;
TenantId tenantId = calculatedField.getTenantId();
CalculatedFieldId calculatedFieldId = calculatedField.getId();

3
application/src/main/java/org/thingsboard/server/service/entitiy/cf/TbCalculatedFieldService.java

@ -15,6 +15,7 @@
*/
package org.thingsboard.server.service.entitiy.cf;
import org.thingsboard.server.common.data.User;
import org.thingsboard.server.common.data.cf.CalculatedField;
import org.thingsboard.server.common.data.exception.ThingsboardException;
import org.thingsboard.server.common.data.id.CalculatedFieldId;
@ -31,6 +32,6 @@ public interface TbCalculatedFieldService {
PageData<CalculatedField> findAllByTenantIdAndEntityId(EntityId entityId, SecurityUser user, PageLink pageLink);
void delete(CalculatedField calculatedField, SecurityUser user);
void delete(CalculatedField calculatedField, User user);
}

1
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.");
}

24
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<AssetProfileId> 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<EntityId, Consumer<AssetProfile>> tenantListeners = profileListeners.get(profile.getTenantId());
if (tenantListeners != null) {

24
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<DeviceProfileId> 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<EntityId, Consumer<DeviceProfile>> tenantListeners = profileListeners.get(profile.getTenantId());
if (tenantListeners != null) {

3
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;

3
application/src/main/java/org/thingsboard/server/service/queue/DefaultTbCoreConsumerService.java

@ -179,9 +179,8 @@ public class DefaultTbCoreConsumerService extends AbstractConsumerService<ToCore
TbImageService imageService,
TbResourceDataCache tbResourceDataCache,
RuleEngineCallService ruleEngineCallService,
CalculatedFieldCache calculatedFieldCache,
EdqsService edqsService) {
super(actorContext, tenantProfileCache, deviceProfileCache, assetProfileCache, tbResourceDataCache, calculatedFieldCache, apiUsageStateService, partitionService,
super(actorContext, tenantProfileCache, deviceProfileCache, assetProfileCache, tbResourceDataCache, apiUsageStateService, partitionService,
eventPublisher, jwtSettingsService);
this.stateService = stateService;
this.localSubscriptionService = localSubscriptionService;

2
application/src/main/java/org/thingsboard/server/service/queue/DefaultTbEdgeConsumerService.java

@ -87,7 +87,7 @@ public class DefaultTbEdgeConsumerService extends AbstractConsumerService<ToEdge
public DefaultTbEdgeConsumerService(TbCoreQueueFactory tbCoreQueueFactory, ActorSystemContext actorContext,
StatsFactory statsFactory, EdgeContextComponent edgeCtx) {
super(actorContext, null, null, null, null, null, null, null,
super(actorContext, null, null, null, null, null, null,
null, null);
this.edgeCtx = edgeCtx;
this.stats = new EdgeConsumerStats(statsFactory);

5
application/src/main/java/org/thingsboard/server/service/queue/DefaultTbRuleEngineConsumerService.java

@ -84,9 +84,8 @@ public class DefaultTbRuleEngineConsumerService extends AbstractPartitionBasedCo
TbApiUsageStateService apiUsageStateService,
PartitionService partitionService,
ApplicationEventPublisher eventPublisher,
JwtSettingsService jwtSettingsService,
CalculatedFieldCache calculatedFieldCache) {
super(actorContext, tenantProfileCache, deviceProfileCache, assetProfileCache, tbResourceDataCache, calculatedFieldCache, apiUsageStateService, partitionService, eventPublisher, jwtSettingsService);
JwtSettingsService jwtSettingsService) {
super(actorContext, tenantProfileCache, deviceProfileCache, assetProfileCache, tbResourceDataCache, apiUsageStateService, partitionService, eventPublisher, jwtSettingsService);
this.ctx = ctx;
this.tbDeviceRpcService = tbDeviceRpcService;
this.queueService = queueService;

13
application/src/main/java/org/thingsboard/server/service/queue/processing/AbstractConsumerService.java

@ -26,7 +26,6 @@ import org.thingsboard.server.actors.ActorSystemContext;
import org.thingsboard.server.common.data.EntityType;
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;
@ -47,7 +46,6 @@ import org.thingsboard.server.queue.discovery.TbApplicationEventListener;
import org.thingsboard.server.queue.discovery.event.PartitionChangeEvent;
import org.thingsboard.server.queue.util.AfterStartUp;
import org.thingsboard.server.service.apiusage.TbApiUsageStateService;
import org.thingsboard.server.service.cf.CalculatedFieldCache;
import org.thingsboard.server.service.profile.TbAssetProfileCache;
import org.thingsboard.server.service.profile.TbDeviceProfileCache;
import org.thingsboard.server.service.queue.TbPackCallback;
@ -75,7 +73,6 @@ public abstract class AbstractConsumerService<N extends com.google.protobuf.Gene
protected final TbDeviceProfileCache deviceProfileCache;
protected final TbAssetProfileCache assetProfileCache;
protected final TbResourceDataCache tbResourceDataCache;
protected final CalculatedFieldCache calculatedFieldCache;
protected final TbApiUsageStateService apiUsageStateService;
protected final PartitionService partitionService;
protected final ApplicationEventPublisher eventPublisher;
@ -197,15 +194,7 @@ public abstract class AbstractConsumerService<N extends com.google.protobuf.Gene
if (componentLifecycleMsg.getEvent() == ComponentLifecycleEvent.DELETED) {
apiUsageStateService.onCustomerDelete((CustomerId) componentLifecycleMsg.getEntityId());
}
} else if (EntityType.CALCULATED_FIELD.equals(componentLifecycleMsg.getEntityId().getEntityType())) {
if (componentLifecycleMsg.getEvent() == ComponentLifecycleEvent.CREATED) {
calculatedFieldCache.addCalculatedField(tenantId, (CalculatedFieldId) componentLifecycleMsg.getEntityId());
} else if (componentLifecycleMsg.getEvent() == ComponentLifecycleEvent.UPDATED) {
calculatedFieldCache.updateCalculatedField(tenantId, (CalculatedFieldId) componentLifecycleMsg.getEntityId());
} else {
calculatedFieldCache.evict((CalculatedFieldId) componentLifecycleMsg.getEntityId());
}
} else if (EntityType.TB_RESOURCE.equals(componentLifecycleMsg.getEntityId().getEntityType())) {
} else if (EntityType.TB_RESOURCE.equals(componentLifecycleMsg.getEntityId().getEntityType())) {
tbResourceDataCache.evictResourceData(tenantId, new TbResourceId(componentLifecycleMsg.getEntityId().getId()));
}

3
application/src/main/java/org/thingsboard/server/service/queue/processing/AbstractPartitionBasedConsumerService.java

@ -45,12 +45,11 @@ public abstract class AbstractPartitionBasedConsumerService<N extends com.google
TbDeviceProfileCache deviceProfileCache,
TbAssetProfileCache assetProfileCache,
TbResourceDataCache tbResourceDataCache,
CalculatedFieldCache calculatedFieldCache,
TbApiUsageStateService apiUsageStateService,
PartitionService partitionService,
ApplicationEventPublisher eventPublisher,
JwtSettingsService jwtSettingsService) {
super(actorContext, tenantProfileCache, deviceProfileCache, assetProfileCache, tbResourceDataCache, calculatedFieldCache, apiUsageStateService, partitionService, eventPublisher, jwtSettingsService);
super(actorContext, tenantProfileCache, deviceProfileCache, assetProfileCache, tbResourceDataCache, apiUsageStateService, partitionService, eventPublisher, jwtSettingsService);
}
@PostConstruct

2
application/src/main/java/org/thingsboard/server/service/ruleengine/DefaultRuleEngineCallService.java

@ -73,7 +73,7 @@ public class DefaultRuleEngineCallService implements RuleEngineCallService {
UUID requestId = new UUID(restApiCallResponseMsg.getRequestIdMSB(), restApiCallResponseMsg.getRequestIdLSB());
Consumer<TbMsg> 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);
}

1437
application/src/main/java/org/thingsboard/server/service/solutions/DefaultSolutionService.java

File diff suppressed because it is too large

40
application/src/main/java/org/thingsboard/server/service/solutions/data/CreatedRuleChainInfo.java

@ -0,0 +1,40 @@
/**
* 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.solutions.data;
import lombok.Data;
import lombok.EqualsAndHashCode;
import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.rule.RuleChainType;
import java.util.UUID;
@Data
@EqualsAndHashCode(callSuper = true)
public class CreatedRuleChainInfo extends CreatedEntityInfo {
private RuleChainType ruleChainType;
public CreatedRuleChainInfo(String name, RuleChainType ruleChainType, String owner) {
super(name, EntityType.RULE_CHAIN, owner);
this.ruleChainType = ruleChainType;
}
@Override
public String getEntityPageLink(UUID id) {
return ruleChainType == RuleChainType.EDGE ? "/edgeManagement/ruleChains/" + id : "/ruleChains/" + id;
}
}

9
application/src/main/java/org/thingsboard/server/service/solutions/data/SolutionInstallContext.java

@ -29,7 +29,6 @@ import org.thingsboard.server.common.data.cf.CalculatedField;
import org.thingsboard.server.common.data.edge.Edge;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.service.security.model.SecurityUser;
import org.thingsboard.server.common.data.rule.RuleChain;
import org.thingsboard.server.service.solutions.data.definition.AssetDefinition;
import org.thingsboard.server.service.solutions.data.definition.AssetProfileDefinition;
@ -57,7 +56,7 @@ public class SolutionInstallContext {
private final TenantId tenantId;
private final Path tempDir;
private final SecurityUser user;
private final User user;
private final TenantSolutionTemplateInstructions solutionInstructions;
private final List<EntityId> createdEntitiesList = new ArrayList<>();
private final Map<String, String> realIds = new HashMap<>();
@ -78,12 +77,12 @@ public class SolutionInstallContext {
private Map<String, EmulatorDefinition> deviceEmulators;
private Map<String, EmulatorDefinition> assetEmulators;
public SolutionInstallContext(TenantId tenantId, String solutionId, Path tempDir, SecurityUser user) {
public SolutionInstallContext(TenantId tenantId, String solutionId, Path tempDir, User user, TenantSolutionTemplateInstructions solutionInstructions) {
this.tenantId = tenantId;
this.solutionId = solutionId;
this.tempDir = tempDir;
this.user = user;
this.solutionInstructions = new TenantSolutionTemplateInstructions();
this.solutionInstructions = solutionInstructions;
this.installTs = System.currentTimeMillis();
put(new EntitySearchKey(tenantId, EntityType.TENANT, null), tenantId);
}
@ -105,7 +104,7 @@ public class SolutionInstallContext {
public void register(String referenceId, RuleChain ruleChain) {
register(referenceId, ruleChain.getId());
createdEntities.put(ruleChain.getUuidId(), new CreatedEntityInfo(ruleChain.getName(), EntityType.RULE_CHAIN, "Tenant"));
createdEntities.put(ruleChain.getUuidId(), new CreatedRuleChainInfo(ruleChain.getName(), ruleChain.getType(), "Tenant"));
}
public void register(DeviceProfileDefinition definition, DeviceProfile deviceProfile) {

74
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<String> existingKeys = imageService.getAllImageKeysByTenantId(TenantId.SYS_TENANT_ID);
try (Stream<Path> 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;

14
application/src/main/java/org/thingsboard/server/service/ws/DefaultWebSocketService.java

@ -144,7 +144,7 @@ public class DefaultWebSocketService implements WebSocketService {
private final ConcurrentMap<TenantId, Set<String>> tenantSubscriptionsMap = new ConcurrentHashMap<>();
private final ConcurrentMap<CustomerId, Set<String>> customerSubscriptionsMap = new ConcurrentHashMap<>();
private final ConcurrentMap<UserId, Set<String>> regularUserSubscriptionsMap = new ConcurrentHashMap<>();
private final ConcurrentMap<UserId, Set<String>> publicUserSubscriptionsMap = new ConcurrentHashMap<>();
private final ConcurrentMap<TenantId, Set<String>> publicUserSubscriptionsMap = new ConcurrentHashMap<>();
private final ConcurrentMap<String, Map<Integer, Integer>> 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<String> publicUserSessions = publicUserSubscriptionsMap.computeIfAbsent(sessionRef.getSecurityCtx().getId(), id -> ConcurrentHashMap.newKeySet());
Set<String> 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<String> publicUserSessions = publicUserSubscriptionsMap.computeIfAbsent(sessionRef.getSecurityCtx().getId(), id -> ConcurrentHashMap.newKeySet());
Set<String> 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"

12
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}"

47
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<AlarmCommentInfo> systemCommentOpt = doGetTyped(
"/api/alarm/" + alarm.getId() + "/comment" + "?page=0&pageSize=10", new TypeReference<PageData<AlarmCommentInfo>>() {
}
).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<AlarmCommentInfo> systemCommentOpt = doGetTyped(
"/api/alarm/" + alarm.getId() + "/comment" + "?page=0&pageSize=10", new TypeReference<PageData<AlarmCommentInfo>>() {
}
).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)))

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

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

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

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

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

277
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<TenantId, Set<String>> publicUserSubscriptionsMap =
(ConcurrentMap<TenantId, Set<String>>) 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<TenantId, Set<String>> publicUserSubscriptionsMap =
(ConcurrentMap<TenantId, Set<String>>) ReflectionTestUtils.getField(service, "publicUserSubscriptionsMap");
Set<String> 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<TenantId, Set<String>> publicUserSubscriptionsMap =
(ConcurrentMap<TenantId, Set<String>>) 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<TenantId, Set<String>> publicUserSubscriptionsMap =
(ConcurrentMap<TenantId, Set<String>>) 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<TenantId, Set<String>> publicUserSubscriptionsMap =
(ConcurrentMap<TenantId, Set<String>>) ReflectionTestUtils.getField(service, "publicUserSubscriptionsMap");
assertThat(publicUserSubscriptionsMap.get(tenantId)).hasSize(3);
service.processSessionClose(session1);
Set<String> 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;
}
}

447
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<Arguments> 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<Arguments> 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));
}
}

2
common/actor/pom.xml

@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.thingsboard</groupId>
<version>4.2.2.1-SNAPSHOT</version>
<version>4.2.2.2-SNAPSHOT</version>
<artifactId>common</artifactId>
</parent>
<groupId>org.thingsboard.common</groupId>

2
common/cache/pom.xml

@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.thingsboard</groupId>
<version>4.2.2.1-SNAPSHOT</version>
<version>4.2.2.2-SNAPSHOT</version>
<artifactId>common</artifactId>
</parent>
<groupId>org.thingsboard.common</groupId>

11
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<K, V> implements TbRedisSerializer<K, V> {
private final Class<V> clazz;
@ -33,6 +35,13 @@ public class TbJsonRedisSerializer<K, V> implements TbRedisSerializer<K, V> {
@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);
}
}
}

2
common/cluster-api/pom.xml

@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.thingsboard</groupId>
<version>4.2.2.1-SNAPSHOT</version>
<version>4.2.2.2-SNAPSHOT</version>
<artifactId>common</artifactId>
</parent>
<groupId>org.thingsboard.common</groupId>

2
common/coap-server/pom.xml

@ -22,7 +22,7 @@
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.thingsboard</groupId>
<version>4.2.2.1-SNAPSHOT</version>
<version>4.2.2.2-SNAPSHOT</version>
<artifactId>common</artifactId>
</parent>
<groupId>org.thingsboard.common</groupId>

2
common/dao-api/pom.xml

@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.thingsboard</groupId>
<version>4.2.2.1-SNAPSHOT</version>
<version>4.2.2.2-SNAPSHOT</version>
<artifactId>common</artifactId>
</parent>
<groupId>org.thingsboard.common</groupId>

3
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<String> getAllImageKeysByTenantId(TenantId tenantId);
TbResourceInfo getPublicImageInfoByKey(String publicResourceKey);
PageData<TbResourceInfo> getImagesByTenantId(TenantId tenantId, ResourceSubType imageSubType, PageLink pageLink);

2
common/data/pom.xml

@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.thingsboard</groupId>
<version>4.2.2.1-SNAPSHOT</version>
<version>4.2.2.2-SNAPSHOT</version>
<artifactId>common</artifactId>
</parent>
<groupId>org.thingsboard.common</groupId>

1
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")

2
common/discovery-api/pom.xml

@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.thingsboard</groupId>
<version>4.2.2.1-SNAPSHOT</version>
<version>4.2.2.2-SNAPSHOT</version>
<artifactId>common</artifactId>
</parent>
<groupId>org.thingsboard.common</groupId>

2
common/edge-api/pom.xml

@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.thingsboard</groupId>
<version>4.2.2.1-SNAPSHOT</version>
<version>4.2.2.2-SNAPSHOT</version>
<artifactId>common</artifactId>
</parent>
<groupId>org.thingsboard.common</groupId>

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

2
common/edqs/pom.xml

@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.thingsboard</groupId>
<version>4.2.2.1-SNAPSHOT</version>
<version>4.2.2.2-SNAPSHOT</version>
<artifactId>common</artifactId>
</parent>
<groupId>org.thingsboard.common</groupId>

2
common/message/pom.xml

@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.thingsboard</groupId>
<version>4.2.2.1-SNAPSHOT</version>
<version>4.2.2.2-SNAPSHOT</version>
<artifactId>common</artifactId>
</parent>
<groupId>org.thingsboard.common</groupId>

12
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()));

2
common/pom.xml

@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.thingsboard</groupId>
<version>4.2.2.1-SNAPSHOT</version>
<version>4.2.2.2-SNAPSHOT</version>
<artifactId>thingsboard</artifactId>
</parent>
<artifactId>common</artifactId>

2
common/proto/pom.xml

@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.thingsboard</groupId>
<version>4.2.2.1-SNAPSHOT</version>
<version>4.2.2.2-SNAPSHOT</version>
<artifactId>common</artifactId>
</parent>
<groupId>org.thingsboard.common</groupId>

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

2
common/queue/pom.xml

@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.thingsboard</groupId>
<version>4.2.2.1-SNAPSHOT</version>
<version>4.2.2.2-SNAPSHOT</version>
<artifactId>common</artifactId>
</parent>
<groupId>org.thingsboard.common</groupId>

2
common/script/pom.xml

@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.thingsboard</groupId>
<version>4.2.2.1-SNAPSHOT</version>
<version>4.2.2.2-SNAPSHOT</version>
<artifactId>common</artifactId>
</parent>
<groupId>org.thingsboard.common</groupId>

2
common/script/remote-js-client/pom.xml

@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.thingsboard.common</groupId>
<version>4.2.2.1-SNAPSHOT</version>
<version>4.2.2.2-SNAPSHOT</version>
<artifactId>script</artifactId>
</parent>
<groupId>org.thingsboard.common.script</groupId>

2
common/script/script-api/pom.xml

@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.thingsboard.common</groupId>
<version>4.2.2.1-SNAPSHOT</version>
<version>4.2.2.2-SNAPSHOT</version>
<artifactId>script</artifactId>
</parent>
<groupId>org.thingsboard.common.script</groupId>

2
common/stats/pom.xml

@ -22,7 +22,7 @@
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.thingsboard</groupId>
<version>4.2.2.1-SNAPSHOT</version>
<version>4.2.2.2-SNAPSHOT</version>
<artifactId>common</artifactId>
</parent>
<groupId>org.thingsboard.common</groupId>

2
common/transport/coap/pom.xml

@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.thingsboard.common</groupId>
<version>4.2.2.1-SNAPSHOT</version>
<version>4.2.2.2-SNAPSHOT</version>
<artifactId>transport</artifactId>
</parent>
<groupId>org.thingsboard.common.transport</groupId>

142
common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/efento/CoapEfentoTransportResource.java

@ -245,7 +245,7 @@ public class CoapEfentoTransportResource extends AbstractCoapTransportResource {
}
List<EfentoTelemetry> 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<Long, JsonObject> 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<Integer> 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) {

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

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

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

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

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

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

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

664
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<Integer> 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<Arguments> 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<Arguments> 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<Arguments> 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<Integer> 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<Integer> 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<Integer> 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<Integer> zeroMemStats = java.util.Collections.nCopies(22, 0);
List<Integer> 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<CoapEfentoTransportResource.EfentoTelemetry> 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);
}
}

2
common/transport/http/pom.xml

@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.thingsboard.common</groupId>
<version>4.2.2.1-SNAPSHOT</version>
<version>4.2.2.2-SNAPSHOT</version>
<artifactId>transport</artifactId>
</parent>
<groupId>org.thingsboard.common.transport</groupId>

2
common/transport/lwm2m/pom.xml

@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.thingsboard.common</groupId>
<version>4.2.2.1-SNAPSHOT</version>
<version>4.2.2.2-SNAPSHOT</version>
<artifactId>transport</artifactId>
</parent>
<groupId>org.thingsboard.common.transport</groupId>

2
common/transport/mqtt/pom.xml

@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.thingsboard.common</groupId>
<version>4.2.2.1-SNAPSHOT</version>
<version>4.2.2.2-SNAPSHOT</version>
<artifactId>transport</artifactId>
</parent>
<groupId>org.thingsboard.common.transport</groupId>

2
common/transport/pom.xml

@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.thingsboard</groupId>
<version>4.2.2.1-SNAPSHOT</version>
<version>4.2.2.2-SNAPSHOT</version>
<artifactId>common</artifactId>
</parent>
<groupId>org.thingsboard.common</groupId>

2
common/transport/snmp/pom.xml

@ -21,7 +21,7 @@
<parent>
<groupId>org.thingsboard.common</groupId>
<version>4.2.2.1-SNAPSHOT</version>
<version>4.2.2.2-SNAPSHOT</version>
<artifactId>transport</artifactId>
</parent>

2
common/transport/transport-api/pom.xml

@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.thingsboard.common</groupId>
<version>4.2.2.1-SNAPSHOT</version>
<version>4.2.2.2-SNAPSHOT</version>
<artifactId>transport</artifactId>
</parent>
<groupId>org.thingsboard.common.transport</groupId>

2
common/util/pom.xml

@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.thingsboard</groupId>
<version>4.2.2.1-SNAPSHOT</version>
<version>4.2.2.2-SNAPSHOT</version>
<artifactId>common</artifactId>
</parent>
<groupId>org.thingsboard.common</groupId>

2
common/version-control/pom.xml

@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.thingsboard</groupId>
<version>4.2.2.1-SNAPSHOT</version>
<version>4.2.2.2-SNAPSHOT</version>
<artifactId>common</artifactId>
</parent>
<groupId>org.thingsboard.common</groupId>

2
dao/pom.xml

@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.thingsboard</groupId>
<version>4.2.2.1-SNAPSHOT</version>
<version>4.2.2.2-SNAPSHOT</version>
<artifactId>thingsboard</artifactId>
</parent>
<artifactId>dao</artifactId>

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

6
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<String> 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);

19
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<AlarmComment> {
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<AlarmComment> {
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;
}
}

25
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("<img src=1 onerror=alert()>")
.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();
}
}

2
edqs/pom.xml

@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.thingsboard</groupId>
<version>4.2.2.1-SNAPSHOT</version>
<version>4.2.2.2-SNAPSHOT</version>
<artifactId>thingsboard</artifactId>
</parent>
<artifactId>edqs</artifactId>

2
monitoring/pom.xml

@ -21,7 +21,7 @@
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.thingsboard</groupId>
<version>4.2.2.1-SNAPSHOT</version>
<version>4.2.2.2-SNAPSHOT</version>
<artifactId>thingsboard</artifactId>
</parent>

2
msa/black-box-tests/pom.xml

@ -21,7 +21,7 @@
<parent>
<groupId>org.thingsboard</groupId>
<version>4.2.2.1-SNAPSHOT</version>
<version>4.2.2.2-SNAPSHOT</version>
<artifactId>msa</artifactId>
</parent>
<groupId>org.thingsboard.msa</groupId>

2
msa/edqs/pom.xml

@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.thingsboard</groupId>
<version>4.2.2.1-SNAPSHOT</version>
<version>4.2.2.2-SNAPSHOT</version>
<artifactId>msa</artifactId>
</parent>
<groupId>org.thingsboard.msa</groupId>

2
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

2
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",

4
msa/js-executor/pom.xml

@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.thingsboard</groupId>
<version>4.2.2.1-SNAPSHOT</version>
<version>4.2.2.2-SNAPSHOT</version>
<artifactId>msa</artifactId>
</parent>
<groupId>org.thingsboard.msa</groupId>
@ -70,7 +70,7 @@
<goal>install-node-and-yarn</goal>
</goals>
<configuration>
<nodeVersion>v22.18.0</nodeVersion>
<nodeVersion>v22.22.2</nodeVersion>
<yarnVersion>v1.22.22</yarnVersion>
</configuration>
</execution>

24
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"

2
msa/monitoring/pom.xml

@ -22,7 +22,7 @@
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.thingsboard</groupId>
<version>4.2.2.1-SNAPSHOT</version>
<version>4.2.2.2-SNAPSHOT</version>
<artifactId>msa</artifactId>
</parent>

2
msa/pom.xml

@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.thingsboard</groupId>
<version>4.2.2.1-SNAPSHOT</version>
<version>4.2.2.2-SNAPSHOT</version>
<artifactId>thingsboard</artifactId>
</parent>
<artifactId>msa</artifactId>

2
msa/tb-node/pom.xml

@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.thingsboard</groupId>
<version>4.2.2.1-SNAPSHOT</version>
<version>4.2.2.2-SNAPSHOT</version>
<artifactId>msa</artifactId>
</parent>
<groupId>org.thingsboard.msa</groupId>

2
msa/tb/pom.xml

@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.thingsboard</groupId>
<version>4.2.2.1-SNAPSHOT</version>
<version>4.2.2.2-SNAPSHOT</version>
<artifactId>msa</artifactId>
</parent>
<groupId>org.thingsboard.msa</groupId>

2
msa/transport/coap/pom.xml

@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.thingsboard.msa</groupId>
<version>4.2.2.1-SNAPSHOT</version>
<version>4.2.2.2-SNAPSHOT</version>
<artifactId>transport</artifactId>
</parent>
<groupId>org.thingsboard.msa.transport</groupId>

2
msa/transport/http/pom.xml

@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.thingsboard.msa</groupId>
<version>4.2.2.1-SNAPSHOT</version>
<version>4.2.2.2-SNAPSHOT</version>
<artifactId>transport</artifactId>
</parent>
<groupId>org.thingsboard.msa.transport</groupId>

2
msa/transport/lwm2m/pom.xml

@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.thingsboard.msa</groupId>
<version>4.2.2.1-SNAPSHOT</version>
<version>4.2.2.2-SNAPSHOT</version>
<artifactId>transport</artifactId>
</parent>
<groupId>org.thingsboard.msa.transport</groupId>

2
msa/transport/mqtt/pom.xml

@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.thingsboard.msa</groupId>
<version>4.2.2.1-SNAPSHOT</version>
<version>4.2.2.2-SNAPSHOT</version>
<artifactId>transport</artifactId>
</parent>
<groupId>org.thingsboard.msa.transport</groupId>

2
msa/transport/pom.xml

@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.thingsboard</groupId>
<version>4.2.2.1-SNAPSHOT</version>
<version>4.2.2.2-SNAPSHOT</version>
<artifactId>msa</artifactId>
</parent>
<groupId>org.thingsboard.msa</groupId>

Some files were not shown because too many files changed in this diff

Loading…
Cancel
Save