diff --git a/application/src/main/data/json/system/scada_symbols/left-analog-water-level-meter.svg b/application/src/main/data/json/system/scada_symbols/left-analog-water-level-meter.svg index 9b7a15bf7c..6eef9bf474 100644 --- a/application/src/main/data/json/system/scada_symbols/left-analog-water-level-meter.svg +++ b/application/src/main/data/json/system/scada_symbols/left-analog-water-level-meter.svg @@ -37,7 +37,7 @@ }, { "tag": "icon", - "stateRenderFunction": "var showIcon = ctx.properties.showIcon;\nif (showIcon) {\n element.show();\n var icon = ctx.properties.icon;\n var iconSize = ctx.properties.iconSize;\n var iconColor = ctx.properties.iconColor;\n ctx.api.icon(element, icon, iconSize, iconColor, true);\n} else {\n element.hide()\n}", + "stateRenderFunction": "var showIcon = ctx.properties.showIcon;\nvar showLabel = ctx.properties.label;\nif (showIcon) {\n element.show();\n var icon = ctx.properties.icon;\n var iconSize = ctx.properties.iconSize;\n var iconColor = ctx.properties.iconColor;\n ctx.api.icon(element, icon, iconSize, iconColor, true);\n if (!showLabel) {\n element.transform({translateX: 83,translateY: 137});\n }\n} else {\n element.hide()\n}\n", "actions": null }, { diff --git a/application/src/main/data/json/system/scada_symbols/right-analog-water-level-meter.svg b/application/src/main/data/json/system/scada_symbols/right-analog-water-level-meter.svg index 592339003f..bc271bbdcf 100644 --- a/application/src/main/data/json/system/scada_symbols/right-analog-water-level-meter.svg +++ b/application/src/main/data/json/system/scada_symbols/right-analog-water-level-meter.svg @@ -37,7 +37,7 @@ }, { "tag": "icon", - "stateRenderFunction": "var showIcon = ctx.properties.showIcon;\nif (showIcon) {\n element.show();\n var icon = ctx.properties.icon;\n var iconSize = ctx.properties.iconSize;\n var iconColor = ctx.properties.iconColor;\n ctx.api.icon(element, icon, iconSize, iconColor, true);\n} else {\n element.hide()\n}", + "stateRenderFunction": "var showIcon = ctx.properties.showIcon;\nvar showLabel = ctx.properties.label;\nif (showIcon) {\n element.show();\n var icon = ctx.properties.icon;\n var iconSize = ctx.properties.iconSize;\n var iconColor = ctx.properties.iconColor;\n ctx.api.icon(element, icon, iconSize, iconColor, true);\n if (!showLabel) {\n element.transform({translateX: 119, translateY: 137});\n }\n} else {\n element.hide()\n}", "actions": null }, { diff --git a/application/src/main/data/json/tenant/dashboards/gateways.json b/application/src/main/data/json/tenant/dashboards/gateways.json index 338762286c..668b92b7b2 100644 --- a/application/src/main/data/json/tenant/dashboards/gateways.json +++ b/application/src/main/data/json/tenant/dashboards/gateways.json @@ -2159,17 +2159,17 @@ } }, { - "type": "entity", + "type": "entityCount", "entityAliasId": "a75d9031-ba51-8da4-81be-de65061b72f4", "filterId": "44038462-1bae-e075-7b31-283341cb2295", "dataKeys": [ { "name": "count", - "type": "entityField", + "type": "count", "label": "modbusCount", - "color": "#4caf50", + "color": "#ff5722", "settings": {}, - "_hash": 0.9300660062254784, + "_hash": 0.46402083951505624, "aggregationType": null, "units": null, "decimals": null, @@ -2192,7 +2192,7 @@ { "name": "count", "type": "count", - "label": "grcpCount", + "label": "grpcCount", "color": "#f44336", "settings": {}, "_hash": 0.16110429492126088, @@ -2605,9 +2605,9 @@ "padding": "0px", "settings": { "useMarkdownTextFunction": false, - "markdownTextPattern": "
\r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n
\r\n", + "markdownTextPattern": "
\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n
\n", "applyDefaultMarkdownStyle": false, - "markdownCss": ".mat-mdc-form-field-subscript-wrapper {\n display: none !important;\n}" + "markdownCss": ".mat-mdc-form-field-subscript-wrapper {\n display: none !important;\n}\n\n.devices-tabs {\n height: 100%;\n}\n\n::ng-deep .mat-mdc-tab-body-wrapper {\n height: 100%;\n}" }, "title": "Gateway devices", "showTitleIcon": false, @@ -6208,7 +6208,7 @@ } }, "gateway_devices_16": { - "name": "gateway_devices_occp", + "name": "gateway_devices_ocpp", "root": false, "layouts": { "main": { diff --git a/application/src/main/java/org/thingsboard/server/actors/ruleChain/RuleChainManagerActor.java b/application/src/main/java/org/thingsboard/server/actors/ruleChain/RuleChainManagerActor.java index f2213c02d0..066f552a50 100644 --- a/application/src/main/java/org/thingsboard/server/actors/ruleChain/RuleChainManagerActor.java +++ b/application/src/main/java/org/thingsboard/server/actors/ruleChain/RuleChainManagerActor.java @@ -95,7 +95,7 @@ public abstract class RuleChainManagerActor extends ContextAwareActor { () -> { RuleChain ruleChain = provider.apply(ruleChainId); if (ruleChain == null) { - return new RuleChainErrorActor.ActorCreator(systemContext, tenantId, + return new RuleChainErrorActor.ActorCreator(systemContext, tenantId, ruleChainId, new RuleEngineException("Rule Chain with id: " + ruleChainId + " not found!")); } else { return new RuleChainActor.ActorCreator(systemContext, tenantId, ruleChain); diff --git a/application/src/main/java/org/thingsboard/server/actors/shared/RuleChainErrorActor.java b/application/src/main/java/org/thingsboard/server/actors/shared/RuleChainErrorActor.java index 4a5e5c6405..cdb3f721f8 100644 --- a/application/src/main/java/org/thingsboard/server/actors/shared/RuleChainErrorActor.java +++ b/application/src/main/java/org/thingsboard/server/actors/shared/RuleChainErrorActor.java @@ -19,16 +19,15 @@ import lombok.extern.slf4j.Slf4j; import org.thingsboard.server.actors.ActorSystemContext; import org.thingsboard.server.actors.TbActor; import org.thingsboard.server.actors.TbActorId; -import org.thingsboard.server.actors.TbStringActorId; +import org.thingsboard.server.actors.TbEntityActorId; import org.thingsboard.server.actors.service.ContextAwareActor; import org.thingsboard.server.actors.service.ContextBasedCreator; +import org.thingsboard.server.common.data.id.RuleChainId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.msg.TbActorMsg; import org.thingsboard.server.common.msg.aware.RuleChainAwareMsg; import org.thingsboard.server.common.msg.queue.RuleEngineException; -import java.util.UUID; - @Slf4j public class RuleChainErrorActor extends ContextAwareActor { @@ -43,9 +42,8 @@ public class RuleChainErrorActor extends ContextAwareActor { @Override protected boolean doProcess(TbActorMsg msg) { - if (msg instanceof RuleChainAwareMsg) { + if (msg instanceof RuleChainAwareMsg rcMsg) { log.debug("[{}] Reply with {} for message {}", tenantId, error.getMessage(), msg); - var rcMsg = (RuleChainAwareMsg) msg; rcMsg.getMsg().getCallback().onFailure(error); return true; } else { @@ -56,17 +54,19 @@ public class RuleChainErrorActor extends ContextAwareActor { public static class ActorCreator extends ContextBasedCreator { private final TenantId tenantId; + private final RuleChainId ruleChainId; private final RuleEngineException error; - public ActorCreator(ActorSystemContext context, TenantId tenantId, RuleEngineException error) { + public ActorCreator(ActorSystemContext context, TenantId tenantId, RuleChainId ruleChainId, RuleEngineException error) { super(context); this.tenantId = tenantId; + this.ruleChainId = ruleChainId; this.error = error; } @Override public TbActorId createActorId() { - return new TbStringActorId(UUID.randomUUID().toString()); + return new TbEntityActorId(ruleChainId); } @Override diff --git a/application/src/main/java/org/thingsboard/server/service/subscription/TbSubscriptionsInfo.java b/application/src/main/java/org/thingsboard/server/service/subscription/TbSubscriptionsInfo.java index 48464d5297..a65fc0bedb 100644 --- a/application/src/main/java/org/thingsboard/server/service/subscription/TbSubscriptionsInfo.java +++ b/application/src/main/java/org/thingsboard/server/service/subscription/TbSubscriptionsInfo.java @@ -20,6 +20,7 @@ import lombok.EqualsAndHashCode; import lombok.RequiredArgsConstructor; import lombok.ToString; +import java.util.HashSet; import java.util.Set; /** @@ -48,7 +49,7 @@ public class TbSubscriptionsInfo { } protected TbSubscriptionsInfo copy(int seqNumber) { - return new TbSubscriptionsInfo(notifications, alarms, tsAllKeys, tsKeys, attrAllKeys, attrKeys, seqNumber); + return new TbSubscriptionsInfo(notifications, alarms, tsAllKeys, tsKeys != null ? new HashSet<>(tsKeys) : null, attrAllKeys, attrKeys != null ? new HashSet<>(attrKeys) : null, seqNumber); } } diff --git a/application/src/test/java/org/thingsboard/server/actors/tenant/TenantActorTest.java b/application/src/test/java/org/thingsboard/server/actors/tenant/TenantActorTest.java index a282452f4e..cad933ccc5 100644 --- a/application/src/test/java/org/thingsboard/server/actors/tenant/TenantActorTest.java +++ b/application/src/test/java/org/thingsboard/server/actors/tenant/TenantActorTest.java @@ -18,57 +18,130 @@ package org.thingsboard.server.actors.tenant; import org.junit.Before; import org.junit.Test; import org.thingsboard.server.actors.ActorSystemContext; +import org.thingsboard.server.actors.DefaultTbActorSystem; import org.thingsboard.server.actors.TbActorCtx; +import org.thingsboard.server.actors.TbActorMailbox; import org.thingsboard.server.actors.TbActorRef; +import org.thingsboard.server.actors.TbActorSystem; +import org.thingsboard.server.actors.TbActorSystemSettings; +import org.thingsboard.server.actors.TbEntityActorId; +import org.thingsboard.server.actors.ruleChain.RuleChainActor; +import org.thingsboard.server.actors.ruleChain.RuleChainToRuleChainMsg; +import org.thingsboard.server.actors.shared.RuleChainErrorActor; +import org.thingsboard.server.common.data.ApiUsageState; import org.thingsboard.server.common.data.id.DeviceId; +import org.thingsboard.server.common.data.id.RuleChainId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.plugin.ComponentLifecycleEvent; +import org.thingsboard.server.common.data.rule.RuleChain; +import org.thingsboard.server.common.msg.TbMsg; import org.thingsboard.server.common.msg.plugin.ComponentLifecycleMsg; import org.thingsboard.server.common.msg.queue.ServiceType; import org.thingsboard.server.common.msg.queue.TopicPartitionInfo; import org.thingsboard.server.common.msg.rule.engine.DeviceDeleteMsg; +import org.thingsboard.server.dao.rule.RuleChainService; import org.thingsboard.server.dao.tenant.TenantService; +import org.thingsboard.server.queue.discovery.PartitionService; +import org.thingsboard.server.queue.discovery.TbServiceInfoProvider; +import org.thingsboard.server.service.apiusage.TbApiUsageStateService; +import java.util.UUID; + +import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; import static org.mockito.Mockito.reset; +import static org.mockito.Mockito.spy; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; +import static org.thingsboard.server.actors.service.DefaultActorService.RULE_DISPATCHER_NAME; public class TenantActorTest { TenantActor tenantActor; - TbActorCtx ctx; ActorSystemContext systemContext; + RuleChainService ruleChainService; + PartitionService partitionService; TenantId tenantId = TenantId.SYS_TENANT_ID; DeviceId deviceId = DeviceId.fromString("78bf9b26-74ef-4af2-9cfb-ad6cf24ad2ec"); + RuleChainId ruleChainId = new RuleChainId(UUID.fromString("48cfa2b0-3dca-11ef-8d1a-37c2894cc59c")); @Before public void setUp() throws Exception { systemContext = mock(ActorSystemContext.class); - ctx = mock(TbActorCtx.class); + ruleChainService = mock(RuleChainService.class); + partitionService = mock(); + + TbServiceInfoProvider serviceInfoProvider = mock(TbServiceInfoProvider.class); + TbApiUsageStateService apiUsageService = mock(TbApiUsageStateService.class); + TenantService tenantService = mock(TenantService.class); + + when(systemContext.getRuleChainService()).thenReturn(ruleChainService); tenantActor = (TenantActor) new TenantActor.ActorCreator(systemContext, tenantId).createActor(); - when(systemContext.getTenantService()).thenReturn(mock(TenantService.class)); - tenantActor.init(ctx); - tenantActor.cantFindTenant = false; + + when(tenantService.findTenantById(tenantId)).thenReturn(mock()); + when(systemContext.getTenantService()).thenReturn(tenantService); + when(serviceInfoProvider.isService(ServiceType.TB_CORE)).thenReturn(true); + when(serviceInfoProvider.isService(ServiceType.TB_RULE_ENGINE)).thenReturn(true); + when(systemContext.getServiceInfoProvider()).thenReturn(serviceInfoProvider); + when(partitionService.isManagedByCurrentService(tenantId)).thenReturn(true); + when(systemContext.getPartitionService()).thenReturn(partitionService); + when(systemContext.getApiUsageStateService()).thenReturn(apiUsageService); + when(apiUsageService.getApiUsageState(tenantId)).thenReturn(new ApiUsageState()); } @Test - public void deleteDeviceTest() { + public void deleteDeviceTest() throws Exception { + TbActorCtx ctx = mock(TbActorCtx.class); + tenantActor.init(ctx); TbActorRef deviceActorRef = mock(TbActorRef.class); - when(systemContext.resolve(ServiceType.TB_CORE, tenantId, deviceId)).thenReturn(new TopicPartitionInfo("Main", tenantId, 0,true)); + when(systemContext.resolve(ServiceType.TB_CORE, tenantId, deviceId)).thenReturn(new TopicPartitionInfo("Main", tenantId, 0, true)); when(ctx.getOrCreateChildActor(any(), any(), any(), any())).thenReturn(deviceActorRef); + ComponentLifecycleMsg componentLifecycleMsg = new ComponentLifecycleMsg(tenantId, deviceId, ComponentLifecycleEvent.DELETED); tenantActor.doProcess(componentLifecycleMsg); verify(deviceActorRef).tellWithHighPriority(eq(new DeviceDeleteMsg(tenantId, deviceId))); - reset(ctx, deviceActorRef); - when(systemContext.resolve(ServiceType.TB_CORE, tenantId, deviceId)).thenReturn(new TopicPartitionInfo("Main", tenantId, 1,false)); + + when(systemContext.resolve(ServiceType.TB_CORE, tenantId, deviceId)).thenReturn(new TopicPartitionInfo("Main", tenantId, 1, false)); tenantActor.doProcess(componentLifecycleMsg); verify(ctx, never()).getOrCreateChildActor(any(), any(), any(), any()); verify(deviceActorRef, never()).tellWithHighPriority(any()); } + @Test + public void ruleChainErrorActorTest() throws Exception { + TbActorSystemSettings settings = new TbActorSystemSettings(0, 0, 0); + TbActorSystem system = spy(new DefaultTbActorSystem(settings)); + system.createDispatcher(RULE_DISPATCHER_NAME, mock()); + TbActorMailbox tenantCtx = new TbActorMailbox(system, settings, null, mock(), mock(), null); + tenantActor.init(tenantCtx); + + TbMsg msg = mock(TbMsg.class); + + when(ruleChainService.findRuleChainById(tenantId, ruleChainId)).thenReturn(new RuleChain(ruleChainId)); + + RuleChainToRuleChainMsg ruleChainMsg = new RuleChainToRuleChainMsg(ruleChainId, null, msg, null); + tenantActor.doProcess(ruleChainMsg); + verify(system).createChildActor(eq(RULE_DISPATCHER_NAME), any(RuleChainActor.ActorCreator.class), any()); + reset(system); + tenantActor.doProcess(ruleChainMsg); + verify(system, never()).createChildActor(any(), any(), any()); + + //Delete rule-chain + TbActorRef ruleChainActor = system.getActor(new TbEntityActorId(ruleChainId)); + assertNotNull(ruleChainActor); + system.stop(ruleChainActor); + when(ruleChainService.findRuleChainById(tenantId, ruleChainId)).thenReturn(null); + + tenantActor.doProcess(ruleChainMsg); + verify(system).createChildActor(eq(RULE_DISPATCHER_NAME), any(RuleChainErrorActor.ActorCreator.class), any()); + reset(system); + tenantActor.doProcess(ruleChainMsg); + verify(system, never()).createChildActor(any(), any(), any()); + system.stop(); + } + } \ No newline at end of file diff --git a/application/src/test/java/org/thingsboard/server/service/subscription/DefaultTbLocalSubscriptionServiceTest.java b/application/src/test/java/org/thingsboard/server/service/subscription/DefaultTbLocalSubscriptionServiceTest.java new file mode 100644 index 0000000000..1ec80b91f7 --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/service/subscription/DefaultTbLocalSubscriptionServiceTest.java @@ -0,0 +1,126 @@ +/** + * Copyright © 2016-2024 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.subscription; + +import ch.qos.logback.classic.Logger; +import ch.qos.logback.classic.spi.ILoggingEvent; +import ch.qos.logback.core.read.ListAppender; +import com.google.common.util.concurrent.Futures; +import com.google.common.util.concurrent.ListenableFuture; +import com.google.common.util.concurrent.ListeningExecutorService; +import com.google.common.util.concurrent.MoreExecutors; +import org.apache.commons.lang3.RandomStringUtils; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.slf4j.LoggerFactory; +import org.springframework.test.util.ReflectionTestUtils; +import org.thingsboard.server.cache.limits.RateLimitService; +import org.thingsboard.server.common.data.id.DeviceId; +import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.limit.LimitedApi; +import org.thingsboard.server.common.msg.queue.TopicPartitionInfo; +import org.thingsboard.server.gen.transport.TransportProtos; +import org.thingsboard.server.queue.discovery.PartitionService; +import org.thingsboard.server.service.ws.WebSocketSessionRef; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.UUID; +import java.util.concurrent.Executors; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.ArgumentMatchers.nullable; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +public class DefaultTbLocalSubscriptionServiceTest { + + ListAppender testLogAppender; + TbLocalSubscriptionService subscriptionService; + + @BeforeEach + public void setUp() throws Exception { + Logger logger = (Logger) LoggerFactory.getLogger(DefaultTbLocalSubscriptionService.class); + testLogAppender = new ListAppender<>(); + testLogAppender.start(); + logger.addAppender(testLogAppender); + + RateLimitService rateLimitService = mock(); + when(rateLimitService.checkRateLimit(eq(LimitedApi.WS_SUBSCRIPTIONS), any(Object.class), nullable(String.class))).thenReturn(true); + PartitionService partitionService = mock(); + when(partitionService.resolve(any(), any(), any())).thenReturn(TopicPartitionInfo.builder().build()); + subscriptionService = new DefaultTbLocalSubscriptionService(mock(), mock(), mock(), partitionService, mock(), mock(), mock(), rateLimitService); + ReflectionTestUtils.setField(subscriptionService, "serviceId", "serviceId"); + } + + @AfterEach + public void tearDown() { + if (testLogAppender != null) { + testLogAppender.stop(); + Logger logger = (Logger) LoggerFactory.getLogger(DefaultTbLocalSubscriptionService.class); + logger.detachAppender(testLogAppender); + } + } + + @Test + public void addSubscriptionConcurrentModificationTest() throws Exception { + ListeningExecutorService executorService = MoreExecutors.listeningDecorator(Executors.newFixedThreadPool(10)); + TenantId tenantId = new TenantId(UUID.randomUUID()); + DeviceId deviceId = new DeviceId(UUID.randomUUID()); + WebSocketSessionRef sessionRef = mock(); + ReflectionTestUtils.setField(subscriptionService, "subscriptionUpdateExecutor", executorService); + + List> futures = new ArrayList<>(); + + try { + subscriptionService.onCoreStartupMsg(TransportProtos.CoreStartupMsg.newBuilder().addAllPartitions(List.of(0)).getDefaultInstanceForType()); + for (int i = 0; i < 50; i++) { + futures.add(executorService.submit(() -> subscriptionService.addSubscription(createSubscription(tenantId, deviceId), sessionRef))); + } + Futures.allAsList(futures).get(); + } finally { + executorService.shutdownNow(); + } + + List logs = testLogAppender.list; + boolean exceptionLogged = logs.stream() + .filter(event -> event.getThrowableProxy() != null) + .map(event -> event.getThrowableProxy().getClassName()) + .anyMatch(log -> log.equals("java.util.ConcurrentModificationException")); + + assertFalse(exceptionLogged, "Detected ConcurrentModificationException!"); + } + + private TbSubscription createSubscription(TenantId tenantId, EntityId entityId) { + Map keys = new HashMap<>(); + for (int i = 0; i < 50; i++) { + keys.put(RandomStringUtils.randomAlphanumeric(5), 1L); + } + return TbAttributeSubscription.builder() + .tenantId(tenantId) + .entityId(entityId) + .subscriptionId(1) + .sessionId(RandomStringUtils.randomAlphanumeric(5)) + .keyStates(keys) + .build(); + } +} diff --git a/application/src/test/java/org/thingsboard/server/transport/lwm2m/Lwm2mTestHelper.java b/application/src/test/java/org/thingsboard/server/transport/lwm2m/Lwm2mTestHelper.java index c627c77cd4..4aa47eddf5 100644 --- a/application/src/test/java/org/thingsboard/server/transport/lwm2m/Lwm2mTestHelper.java +++ b/application/src/test/java/org/thingsboard/server/transport/lwm2m/Lwm2mTestHelper.java @@ -42,6 +42,7 @@ public class Lwm2mTestHelper { public static final int RESOURCE_ID_11 = 11; public static final int RESOURCE_ID_14 = 14; public static final int RESOURCE_ID_15 = 15; + public static final int RESOURCE_ID_5700 = 5700; public static final int RESOURCE_INSTANCE_ID_0 = 0; public static final int RESOURCE_INSTANCE_ID_2 = 2; @@ -51,6 +52,12 @@ public class Lwm2mTestHelper { public static final String RESOURCE_ID_NAME_19_0_2 = "dataCreationTime"; public static final String RESOURCE_ID_NAME_19_1_0 = "dataWrite"; public static final String RESOURCE_ID_NAME_19_0_3 = "dataDescription"; + public static final String RESOURCE_ID_NAME_3303_12_5700 = "sensorValue"; + public static final double RESOURCE_ID_3303_12_5700_VALUE_0 = 25.05d; + public static final double RESOURCE_ID_3303_12_5700_VALUE_1 = 35.12d; + public static long RESOURCE_ID_3303_12_5700_TS_0 = 0; + public static long RESOURCE_ID_3303_12_5700_TS_1 = 0; + public static final int RESOURCE_ID_VALUE_3303_12_5700_DELTA_TS = 3000; public enum LwM2MClientState { @@ -72,8 +79,8 @@ public class Lwm2mTestHelper { ON_DEREGISTRATION_FAILURE(14, "onDeregistrationFailure"), ON_DEREGISTRATION_TIMEOUT(15, "onDeregistrationTimeout"), ON_EXPECTED_ERROR(16, "onUnexpectedError"), - ON_READ_CONNECTION_ID (17, "onReadConnection"), - ON_WRITE_CONNECTION_ID (18, "onWriteConnection"); + ON_READ_CONNECTION_ID(17, "onReadConnection"), + ON_WRITE_CONNECTION_ID(18, "onWriteConnection"); public int code; public String type; diff --git a/application/src/test/java/org/thingsboard/server/transport/lwm2m/client/LwM2MTestClient.java b/application/src/test/java/org/thingsboard/server/transport/lwm2m/client/LwM2MTestClient.java index b79399170e..655edc6db6 100644 --- a/application/src/test/java/org/thingsboard/server/transport/lwm2m/client/LwM2MTestClient.java +++ b/application/src/test/java/org/thingsboard/server/transport/lwm2m/client/LwM2MTestClient.java @@ -137,7 +137,6 @@ public class LwM2MTestClient { private Map clientDtlsCid; private LwM2mUplinkMsgHandler defaultLwM2mUplinkMsgHandlerTest; private LwM2mClientContext clientContext; - public void init(Security security, Security securityBs, int port, boolean isRpc, LwM2mUplinkMsgHandler defaultLwM2mUplinkMsgHandler, LwM2mClientContext clientContext, boolean isWriteAttribute, Integer cIdLength, boolean queueMode, @@ -159,11 +158,11 @@ public class LwM2MTestClient { initializer.setClassForObject(SECURITY, Security.class); initializer.setInstancesForObject(SECURITY, instances); // SERVER - Server lwm2mServer = new Server(shortServerId, TimeUnit.MINUTES.toSeconds(60)); + Server lwm2mServer = new Server(shortServerId, TimeUnit.MINUTES.toSeconds(60)); lwm2mServer.setId(serverId); - Server serverBs = new Server(shortServerIdBs0, TimeUnit.MINUTES.toSeconds(60)); + Server serverBs = new Server(shortServerIdBs0, TimeUnit.MINUTES.toSeconds(60)); serverBs.setId(serverIdBs); - instances = new LwM2mInstanceEnabler[]{serverBs, lwm2mServer}; + instances = new LwM2mInstanceEnabler[]{serverBs, lwm2mServer}; initializer.setClassForObject(SERVER, Server.class); initializer.setInstancesForObject(SERVER, instances); } else if (securityBs != null) { @@ -177,7 +176,7 @@ public class LwM2MTestClient { // SERVER Server lwm2mServer = new Server(shortServerId, TimeUnit.MINUTES.toSeconds(60)); lwm2mServer.setId(serverId); - initializer.setInstancesForObject(SERVER, lwm2mServer ); + initializer.setInstancesForObject(SERVER, lwm2mServer); } initializer.setInstancesForObject(DEVICE, lwM2MDevice = new SimpleLwM2MDevice(executor)); @@ -239,11 +238,11 @@ public class LwM2MTestClient { boolean supportDeprecatedCiphers = false; clientCoapConfig.set(DTLS_RECOMMENDED_CIPHER_SUITES_ONLY, !supportDeprecatedCiphers); - if (cIdLength!= null) { + if (cIdLength != null) { setDtlsConnectorConfigCidLength(clientCoapConfig, cIdLength); } - if (cIdLength!= null) { + if (cIdLength != null) { setDtlsConnectorConfigCidLength(clientCoapConfig, cIdLength); } @@ -262,12 +261,12 @@ public class LwM2MTestClient { // Configure Registration Engine DefaultRegistrationEngineFactory engineFactory = new DefaultRegistrationEngineFactory(); - // old + // old /** * Force reconnection/rehandshake on registration update. */ int comPeriodInSec = 5; - if (comPeriodInSec > 0) engineFactory.setCommunicationPeriod(comPeriodInSec * 1000); + if (comPeriodInSec > 0) engineFactory.setCommunicationPeriod(comPeriodInSec * 1000); // engineFactory.setCommunicationPeriod(5000); // old /** * By default client will try to resume DTLS session by using abbreviated Handshake. This option force to always do a full handshake." @@ -288,7 +287,7 @@ public class LwM2MTestClient { builder.setDataSenders(new ManualDataSender()); builder.setRegistrationEngineFactory(engineFactory); Map decoders = new HashMap<>(); - Map encoders = new HashMap<>(); + Map encoders = new HashMap<>(); if (supportFormatOnly_SenMLJSON_SenMLCBOR) { // decoders.put(ContentFormat.OPAQUE, new LwM2mNodeOpaqueDecoder()); decoders.put(ContentFormat.CBOR, new LwM2mNodeCborDecoder()); diff --git a/application/src/test/java/org/thingsboard/server/transport/lwm2m/client/LwM2mTemperatureSensor.java b/application/src/test/java/org/thingsboard/server/transport/lwm2m/client/LwM2mTemperatureSensor.java index cfdef139c2..4f594ed4c0 100644 --- a/application/src/test/java/org/thingsboard/server/transport/lwm2m/client/LwM2mTemperatureSensor.java +++ b/application/src/test/java/org/thingsboard/server/transport/lwm2m/client/LwM2mTemperatureSensor.java @@ -26,16 +26,20 @@ import org.eclipse.leshan.core.request.ContentFormat; import org.eclipse.leshan.core.request.argument.Arguments; import org.eclipse.leshan.core.response.ExecuteResponse; import org.eclipse.leshan.core.response.ReadResponse; +import org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper; import javax.security.auth.Destroyable; import java.math.BigDecimal; import java.math.RoundingMode; -import java.util.ArrayList; +import java.time.Instant; import java.util.Arrays; import java.util.List; import java.util.Random; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; +import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.RESOURCE_ID_3303_12_5700_VALUE_0; +import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.RESOURCE_ID_3303_12_5700_VALUE_1; +import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.RESOURCE_ID_VALUE_3303_12_5700_DELTA_TS; @Slf4j public class LwM2mTemperatureSensor extends BaseInstanceEnabler implements Destroyable { @@ -46,7 +50,9 @@ public class LwM2mTemperatureSensor extends BaseInstanceEnabler implements Destr private double maxMeasuredValue = currentTemp; private LeshanClient leshanClient; - private List containingValues; + private int cntRead_5700; + private int cntIdentitySystem; + protected static final Random RANDOM = new Random(); private static final List supportedResources = Arrays.asList(5601, 5602, 5700, 5701); @@ -57,7 +63,7 @@ public class LwM2mTemperatureSensor extends BaseInstanceEnabler implements Destr public LwM2mTemperatureSensor(ScheduledExecutorService executorService, Integer id) { try { if (id != null) this.setId(id); - executorService.scheduleWithFixedDelay(this::adjustTemperature, 2000, 2000, TimeUnit.MILLISECONDS); + executorService.scheduleWithFixedDelay(this::adjustTemperature, 2000, 2000, TimeUnit.MILLISECONDS); } catch (Throwable e) { log.error("[{}]Throwable", e.toString()); e.printStackTrace(); @@ -73,15 +79,18 @@ public class LwM2mTemperatureSensor extends BaseInstanceEnabler implements Destr case 5602: return ReadResponse.success(resourceId, getTwoDigitValue(maxMeasuredValue)); case 5700: - if (identity == LwM2mServer.SYSTEM) { - setTemperature(); - setData(); + if (identity == LwM2mServer.SYSTEM) { // return value for ForCollectedValue + cntIdentitySystem++; + return ReadResponse.success(resourceId, cntIdentitySystem == 1 ? + RESOURCE_ID_3303_12_5700_VALUE_0 : RESOURCE_ID_3303_12_5700_VALUE_1); + } + cntRead_5700++; + if (cntRead_5700 == 1) { // read value after start return ReadResponse.success(resourceId, getTwoDigitValue(currentTemp)); - } else if (this.getId() == 12 && this.leshanClient != null) { - containingValues = new ArrayList<>(); - sendCollected(5700); - return ReadResponse.success(resourceId, getData()); } else { + if (this.getId() == 12 && this.leshanClient != null) { + sendCollected(); + } return ReadResponse.success(resourceId, getTwoDigitValue(currentTemp)); } case 5701: @@ -117,10 +126,11 @@ public class LwM2mTemperatureSensor extends BaseInstanceEnabler implements Destr } } - private void setTemperature(){ + private void setTemperature() { float delta = (RANDOM.nextInt(20) - 10) / 10f; currentTemp += delta; } + private synchronized Integer adjustMinMaxMeasuredValue(double newTemperature) { if (newTemperature > maxMeasuredValue) { maxMeasuredValue = newTemperature; @@ -143,7 +153,7 @@ public class LwM2mTemperatureSensor extends BaseInstanceEnabler implements Destr return supportedResources; } - protected void setLeshanClient(LeshanClient leshanClient){ + protected void setLeshanClient(LeshanClient leshanClient) { this.leshanClient = leshanClient; } @@ -151,40 +161,26 @@ public class LwM2mTemperatureSensor extends BaseInstanceEnabler implements Destr public void destroy() { } - private void sendCollected(int resourceId) { + private void sendCollected() { try { + int resourceId = 5700; LwM2mServer registeredServer = this.leshanClient.getRegisteredServers().values().iterator().next(); ManualDataSender sender = this.leshanClient.getSendService().getDataSender(ManualDataSender.DEFAULT_NAME, ManualDataSender.class); sender.collectData(Arrays.asList(getPathForCollectedValue(resourceId))); - Thread.sleep(1000); + Lwm2mTestHelper.RESOURCE_ID_3303_12_5700_TS_0 = Instant.now().toEpochMilli(); + Thread.sleep(RESOURCE_ID_VALUE_3303_12_5700_DELTA_TS); sender.collectData(Arrays.asList(getPathForCollectedValue(resourceId))); + Lwm2mTestHelper.RESOURCE_ID_3303_12_5700_TS_1 = Instant.now().toEpochMilli(); sender.sendCollectedData(registeredServer, ContentFormat.SENML_JSON, 1000, false); } catch (InterruptedException e) { throw new RuntimeException(e); } } + private LwM2mPath getPathForCollectedValue(int resourceId) { return new LwM2mPath(3303, this.getId(), resourceId); } - - private double getData() { - if (containingValues.size() > 1) { - Integer t0 = Math.toIntExact(Math.round(containingValues.get(0) * 100)); - Integer t1 = Math.toIntExact(Math.round(containingValues.get(1) * 100)); - long to_t1 = (((long) t0) << 32) | (t1 & 0xffffffffL); - return Double.longBitsToDouble(to_t1); - } else { - return currentTemp; - } - - } - - private void setData() { - if (containingValues == null){ - containingValues = new ArrayList<>(); - } - containingValues.add(getTwoDigitValue(currentTemp)); - } } + diff --git a/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/AbstractRpcLwM2MIntegrationTest.java b/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/AbstractRpcLwM2MIntegrationTest.java index 1f61a08e15..d0b86fdab0 100644 --- a/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/AbstractRpcLwM2MIntegrationTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/AbstractRpcLwM2MIntegrationTest.java @@ -15,20 +15,30 @@ */ package org.thingsboard.server.transport.lwm2m.rpc; +import lombok.extern.slf4j.Slf4j; import org.eclipse.leshan.core.link.LinkParser; import org.eclipse.leshan.core.link.lwm2m.DefaultLwM2mLinkParser; import org.junit.Before; +import org.mockito.Mockito; +import org.springframework.boot.test.mock.mockito.SpyBean; import org.thingsboard.server.common.data.Device; import org.thingsboard.server.common.data.device.credentials.lwm2m.LwM2MDeviceCredentials; import org.thingsboard.server.common.data.device.profile.Lwm2mDeviceProfileTransportConfiguration; import org.thingsboard.server.dao.service.DaoSqlTest; +import org.thingsboard.server.gen.transport.TransportProtos; import org.thingsboard.server.transport.lwm2m.AbstractLwM2MIntegrationTest; +import org.thingsboard.server.transport.lwm2m.server.LwM2mTransportServerHelper; +import org.thingsboard.server.transport.lwm2m.server.uplink.DefaultLwM2mUplinkMsgHandler; +import java.util.List; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicLong; import java.util.function.Predicate; +import static org.awaitility.Awaitility.await; import static org.eclipse.leshan.core.LwM2mId.ACCESS_CONTROL; import static org.eclipse.leshan.core.LwM2mId.DEVICE; import static org.eclipse.leshan.core.LwM2mId.FIRMWARE; @@ -40,19 +50,23 @@ import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.OBJECT_ID_0 import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.OBJECT_ID_1; import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.OBJECT_INSTANCE_ID_0; import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.OBJECT_INSTANCE_ID_1; +import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.OBJECT_INSTANCE_ID_12; import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.RESOURCE_ID_0; import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.RESOURCE_ID_14; import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.RESOURCE_ID_2; +import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.RESOURCE_ID_5700; import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.RESOURCE_ID_9; import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.RESOURCE_ID_NAME_19_0_0; import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.RESOURCE_ID_NAME_19_0_2; import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.RESOURCE_ID_NAME_19_1_0; +import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.RESOURCE_ID_NAME_3303_12_5700; import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.RESOURCE_ID_NAME_3_14; import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.RESOURCE_ID_NAME_3_9; import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.TEMPERATURE_SENSOR; import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.resources; import static org.thingsboard.server.transport.lwm2m.utils.LwM2MTransportUtil.fromVersionedIdToObjectId; +@Slf4j @DaoSqlTest public abstract class AbstractRpcLwM2MIntegrationTest extends AbstractLwM2MIntegrationTest { @@ -84,6 +98,12 @@ public abstract class AbstractRpcLwM2MIntegrationTest extends AbstractLwM2MInteg protected String idVer_19_0_0; + @SpyBean + protected DefaultLwM2mUplinkMsgHandler defaultUplinkMsgHandlerTest; + + @SpyBean + protected LwM2mTransportServerHelper lwM2mTransportServerHelperTest; + public AbstractRpcLwM2MIntegrationTest() { setResources(resources); } @@ -144,7 +164,8 @@ public abstract class AbstractRpcLwM2MIntegrationTest extends AbstractLwM2MInteg " \"" + objectIdVer_3 + "/" + OBJECT_INSTANCE_ID_0 + "/" + RESOURCE_ID_14 + "\": \"" + RESOURCE_ID_NAME_3_14 + "\",\n" + " \"" + idVer_19_0_0 + "\": \"" + RESOURCE_ID_NAME_19_0_0 + "\",\n" + " \"" + objectIdVer_19 + "/" + OBJECT_INSTANCE_ID_1 + "/" + RESOURCE_ID_0 + "\": \"" + RESOURCE_ID_NAME_19_1_0 + "\",\n" + - " \"" + objectIdVer_19 + "/" + OBJECT_INSTANCE_ID_0 + "/" + RESOURCE_ID_2 + "\": \"" + RESOURCE_ID_NAME_19_0_2 + "\"\n" + + " \"" + objectIdVer_19 + "/" + OBJECT_INSTANCE_ID_0 + "/" + RESOURCE_ID_2 + "\": \"" + RESOURCE_ID_NAME_19_0_2 + "\",\n" + + " \"" + objectIdVer_3303 + "/" + OBJECT_INSTANCE_ID_12 + "/" + RESOURCE_ID_5700 + "\": \"" + RESOURCE_ID_NAME_3303_12_5700 + "\"\n" + " },\n" + " \"observe\": [\n" + " \"" + idVer_3_0_9 + "\",\n" + @@ -159,7 +180,8 @@ public abstract class AbstractRpcLwM2MIntegrationTest extends AbstractLwM2MInteg " \"telemetry\": [\n" + " \"" + idVer_3_0_9 + "\",\n" + " \"" + idVer_19_0_0 + "\",\n" + - " \"" + objectIdVer_19 + "/" + OBJECT_INSTANCE_ID_1 + "/" + RESOURCE_ID_0 + "\"\n" + + " \"" + objectIdVer_19 + "/" + OBJECT_INSTANCE_ID_1 + "/" + RESOURCE_ID_0 + "\",\n" + + " \"" + objectIdVer_3303 + "/" + OBJECT_INSTANCE_ID_12 + "/" + RESOURCE_ID_5700 + "\"\n" + " ],\n" + " \"attributeLwm2m\": {}\n" + " }"; @@ -183,4 +205,56 @@ public abstract class AbstractRpcLwM2MIntegrationTest extends AbstractLwM2MInteg return pathIdVer; } + protected long countUpdateAttrTelemetryAll() { + return Mockito.mockingDetails(defaultUplinkMsgHandlerTest) + .getInvocations().stream() + .filter(invocation -> invocation.getMethod().getName().equals("updateAttrTelemetry")) + .count(); + } + + protected long countUpdateAttrTelemetryResource(String idVerRez) { + return Mockito.mockingDetails(defaultUplinkMsgHandlerTest) + .getInvocations().stream() + .filter(invocation -> + invocation.getMethod().getName().equals("updateAttrTelemetry") && + invocation.getArguments().length > 1 && + idVerRez.equals(invocation.getArguments()[1]) + ) + .count(); + } + + protected void updateRegAtLeastOnceAfterAction() { + long initialInvocationCount = countUpdateReg(); + AtomicLong newInvocationCount = new AtomicLong(initialInvocationCount); + log.trace("updateRegAtLeastOnceAfterAction: initialInvocationCount [{}]", initialInvocationCount); + await("Update Registration at-least-once after action") + .atMost(50, TimeUnit.SECONDS) + .until(() -> { + newInvocationCount.set(countUpdateReg()); + return newInvocationCount.get() > initialInvocationCount; + }); + log.trace("updateRegAtLeastOnceAfterAction: newInvocationCount [{}]", newInvocationCount.get()); + } + + protected long countUpdateReg() { + return Mockito.mockingDetails(defaultUplinkMsgHandlerTest) + .getInvocations().stream() + .filter(invocation -> invocation.getMethod().getName().equals("updatedReg")) + .count(); + } + + protected long countSendParametersOnThingsboardTelemetryResource(String rezName) { + return Mockito.mockingDetails(lwM2mTransportServerHelperTest) + .getInvocations().stream() + .filter(invocation -> + invocation.getMethod().getName().equals("sendParametersOnThingsboardTelemetry") && + invocation.getArguments().length > 0 && + invocation.getArguments()[0] instanceof List && + ((List) invocation.getArguments()[0]).stream() + .filter(arg -> arg instanceof TransportProtos.KeyValueProto) + .anyMatch(arg -> rezName.equals(((TransportProtos.KeyValueProto) arg).getKey())) + ) + .count(); + } + } diff --git a/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2MIntegrationObserveCompositeTest.java b/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2MIntegrationObserveCompositeTest.java index cf78c58288..a1ad762aaa 100644 --- a/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2MIntegrationObserveCompositeTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2MIntegrationObserveCompositeTest.java @@ -20,11 +20,8 @@ import com.fasterxml.jackson.databind.node.ObjectNode; import lombok.extern.slf4j.Slf4j; import org.eclipse.leshan.core.ResponseCode; import org.junit.Test; -import org.mockito.Mockito; -import org.springframework.boot.test.mock.mockito.SpyBean; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.server.transport.lwm2m.rpc.AbstractRpcLwM2MIntegrationObserveTest; -import org.thingsboard.server.transport.lwm2m.server.uplink.DefaultLwM2mUplinkMsgHandler; import java.util.Objects; import java.util.concurrent.TimeUnit; @@ -55,10 +52,6 @@ import static org.thingsboard.server.transport.lwm2m.utils.LwM2MTransportUtil.fr @Slf4j public class RpcLwm2MIntegrationObserveCompositeTest extends AbstractRpcLwM2MIntegrationObserveTest { - @SpyBean - DefaultLwM2mUplinkMsgHandler defaultUplinkMsgHandlerTest; - - /** * ObserveComposite {"ids":["5/0/7", "5/0/5", "5/0/3", "3/0/9", "19/1/0/0"]} - Ok * @throws Exception @@ -517,13 +510,6 @@ public class RpcLwm2MIntegrationObserveCompositeTest extends AbstractRpcLwM2MInt return doPostAsync("/api/plugins/rpc/twoway/" + deviceId, sendRpcRequest, String.class, status().isOk()); } - private long countUpdateAttrTelemetryAll() { - return Mockito.mockingDetails(defaultUplinkMsgHandlerTest) - .getInvocations().stream() - .filter(invocation -> invocation.getMethod().getName().equals("updateAttrTelemetry")) - .count(); - } - private void updateAttrTelemetryAllAtLeastOnceAfterAction(long initialInvocationCount) { AtomicLong newInvocationCount = new AtomicLong(initialInvocationCount); log.warn("countUpdateAttrTelemetryAllAtLeastOnceAfterAction: initialInvocationCount [{}]", initialInvocationCount); @@ -536,19 +522,6 @@ public class RpcLwm2MIntegrationObserveCompositeTest extends AbstractRpcLwM2MInt log.warn("countUpdateAttrTelemetryAllAtLeastOnceAfterAction: newInvocationCount [{}]", newInvocationCount.get()); } - - private long countUpdateAttrTelemetryResource(String idVerRez) { - return Mockito.mockingDetails(defaultUplinkMsgHandlerTest) - .getInvocations().stream() - .filter(invocation -> - invocation.getMethod().getName().equals("updateAttrTelemetry") && - invocation.getArguments().length > 1 && - idVerRez.equals(invocation.getArguments()[1]) - ) - .count(); - } - - private void updateAttrTelemetryResourceAtLeastOnceAfterAction(long initialInvocationCount, String idVerRez) { AtomicLong newInvocationCount = new AtomicLong(initialInvocationCount); log.warn("countUpdateAttrTelemetryResourceAtLeastOnceAfterAction: initialInvocationCount [{}]", initialInvocationCount); @@ -560,24 +533,4 @@ public class RpcLwm2MIntegrationObserveCompositeTest extends AbstractRpcLwM2MInt }); log.warn("countUpdateAttrTelemetryResourceAtLeastOnceAfterAction: newInvocationCount [{}]", newInvocationCount.get()); } - - private long countUpdateReg() { - return Mockito.mockingDetails(defaultUplinkMsgHandlerTest) - .getInvocations().stream() - .filter(invocation -> invocation.getMethod().getName().equals("updatedReg")) - .count(); - } - - private void updateRegAtLeastOnceAfterAction() { - long initialInvocationCount = countUpdateReg(); - AtomicLong newInvocationCount = new AtomicLong(initialInvocationCount); - log.warn("updateRegAtLeastOnceAfterAction: initialInvocationCount [{}]", initialInvocationCount); - await("Update Registration at-least-once after action") - .atMost(50, TimeUnit.SECONDS) - .until(() -> { - newInvocationCount.set(countUpdateReg()); - return newInvocationCount.get() > initialInvocationCount; - }); - log.warn("updateRegAtLeastOnceAfterAction: newInvocationCount [{}]", newInvocationCount.get()); - } } diff --git a/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2mIntegrationObserveTest.java b/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2mIntegrationObserveTest.java index ce2622c4db..a665f7f53c 100644 --- a/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2mIntegrationObserveTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2mIntegrationObserveTest.java @@ -24,9 +24,7 @@ import org.eclipse.leshan.core.response.ReadResponse; import org.eclipse.leshan.server.registration.Registration; import org.junit.Test; import org.mockito.Mockito; -import org.springframework.boot.test.mock.mockito.SpyBean; import org.thingsboard.server.transport.lwm2m.rpc.AbstractRpcLwM2MIntegrationObserveTest; -import org.thingsboard.server.transport.lwm2m.server.uplink.DefaultLwM2mUplinkMsgHandler; import java.util.Optional; @@ -41,14 +39,12 @@ import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.OBJECT_INST import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.RESOURCE_ID_0; import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.RESOURCE_ID_2; import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.RESOURCE_ID_3; +import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.RESOURCE_ID_NAME_3_9; import static org.thingsboard.server.transport.lwm2m.utils.LwM2MTransportUtil.fromVersionedIdToObjectId; @Slf4j public class RpcLwm2mIntegrationObserveTest extends AbstractRpcLwM2MIntegrationObserveTest { - @SpyBean - DefaultLwM2mUplinkMsgHandler defaultUplinkMsgHandlerTest; - @Test public void testObserveReadAll_Count_4_CancelAll_Count_0_Ok() throws Exception { String actualValuesReadAll = sendRpcObserveOkWithResultValue("ObserveReadAll", null); @@ -64,12 +60,12 @@ public class RpcLwm2mIntegrationObserveTest extends AbstractRpcLwM2MIntegrationO */ @Test public void testObserveOneResource_Result_CONTENT_Value_Count_3_After_Cancel_Count_2() throws Exception { + long initSendTelemetryAtCount = countSendParametersOnThingsboardTelemetryResource(RESOURCE_ID_NAME_3_9); sendObserveCancelAllWithAwait(deviceId); sendRpcObserveWithContainsLwM2mSingleResource(idVer_3_0_9); - - int cntUpdate = 3; - verify(defaultUplinkMsgHandlerTest, timeout(10000).times(cntUpdate)) - .onUpdateValueAfterReadResponse(Mockito.any(Registration.class), eq(idVer_3_0_9), Mockito.any(ReadResponse.class)); + updateRegAtLeastOnceAfterAction(); + long lastSendTelemetryAtCount = countSendParametersOnThingsboardTelemetryResource(RESOURCE_ID_NAME_3_9); + assertTrue(lastSendTelemetryAtCount > initSendTelemetryAtCount); } /** @@ -84,7 +80,7 @@ public class RpcLwm2mIntegrationObserveTest extends AbstractRpcLwM2MIntegrationO int cntUpdate = 3; verify(defaultUplinkMsgHandlerTest, timeout(10000).times(cntUpdate)) - .updateAttrTelemetry(Mockito.any(Registration.class), eq(idVer_3_0_9)); + .updateAttrTelemetry(Mockito.any(Registration.class), eq(idVer_3_0_9), eq(null)); } /** @@ -99,7 +95,7 @@ public class RpcLwm2mIntegrationObserveTest extends AbstractRpcLwM2MIntegrationO int cntUpdate = 3; verify(defaultUplinkMsgHandlerTest, timeout(10000).times(cntUpdate)) - .updateAttrTelemetry(Mockito.any(Registration.class), eq(idVer_3_0_9)); + .updateAttrTelemetry(Mockito.any(Registration.class), eq(idVer_3_0_9), eq(null)); } /** @@ -334,7 +330,7 @@ public class RpcLwm2mIntegrationObserveTest extends AbstractRpcLwM2MIntegrationO cntUpdate = 10; verify(defaultUplinkMsgHandlerTest, timeout(50000).atLeast(cntUpdate)) - .updateAttrTelemetry(Mockito.any(Registration.class), eq(idVer_3_0_9)); + .updateAttrTelemetry(Mockito.any(Registration.class), eq(idVer_3_0_9), eq(null)); } private void sendRpcObserveWithWithTwoResource(String expectedId_1, String expectedId_2) throws Exception { diff --git a/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2mIntegrationReadTest.java b/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2mIntegrationReadTest.java index 6d4e11ffc1..1ab4893ec4 100644 --- a/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2mIntegrationReadTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2mIntegrationReadTest.java @@ -15,30 +15,26 @@ */ package org.thingsboard.server.transport.lwm2m.rpc.sql; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ArrayNode; import com.fasterxml.jackson.databind.node.ObjectNode; import lombok.extern.slf4j.Slf4j; +import org.apache.commons.collections4.map.HashedMap; import org.eclipse.leshan.core.ResponseCode; -import org.eclipse.leshan.core.node.LwM2mNode; import org.eclipse.leshan.core.node.LwM2mPath; -import org.eclipse.leshan.core.node.LwM2mResource; -import org.eclipse.leshan.core.node.TimestampedLwM2mNodes; -import org.eclipse.leshan.server.registration.Registration; import org.junit.Test; -import org.mockito.Mockito; -import org.springframework.boot.test.mock.mockito.SpyBean; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.server.transport.lwm2m.rpc.AbstractRpcLwM2MIntegrationTest; -import org.thingsboard.server.transport.lwm2m.server.uplink.DefaultLwM2mUplinkMsgHandler; import java.time.Instant; import java.util.Map; +import java.util.concurrent.atomic.AtomicReference; +import static java.util.concurrent.TimeUnit.SECONDS; +import static org.awaitility.Awaitility.await; import static org.eclipse.leshan.core.LwM2mId.SERVER; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; -import static org.mockito.Mockito.doAnswer; -import static org.mockito.Mockito.timeout; -import static org.mockito.Mockito.verify; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.BINARY_APP_DATA_CONTAINER; import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.OBJECT_INSTANCE_ID_0; @@ -49,20 +45,22 @@ import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.RESOURCE_ID import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.RESOURCE_ID_11; import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.RESOURCE_ID_14; import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.RESOURCE_ID_2; +import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.RESOURCE_ID_3303_12_5700_TS_0; +import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.RESOURCE_ID_3303_12_5700_TS_1; import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.RESOURCE_ID_9; import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.RESOURCE_ID_NAME_19_0_0; import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.RESOURCE_ID_NAME_19_0_3; import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.RESOURCE_ID_NAME_19_1_0; +import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.RESOURCE_ID_NAME_3303_12_5700; import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.RESOURCE_ID_NAME_3_14; import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.RESOURCE_ID_NAME_3_9; +import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.RESOURCE_ID_3303_12_5700_VALUE_0; +import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.RESOURCE_ID_3303_12_5700_VALUE_1; +import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.RESOURCE_ID_VALUE_3303_12_5700_DELTA_TS; @Slf4j public class RpcLwm2mIntegrationReadTest extends AbstractRpcLwM2MIntegrationTest { - @SpyBean - DefaultLwM2mUplinkMsgHandler defaultUplinkMsgHandlerTest; - - /** * Read {"id":"/3"} * Read {"id":"/6"}... @@ -88,7 +86,7 @@ public class RpcLwm2mIntegrationReadTest extends AbstractRpcLwM2MIntegrationTest e.printStackTrace(); } }); - } catch (Exception e2){ + } catch (Exception e2) { e2.printStackTrace(); } } @@ -99,10 +97,10 @@ public class RpcLwm2mIntegrationReadTest extends AbstractRpcLwM2MIntegrationTest * @throws Exception */ @Test - public void testReadAllInstancesInClientById_Result_CONTENT_Value_IsInstances_IsResources() throws Exception{ + public void testReadAllInstancesInClientById_Result_CONTENT_Value_IsInstances_IsResources() throws Exception { expectedObjectIdVerInstances.forEach(expected -> { try { - String actualResult = sendRPCById((String) expected); + String actualResult = sendRPCById((String) expected); String expectedObjectId = pathIdVerToObjectId((String) expected); LwM2mPath expectedPath = new LwM2mPath(expectedObjectId); ObjectNode rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class); @@ -122,7 +120,7 @@ public class RpcLwm2mIntegrationReadTest extends AbstractRpcLwM2MIntegrationTest */ @Test public void testReadMultipleResourceById_Result_CONTENT_Value_IsLwM2mMultipleResource() throws Exception { - String expectedIdVer = objectInstanceIdVer_3 +"/" + RESOURCE_ID_11; + String expectedIdVer = objectInstanceIdVer_3 + "/" + RESOURCE_ID_11; String actualResult = sendRPCById(expectedIdVer); ObjectNode rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class); assertEquals(ResponseCode.CONTENT.getName(), rpcActualResult.get("result").asText()); @@ -135,7 +133,7 @@ public class RpcLwm2mIntegrationReadTest extends AbstractRpcLwM2MIntegrationTest */ @Test public void testReadSingleResourceById_Result_CONTENT_Value_IsLwM2mSingleResource() throws Exception { - String expectedIdVer = objectInstanceIdVer_3 +"/" + RESOURCE_ID_14; + String expectedIdVer = objectInstanceIdVer_3 + "/" + RESOURCE_ID_14; String actualResult = sendRPCById(expectedIdVer); ObjectNode rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class); assertEquals(ResponseCode.CONTENT.getName(), rpcActualResult.get("result").asText()); @@ -161,7 +159,7 @@ public class RpcLwm2mIntegrationReadTest extends AbstractRpcLwM2MIntegrationTest */ @Test public void testReadCompositeSingleResourceByIds_Result_CONTENT_Value_IsObjectIsLwM2mSingleResourceIsLwM2mMultipleResource() throws Exception { - String expectedIdVer_1 = (String) expectedObjectIdVers.stream().filter(path -> (!((String)path).contains("/" + BINARY_APP_DATA_CONTAINER) && ((String)path).contains("/" + SERVER))).findFirst().get(); + String expectedIdVer_1 = (String) expectedObjectIdVers.stream().filter(path -> (!((String) path).contains("/" + BINARY_APP_DATA_CONTAINER) && ((String) path).contains("/" + SERVER))).findFirst().get(); String objectId_1 = pathIdVerToObjectId(expectedIdVer_1); String expectedIdVer3_0_1 = objectInstanceIdVer_3 + "/" + RESOURCE_ID_1; String expectedIdVer3_0_11 = objectInstanceIdVer_3 + "/" + RESOURCE_ID_11; @@ -221,8 +219,8 @@ public class RpcLwm2mIntegrationReadTest extends AbstractRpcLwM2MIntegrationTest String objectId_19 = pathIdVerToObjectId(objectIdVer_19); String expected3_0_9 = objectInstanceId_3 + "/" + RESOURCE_ID_9 + "=LwM2mSingleResource [id=" + RESOURCE_ID_9 + ", value="; String expected3_0_14 = objectInstanceId_3 + "/" + RESOURCE_ID_14 + "=LwM2mSingleResource [id=" + RESOURCE_ID_14 + ", value="; - String expected19_0_0 = objectId_19 + "/" + OBJECT_INSTANCE_ID_0 + "/" + RESOURCE_ID_0 + expectedKey19_X_0; - String expected19_1_0 = objectId_19 + "/" + OBJECT_INSTANCE_ID_1 + "/" + RESOURCE_ID_0 + expectedKey19_X_0; + String expected19_0_0 = objectId_19 + "/" + OBJECT_INSTANCE_ID_0 + "/" + RESOURCE_ID_0 + expectedKey19_X_0; + String expected19_1_0 = objectId_19 + "/" + OBJECT_INSTANCE_ID_1 + "/" + RESOURCE_ID_0 + expectedKey19_X_0; String actualValues = rpcActualResult.get("value").asText(); assertTrue(actualValues.contains(expected3_0_9)); assertTrue(actualValues.contains(expected3_0_14)); @@ -232,56 +230,55 @@ public class RpcLwm2mIntegrationReadTest extends AbstractRpcLwM2MIntegrationTest /** - * /3303/0/5700 - * Read {"id":"/3303/0/5700"} + * Read {"id":"/3303/12/5700"} * Trigger a Send operation from the client with multiple values for the same resource as a payload * acked "[{"bn":"/3303/12/5700","bt":1724".. 116 bytes] - * 2 values for the resource /3303/12/5700 should be stored with timestamps1 = Instance.now(), timestamps2 = Instance.now() - * + * 2 values for the resource /3303/12/5700 should be stored with: + * - timestamps1 = Instance.now() + RESOURCE_ID_VALUE_3303_12_5700_1 + * - timestamps2 = (timestamps1 + 3 sec) + RESOURCE_ID_VALUE_3303_12_5700_2 * @throws Exception */ @Test public void testReadSingleResource_sendFromClient_CollectedValue() throws Exception { - TimestampedLwM2mNodes[] tsNodesHolder = new TimestampedLwM2mNodes[1]; - doAnswer(inv -> { - tsNodesHolder[0] = inv.getArgument(1); - return null; - }).when(defaultUplinkMsgHandlerTest).onUpdateValueWithSendRequest( - Mockito.any(Registration.class), - Mockito.any(TimestampedLwM2mNodes.class) - ); + // init test + long startTs = Instant.now().toEpochMilli(); + int cntValues = 4; int resourceId = 5700; String expectedIdVer = objectIdVer_3303 + "/" + OBJECT_INSTANCE_ID_12 + "/" + resourceId; - String actualResult = sendRPCById(expectedIdVer); - verify(defaultUplinkMsgHandlerTest, timeout(10000).times(1)) - .onUpdateValueWithSendRequest(Mockito.any(Registration.class), Mockito.any(TimestampedLwM2mNodes.class)); - - ObjectNode rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class); - assertEquals(ResponseCode.CONTENT.getName(), rpcActualResult.get("result").asText()); - String expected = "LwM2mSingleResource [id=" + resourceId + ", value="; - String actual = rpcActualResult.get("value").asText(); - assertTrue(actual.contains(expected)); - int indStart = actual.indexOf(expected) + expected.length(); - int indEnd = actual.indexOf(",", indStart); - String valStr = actual.substring(indStart, indEnd); - double dd = Double.parseDouble(valStr); - long combined = Double.doubleToRawLongBits(dd); - int t0 = (int) (combined >> 32); - int t1 = (int) combined; - double[] expectedValues ={(double)t0/100, (double)t1/100}; - int ind = 0; - LwM2mPath expectedPath = new LwM2mPath("/3303/12/5700"); - for (Instant ts : tsNodesHolder[0].getTimestamps()) { - Map nodesAt = tsNodesHolder[0].getNodesAt(ts); - for (var instant : nodesAt.entrySet()) { - LwM2mPath actualPath = instant.getKey(); - LwM2mNode node = instant.getValue(); - LwM2mResource lwM2mResource = (LwM2mResource) node; - assertEquals(expectedPath, actualPath); - assertEquals(expectedValues[ind], lwM2mResource.getValue()); - ind++; + sendRPCById(expectedIdVer); + // verify result read: verify count value: 1-2: send CollectedValue; 3 - response for read; + long endTs = Instant.now().toEpochMilli() + RESOURCE_ID_VALUE_3303_12_5700_DELTA_TS * 4; + String expectedVal_1 = String.valueOf(RESOURCE_ID_3303_12_5700_VALUE_0); + String expectedVal_2 = String.valueOf(RESOURCE_ID_3303_12_5700_VALUE_1); + AtomicReference actualValues = new AtomicReference<>(); + await().atMost(40, SECONDS).until(() -> { + actualValues.set(doGetAsync( + "/api/plugins/telemetry/DEVICE/" + deviceId + "/values/timeseries?keys=" + + RESOURCE_ID_NAME_3303_12_5700 + + "&startTs=" + startTs + + "&endTs=" + endTs + + "&interval=0&limit=100&useStrictDataTypes=false", + ObjectNode.class)); + // verify cntValues + return actualValues.get() != null && actualValues.get().get(RESOURCE_ID_NAME_3303_12_5700).size() == cntValues; + }); + // verify ts + ArrayNode actual = (ArrayNode) actualValues.get().get(RESOURCE_ID_NAME_3303_12_5700); + Map keyTsMaps = new HashedMap(); + for (JsonNode tsNode: actual) { + if (tsNode.get("value").asText().equals(expectedVal_1) || tsNode.get("value").asText().equals(expectedVal_2)) { + keyTsMaps.put(tsNode.get("value").asText(), tsNode.get("ts").asLong()); } } + assertTrue(keyTsMaps.size() == 2); + long actualTS0 = keyTsMaps.get(expectedVal_1).longValue(); + long actualTS1 = keyTsMaps.get(expectedVal_2).longValue(); + assertTrue(actualTS0 > 0); + assertTrue(actualTS1 > 0); + assertTrue(actualTS1 > actualTS0); + assertTrue((actualTS1 - actualTS0) >= RESOURCE_ID_VALUE_3303_12_5700_DELTA_TS); + assertTrue(actualTS0 <= RESOURCE_ID_3303_12_5700_TS_0); + assertTrue(actualTS1 <= RESOURCE_ID_3303_12_5700_TS_1); } /** @@ -301,7 +298,6 @@ public class RpcLwm2mIntegrationReadTest extends AbstractRpcLwM2MIntegrationTest assertEquals(actualValue, expectedValue); } - private String sendRPCById(String path) throws Exception { String setRpcRequest = "{\"method\": \"Read\", \"params\": {\"id\": \"" + path + "\"}}"; return doPostAsync("/api/plugins/rpc/twoway/" + deviceId, setRpcRequest, String.class, status().isOk()); diff --git a/common/proto/src/main/java/org/thingsboard/server/common/util/ProtoUtils.java b/common/proto/src/main/java/org/thingsboard/server/common/util/ProtoUtils.java index b61dc16384..ab7a50705e 100644 --- a/common/proto/src/main/java/org/thingsboard/server/common/util/ProtoUtils.java +++ b/common/proto/src/main/java/org/thingsboard/server/common/util/ProtoUtils.java @@ -407,6 +407,7 @@ public class ProtoUtils { .setRequestIdMSB(msg.getMsg().getId().getMostSignificantBits()) .setRequestIdLSB(msg.getMsg().getId().getLeastSignificantBits()) .setOneway(msg.getMsg().isOneway()) + .setPersisted(msg.getMsg().isPersisted()) .build(); return TransportProtos.ToDeviceRpcRequestActorMsgProto.newBuilder() diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/bootstrap/store/LwM2MBootstrapSecurityStore.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/bootstrap/store/LwM2MBootstrapSecurityStore.java index 4dcbb908d1..025f665afa 100644 --- a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/bootstrap/store/LwM2MBootstrapSecurityStore.java +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/bootstrap/store/LwM2MBootstrapSecurityStore.java @@ -133,7 +133,7 @@ public class LwM2MBootstrapSecurityStore implements BootstrapSecurityStore { log.error(" [{}] Different values SecurityMode between of client and profile.", store.getEndpoint()); log.error("{} getParametersBootstrap: [{}] Different values SecurityMode between of client and profile.", LOG_LWM2M_ERROR, store.getEndpoint()); String logMsg = String.format("%s: Different values SecurityMode between of client and profile.", LOG_LWM2M_ERROR); - helper.sendParametersOnThingsboardTelemetry(helper.getKvStringtoThingsboard(LOG_LWM2M_TELEMETRY, logMsg), sessionInfo); + helper.sendParametersOnThingsboardTelemetry(helper.getKvStringtoThingsboard(LOG_LWM2M_TELEMETRY, logMsg), sessionInfo, null); return null; } } diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/LwM2mTransportServerHelper.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/LwM2mTransportServerHelper.java index 86df96f40e..625a9fb61a 100644 --- a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/LwM2mTransportServerHelper.java +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/LwM2mTransportServerHelper.java @@ -36,6 +36,7 @@ import org.thingsboard.server.queue.util.TbLwM2mTransportComponent; import java.io.ByteArrayInputStream; import java.io.IOException; +import java.time.Instant; import java.util.ArrayList; import java.util.List; import java.util.Map; @@ -58,12 +59,12 @@ public class LwM2mTransportServerHelper { context.getTransportService().process(sessionInfo, postAttributeMsg, TransportServiceCallback.EMPTY); } - public void sendParametersOnThingsboardTelemetry(List kvList, SessionInfoProto sessionInfo) { - sendParametersOnThingsboardTelemetry(kvList, sessionInfo, null); + public void sendParametersOnThingsboardTelemetry(List kvList, SessionInfoProto sessionInfo, @Nullable Map keyTsLatestMaps){ + sendParametersOnThingsboardTelemetry(kvList, sessionInfo, keyTsLatestMaps, null); } - public void sendParametersOnThingsboardTelemetry(List kvList, SessionInfoProto sessionInfo, @Nullable Map keyTsLatestMap) { - TransportProtos.TsKvListProto tsKvList = toTsKvList(kvList, keyTsLatestMap); + public void sendParametersOnThingsboardTelemetry(List kvList, SessionInfoProto sessionInfo, @Nullable Map keyTsLatestMap, @Nullable Instant ts) { + TransportProtos.TsKvListProto tsKvList = toTsKvList(kvList, keyTsLatestMap, ts); PostTelemetryMsg postTelemetryMsg = PostTelemetryMsg.newBuilder() .addTsKvList(tsKvList) @@ -72,9 +73,9 @@ public class LwM2mTransportServerHelper { context.getTransportService().process(sessionInfo, postTelemetryMsg, TransportServiceCallback.EMPTY); } - TransportProtos.TsKvListProto toTsKvList(List kvList, Map keyTsLatestMap) { + TransportProtos.TsKvListProto toTsKvList(List kvList, Map keyTsLatestMap, @Nullable Instant ts) { return TransportProtos.TsKvListProto.newBuilder() - .setTs(getTs(kvList, keyTsLatestMap)) + .setTs(ts == null ? getTs(kvList, keyTsLatestMap) : ts.toEpochMilli()) .addAllKv(kvList) .build(); } diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/uplink/DefaultLwM2mUplinkMsgHandler.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/uplink/DefaultLwM2mUplinkMsgHandler.java index df1a86d2eb..a79ed43784 100644 --- a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/uplink/DefaultLwM2mUplinkMsgHandler.java +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/uplink/DefaultLwM2mUplinkMsgHandler.java @@ -42,7 +42,6 @@ import org.eclipse.leshan.core.observation.Observation; import org.eclipse.leshan.core.request.CreateRequest; import org.eclipse.leshan.core.request.ObserveRequest; import org.eclipse.leshan.core.request.ReadRequest; -import org.eclipse.leshan.core.request.SendRequest; import org.eclipse.leshan.core.request.WriteCompositeRequest; import org.eclipse.leshan.core.request.WriteRequest; import org.eclipse.leshan.core.request.WriteRequest.Mode; @@ -117,6 +116,7 @@ import java.util.UUID; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicLong; import java.util.stream.Collectors; import static org.thingsboard.server.common.data.lwm2m.LwM2mConstants.LWM2M_SEPARATOR_PATH; @@ -382,7 +382,7 @@ public class DefaultLwM2mUplinkMsgHandler extends LwM2MExecutorAwareService impl this.updateObjectInstanceResourceValue(lwM2MClient, lwM2mObjectInstance, path.toString(), 0); } else if (node instanceof LwM2mResource) { LwM2mResource lwM2mResource = (LwM2mResource) node; - this.updateResourcesValue(lwM2MClient, lwM2mResource, path.toString(), Mode.UPDATE, 0); + this.updateResourcesValueWithTs(lwM2MClient, lwM2mResource, path.toString(), Mode.UPDATE, ts); } } tryAwake(lwM2MClient); @@ -612,12 +612,21 @@ public class DefaultLwM2mUplinkMsgHandler extends LwM2MExecutorAwareService impl otaService.onCurrentSoftwareResultUpdate(lwM2MClient, (Long) lwM2mResource.getValue()); } if (ResponseCode.BAD_REQUEST.getCode() > code) { - this.updateAttrTelemetry(registration, path); + this.updateAttrTelemetry(registration, path, null); } } else { log.error("Fail update path [{}] Resource [{}]", path, lwM2mResource); } } + private void updateResourcesValueWithTs(LwM2mClient lwM2MClient, LwM2mResource lwM2mResource, String stringPath, Mode mode, Instant ts) { + Registration registration = lwM2MClient.getRegistration(); + String path = convertObjectIdToVersionedId(stringPath, lwM2MClient); + if (lwM2MClient.saveResourceValue(path, lwM2mResource, modelProvider, mode)) { + this.updateAttrTelemetry(registration, path, ts); + } else { + log.error("Fail update path [{}] Resource [{}] with ts.", path, lwM2mResource); + } + } /** @@ -629,7 +638,7 @@ public class DefaultLwM2mUplinkMsgHandler extends LwM2MExecutorAwareService impl * * @param registration - Registration LwM2M Client */ - public void updateAttrTelemetry(Registration registration, String path) { + public void updateAttrTelemetry(Registration registration, String path, Instant ts) { log.trace("UpdateAttrTelemetry paths [{}]", path); try { ResultsAddKeyValueProto results = this.getParametersFromProfile(registration, path); @@ -640,8 +649,8 @@ public class DefaultLwM2mUplinkMsgHandler extends LwM2MExecutorAwareService impl this.helper.sendParametersOnThingsboardAttribute(results.getResultAttributes(), sessionInfo); } if (results.getResultTelemetries().size() > 0) { - log.trace("UpdateTelemetry paths [{}] value [{}]", path, results.getResultTelemetries().get(0).toString()); - this.helper.sendParametersOnThingsboardTelemetry(results.getResultTelemetries(), sessionInfo); + log.trace("UpdateTelemetry paths [{}] value [{}] ts [{}]", path, results.getResultTelemetries().get(0).toString(), ts == null ? "null" : ts.toEpochMilli()); + this.helper.sendParametersOnThingsboardTelemetry(results.getResultTelemetries(), sessionInfo, null, ts); } } } catch (Exception e) { diff --git a/common/version-control/src/main/java/org/thingsboard/server/service/sync/vc/DefaultClusterVersionControlService.java b/common/version-control/src/main/java/org/thingsboard/server/service/sync/vc/DefaultClusterVersionControlService.java index 138505bd8d..ca0636761d 100644 --- a/common/version-control/src/main/java/org/thingsboard/server/service/sync/vc/DefaultClusterVersionControlService.java +++ b/common/version-control/src/main/java/org/thingsboard/server/service/sync/vc/DefaultClusterVersionControlService.java @@ -223,7 +223,7 @@ public class DefaultClusterVersionControlService extends TbApplicationEventListe var currentSettings = vcService.getRepositorySettings(ctx.getTenantId()); var newSettings = ctx.getSettings(); if (!newSettings.equals(currentSettings)) { - vcService.initRepository(ctx.getTenantId(), ctx.getSettings()); + vcService.initRepository(ctx.getTenantId(), ctx.getSettings(), false); } if (msg.hasCommitRequest()) { handleCommitRequest(ctx, msg.getCommitRequest()); @@ -464,7 +464,7 @@ public class DefaultClusterVersionControlService extends TbApplicationEventListe private void handleInitRepositoryCommand(VersionControlRequestCtx ctx) { try { - vcService.initRepository(ctx.getTenantId(), ctx.getSettings()); + vcService.initRepository(ctx.getTenantId(), ctx.getSettings(), false); reply(ctx, Optional.empty()); } catch (Exception e) { log.debug("[{}] Failed to connect to the repository: ", ctx, e); @@ -564,4 +564,5 @@ public class DefaultClusterVersionControlService extends TbApplicationEventListe }, MoreExecutors.directExecutor()); } } + } diff --git a/common/version-control/src/main/java/org/thingsboard/server/service/sync/vc/DefaultGitRepositoryService.java b/common/version-control/src/main/java/org/thingsboard/server/service/sync/vc/DefaultGitRepositoryService.java index bf3e202c2d..0788143030 100644 --- a/common/version-control/src/main/java/org/thingsboard/server/service/sync/vc/DefaultGitRepositoryService.java +++ b/common/version-control/src/main/java/org/thingsboard/server/service/sync/vc/DefaultGitRepositoryService.java @@ -205,7 +205,7 @@ public class DefaultGitRepositoryService implements GitRepositoryService { if (!Files.exists(Path.of(gitRepository.getDirectory()))) { try { - return cloneRepository(tenantId, gitRepository.getSettings()); + return openOrCloneRepository(tenantId, gitRepository.getSettings(), false); } catch (Exception e) { throw new IllegalStateException("Could not initialize the repository: " + e.getMessage(), e); } @@ -239,11 +239,11 @@ public class DefaultGitRepositoryService implements GitRepositoryService { } @Override - public void initRepository(TenantId tenantId, RepositorySettings settings) throws Exception { + public void initRepository(TenantId tenantId, RepositorySettings settings, boolean fetch) throws Exception { if (!settings.isLocalOnly()) { clearRepository(tenantId); } - cloneRepository(tenantId, settings); + openOrCloneRepository(tenantId, settings, fetch); } @Override @@ -280,13 +280,16 @@ public class DefaultGitRepositoryService implements GitRepositoryService { return EntityIdFactory.getByTypeAndUuid(entityType, entityId); } - private GitRepository cloneRepository(TenantId tenantId, RepositorySettings settings) throws Exception { + private GitRepository openOrCloneRepository(TenantId tenantId, RepositorySettings settings, boolean fetch) throws Exception { log.debug("[{}] Init tenant repository started.", tenantId); Path repositoryDirectory = Path.of(repositoriesFolder, settings.isLocalOnly() ? "local_" + settings.getRepositoryUri() : tenantId.getId().toString()); GitRepository repository; if (Files.exists(repositoryDirectory)) { repository = GitRepository.open(repositoryDirectory.toFile(), settings); + if (fetch) { + repository.fetch(); + } } else { Files.createDirectories(repositoryDirectory); if (settings.isLocalOnly()) { diff --git a/common/version-control/src/main/java/org/thingsboard/server/service/sync/vc/GitRepositoryService.java b/common/version-control/src/main/java/org/thingsboard/server/service/sync/vc/GitRepositoryService.java index 1382a69599..d5d51397f9 100644 --- a/common/version-control/src/main/java/org/thingsboard/server/service/sync/vc/GitRepositoryService.java +++ b/common/version-control/src/main/java/org/thingsboard/server/service/sync/vc/GitRepositoryService.java @@ -42,7 +42,7 @@ public interface GitRepositoryService { void testRepository(TenantId tenantId, RepositorySettings settings) throws Exception; - void initRepository(TenantId tenantId, RepositorySettings settings) throws Exception; + void initRepository(TenantId tenantId, RepositorySettings settings, boolean fetch) throws Exception; RepositorySettings getRepositorySettings(TenantId tenantId) throws Exception; @@ -67,4 +67,5 @@ public interface GitRepositoryService { String getContentsDiff(TenantId tenantId, String content1, String content2) throws IOException; void fetch(TenantId tenantId) throws GitAPIException; + } diff --git a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/TestRestClient.java b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/TestRestClient.java index 87d466e11a..d137f8ed59 100644 --- a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/TestRestClient.java +++ b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/TestRestClient.java @@ -49,6 +49,7 @@ 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.EntityViewId; +import org.thingsboard.server.common.data.id.RpcId; import org.thingsboard.server.common.data.id.RuleChainId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.UserId; @@ -57,6 +58,7 @@ import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.common.data.page.TimePageLink; import org.thingsboard.server.common.data.relation.EntityRelation; import org.thingsboard.server.common.data.relation.RelationTypeGroup; +import org.thingsboard.server.common.data.rpc.Rpc; import org.thingsboard.server.common.data.rule.RuleChain; import org.thingsboard.server.common.data.rule.RuleChainMetaData; import org.thingsboard.server.common.data.security.DeviceCredentials; @@ -64,6 +66,7 @@ import org.thingsboard.server.common.data.security.DeviceCredentials; import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.UUID; import static io.restassured.RestAssured.given; import static java.net.HttpURLConnection.HTTP_BAD_REQUEST; @@ -320,6 +323,27 @@ public class TestRestClient { .as(JsonNode.class); } + public Rpc getPersistedRpc(RpcId rpcId) { + return given().spec(requestSpec) + .get("/api/rpc/persistent/{rpcId}", rpcId.toString()) + .then() + .statusCode(HTTP_OK) + .extract() + .as(Rpc.class); + } + + public PageData getPersistedRpcByDevice(DeviceId deviceId, PageLink pageLink) { + Map params = new HashMap<>(); + addPageLinkToParam(params, pageLink); + return given().spec(requestSpec).queryParams(params) + .get("/api/rpc/persistent/device/{deviceId}", deviceId.toString()) + .then() + .statusCode(HTTP_OK) + .extract() + .as(new TypeRef<>() { + }); + } + public PageData getDeviceProfiles(PageLink pageLink) { Map params = new HashMap<>(); addPageLinkToParam(params, pageLink); diff --git a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/connectivity/MqttClientTest.java b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/connectivity/MqttClientTest.java index 55198accce..c298d4fea3 100644 --- a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/connectivity/MqttClientTest.java +++ b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/connectivity/MqttClientTest.java @@ -46,9 +46,11 @@ import org.thingsboard.server.common.data.Device; import org.thingsboard.server.common.data.DeviceProfile; import org.thingsboard.server.common.data.DeviceProfileProvisionType; import org.thingsboard.server.common.data.StringUtils; +import org.thingsboard.server.common.data.id.RpcId; import org.thingsboard.server.common.data.id.RuleChainId; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageLink; +import org.thingsboard.server.common.data.rpc.Rpc; import org.thingsboard.server.common.data.rule.NodeConnectionInfo; import org.thingsboard.server.common.data.rule.RuleChain; import org.thingsboard.server.common.data.rule.RuleChainMetaData; @@ -68,6 +70,7 @@ import java.util.List; import java.util.Objects; import java.util.Optional; import java.util.Random; +import java.util.UUID; import java.util.concurrent.ArrayBlockingQueue; import java.util.concurrent.BlockingQueue; import java.util.concurrent.ExecutionException; @@ -76,6 +79,7 @@ import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; import static org.assertj.core.api.Assertions.assertThat; +import static org.testng.Assert.assertNotNull; import static org.testng.Assert.fail; import static org.thingsboard.server.common.data.DataConstants.DEVICE; import static org.thingsboard.server.common.data.DataConstants.SHARED_SCOPE; @@ -307,6 +311,60 @@ public class MqttClientTest extends AbstractContainerTest { assertThat(serverResponse).isEqualTo(mapper.readTree(clientResponse.toString())); } + @Test + public void serverSidePersistedRpc() throws Exception { + DeviceCredentials deviceCredentials = testRestClient.getDeviceCredentialsByDeviceId(device.getId()); + + MqttMessageListener listener = new MqttMessageListener(); + MqttClient mqttClient = getMqttClient(deviceCredentials, listener); + mqttClient.on("v1/devices/me/rpc/request/+", listener, MqttQoS.AT_LEAST_ONCE).get(); + + // Wait until subscription is processed + TimeUnit.SECONDS.sleep(3 * timeoutMultiplier); + + // Send an RPC from the server + JsonObject serverRpcPayload = new JsonObject(); + serverRpcPayload.addProperty("method", "getValue"); + serverRpcPayload.addProperty("params", true); + serverRpcPayload.addProperty("persistent", true); + + JsonNode persistentRpcId = testRestClient.postServerSideRpc(device.getId(), mapper.readTree(serverRpcPayload.toString())); + + assertNotNull(persistentRpcId); + + RpcId rpcId = new RpcId(UUID.fromString(persistentRpcId.get("rpcId").asText())); + + // Wait for RPC call from the server and send the response + MqttEvent requestFromServer = listener.getEvents().poll(10 * timeoutMultiplier, TimeUnit.SECONDS); + + assertThat(Objects.requireNonNull(requestFromServer).getMessage()).isEqualTo("{\"method\":\"getValue\",\"params\":true}"); + + Integer requestId = Integer.valueOf(Objects.requireNonNull(requestFromServer).getTopic().substring("v1/devices/me/rpc/request/".length())); + JsonObject clientResponse = new JsonObject(); + clientResponse.addProperty("response", "someResponse"); + // Send a response to the server's RPC request + mqttClient.publish("v1/devices/me/rpc/response/" + requestId, Unpooled.wrappedBuffer(clientResponse.toString().getBytes())).get(); + + PageLink pageLink = new PageLink(10); + + Awaitility.await() + .pollInterval(500, TimeUnit.MILLISECONDS) + .atMost(5 * timeoutMultiplier, TimeUnit.SECONDS) + .until(() -> { + PageData rpcByDevice = testRestClient.getPersistedRpcByDevice(device.getId(), pageLink); + for (Rpc rpc : rpcByDevice.getData()) { + if (rpc.getId().equals(rpcId)) { + return true; + } + } + return false; + }); + + Rpc persistentRpc = testRestClient.getPersistedRpc(rpcId); + + assertThat(persistentRpc.getResponse()).isEqualTo(mapper.readTree(clientResponse.toString())); + } + @Test public void clientSideRpc() throws Exception { DeviceCredentials deviceCredentials = testRestClient.getDeviceCredentialsByDeviceId(device.getId()); diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/credentials/CertPemCredentials.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/credentials/CertPemCredentials.java index 40d899412d..7e25840e33 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/credentials/CertPemCredentials.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/credentials/CertPemCredentials.java @@ -47,7 +47,7 @@ public class CertPemCredentials implements ClientCredentials { protected String caCert; private String cert; private String privateKey; - private String password = ""; + private String password; @Override public CredentialsType getType() { diff --git a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/credentials/CertPemCredentialsTest.java b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/credentials/CertPemCredentialsTest.java index db2a015f28..3df1f467b2 100644 --- a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/credentials/CertPemCredentialsTest.java +++ b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/credentials/CertPemCredentialsTest.java @@ -38,7 +38,6 @@ import static org.thingsboard.rule.engine.credentials.CertPemCredentials.PRIVATE public class CertPemCredentialsTest { private static final String PASS = "test"; - private static final String EMPTY_PASS = ""; private static final String RSA = "RSA"; private static final String EC = "EC"; @@ -81,10 +80,10 @@ public class CertPemCredentialsTest { private static Stream testLoadKeyStore() { return Stream.of( - Arguments.of("pem/rsa_cert.pem", "pem/rsa_key.pem", EMPTY_PASS, RSA), + Arguments.of("pem/rsa_cert.pem", "pem/rsa_key.pem", null, RSA), Arguments.of("pem/rsa_encrypted_cert.pem", "pem/rsa_encrypted_key.pem", PASS, RSA), Arguments.of("pem/rsa_encrypted_traditional_cert.pem", "pem/rsa_encrypted_traditional_key.pem", PASS, RSA), - Arguments.of("pem/ec_cert.pem", "pem/ec_key.pem", EMPTY_PASS, EC) + Arguments.of("pem/ec_cert.pem", "pem/ec_key.pem", null, EC) ); } @@ -98,7 +97,7 @@ public class CertPemCredentialsTest { certPemCredentials.setPassword(password); KeyStore keyStore = certPemCredentials.loadKeyStore(); Assertions.assertNotNull(keyStore); - Key key = keyStore.getKey(PRIVATE_KEY_ALIAS, password.toCharArray()); + Key key = keyStore.getKey(PRIVATE_KEY_ALIAS, SslUtil.getPassword(password)); Assertions.assertNotNull(key); Assertions.assertEquals(algorithm, key.getAlgorithm()); diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/analogue-gauge.models.ts b/ui-ngx/src/app/modules/home/components/widget/lib/analogue-gauge.models.ts index db3bc5559d..46ec39c3be 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/analogue-gauge.models.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/analogue-gauge.models.ts @@ -64,9 +64,13 @@ export interface AnalogueGaugeSettings { animationRule: AnimationRule; } +interface BaseGaugeModel extends BaseGauge { + _value?: number; +} + export abstract class TbBaseGauge { - private gauge: BaseGauge; + private gauge: BaseGaugeModel; protected constructor(protected ctx: WidgetContext, canvasId: string) { const gaugeElement = $('#' + canvasId, ctx.$container)[0]; @@ -77,16 +81,20 @@ export abstract class TbBaseGauge { protected abstract createGaugeOptions(gaugeElement: HTMLElement, settings: S): O; - protected abstract createGauge(gaugeData: O): BaseGauge; + protected abstract createGauge(gaugeData: O): BaseGaugeModel; update() { if (this.ctx.data.length > 0) { const cellData = this.ctx.data[0]; if (cellData.data.length > 0) { - const tvPair = cellData.data[cellData.data.length - - 1]; + const tvPair = cellData.data[cellData.data.length - 1]; const value = parseFloat(tvPair[1]); if (value !== this.gauge.value) { + if (!this.gauge.options.animation) { + this.gauge._value = value; + } else { + delete this.gauge._value; + } this.gauge.value = value; } } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/digital-gauge.ts b/ui-ngx/src/app/modules/home/components/widget/lib/digital-gauge.ts index ede5962922..cc5a3e08dd 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/digital-gauge.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/digital-gauge.ts @@ -26,7 +26,6 @@ import { prepareFontSettings } from '@home/components/widget/lib/settings.models import { CanvasDigitalGauge, CanvasDigitalGaugeOptions } from '@home/components/widget/lib/canvas-digital-gauge'; import { DatePipe } from '@angular/common'; import { IWidgetSubscription } from '@core/api/widget-api.models'; -import { Subscription } from 'rxjs'; import { ColorProcessor, createValueSubscription, ValueSourceType } from '@shared/models/widget-settings.models'; import GenericOptions = CanvasGauges.GenericOptions; @@ -260,6 +259,8 @@ export class TbCanvasDigitalGauge { if (value !== this.gauge.value) { if (!this.gauge.options.animation) { this.gauge._value = value; + } else { + delete this.gauge._value; } this.gauge.value = value; } else if (this.localSettings.showTimestamp && this.gauge.timestamp !== timestamp) { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/configuration/gateway-configuration.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/configuration/gateway-configuration.component.html index 01fc12f167..7d8e0e78b3 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/configuration/gateway-configuration.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/configuration/gateway-configuration.component.html @@ -18,7 +18,7 @@
-
+

gateway.gateway-configuration

diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-data-keys-panel/modbus-data-keys-panel.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-data-keys-panel/modbus-data-keys-panel.component.html index ee0f8d944b..890c0428f0 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-data-keys-panel/modbus-data-keys-panel.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-data-keys-panel/modbus-data-keys-panel.component.html @@ -223,6 +223,7 @@
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-data-keys-panel/modbus-data-keys-panel.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-data-keys-panel/modbus-data-keys-panel.component.ts index f361ae70e7..fcf3d49cf0 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-data-keys-panel/modbus-data-keys-panel.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-data-keys-panel/modbus-data-keys-panel.component.ts @@ -28,7 +28,8 @@ import { import { TbPopoverComponent } from '@shared/components/popover.component'; import { ModbusDataType, - ModbusEditableDataTypes, ModbusFormValue, + ModbusEditableDataTypes, + ModbusFormValue, ModbusFunctionCodeTranslationsMap, ModbusObjectCountByDataType, ModbusValue, @@ -37,6 +38,7 @@ import { ModifierTypesMap, noLeadTrailSpacesRegex, nonZeroFloat, + ReportStrategyDefaultValue, } from '@home/components/widget/lib/gateway/gateway-widget.models'; import { CommonModule } from '@angular/common'; import { SharedModule } from '@shared/shared.module'; @@ -91,6 +93,7 @@ export class ModbusDataKeysPanelComponent implements OnInit, OnDestroy { readonly ModbusEditableDataTypes = ModbusEditableDataTypes; readonly ModbusFunctionCodeTranslationsMap = ModbusFunctionCodeTranslationsMap; readonly ModifierTypesMap = ModifierTypesMap; + readonly ReportStrategyDefaultValue = ReportStrategyDefaultValue; private destroy$ = new Subject(); diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-master-table/modbus-master-table.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-master-table/modbus-master-table.component.html index eb3fb8e3f5..0bdfc9ab87 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-master-table/modbus-master-table.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-master-table/modbus-master-table.component.html @@ -62,6 +62,14 @@
+ + +
{{ 'gateway.device-name' | translate }}
+
+ +
{{ slave['deviceName'] }}
+
+
{{ 'gateway.info' | translate }} @@ -80,7 +88,7 @@ -
{{ 'gateway.client-communication-type' | translate }}
+
{{ 'gateway.type' | translate }}
{{ ModbusProtocolLabelsMap.get(slave['type']) }} @@ -121,8 +129,8 @@
- - + +
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-slave-dialog/modbus-slave-dialog.abstract.ts b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-slave-dialog/modbus-slave-dialog.abstract.ts index ff2b642621..f9955b839b 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-slave-dialog/modbus-slave-dialog.abstract.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-slave-dialog/modbus-slave-dialog.abstract.ts @@ -35,6 +35,7 @@ import { ModbusSlaveInfo, noLeadTrailSpacesRegex, PortLimits, + ReportStrategyDefaultValue, } from '@home/components/widget/lib/gateway/gateway-widget.models'; import { Subject } from 'rxjs'; import { DialogComponent } from '@shared/components/dialog.component'; @@ -64,6 +65,7 @@ export abstract class ModbusSlaveDialogAbstract extends Dialo readonly ModbusParityLabelsMap = ModbusParityLabelsMap; readonly ModbusProtocolLabelsMap = ModbusProtocolLabelsMap; readonly ModbusMethodLabelsMap = ModbusMethodLabelsMap; + readonly ReportStrategyDefaultValue = ReportStrategyDefaultValue; readonly modbusHelpLink = helpBaseUrl + '/docs/iot-gateway/config/modbus/#section-master-description-and-configuration-parameters'; diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-slave-dialog/modbus-slave-dialog.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-slave-dialog/modbus-slave-dialog.component.html index bb7e90f280..0a480a88ae 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-slave-dialog/modbus-slave-dialog.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-slave-dialog/modbus-slave-dialog.component.html @@ -218,7 +218,7 @@
- +
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/report-strategy/report-strategy.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/report-strategy/report-strategy.component.ts index fb0819d119..10eaf98f86 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/report-strategy/report-strategy.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/report-strategy/report-strategy.component.ts @@ -34,6 +34,7 @@ import { } from '@angular/forms'; import { ReportStrategyConfig, + ReportStrategyDefaultValue, ReportStrategyType, ReportStrategyTypeTranslationsMap } from '@home/components/widget/lib/gateway/gateway-widget.models'; @@ -43,7 +44,7 @@ import { CommonModule } from '@angular/common'; import { ModbusSecurityConfigComponent } from '@home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-security-config/modbus-security-config.component'; -import { coerceBoolean } from '@shared/decorators/coercion'; +import { coerceBoolean, coerceNumber } from '@shared/decorators/coercion'; @Component({ selector: 'tb-report-strategy', @@ -73,6 +74,9 @@ export class ReportStrategyComponent implements ControlValueAccessor, OnDestroy @coerceBoolean() @Input() isExpansionMode = false; + @coerceNumber() + @Input() defaultValue = ReportStrategyDefaultValue.Key; + reportStrategyFormGroup: UntypedFormGroup; showStrategyControl: FormControl; @@ -90,7 +94,7 @@ export class ReportStrategyComponent implements ControlValueAccessor, OnDestroy this.reportStrategyFormGroup = this.fb.group({ type: [{ value: ReportStrategyType.OnReportPeriod, disabled: true }, []], - reportPeriod: [{ value: 5000, disabled: true }, [Validators.required]], + reportPeriod: [{ value: this.defaultValue, disabled: true }, [Validators.required]], }); this.observeStrategyFormChange(); @@ -109,7 +113,7 @@ export class ReportStrategyComponent implements ControlValueAccessor, OnDestroy if (reportStrategyConfig) { this.reportStrategyFormGroup.enable({emitEvent: false}); } - const { type = ReportStrategyType.OnReportPeriod, reportPeriod = 5000 } = reportStrategyConfig ?? {}; + const { type = ReportStrategyType.OnReportPeriod, reportPeriod = this.defaultValue } = reportStrategyConfig ?? {}; this.reportStrategyFormGroup.setValue({ type, reportPeriod }, {emitEvent: false}); this.onTypeChange(type); } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-connectors.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-connectors.component.html index cec81ab637..cad7af4392 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-connectors.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-connectors.component.html @@ -23,7 +23,7 @@ @@ -244,7 +244,7 @@
@@ -315,6 +315,7 @@
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-connectors.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-connectors.component.ts index a4c1985595..8ca92d4ca7 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-connectors.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-connectors.component.ts @@ -42,7 +42,7 @@ import { MatTableDataSource } from '@angular/material/table'; import { ActionNotificationShow } from '@core/notification/notification.actions'; import { DialogService } from '@core/services/dialog.service'; import { WidgetContext } from '@home/models/widget-component.models'; -import { camelCase, deepClone, generateSecret, isEqual, isString } from '@core/utils'; +import { camelCase, deepClone, isEqual, isString } from '@core/utils'; import { NULL_UUID } from '@shared/models/id/has-uuid'; import { IWidgetSubscription, WidgetSubscriptionOptions } from '@core/api/widget-api.models'; import { DatasourceType, widgetType } from '@shared/models/widget.models'; @@ -60,6 +60,8 @@ import { GatewayLogLevel, noLeadTrailSpacesRegex, GatewayVersion, + ReportStrategyDefaultValue, + ReportStrategyType, } from './gateway-widget.models'; import { MatDialog } from '@angular/material/dialog'; import { AddConnectorDialogComponent } from '@home/components/widget/lib/gateway/dialog/add-connector-dialog.component'; @@ -103,6 +105,7 @@ export class GatewayConnectorComponent extends PageComponent implements AfterVie readonly GatewayConnectorTypesTranslatesMap = GatewayConnectorDefaultTypesTranslatesMap; readonly ConnectorConfigurationModes = ConfigurationModes; readonly GatewayVersion = GatewayVersion; + readonly ReportStrategyDefaultValue = ReportStrategyDefaultValue; pageLink: PageLink; dataSource: MatTableDataSource; @@ -170,18 +173,21 @@ export class GatewayConnectorComponent extends PageComponent implements AfterVie super.ngOnDestroy(); } - saveConnector(isNew = true): void { - const value = this.getConnectorData(); + onSaveConnector(): void { + this.saveConnector(this.getUpdatedConnectorData(this.connectorForm.value), false); + } + + private saveConnector(connector: GatewayConnector, isNew = true): void { const scope = (isNew || this.activeConnectors.includes(this.initialConnector.name)) ? AttributeScope.SHARED_SCOPE : AttributeScope.SERVER_SCOPE; - forkJoin(this.getEntityAttributeTasks(value, scope)).pipe(take(1)).subscribe(_ => { - this.showToast(!this.initialConnector - ? this.translate.instant('gateway.connector-created') - : this.translate.instant('gateway.connector-updated') + forkJoin(this.getEntityAttributeTasks(connector, scope)).pipe(take(1)).subscribe(_ => { + this.showToast(isNew + ? this.translate.instant('gateway.connector-created') + : this.translate.instant('gateway.connector-updated') ); - this.initialConnector = value; + this.initialConnector = connector; this.updateData(true); this.connectorForm.markAsPristine(); }); @@ -237,8 +243,8 @@ export class GatewayConnectorComponent extends PageComponent implements AfterVie } } - private getConnectorData(): GatewayConnector { - const value = { ...this.connectorForm.value }; + private getUpdatedConnectorData(connector: GatewayConnector): GatewayConnector { + const value = {...connector }; value.configuration = `${camelCase(value.name)}.json`; delete value.basicConfig; @@ -249,6 +255,16 @@ export class GatewayConnectorComponent extends PageComponent implements AfterVie delete value.class; } + if (value.type === ConnectorType.MODBUS && value.configVersion === GatewayVersion.Current) { + if (!value.reportStrategy) { + value.reportStrategy = { + type: ReportStrategyType.OnReportPeriod, + reportPeriod: ReportStrategyDefaultValue.Connector + }; + delete value.sendDataOnlyOnChange; + } + } + if (this.gatewayVersion && !value.configVersion) { value.configVersion = this.gatewayVersion; } @@ -472,7 +488,7 @@ export class GatewayConnectorComponent extends PageComponent implements AfterVie return (connector && this.activeConnectors.includes(connectorName)) ? (connector.data[0][1] || 0) : 'Inactive'; } - addConnector(event?: Event): void { + onAddConnector(event?: Event): void { event?.stopPropagation(); this.confirmConnectorChange() @@ -482,25 +498,42 @@ export class GatewayConnectorComponent extends PageComponent implements AfterVie switchMap(() => this.openAddConnectorDialog()), filter(Boolean), ) - .subscribe(value => { - if (this.connectorForm.disabled) { - this.connectorForm.enable(); - } - if (!value.configurationJson) { - value.configurationJson = {} as ConnectorBaseConfig; - } - value.basicConfig = value.configurationJson; - this.initialConnector = value; - this.connectorForm.patchValue(value, {emitEvent: false}); - this.generate('basicConfig.broker.clientId'); - if (this.connectorForm.get('type').value === value.type || !this.allowBasicConfig.has(value.type)) { - this.saveConnector(); - } else { - this.basicConfigInitSubject.pipe(take(1)).subscribe(() => { - this.saveConnector(); - }); - } - }); + .subscribe(connector => this.addConnector(connector)); + } + + private addConnector(connector: GatewayConnector): void { + if (this.connectorForm.disabled) { + this.connectorForm.enable(); + } + if (!connector.configurationJson) { + connector.configurationJson = {} as ConnectorBaseConfig; + } + connector.basicConfig = connector.configurationJson; + this.initialConnector = connector; + + const previousType = this.connectorForm.get('type').value; + + this.setInitialConnectorValues(connector); + + this.saveConnector(this.getUpdatedConnectorData(connector)); + + if (!previousType || previousType === connector.type || !this.allowBasicConfig.has(connector.type)) { + this.patchBasicConfigConnector(connector); + } else { + this.basicConfigInitSubject.pipe(take(1)).subscribe(() => { + this.patchBasicConfigConnector(connector); + }); + } + } + + private setInitialConnectorValues(connector: GatewayConnector): void { + this.toggleReportStrategy(connector.type); + this.connectorForm.get('mode').setValue(this.allowBasicConfig.has(connector.type) + ? connector.mode ?? ConfigurationModes.BASIC + : null, {emitEvent: false} + ); + this.connectorForm.get('configVersion').setValue(connector.configVersion, {emitEvent: false}); + this.connectorForm.get('type').setValue(connector.type, {emitEvent: false}); } private openAddConnectorDialog(): Observable { @@ -514,10 +547,6 @@ export class GatewayConnectorComponent extends PageComponent implements AfterVie }).afterClosed(); } - generate(formControlName: string): void { - this.connectorForm.get(formControlName)?.patchValue('tb_gw_' + generateSecret(5)); - } - uniqNameRequired(): ValidatorFn { return (control: UntypedFormControl) => { const newName = control.value?.trim().toLowerCase(); @@ -607,7 +636,7 @@ export class GatewayConnectorComponent extends PageComponent implements AfterVie const active = attributes.find(data => data.key === 'active').value; const lastDisconnectedTime = attributes.find(data => data.key === 'lastDisconnectTime')?.value; - const lastConnectedTime = attributes.find(data => data.key === 'lastConnectTime').value; + const lastConnectedTime = attributes.find(data => data.key === 'lastConnectTime')?.value; this.isGatewayActive = this.getGatewayStatus(active, lastConnectedTime, lastDisconnectedTime); }); @@ -804,7 +833,6 @@ export class GatewayConnectorComponent extends PageComponent implements AfterVie } private updateConnector(connector: GatewayConnector): void { - this.toggleReportStrategy(connector.type); switch (connector.type) { case ConnectorType.MQTT: case ConnectorType.OPCUA: @@ -819,8 +847,7 @@ export class GatewayConnectorComponent extends PageComponent implements AfterVie } private updateBasicConfigConnector(connector: GatewayConnector): void { - this.connectorForm.get('mode').setValue(connector.mode || ConfigurationModes.BASIC, {emitEvent: false}); - this.connectorForm.get('configVersion').setValue(connector.configVersion, {emitEvent: false}); + this.setInitialConnectorValues(connector); if ((!connector.mode || connector.mode === ConfigurationModes.BASIC) && this.connectorForm.get('type').value !== connector.type) { this.basicConfigInitSubject.asObservable().pipe(take(1)).subscribe(() => { this.patchBasicConfigConnector(connector); diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-widget.models.ts b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-widget.models.ts index f1fbd4d0f5..f8f1c99289 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-widget.models.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-widget.models.ts @@ -123,6 +123,9 @@ export interface GatewayConnectorBase { class?: string; mode?: ConfigurationModes; configVersion?: string; + reportStrategy?: ReportStrategyConfig; + sendDataOnlyOnChange?: boolean; + ts?: number; } export interface GatewayConnector extends GatewayConnectorBase { @@ -645,6 +648,12 @@ export enum ReportStrategyType { OnChangeOrReportPeriod = 'ON_CHANGE_OR_REPORT_PERIOD' } +export enum ReportStrategyDefaultValue { + Connector = 60000, + Device = 30000, + Key = 15000 +} + export const ReportStrategyTypeTranslationsMap = new Map( [ [ReportStrategyType.OnChange, 'gateway.report-strategy.on-change'], diff --git a/ui-ngx/src/app/modules/home/pages/notification/recipient/recipient-notification-dialog.component.html b/ui-ngx/src/app/modules/home/pages/notification/recipient/recipient-notification-dialog.component.html index 311dbee6d9..3fd10c8729 100644 --- a/ui-ngx/src/app/modules/home/pages/notification/recipient/recipient-notification-dialog.component.html +++ b/ui-ngx/src/app/modules/home/pages/notification/recipient/recipient-notification-dialog.component.html @@ -135,10 +135,11 @@ - notification.webhook-url + {{ (targetNotificationForm.get('configuration.useOldApi').value ? + 'notification.webhook-url' : 'notification.workflow-url') | translate }} - {{ 'notification.webhook-url-required' | translate }} + {{ (targetNotificationForm.get('configuration.useOldApi').value ? 'notification.webhook-url-required' : 'notification.workflow-url-required') | translate }} diff --git a/ui-ngx/src/app/shared/components/time/datapoints-limit.component.scss b/ui-ngx/src/app/shared/components/time/datapoints-limit.component.scss index c2ffa0bb34..c39f4f3e6a 100644 --- a/ui-ngx/src/app/shared/components/time/datapoints-limit.component.scss +++ b/ui-ngx/src/app/shared/components/time/datapoints-limit.component.scss @@ -25,6 +25,27 @@ mat-form-field input[type=number] { text-align: center; } + + .mdc-slider { + height: 40px; + + ::ng-deep { + .mdc-slider__thumb { + left: -20px; + height: 40px; + width: 40px; + + .mat-ripple-element.mat-mdc-slider-focus-ripple, + .mat-ripple-element.mat-mdc-slider-active-ripple, + .mat-ripple-element.mat-mdc-slider-hover-ripple { + left: 0 !important; + top: 0 !important; + height: 40px !important; + width: 40px !important; + } + } + } + } } @media #{$mat-gt-sm} { diff --git a/ui-ngx/src/app/shared/components/time/timewindow-config-dialog.component.html b/ui-ngx/src/app/shared/components/time/timewindow-config-dialog.component.html index 333c70f24f..9d786e14c1 100644 --- a/ui-ngx/src/app/shared/components/time/timewindow-config-dialog.component.html +++ b/ui-ngx/src/app/shared/components/time/timewindow-config-dialog.component.html @@ -54,7 +54,9 @@
- {{ 'timewindow.hide' | translate }} +
+ {{ 'timewindow.hide' | translate }} +
- {{ 'timewindow.hide' | translate }} +
+ {{ 'timewindow.hide' | translate }} +
- {{ 'timewindow.hide' | translate }} +
+ {{ 'timewindow.hide' | translate }} +
- {{ 'timewindow.hide' | translate }} +
+ {{ 'timewindow.hide' | translate }} +
- {{ 'timewindow.hide' | translate }} +
+ {{ 'timewindow.hide' | translate }} +
{{ 'aggregation.aggregation' | translate }}
- {{ 'timewindow.hide' | translate }} +
+ {{ 'timewindow.hide' | translate }} +
@@ -171,7 +183,9 @@
{{ 'aggregation.limit' | translate }}
- {{ 'timewindow.hide' | translate }} +
+ {{ 'timewindow.hide' | translate }} +
- {{ 'timewindow.hide' | translate }} +
+ {{ 'timewindow.hide' | translate }} +
@@ -239,7 +255,9 @@
{{ 'timezone.timezone' | translate }}
- {{ 'timewindow.hide' | translate }} +
+ {{ 'timewindow.hide' | translate }} +
-
+
diff --git a/ui-ngx/src/app/shared/models/widget-settings.models.ts b/ui-ngx/src/app/shared/models/widget-settings.models.ts index acb82c6d5d..c8c2ed9a40 100644 --- a/ui-ngx/src/app/shared/models/widget-settings.models.ts +++ b/ui-ngx/src/app/shared/models/widget-settings.models.ts @@ -329,11 +329,9 @@ export const resolveCssSize = (strSize?: string): [number, cssUnit] => { } let resolvedUnit: cssUnit; let resolvedSize = strSize; - for (const unit of cssUnits) { - if (strSize.endsWith(unit)) { - resolvedUnit = unit; - break; - } + const unitMatch = strSize.match(new RegExp(`(${cssUnits.join('|')})$`)); + if (unitMatch) { + resolvedUnit = unitMatch[0] as cssUnit; } if (resolvedUnit) { resolvedSize = strSize.substring(0, strSize.length - resolvedUnit.length); diff --git a/ui-ngx/src/assets/help/en_US/scada/symbol_state_render_fn.md b/ui-ngx/src/assets/help/en_US/scada/symbol_state_render_fn.md index 9a2c811817..7a384157f6 100644 --- a/ui-ngx/src/assets/help/en_US/scada/symbol_state_render_fn.md +++ b/ui-ngx/src/assets/help/en_US/scada/symbol_state_render_fn.md @@ -20,12 +20,15 @@ A JavaScript function used to render SCADA symbol state. ##### Examples +**Handle volume buttons state and appearance** + This JavaScript snippet manages the enablement and appearance of SVG buttons used to control volume settings based on device connectivity and value thresholds. -The 'Up' button is disabled when the volume reaches its maximum allowable value (`maxValue`), and the 'Down' button is disabled when the volume is at its minimum allowable value (`minValue`). +The 'Up' button is disabled when the volume reaches its maximum allowable value `maxValue`, and the 'Down' button is disabled when the volume is at its minimum allowable value `minValue`. Both buttons are disabled when the device is offline. The visual cues for enabled/disabled states are represented by color changes. -```javascript +
+```javascript var levelUpButton = ctx.tags.levelUpButton; // Button for increasing volume var levelDownButton = ctx.tags.levelDownButton; // Button for decreasing volume var levelArrowUp = ctx.tags.levelArrowUp; // Visual arrow for 'Up' button @@ -61,3 +64,70 @@ if (levelDownEnabled) { {:copy-code} ``` + +
+ +**Manage icon and label visibility and styling** + +This JavaScript code snippet dynamically handles the visibility and styling of an icon and label within a SCADA symbol. +It checks the `showIcon` and `showLabel` properties from the context to determine whether to display the icon and/or label. +If the icon or label is shown, the script sets their respective properties like size, color, and position. +The script utilizes the ScadaSymbolApi to update the visual appearance of these elements based on the context properties. +
+This approach ensures that both the icon and label elements are dynamically shown or hidden based on context properties, with appropriate styling applied. + +
+ +```javascript +// Retrieve the first icon and label elements from the context tags +var iconTag = ctx.tags.icon[0]; +var labelTag = ctx.tags.label[0]; + +// Check whether the icon should be displayed, based on the context property +var showIcon = ctx.properties.showIcon; +var showLabel = ctx.properties.label; + +if (showIcon) { + // Show the icon if the 'showIcon' property is true + iconTag.show(); + // Set icon attributes such as icon type, size, and color + var icon = ctx.properties.icon; + var iconSize = ctx.properties.iconSize; + var iconColor = ctx.properties.iconColor; + + // Use the ScadaSymbolApi to apply the icon, size, and color to the iconTag + ctx.api.icon(iconTag, icon, iconSize, iconColor, true); + + // If the label is not shown, adjust the icon's position + if (!showLabel) { + iconTag.transform({translateX: 83, translateY: 137}); + } +} else { + // Hide the icon if 'showIcon' is false + iconTag.hide(); +} + +if (showLabel) { + // Show the label if the 'showLabel' property is true + var labelTextFont = ctx.properties.labelTextFont; + var labelTextColor = ctx.properties.labelTextColor; + + // Apply font and color to the label using the ScadaSymbolApi + ctx.api.font(labelTag, labelTextFont, labelTextColor); + + // Set the text content of the label + ctx.api.text(labelTag, ctx.properties.labelText); + + // If the icon is not shown, adjust the label's position + if (!showIcon) { + labelTag.transform({translateX: 10}); + } + + // Show the label + labelTag.show(); +} else { + // Hide the label if 'showLabel' is false + labelTag.hide(); +} +{:copy-code} +``` diff --git a/ui-ngx/src/assets/help/en_US/scada/tag_click_action_fn.md b/ui-ngx/src/assets/help/en_US/scada/tag_click_action_fn.md index 8675419803..10ff5f2ebf 100644 --- a/ui-ngx/src/assets/help/en_US/scada/tag_click_action_fn.md +++ b/ui-ngx/src/assets/help/en_US/scada/tag_click_action_fn.md @@ -21,7 +21,34 @@ A JavaScript function invoked when user clicks on SVG element with specific tag.
-##### Examples +##### Examples without setting values for callAction function + + +**Invoke widget click action** + +
+ +This JavaScript snippet demonstrates triggering a widget action using the ScadaSymbolContext API when the click event occurs. The widget action will be linked to the behaviorId 'click', which defines the action that will be executed upon the event. +The behavior of this action depends on the type of widget action configured in the ThingsBoard platform (e.g., navigating to a dashboard state, updating the current state, opening a URL, etc.). + +
+ +```javascript +ctx.api.callAction(event, 'click'); // Trigger widget action 'click' on event +{:copy-code} +``` + +
+ +This action is executed automatically upon the 'click' event, making it useful for scenarios where the user interacts with a widget by clicking on it. +*Example Use Case* + +- **Navigate to Dashboard State:** If the 'click' action is configured to navigate to a new dashboard state, the user will be redirected to that state when clicking on the widget. +- **Open URL:** If configured to open a URL, clicking on the widget will take the user to a specified web resource. + +
+ +**Handle device activation toggle** The example demonstrates how to dynamically call the 'turnOn' or 'turnOff' actions based on the 'active' status from the context. The actions are implemented using the following methods from the Scada Symbol API: @@ -34,10 +61,9 @@ For more detailed guidelines on device interaction, consider reviewing the { // Action succeeded; toggle the 'activate' status for debugging ctx.api.setValue('activate', !active); @@ -47,4 +73,116 @@ ctx.api.callAction(event, action, parameter, { ctx.api.setValue('activate', active); } }); +{:copy-code} ``` + +
+ +This example utilizes two specific action behaviors, `turnOn` and `turnOff`, which interact with the target device based on the `active` status from the context: +1. **turnOn Behavior** + * **Type**: Action + * **Default Value**: `true` + * **Description**: This behavior is triggered when the device is activated. It sends a command to the target device to turn it on, indicating that the device should be in an operational state. The default value of `true` signifies that the action is intended to activate or enable the device. +2. **turnOf Behavior** +* **Type**: Action +* **Default Value**: `false` +* **Description**: This behavior is triggered when the device is deactivated. It sends a command to the target device to turn it off, indicating that the device should be in a non-operational state. The default value of `false` signifies that the action is intended to deactivate or disable the device. + + +##### Example with setting values for callAction function + +**Temperature Adjustment Handler** + +
+ +This JavaScript code demonstrates click actions for buttons that either increase or decrease the temperature value within a predefined range. +The behavior triggered by the click event updates the time series data for the temperature and ensures that the value stays within the defined limits. +The widget uses the
ScadaSymbolContext API to manage both temperature changes and the corresponding actions. + +1. **Decrease Temperature (levelDown button)**: + * The user clicks the levelDown button to decrease the temperature. If the system is running, the temperature decreases by a step defined by `temperatureStep`, but it will never go below the `minTemperature`. + * The updateTemperatureState action is then triggered to update the time series value for temperature. + * In case of an error during the action, the temperature reverts to its previous value. + +
+ +```javascript +if (ctx.values.running) { + // Retrieve the current temperature from the context values + var temperature = ctx.values.temperature; + + // Retrieve the minimum allowable temperature from the context properties + var minTemperature = ctx.properties.minTemperature; + + // Retrieve the step size by which the temperature should change + var step = ctx.properties.temperatureStep; + + // Calculate the new temperature, ensuring it does not go below the minimum temperature + var newTemperature = Math.max(minTemperature, temperature - step); + + // Set the new temperature value in the context + ctx.api.setValue('temperature', newTemperature); + + // Trigger the action to update the temperature state, passing the new temperature value + ctx.api.callAction(event, 'updateTemperatureState', newTemperature, { + // In case of an error, revert to the original temperature + error: () => { + ctx.api.setValue('temperature', temperature); + } + }); +} +{:copy-code} +``` + +
+ +2. **Increase Temperature (levelUp button)**: + * The user clicks the levelUp button to increase the temperature. If the system is running, the temperature increases by the step, but will not exceed the `maxTemperature`. + * The action `updateTemperatureState` is called to update the time series value for temperature. + * If an error occurs, the previous temperature is restored to ensure consistency. + +
+ +```javascript +if (ctx.values.running) { + // Retrieve the current temperature from the context values + var temperature = ctx.values.temperature; + + // Retrieve the maximum and minimum allowable temperature from the context properties + var maxTemperature = ctx.properties.maxTemperature; + var minTemperature = ctx.properties.minTemperature; + + // Retrieve the step size by which the temperature should change + var step = ctx.properties.temperatureStep; + + // Calculate the new temperature: + // - Add the step to the current temperature + // - Ensure it doesn't exceed the maxTemperature + // - If temperature is null/undefined, use minTemperature as the initial value + var newTemperature = temperature || minTemperature === 0 ? Math.min(maxTemperature, temperature + step) : minTemperature; + + // Set the new temperature value in the context + ctx.api.setValue('temperature', newTemperature); + + // Trigger the action to update the temperature state, passing the new temperature value + ctx.api.callAction(event, 'updateTemperatureState', newTemperature, { + // In case of an error, revert to the original temperature + error: () => { + ctx.api.setValue('temperature', temperature); + } + }); +} +{:copy-code} +``` + +
+ +The `updateTemperatureState` action is triggered to update the temperature value in the system. The action is configured with the following behavior: + +*Action Behavior*: 'updateTemperatureState' + * **Type**: action + * **Value Type**: double + * **Default Settings**: Add time series + * **Time Series Key**: 'temperature' + +This behavior updates the time series data for the temperature key, ensuring that the new temperature value is stored and displayed correctly. diff --git a/ui-ngx/src/assets/help/en_US/scada/tag_state_render_fn.md b/ui-ngx/src/assets/help/en_US/scada/tag_state_render_fn.md index c16ad96c58..15e48b1453 100644 --- a/ui-ngx/src/assets/help/en_US/scada/tag_state_render_fn.md +++ b/ui-ngx/src/assets/help/en_US/scada/tag_state_render_fn.md @@ -21,43 +21,64 @@ A JavaScript function used to render SCADA symbol element with specific tag. ##### Examples -* Change the background of the element based on the value of the “active” +**Update element state based on device activity** -```javascript -if(ctx.values.active){ - element.attr({fill: ctx.properties.activeColor}); -} else { - element.attr({fill: ctx.properties.inactiveColor}); -} -{:copy-code} -``` +This JavaScript snippet demonstrates how to dynamically change the background color of an SVG element based on the `active` status from the context. +Additionally, it enables or disables the element’s click functionality depending on the `active` status. +If the device is active, the element is given the activeColor and click actions are allowed. Otherwise, it is assigned the inactiveColor and the click action is disabled. -* Enable and disable the “On” button based on the state of the "active" (avoid or prevent click action) +
```javascript -if (ctx.values.active) { - ctx.api.disable(element); -} else { +// Context values +var active = ctx.values.active; // Device connectivity status +// Colors from context properties +var activeColor = ctx.properties.activeColor || '#4CAF50'; // Color for enabled state +var inactiveColor = ctx.properties.inactiveColor || '#A1ADB1'; // Color for disabled state +// Check if the device is active +if (active) { + // Set the background color to activeColor if active + element.attr({fill: activeColor}); + // Enable the element to allow click actions ctx.api.enable(element); +} else { + // Set the background color to inactiveColor if not active + element.attr({fill: inactiveColor}); + // Disable the element to forbid click actions + ctx.api.disable(element); } {:copy-code} ``` -* Smooth infinite rotation animation based on the value of the “active” with speed based on the value of the “speed” +
+ +**Smooth rotation based on activity and speed** + +This JavaScript snippet creates a smooth, infinite rotation animation for an element based on the `active` status and adjusts the animation speed dynamically according to the `speed` value. +If the element is active, the animation starts or continues rotating with a speed proportional to the speed value. If inactive, the animation pauses. + +
```javascript +// Get the 'active' status and the current speed var on = ctx.values.active; -var speed = ctx.values.speed ? ctx.values.speed / 60 : 1; -var animation = ctx.api.cssAnimation(element); +var animation = ctx.api.cssAnimation(element); // Retrieve any existing animation on the element +var speed = ctx.values.speed ? ctx.values.speed / 60 : 1; // Calculate speed, default to 1 if not provided +var animationDuration = 2000; // Duration for one full rotation (optional, can be adjusted) +var rotationAngle = 360; // Full rotation in degrees if (on) { + // If active, either create a new rotation animation or adjust the existing one if (!animation) { - animation = ctx.api.cssAnimate(element, 2000) - .rotate(360).loop().speed(speed); + animation = ctx.api.cssAnimate(element) // Create new animation if not already active + .rotate(rotationAngle) // Set rotation angle to 360 degrees + .loop() // Loop the animation infinitely + .speed(speed); // Adjust speed based on the 'speed' value from the context } else { - animation.speed(speed).play(); + animation.speed(speed).play(); // If animation exists, adjust its speed and continue playing } } else { + // If inactive, pause the animation if (animation) { animation.pause(); } diff --git a/ui-ngx/src/assets/locale/locale.constant-en_US.json b/ui-ngx/src/assets/locale/locale.constant-en_US.json index bb4fc57e8a..6f41308f43 100644 --- a/ui-ngx/src/assets/locale/locale.constant-en_US.json +++ b/ui-ngx/src/assets/locale/locale.constant-en_US.json @@ -1251,7 +1251,7 @@ "display-dashboards-selection": "Display dashboards selection", "display-entities-selection": "Display entities selection", "display-filters": "Display filters", - "display-dashboard-timewindow": "Display timewindow", + "display-dashboard-timewindow": "Display time window", "display-dashboard-export": "Display export", "display-update-dashboard-image": "Display update dashboard image", "dashboard-logo-settings": "Dashboard logo settings", @@ -2955,7 +2955,6 @@ "configuration-delete-dialog-confirm": "Turn Off", "connector-duplicate-name": "Connector with such name already exists.", "connector-side": "Connector side", - "client-communication-type": "Client communication type", "payload-type": "Payload type", "platform-side": "Platform side", "JSON": "JSON", @@ -4327,6 +4326,8 @@ "warning": "Warning", "webhook-url": "Webhook URL", "webhook-url-required": "Webhook URL is required", + "workflow-url": "Workflow URL", + "workflow-url-required": "Workflow URL is required", "channel-name": "Channel name", "channel-name-required": "Channel name is required", "settings": { @@ -4739,10 +4740,10 @@ "queue-singleton-hint": "Select a queue for message forwarding in multi-instance environments. 'Main' queue is used by default." }, "timezone": { - "timezone": "Timezone", - "select-timezone": "Select timezone", - "no-timezones-matching": "No timezones matching '{{timezone}}' were found.", - "timezone-required": "Timezone is required.", + "timezone": "Time zone", + "select-timezone": "Select time zone", + "no-timezones-matching": "No time zones matching '{{timezone}}' were found.", + "timezone-required": "Time zone is required.", "browser-time": "Browser Time" }, "queue": { @@ -5157,8 +5158,8 @@ "days": "Days" }, "timewindow": { - "timewindow": "Timewindow", - "timewindow-settings": "Timewindow settings", + "timewindow": "Time window", + "timewindow-settings": "Time window settings", "years": "{ years, plural, =1 { year } other {# years } }", "years-short": "{{ years }}y", "months": "{ months, plural, =1 { month } other {# months } }", @@ -5186,7 +5187,7 @@ "history": "History", "last-prefix": "last", "period": "from {{ startTime }} to {{ endTime }}", - "edit": "Edit timewindow", + "edit": "Edit time window", "date-range": "Date range", "for-all-time": "For all time", "last": "Last", @@ -5196,7 +5197,7 @@ "just-now": "Just now", "just-now-lower": "just now", "ago": "ago", - "style": "Timewindow style", + "style": "Time window style", "icon": "Icon", "icon-position": "Icon position", "icon-position-left": "Left", @@ -5207,7 +5208,14 @@ "preview": "Preview", "relative": "Relative", "range": "Range", - "hide-timewindow-section": "Hide timewindow section from end-users", + "hide-timewindow-section": "Hide time window section from end-users", + "hide-last-interval": "Hide last interval from end-users", + "hide-relative-interval": "Hide relative interval from end-users", + "hide-fixed-interval": "Hide fixed interval from end-users", + "hide-aggregation": "Hide aggregation from end-users", + "hide-group-interval": "Hide grouping interval from end-users", + "hide-max-values": "Hide max values from end-users", + "hide-timezone": "Hide time zone from end-users", "disable-custom-interval": "Disable custom interval selection" }, "tooltip": { @@ -6033,10 +6041,10 @@ "units-short": "Units", "decimals-short": "Decimals", "decimals-suffix": "decimals", - "timewindow": "Timewindow", - "use-dashboard-timewindow": "Use dashboard timewindow", - "use-widget-timewindow": "Use widget timewindow", - "display-timewindow": "Display timewindow", + "timewindow": "Time window", + "use-dashboard-timewindow": "Use dashboard time window", + "use-widget-timewindow": "Use widget time window", + "display-timewindow": "Display time window", "legend": "Legend", "display-legend": "Display legend", "datasources": "Datasources",