From 4152cd95550a23484c7d50a4efde1e7d2af72cc0 Mon Sep 17 00:00:00 2001 From: dashevchenko Date: Wed, 3 Sep 2025 15:51:15 +0300 Subject: [PATCH 01/42] updated java rest client with missing methods --- .../msa/connectivity/RestClientTest.java | 78 ++++++ .../thingsboard/rest/client/RestClient.java | 265 +++++++++++++++++- 2 files changed, 337 insertions(+), 6 deletions(-) create mode 100644 msa/black-box-tests/src/test/java/org/thingsboard/server/msa/connectivity/RestClientTest.java diff --git a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/connectivity/RestClientTest.java b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/connectivity/RestClientTest.java new file mode 100644 index 0000000000..e7a9fa3acb --- /dev/null +++ b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/connectivity/RestClientTest.java @@ -0,0 +1,78 @@ +package org.thingsboard.server.msa.connectivity; + +import org.springframework.web.client.RestTemplate; +import org.testcontainers.shaded.org.apache.commons.lang3.RandomStringUtils; +import org.testng.annotations.AfterMethod; +import org.testng.annotations.BeforeMethod; +import org.testng.annotations.Test; +import org.thingsboard.rest.client.RestClient; +import org.thingsboard.server.common.data.Device; +import org.thingsboard.server.common.data.alarm.Alarm; +import org.thingsboard.server.common.data.alarm.AlarmInfo; +import org.thingsboard.server.common.data.alarm.AlarmSearchStatus; +import org.thingsboard.server.common.data.alarm.AlarmSeverity; +import org.thingsboard.server.common.data.page.PageData; +import org.thingsboard.server.common.data.page.TimePageLink; +import org.thingsboard.server.msa.AbstractContainerTest; +import org.thingsboard.server.msa.TestProperties; + +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.thingsboard.server.msa.prototypes.DevicePrototypes.defaultDevicePrototype; + +public class RestClientTest extends AbstractContainerTest { + + private static final RestClient restClient = new RestClient(new RestTemplate(), TestProperties.getBaseUrl()); + + @BeforeMethod + public void setUp() throws Exception { + restClient.login("tenant@thingsboard.org", "tenant"); + } + + @AfterMethod + public void tearDown() { + } + + @Test + public void testGetAlarmsV2() { + Device device = restClient.saveDevice(defaultDevicePrototype(RandomStringUtils.randomAlphabetic(5))); + assertThat(device).isNotNull(); + + String type = "High temp" + RandomStringUtils.randomAlphabetic(5); + Alarm alarm = Alarm.builder() + .originator(device.getId()) + .severity(AlarmSeverity.CRITICAL) + .type(type) + .build(); + restClient.saveAlarm(alarm); + + // get /api/v2/alarm + PageData alarmsV2 = restClient.getAlarmsV2(device.getId(), null, null, List.of(type), null, new TimePageLink(10, 0)); + assertThat(alarmsV2.getData()).hasSize(1); + + PageData activeAlarms = restClient.getAlarmsV2(device.getId(), List.of(AlarmSearchStatus.ACTIVE), null, List.of(type), null, new TimePageLink(10, 0)); + assertThat(activeAlarms.getData()).hasSize(1); + + PageData cleared = restClient.getAlarmsV2(device.getId(), List.of(AlarmSearchStatus.CLEARED), null, List.of(type), null, new TimePageLink(10, 0)); + assertThat(cleared.getData()).hasSize(0); + + PageData activeAndClearedAlarms = restClient.getAlarmsV2(device.getId(), List.of(AlarmSearchStatus.CLEARED, AlarmSearchStatus.ACTIVE), null, null, null, new TimePageLink(10, 0)); + assertThat(activeAndClearedAlarms.getData()).hasSize(1); + + // get /api/v2/alarms + PageData allAlarmsV2 = restClient.getAllAlarmsV2(List.of(AlarmSearchStatus.ACTIVE), null, List.of(type), null, new TimePageLink(10, 0)); + assertThat(allAlarmsV2.getData()).hasSize(1); + + PageData allClearedAlarmsV2 = restClient.getAllAlarmsV2(List.of(AlarmSearchStatus.CLEARED), null, List.of(type), null, new TimePageLink(10, 0)); + assertThat(allClearedAlarmsV2.getData()).hasSize(0); + + // get /api/alarms + PageData allAlarms = restClient.getAllAlarms(AlarmSearchStatus.ACTIVE, null, new TimePageLink(10, 0), null); + assertThat(allAlarms.getData()).hasSize(1); + + PageData allClearedAlarms = restClient.getAllAlarms(AlarmSearchStatus.CLEARED, null, new TimePageLink(10, 0), null); + assertThat(allClearedAlarms.getData()).hasSize(0); + + } +} diff --git a/rest-client/src/main/java/org/thingsboard/rest/client/RestClient.java b/rest-client/src/main/java/org/thingsboard/rest/client/RestClient.java index 0e559efd46..a80216c7ec 100644 --- a/rest-client/src/main/java/org/thingsboard/rest/client/RestClient.java +++ b/rest-client/src/main/java/org/thingsboard/rest/client/RestClient.java @@ -113,6 +113,8 @@ import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.EntityViewId; import org.thingsboard.server.common.data.id.MobileAppBundleId; import org.thingsboard.server.common.data.id.MobileAppId; +import org.thingsboard.server.common.data.id.NotificationId; +import org.thingsboard.server.common.data.id.NotificationRequestId; import org.thingsboard.server.common.data.id.OAuth2ClientId; import org.thingsboard.server.common.data.id.OAuth2ClientRegistrationTemplateId; import org.thingsboard.server.common.data.id.OtaPackageId; @@ -132,6 +134,13 @@ import org.thingsboard.server.common.data.kv.TsKvEntry; import org.thingsboard.server.common.data.mobile.app.MobileApp; import org.thingsboard.server.common.data.mobile.bundle.MobileAppBundle; import org.thingsboard.server.common.data.mobile.bundle.MobileAppBundleInfo; +import org.thingsboard.server.common.data.notification.Notification; +import org.thingsboard.server.common.data.notification.NotificationDeliveryMethod; +import org.thingsboard.server.common.data.notification.NotificationRequest; +import org.thingsboard.server.common.data.notification.NotificationRequestInfo; +import org.thingsboard.server.common.data.notification.NotificationRequestPreview; +import org.thingsboard.server.common.data.notification.settings.NotificationSettings; +import org.thingsboard.server.common.data.notification.settings.UserNotificationSettings; import org.thingsboard.server.common.data.oauth2.OAuth2Client; import org.thingsboard.server.common.data.oauth2.OAuth2ClientInfo; import org.thingsboard.server.common.data.oauth2.OAuth2ClientLoginInfo; @@ -509,6 +518,99 @@ public class RestClient implements Closeable { params).getBody(); } + public PageData getAllAlarms(AlarmSearchStatus searchStatus, AlarmStatus status, TimePageLink pageLink, Boolean fetchOriginator) { + String urlSecondPart = "/api/alarms?"; + Map params = new HashMap<>(); + if (fetchOriginator != null) { + params.put("fetchOriginator", String.valueOf(fetchOriginator)); + urlSecondPart += "&fetchOriginator={fetchOriginator}"; + } + if (searchStatus != null) { + params.put("searchStatus", searchStatus.name()); + urlSecondPart += "&searchStatus={searchStatus}"; + } + if (status != null) { + params.put("status", status.name()); + urlSecondPart += "&status={status}"; + } + + addTimePageLinkToParam(params, pageLink); + + return restTemplate.exchange( + baseURL + urlSecondPart + "&" + getTimeUrlParams(pageLink), + HttpMethod.GET, + HttpEntity.EMPTY, + new ParameterizedTypeReference>() { + }, + params).getBody(); + } + + public PageData getAlarmsV2(EntityId entityId, List statusList, List severityList, + List typeList, String assignedId, TimePageLink pageLink) { + String urlSecondPart = "/api/v2/alarm/{entityType}/{entityId}?"; + Map params = new HashMap<>(); + params.put("entityType", entityId.getEntityType().name()); + params.put("entityId", entityId.getId().toString()); + if (!CollectionUtils.isEmpty(statusList)) { + params.put("statusList", listEnumToString(statusList)); + urlSecondPart += "&statusList={statusList}"; + } + if (!CollectionUtils.isEmpty(severityList)) { + params.put("severityList", listEnumToString(severityList)); + urlSecondPart += "&severityList={severityList}"; + } + if (!CollectionUtils.isEmpty(typeList)) { + params.put("typeList", String.join(",", typeList)); + urlSecondPart += "&typeList={typeList}"; + } + if (assignedId != null) { + params.put("assignedId", assignedId); + urlSecondPart += "&assignedId={assignedId}"; + } + + addTimePageLinkToParam(params, pageLink); + + return restTemplate.exchange( + baseURL + urlSecondPart + "&" + getTimeUrlParams(pageLink), + HttpMethod.GET, + HttpEntity.EMPTY, + new ParameterizedTypeReference>() { + }, + params).getBody(); + } + + public PageData getAllAlarmsV2(List statusList, List severityList, + List typeList, String assignedId, TimePageLink pageLink) { + String urlSecondPart = "/api/v2/alarms?"; + Map params = new HashMap<>(); + if (!CollectionUtils.isEmpty(statusList)) { + params.put("statusList", listEnumToString(statusList)); + urlSecondPart += "&statusList={statusList}"; + } + if (!CollectionUtils.isEmpty(severityList)) { + params.put("severityList", listEnumToString(severityList)); + urlSecondPart += "&severityList={severityList}"; + } + if (!CollectionUtils.isEmpty(typeList)) { + params.put("typeList", String.join(",", typeList)); + urlSecondPart += "&typeList={typeList}"; + } + if (assignedId != null) { + params.put("assignedId", assignedId); + urlSecondPart += "&assignedId={assignedId}"; + } + + addTimePageLinkToParam(params, pageLink); + + return restTemplate.exchange( + baseURL + urlSecondPart + "&" + getTimeUrlParams(pageLink), + HttpMethod.GET, + HttpEntity.EMPTY, + new ParameterizedTypeReference>() { + }, + params).getBody(); + } + public Optional getHighestAlarmSeverity(EntityId entityId, AlarmSearchStatus searchStatus, AlarmStatus status) { Map params = new HashMap<>(); params.put("entityType", entityId.getEntityType().name()); @@ -1710,6 +1812,14 @@ public class RestClient implements Closeable { }).getBody(); } + public JsonNode findEntityTimeseriesAndAttributesKeysByQuery(EntityDataQuery query) { + return restTemplate.exchange( + baseURL + "/api/entitiesQuery/find/keys", + HttpMethod.POST, new HttpEntity<>(query), + new ParameterizedTypeReference() { + }).getBody(); + } + public PageData findAlarmDataByQuery(AlarmDataQuery query) { return restTemplate.exchange( baseURL + "/api/alarmsQuery/find", @@ -2158,9 +2268,9 @@ public class RestClient implements Closeable { restTemplate.delete(baseURL + "/api/oauth2/client/{id}", oAuth2ClientId.getId()); } - public PageData getTenantDomainInfos() { + public PageData getTenantDomainInfos(PageLink pageLink) { return restTemplate.exchange( - baseURL + "/api/domain/infos", + baseURL + "/api/domain/infos?" + getUrlParams(pageLink), HttpMethod.GET, HttpEntity.EMPTY, new ParameterizedTypeReference>() { @@ -2192,9 +2302,9 @@ public class RestClient implements Closeable { restTemplate.postForLocation(baseURL + "/api/domain/{id}/oauth2Clients", oauth2ClientIds, domainId.getId()); } - public PageData getTenantMobileApps() { + public PageData getTenantMobileApps(PageLink pageLink) { return restTemplate.exchange( - baseURL + "/api/mobile/app", + baseURL + "/api/mobile/app?" + getUrlParams(pageLink), HttpMethod.GET, HttpEntity.EMPTY, new ParameterizedTypeReference>() { @@ -2222,9 +2332,9 @@ public class RestClient implements Closeable { restTemplate.delete(baseURL + "/api/mobile/app/{id}", mobileAppId.getId()); } - public PageData getTenantMobileBundleInfos() { + public PageData getTenantMobileBundleInfos(PageLink pageLink) { return restTemplate.exchange( - baseURL + "/api/mobile/bundle/infos", + baseURL + "/api/mobile/bundle/infos?" + getUrlParams(pageLink), HttpMethod.GET, HttpEntity.EMPTY, new ParameterizedTypeReference>() { @@ -2846,6 +2956,17 @@ public class RestClient implements Closeable { }, params).getBody(); } + public PageData getUsersByQuery(PageLink pageLink) { + Map params = new HashMap<>(); + addPageLinkToParam(params, pageLink); + return restTemplate.exchange( + baseURL + "/api/users/info?" + getUrlParams(pageLink), + HttpMethod.GET, + HttpEntity.EMPTY, + new ParameterizedTypeReference>() { + }, params).getBody(); + } + public PageData getTenantAdmins(TenantId tenantId, PageLink pageLink) { Map params = new HashMap<>(); params.put("tenantId", tenantId.getId().toString()); @@ -4144,6 +4265,138 @@ public class RestClient implements Closeable { } } + public PageData getNotifications(PageLink pageLink) { + Map params = new HashMap<>(); + addPageLinkToParam(params, pageLink); + return restTemplate.exchange( + baseURL + "/api/notifications?" + getUrlParams(pageLink), + HttpMethod.GET, + HttpEntity.EMPTY, + new ParameterizedTypeReference>() { + }, params).getBody(); + } + + public Integer getUnreadNotificationsCount(NotificationDeliveryMethod deliveryMethod) { + String uri = "/api/notifications/unread/count?"; + Map params = new HashMap<>(); + if (deliveryMethod != null) { + params.put("deliveryMethod", deliveryMethod.name()); + uri += "&deliveryMethod={deliveryMethod}"; + } + return restTemplate.exchange( + baseURL + uri, + HttpMethod.GET, + HttpEntity.EMPTY, + Integer.class, params).getBody(); + } + + public void markNotificationAsRead(NotificationId notificationId) { + restTemplate.exchange( + baseURL + "/api/notification/{id}/read", + HttpMethod.PUT, + HttpEntity.EMPTY, + Void.class, + notificationId.getId()); + } + + public void markAllNotificationsAsRead(NotificationDeliveryMethod deliveryMethod) { + String uri = "/api/notifications/read?"; + Map params = new HashMap<>(); + if (deliveryMethod != null) { + params.put("deliveryMethod", deliveryMethod.name()); + uri += "&deliveryMethod={deliveryMethod}"; + } + restTemplate.exchange( + baseURL + uri, + HttpMethod.PUT, + HttpEntity.EMPTY, + Void.class); + } + + + public void deleteNotification(NotificationId notificationId) { + restTemplate.delete(baseURL + "/api/notification/{id}", notificationId.getId()); + } + + public NotificationRequest createNotificationRequest(NotificationRequest notificationRequest) { + return restTemplate.postForEntity(baseURL + "/api/notification/request", notificationRequest, NotificationRequest.class).getBody(); + } + + public NotificationRequestPreview getNotificationRequestPreview(NotificationRequest notificationRequest, int recipientsPreviewSize) { + return restTemplate.postForEntity(baseURL + "/api/notification/request/preview?recipientsPreviewSize={recipientsPreviewSize}", notificationRequest, NotificationRequestPreview.class, recipientsPreviewSize).getBody(); + } + + public Optional getNotificationRequestById(NotificationRequestId notificationRequestId) { + try { + ResponseEntity notificationRequest = restTemplate.getForEntity(baseURL + "/api/notification/request/{id}", NotificationRequestInfo.class, notificationRequestId.getId()); + return Optional.ofNullable(notificationRequest.getBody()); + } catch (HttpClientErrorException exception) { + if (exception.getStatusCode() == HttpStatus.NOT_FOUND) { + return Optional.empty(); + } else { + throw exception; + } + } + } + + public PageData getNotificationRequests(PageLink pageLink) { + Map params = new HashMap<>(); + addPageLinkToParam(params, pageLink); + return restTemplate.exchange( + baseURL + "/api/notification/requests?" + getUrlParams(pageLink), + HttpMethod.GET, + HttpEntity.EMPTY, + new ParameterizedTypeReference>() { + }, params).getBody(); + } + + public void deleteNotificationRequest(NotificationRequestId notificationRequestId) { + restTemplate.delete(baseURL + "/api/notification/request/{id}", notificationRequestId.getId()); + } + + public NotificationSettings saveNotificationSettings(NotificationSettings notificationSettings) { + return restTemplate.postForEntity(baseURL + "/api/notification/settings", notificationSettings, NotificationSettings.class).getBody(); + } + + public Optional getNotificationSettings() { + try { + ResponseEntity notificationSettings = restTemplate.getForEntity(baseURL + "/api/notification/settings", NotificationSettings.class); + return Optional.ofNullable(notificationSettings.getBody()); + } catch (HttpClientErrorException exception) { + if (exception.getStatusCode() == HttpStatus.NOT_FOUND) { + return Optional.empty(); + } else { + throw exception; + } + } + } + + public List getAvailableDeliveryMethods() { + return restTemplate.exchange(URI.create( + baseURL + "/api/notification/deliveryMethods"), + HttpMethod.GET, + HttpEntity.EMPTY, + new ParameterizedTypeReference>() { + }).getBody(); + } + + public UserNotificationSettings saveUserNotificationSettings(UserNotificationSettings userNotificationSettings) { + return restTemplate.postForEntity(baseURL + "/api/notification/settings/user", userNotificationSettings, UserNotificationSettings.class).getBody(); + } + + public Optional getUserNotificationSettings() { + try { + ResponseEntity userNotificationSettings = restTemplate.getForEntity(baseURL + "/api/notification/settings/user", UserNotificationSettings.class); + return Optional.ofNullable(userNotificationSettings.getBody()); + } catch (HttpClientErrorException exception) { + if (exception.getStatusCode() == HttpStatus.NOT_FOUND) { + return Optional.empty(); + } else { + throw exception; + } + } + } + private String getTimeUrlParams(TimePageLink pageLink) { String urlParams = getUrlParams(pageLink); if (pageLink.getStartTime() != null) { From a2afc6f1b206148b10da5a18f1c39003ebd9f954 Mon Sep 17 00:00:00 2001 From: dashevchenko Date: Wed, 3 Sep 2025 16:06:01 +0300 Subject: [PATCH 02/42] fixed methods missing page link params --- .../main/java/org/thingsboard/rest/client/RestClient.java | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/rest-client/src/main/java/org/thingsboard/rest/client/RestClient.java b/rest-client/src/main/java/org/thingsboard/rest/client/RestClient.java index a80216c7ec..0725818f43 100644 --- a/rest-client/src/main/java/org/thingsboard/rest/client/RestClient.java +++ b/rest-client/src/main/java/org/thingsboard/rest/client/RestClient.java @@ -2269,6 +2269,8 @@ public class RestClient implements Closeable { } public PageData getTenantDomainInfos(PageLink pageLink) { + Map params = new HashMap<>(); + addPageLinkToParam(params, pageLink); return restTemplate.exchange( baseURL + "/api/domain/infos?" + getUrlParams(pageLink), HttpMethod.GET, @@ -2303,6 +2305,8 @@ public class RestClient implements Closeable { } public PageData getTenantMobileApps(PageLink pageLink) { + Map params = new HashMap<>(); + addPageLinkToParam(params, pageLink); return restTemplate.exchange( baseURL + "/api/mobile/app?" + getUrlParams(pageLink), HttpMethod.GET, @@ -2333,6 +2337,8 @@ public class RestClient implements Closeable { } public PageData getTenantMobileBundleInfos(PageLink pageLink) { + Map params = new HashMap<>(); + addPageLinkToParam(params, pageLink); return restTemplate.exchange( baseURL + "/api/mobile/bundle/infos?" + getUrlParams(pageLink), HttpMethod.GET, From a66141308d2b9fa32467dfccb12721cb26e15d3f Mon Sep 17 00:00:00 2001 From: dashevchenko Date: Wed, 10 Sep 2025 18:01:40 +0300 Subject: [PATCH 03/42] fixed license --- .../server/msa/connectivity/RestClientTest.java | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/connectivity/RestClientTest.java b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/connectivity/RestClientTest.java index e7a9fa3acb..6c470188c0 100644 --- a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/connectivity/RestClientTest.java +++ b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/connectivity/RestClientTest.java @@ -1,3 +1,18 @@ +/** + * Copyright © 2016-2025 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.msa.connectivity; import org.springframework.web.client.RestTemplate; From ccbf3a9707dc00788ca6d730590a437c3f026776 Mon Sep 17 00:00:00 2001 From: dashevchenko Date: Mon, 15 Sep 2025 16:28:55 +0300 Subject: [PATCH 04/42] fixed msa tests --- ...lientTest.java => JavaRestClientTest.java} | 39 ++++++++++++++++++- 1 file changed, 37 insertions(+), 2 deletions(-) rename msa/black-box-tests/src/test/java/org/thingsboard/server/msa/connectivity/{RestClientTest.java => JavaRestClientTest.java} (71%) diff --git a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/connectivity/RestClientTest.java b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/connectivity/JavaRestClientTest.java similarity index 71% rename from msa/black-box-tests/src/test/java/org/thingsboard/server/msa/connectivity/RestClientTest.java rename to msa/black-box-tests/src/test/java/org/thingsboard/server/msa/connectivity/JavaRestClientTest.java index 6c470188c0..636c80d5b3 100644 --- a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/connectivity/RestClientTest.java +++ b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/connectivity/JavaRestClientTest.java @@ -15,9 +15,19 @@ */ package org.thingsboard.server.msa.connectivity; +import org.apache.hc.client5.http.impl.classic.CloseableHttpClient; +import org.apache.hc.client5.http.impl.classic.HttpClients; +import org.apache.hc.client5.http.io.HttpClientConnectionManager; +import org.apache.hc.client5.http.impl.io.PoolingHttpClientConnectionManagerBuilder; +import org.apache.hc.client5.http.ssl.DefaultClientTlsStrategy; +import org.apache.hc.client5.http.ssl.HostnameVerificationPolicy; +import org.apache.hc.client5.http.ssl.NoopHostnameVerifier; +import org.apache.hc.core5.ssl.SSLContexts; +import org.springframework.http.client.HttpComponentsClientHttpRequestFactory; import org.springframework.web.client.RestTemplate; import org.testcontainers.shaded.org.apache.commons.lang3.RandomStringUtils; import org.testng.annotations.AfterMethod; +import org.testng.annotations.BeforeClass; import org.testng.annotations.BeforeMethod; import org.testng.annotations.Test; import org.thingsboard.rest.client.RestClient; @@ -31,14 +41,39 @@ import org.thingsboard.server.common.data.page.TimePageLink; import org.thingsboard.server.msa.AbstractContainerTest; import org.thingsboard.server.msa.TestProperties; +import javax.net.ssl.SSLContext; import java.util.List; import static org.assertj.core.api.Assertions.assertThat; import static org.thingsboard.server.msa.prototypes.DevicePrototypes.defaultDevicePrototype; -public class RestClientTest extends AbstractContainerTest { +public class JavaRestClientTest extends AbstractContainerTest { - private static final RestClient restClient = new RestClient(new RestTemplate(), TestProperties.getBaseUrl()); + private RestClient restClient; + + @BeforeClass + public void beforeClass() throws Exception { + SSLContext ssl = SSLContexts.custom() + .loadTrustMaterial((chain, authType) -> true) + .build(); + + var tls = new DefaultClientTlsStrategy( + ssl, + HostnameVerificationPolicy.CLIENT, + NoopHostnameVerifier.INSTANCE + ); + + HttpClientConnectionManager cm = PoolingHttpClientConnectionManagerBuilder.create() + .setTlsSocketStrategy(tls) + .build(); + + CloseableHttpClient httpClient = HttpClients.custom() + .setConnectionManager(cm) + .build(); + + RestTemplate rt = new RestTemplate(new HttpComponentsClientHttpRequestFactory(httpClient)); + restClient = new RestClient(rt, TestProperties.getBaseUrl()); + } @BeforeMethod public void setUp() throws Exception { From c01302fe6bd349f5a8524ce8e9ec3133a4d7f50f Mon Sep 17 00:00:00 2001 From: dashevchenko Date: Tue, 21 Oct 2025 18:20:45 +0300 Subject: [PATCH 05/42] added lock on entity creation to fix race condition on multiple entity creation --- .../server/dao/asset/BaseAssetService.java | 4 ++ .../dao/customer/CustomerServiceImpl.java | 2 +- .../dao/dashboard/DashboardServiceImpl.java | 4 ++ .../server/dao/device/DeviceServiceImpl.java | 4 ++ .../server/dao/edge/EdgeServiceImpl.java | 4 ++ .../dao/entity/AbstractEntityService.java | 23 ++++++++ .../server/dao/rule/BaseRuleChainService.java | 4 ++ .../server/dao/user/UserServiceImpl.java | 4 ++ .../dao/service/AbstractServiceTest.java | 6 ++ .../server/dao/service/AssetServiceTest.java | 57 +++++++++++++++++++ .../server/dao/service/DeviceServiceTest.java | 31 ++++++++++ 11 files changed, 142 insertions(+), 1 deletion(-) diff --git a/dao/src/main/java/org/thingsboard/server/dao/asset/BaseAssetService.java b/dao/src/main/java/org/thingsboard/server/dao/asset/BaseAssetService.java index fed201d403..3e49c6d6b3 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/asset/BaseAssetService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/asset/BaseAssetService.java @@ -148,6 +148,10 @@ public class BaseAssetService extends AbstractCachedEntityService doSaveAsset(asset, doValidate)); + } + + private Asset doSaveAsset(Asset asset, boolean doValidate) { log.trace("Executing saveAsset [{}]", asset); Asset oldAsset = null; if (doValidate) { diff --git a/dao/src/main/java/org/thingsboard/server/dao/customer/CustomerServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/customer/CustomerServiceImpl.java index b06902d95e..fa1de490fa 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/customer/CustomerServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/customer/CustomerServiceImpl.java @@ -139,7 +139,7 @@ public class CustomerServiceImpl extends AbstractCachedEntityService saveCustomer(customer, true)); } private Customer saveCustomer(Customer customer, boolean doValidate) { diff --git a/dao/src/main/java/org/thingsboard/server/dao/dashboard/DashboardServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/dashboard/DashboardServiceImpl.java index 1b21310237..b044f8fbe7 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/dashboard/DashboardServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/dashboard/DashboardServiceImpl.java @@ -157,6 +157,10 @@ public class DashboardServiceImpl extends AbstractEntityService implements Dashb @Override public Dashboard saveDashboard(Dashboard dashboard, boolean doValidate) { + return saveLimitedEntity(dashboard, () -> doSaveDashboard(dashboard, doValidate)); + } + + private Dashboard doSaveDashboard(Dashboard dashboard, boolean doValidate) { log.trace("Executing saveDashboard [{}]", dashboard); if (doValidate) { dashboardValidator.validate(dashboard, DashboardInfo::getTenantId); diff --git a/dao/src/main/java/org/thingsboard/server/dao/device/DeviceServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/device/DeviceServiceImpl.java index 6d993f3e3d..cd64ddccda 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/device/DeviceServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/device/DeviceServiceImpl.java @@ -210,6 +210,10 @@ public class DeviceServiceImpl extends CachedVersionedEntityService doSaveDeviceWithoutCredentials(device, doValidate)); + } + + private Device doSaveDeviceWithoutCredentials(Device device, boolean doValidate) { log.trace("Executing saveDevice [{}]", device); Device oldDevice = null; if (doValidate) { diff --git a/dao/src/main/java/org/thingsboard/server/dao/edge/EdgeServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/edge/EdgeServiceImpl.java index 0655d05572..b71decf5f6 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/edge/EdgeServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/edge/EdgeServiceImpl.java @@ -201,6 +201,10 @@ public class EdgeServiceImpl extends AbstractCachedEntityService doSaveEdge(edge)); + } + + private Edge doSaveEdge(Edge edge) { log.trace("Executing saveEdge [{}]", edge); Edge oldEdge = edgeValidator.validate(edge, Edge::getTenantId); EdgeCacheEvictEvent evictEvent = new EdgeCacheEvictEvent(edge.getTenantId(), edge.getName(), oldEdge != null ? oldEdge.getName() : null); diff --git a/dao/src/main/java/org/thingsboard/server/dao/entity/AbstractEntityService.java b/dao/src/main/java/org/thingsboard/server/dao/entity/AbstractEntityService.java index 7560c7fb76..8a8f74671b 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/entity/AbstractEntityService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/entity/AbstractEntityService.java @@ -21,13 +21,16 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.ApplicationEventPublisher; import org.springframework.context.annotation.Lazy; +import org.springframework.util.ConcurrentReferenceHashMap; import org.thingsboard.common.util.DebugModeUtil; import org.thingsboard.server.common.data.EntityView; import org.thingsboard.server.common.data.HasDebugSettings; +import org.thingsboard.server.common.data.HasTenantId; import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.debug.DebugSettings; import org.thingsboard.server.common.data.id.EdgeId; import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.data.id.HasId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.relation.EntityRelation; import org.thingsboard.server.common.data.relation.RelationTypeGroup; @@ -44,7 +47,10 @@ import java.util.Collections; import java.util.List; import java.util.Map; import java.util.Optional; +import java.util.concurrent.ConcurrentMap; import java.util.concurrent.TimeUnit; +import java.util.concurrent.locks.ReentrantLock; +import java.util.function.Supplier; @Slf4j public abstract class AbstractEntityService { @@ -52,6 +58,8 @@ public abstract class AbstractEntityService { public static final String INCORRECT_EDGE_ID = "Incorrect edgeId "; public static final String INCORRECT_PAGE_LINK = "Incorrect page link "; + private final ConcurrentMap entityCreationLocks = new ConcurrentReferenceHashMap<>(16); + @Autowired protected ApplicationEventPublisher eventPublisher; @@ -86,6 +94,21 @@ public abstract class AbstractEntityService { @Value("${debug.settings.default_duration:15}") private int defaultDebugDurationMinutes; + protected E saveLimitedEntity(E entity, Supplier saveFunction) { + log.debug("Creating limited entity: {}", entity); + if (entity.getId() == null) { + ReentrantLock lock = entityCreationLocks.computeIfAbsent(entity.getTenantId(), id -> new ReentrantLock()); + lock.lock(); + try { + return saveFunction.get(); + } finally { + lock.unlock(); + } + } else { + return saveFunction.get(); + } + } + protected void createRelation(TenantId tenantId, EntityRelation relation) { log.debug("Creating relation: {}", relation); relationService.saveRelation(tenantId, relation); diff --git a/dao/src/main/java/org/thingsboard/server/dao/rule/BaseRuleChainService.java b/dao/src/main/java/org/thingsboard/server/dao/rule/BaseRuleChainService.java index 8538bd9492..47e82f7df1 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/rule/BaseRuleChainService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/rule/BaseRuleChainService.java @@ -125,6 +125,10 @@ public class BaseRuleChainService extends AbstractEntityService implements RuleC @Override @Transactional public RuleChain saveRuleChain(RuleChain ruleChain, boolean publishSaveEvent, boolean doValidate) { + return saveLimitedEntity(ruleChain, () -> doSaveRuleChain(ruleChain, publishSaveEvent, true)); + } + + private RuleChain doSaveRuleChain(RuleChain ruleChain, boolean publishSaveEvent, boolean doValidate) { log.trace("Executing doSaveRuleChain [{}]", ruleChain); if (doValidate) { ruleChainValidator.validate(ruleChain, RuleChain::getTenantId); diff --git a/dao/src/main/java/org/thingsboard/server/dao/user/UserServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/user/UserServiceImpl.java index 5c94ba1891..8b92c5f8ac 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/user/UserServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/user/UserServiceImpl.java @@ -159,6 +159,10 @@ public class UserServiceImpl extends AbstractCachedEntityService doSaveUser(tenantId, user)); + } + + private User doSaveUser(TenantId tenantId, User user) { log.trace("Executing saveUser [{}]", user); User oldUser = userValidator.validate(user, User::getTenantId); if (!userLoginCaseSensitive) { diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/AbstractServiceTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/AbstractServiceTest.java index 25339244fd..0d2b9b5d9f 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/service/AbstractServiceTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/service/AbstractServiceTest.java @@ -48,6 +48,7 @@ import org.thingsboard.server.common.data.id.DeviceProfileId; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.HasId; import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.id.TenantProfileId; import org.thingsboard.server.common.data.oauth2.MapperType; import org.thingsboard.server.common.data.oauth2.OAuth2Client; import org.thingsboard.server.common.data.oauth2.OAuth2CustomMapperConfig; @@ -185,8 +186,13 @@ public abstract class AbstractServiceTest { } public Tenant createTenant() { + return createTenant(null); + } + + public Tenant createTenant(TenantProfileId tenantProfileId) { Tenant tenant = new Tenant(); tenant.setTitle("My tenant " + UUID.randomUUID()); + tenant.setTenantProfileId(tenantProfileId); Tenant savedTenant = tenantService.saveTenant(tenant); assertNotNull(savedTenant); return savedTenant; diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/AssetServiceTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/AssetServiceTest.java index 462e7a894c..ccd0a1ca9b 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/service/AssetServiceTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/service/AssetServiceTest.java @@ -16,17 +16,25 @@ package org.thingsboard.server.dao.service; import com.datastax.oss.driver.api.core.uuid.Uuids; +import com.google.common.util.concurrent.ListeningExecutorService; +import com.google.common.util.concurrent.MoreExecutors; +import org.junit.After; import org.junit.Assert; +import org.junit.Before; import org.junit.Test; import org.junit.jupiter.api.Assertions; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.transaction.PlatformTransactionManager; import org.springframework.transaction.TransactionStatus; import org.springframework.transaction.support.DefaultTransactionDefinition; +import org.testcontainers.shaded.org.apache.commons.lang3.RandomStringUtils; +import org.testcontainers.shaded.org.awaitility.Awaitility; +import org.thingsboard.common.util.ThingsBoardThreadFactory; import org.thingsboard.server.common.data.Customer; import org.thingsboard.server.common.data.EntitySubtype; import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.Tenant; +import org.thingsboard.server.common.data.TenantProfile; import org.thingsboard.server.common.data.asset.Asset; import org.thingsboard.server.common.data.asset.AssetInfo; import org.thingsboard.server.common.data.asset.AssetProfile; @@ -44,6 +52,8 @@ import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.common.data.relation.EntityRelation; import org.thingsboard.server.common.data.relation.RelationTypeGroup; +import org.thingsboard.server.common.data.tenant.profile.DefaultTenantProfileConfiguration; +import org.thingsboard.server.common.data.tenant.profile.TenantProfileData; import org.thingsboard.server.dao.asset.AssetDao; import org.thingsboard.server.dao.asset.AssetProfileService; import org.thingsboard.server.dao.asset.AssetService; @@ -51,11 +61,14 @@ import org.thingsboard.server.dao.cf.CalculatedFieldService; import org.thingsboard.server.dao.customer.CustomerService; import org.thingsboard.server.dao.exception.DataValidationException; import org.thingsboard.server.dao.relation.RelationService; +import org.thingsboard.server.dao.tenant.TenantProfileService; import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.Map; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.thingsboard.server.dao.model.ModelConstants.NULL_UUID; @@ -72,6 +85,8 @@ public class AssetServiceTest extends AbstractServiceTest { @Autowired RelationService relationService; @Autowired + TenantProfileService tenantProfileService; + @Autowired private AssetProfileService assetProfileService; @Autowired private CalculatedFieldService calculatedFieldService; @@ -79,6 +94,18 @@ public class AssetServiceTest extends AbstractServiceTest { private PlatformTransactionManager platformTransactionManager; private IdComparator idComparator = new IdComparator<>(); + ListeningExecutorService executor; + private TenantId anotherTenantId; + + @Before + public void before() { + executor = MoreExecutors.listeningDecorator(Executors.newFixedThreadPool(10, ThingsBoardThreadFactory.forName(getClass().getSimpleName() + "-test-scope"))); + } + + @After + public void after() { + executor.shutdownNow(); + } @Test public void testSaveAsset() { @@ -105,6 +132,36 @@ public class AssetServiceTest extends AbstractServiceTest { assetService.deleteAsset(tenantId, savedAsset.getId()); } + @Test + public void testAssetLimitOnTenantProfileLevel() { + TenantProfile tenantProfile = new TenantProfile(); + tenantProfile.setName("Test profile"); + tenantProfile.setDescription("Test"); + TenantProfileData profileData = new TenantProfileData(); + profileData.setConfiguration(DefaultTenantProfileConfiguration.builder().maxAssets(5l).build()); + tenantProfile.setProfileData(profileData); + tenantProfile.setDefault(false); + tenantProfile.setIsolatedTbRuleEngine(false); + + tenantProfile = tenantProfileService.saveTenantProfile(anotherTenantId, tenantProfile); + anotherTenantId = createTenant(tenantProfile.getId()).getId(); + + for (int i = 0; i < 20; i++) { + executor.submit(() -> { + Asset asset = new Asset(); + asset.setTenantId(anotherTenantId); + asset.setName(RandomStringUtils.randomAlphabetic(10)); + asset.setType("default"); + assetService.saveAsset(asset); + }); + } + + Awaitility.await().atMost(10, TimeUnit.SECONDS).until(() -> { + long countByTenantId = assetService.countByTenantId(anotherTenantId); + return countByTenantId == 5; + }); + } + @Test public void testShouldNotPutInCacheRolledbackAssetProfile() { AssetProfile assetProfile = new AssetProfile(); diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/DeviceServiceTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/DeviceServiceTest.java index 32767043d7..257eed8232 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/service/DeviceServiceTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/service/DeviceServiceTest.java @@ -16,6 +16,8 @@ package org.thingsboard.server.dao.service; import com.datastax.oss.driver.api.core.uuid.Uuids; +import com.google.common.util.concurrent.ListeningExecutorService; +import com.google.common.util.concurrent.MoreExecutors; import org.junit.After; import org.junit.Assert; import org.junit.Before; @@ -27,6 +29,8 @@ import org.springframework.boot.test.mock.mockito.SpyBean; import org.springframework.transaction.PlatformTransactionManager; import org.springframework.transaction.TransactionStatus; import org.springframework.transaction.support.DefaultTransactionDefinition; +import org.testcontainers.shaded.org.awaitility.Awaitility; +import org.thingsboard.common.util.ThingsBoardThreadFactory; import org.thingsboard.server.common.data.Customer; import org.thingsboard.server.common.data.Device; import org.thingsboard.server.common.data.DeviceInfo; @@ -74,6 +78,8 @@ import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.Map; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.mockito.ArgumentMatchers.any; @@ -105,10 +111,12 @@ public class DeviceServiceTest extends AbstractServiceTest { private IdComparator idComparator = new IdComparator<>(); private TenantId anotherTenantId; + private ListeningExecutorService executor; @Before public void before() { anotherTenantId = createTenant().getId(); + executor = MoreExecutors.listeningDecorator(Executors.newFixedThreadPool(10, ThingsBoardThreadFactory.forName(getClass().getSimpleName() + "-test-scope"))); } @After @@ -118,6 +126,7 @@ public class DeviceServiceTest extends AbstractServiceTest { tenantProfileService.deleteTenantProfiles(tenantId); tenantProfileService.deleteTenantProfiles(anotherTenantId); + executor.shutdownNow(); } @Test @@ -136,6 +145,28 @@ public class DeviceServiceTest extends AbstractServiceTest { deleteDevice(tenantId, device); } + @Test + public void testDeviceLimitOnTenantProfileLevel() { + TenantProfile defaultTenantProfile = tenantProfileService.findDefaultTenantProfile(tenantId); + defaultTenantProfile.getProfileData().setConfiguration(DefaultTenantProfileConfiguration.builder().maxDevices(5l).build()); + tenantProfileService.saveTenantProfile(tenantId, defaultTenantProfile); + + for (int i = 0; i < 20; i++) { + executor.submit(() -> { + Device device = new Device(); + device.setTenantId(tenantId); + device.setName(StringUtils.randomAlphabetic(10)); + device.setType("default"); + deviceService.saveDevice(device, true); + }); + } + + Awaitility.await().atMost(10, TimeUnit.SECONDS).until(() -> { + long countByTenantId = deviceService.countByTenantId(tenantId); + return countByTenantId == 5; + }); + } + @Test public void testSaveDevicesWithMaxDeviceOutOfLimit() { TenantProfile defaultTenantProfile = tenantProfileService.findDefaultTenantProfile(tenantId); From e8e8ca13ecf81a7d5f53a8ef8c6bd8bf52ce0d77 Mon Sep 17 00:00:00 2001 From: dashevchenko Date: Wed, 22 Oct 2025 17:24:40 +0300 Subject: [PATCH 06/42] refactoring --- .../server/dao/asset/BaseAssetService.java | 2 +- .../dao/customer/CustomerServiceImpl.java | 2 +- .../dao/dashboard/DashboardServiceImpl.java | 2 +- .../server/dao/device/DeviceServiceImpl.java | 2 +- .../server/dao/edge/EdgeServiceImpl.java | 2 +- .../dao/entity/AbstractEntityService.java | 3 +-- .../server/dao/rule/BaseRuleChainService.java | 2 +- .../server/dao/user/UserServiceImpl.java | 2 +- .../server/dao/service/AssetServiceTest.java | 17 +++++++++-------- .../server/dao/service/DeviceServiceTest.java | 16 +++++++++++++--- 10 files changed, 30 insertions(+), 20 deletions(-) diff --git a/dao/src/main/java/org/thingsboard/server/dao/asset/BaseAssetService.java b/dao/src/main/java/org/thingsboard/server/dao/asset/BaseAssetService.java index 3e49c6d6b3..755a39fddd 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/asset/BaseAssetService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/asset/BaseAssetService.java @@ -148,7 +148,7 @@ public class BaseAssetService extends AbstractCachedEntityService doSaveAsset(asset, doValidate)); + return saveEntity(asset, () -> doSaveAsset(asset, doValidate)); } private Asset doSaveAsset(Asset asset, boolean doValidate) { diff --git a/dao/src/main/java/org/thingsboard/server/dao/customer/CustomerServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/customer/CustomerServiceImpl.java index fa1de490fa..2e77c5b866 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/customer/CustomerServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/customer/CustomerServiceImpl.java @@ -139,7 +139,7 @@ public class CustomerServiceImpl extends AbstractCachedEntityService saveCustomer(customer, true)); + return saveEntity(customer, () -> saveCustomer(customer, true)); } private Customer saveCustomer(Customer customer, boolean doValidate) { diff --git a/dao/src/main/java/org/thingsboard/server/dao/dashboard/DashboardServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/dashboard/DashboardServiceImpl.java index b044f8fbe7..b41e4053eb 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/dashboard/DashboardServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/dashboard/DashboardServiceImpl.java @@ -157,7 +157,7 @@ public class DashboardServiceImpl extends AbstractEntityService implements Dashb @Override public Dashboard saveDashboard(Dashboard dashboard, boolean doValidate) { - return saveLimitedEntity(dashboard, () -> doSaveDashboard(dashboard, doValidate)); + return saveEntity(dashboard, () -> doSaveDashboard(dashboard, doValidate)); } private Dashboard doSaveDashboard(Dashboard dashboard, boolean doValidate) { diff --git a/dao/src/main/java/org/thingsboard/server/dao/device/DeviceServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/device/DeviceServiceImpl.java index cd64ddccda..d387ac82a0 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/device/DeviceServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/device/DeviceServiceImpl.java @@ -210,7 +210,7 @@ public class DeviceServiceImpl extends CachedVersionedEntityService doSaveDeviceWithoutCredentials(device, doValidate)); + return saveEntity(device, () -> doSaveDeviceWithoutCredentials(device, doValidate)); } private Device doSaveDeviceWithoutCredentials(Device device, boolean doValidate) { diff --git a/dao/src/main/java/org/thingsboard/server/dao/edge/EdgeServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/edge/EdgeServiceImpl.java index b71decf5f6..ee3ac36c31 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/edge/EdgeServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/edge/EdgeServiceImpl.java @@ -201,7 +201,7 @@ public class EdgeServiceImpl extends AbstractCachedEntityService doSaveEdge(edge)); + return saveEntity(edge, () -> doSaveEdge(edge)); } private Edge doSaveEdge(Edge edge) { diff --git a/dao/src/main/java/org/thingsboard/server/dao/entity/AbstractEntityService.java b/dao/src/main/java/org/thingsboard/server/dao/entity/AbstractEntityService.java index 8a8f74671b..3f653c673b 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/entity/AbstractEntityService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/entity/AbstractEntityService.java @@ -94,8 +94,7 @@ public abstract class AbstractEntityService { @Value("${debug.settings.default_duration:15}") private int defaultDebugDurationMinutes; - protected E saveLimitedEntity(E entity, Supplier saveFunction) { - log.debug("Creating limited entity: {}", entity); + protected E saveEntity(E entity, Supplier saveFunction) { if (entity.getId() == null) { ReentrantLock lock = entityCreationLocks.computeIfAbsent(entity.getTenantId(), id -> new ReentrantLock()); lock.lock(); diff --git a/dao/src/main/java/org/thingsboard/server/dao/rule/BaseRuleChainService.java b/dao/src/main/java/org/thingsboard/server/dao/rule/BaseRuleChainService.java index 47e82f7df1..97a732f94a 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/rule/BaseRuleChainService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/rule/BaseRuleChainService.java @@ -125,7 +125,7 @@ public class BaseRuleChainService extends AbstractEntityService implements RuleC @Override @Transactional public RuleChain saveRuleChain(RuleChain ruleChain, boolean publishSaveEvent, boolean doValidate) { - return saveLimitedEntity(ruleChain, () -> doSaveRuleChain(ruleChain, publishSaveEvent, true)); + return saveEntity(ruleChain, () -> doSaveRuleChain(ruleChain, publishSaveEvent, doValidate)); } private RuleChain doSaveRuleChain(RuleChain ruleChain, boolean publishSaveEvent, boolean doValidate) { diff --git a/dao/src/main/java/org/thingsboard/server/dao/user/UserServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/user/UserServiceImpl.java index 8b92c5f8ac..70c356eb45 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/user/UserServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/user/UserServiceImpl.java @@ -159,7 +159,7 @@ public class UserServiceImpl extends AbstractCachedEntityService doSaveUser(tenantId, user)); + return saveEntity(user, () -> doSaveUser(tenantId, user)); } private User doSaveUser(TenantId tenantId, User user) { diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/AssetServiceTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/AssetServiceTest.java index ccd0a1ca9b..9ad8b42c21 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/service/AssetServiceTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/service/AssetServiceTest.java @@ -18,9 +18,9 @@ package org.thingsboard.server.dao.service; import com.datastax.oss.driver.api.core.uuid.Uuids; import com.google.common.util.concurrent.ListeningExecutorService; import com.google.common.util.concurrent.MoreExecutors; -import org.junit.After; +import org.junit.AfterClass; import org.junit.Assert; -import org.junit.Before; +import org.junit.BeforeClass; import org.junit.Test; import org.junit.jupiter.api.Assertions; import org.springframework.beans.factory.annotation.Autowired; @@ -93,17 +93,18 @@ public class AssetServiceTest extends AbstractServiceTest { @Autowired private PlatformTransactionManager platformTransactionManager; + private static ListeningExecutorService executor; + private IdComparator idComparator = new IdComparator<>(); - ListeningExecutorService executor; private TenantId anotherTenantId; - @Before - public void before() { - executor = MoreExecutors.listeningDecorator(Executors.newFixedThreadPool(10, ThingsBoardThreadFactory.forName(getClass().getSimpleName() + "-test-scope"))); + @BeforeClass + public static void before() { + executor = MoreExecutors.listeningDecorator(Executors.newFixedThreadPool(10, ThingsBoardThreadFactory.forName("AssetServiceTestScope"))); } - @After - public void after() { + @AfterClass + public static void after() { executor.shutdownNow(); } diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/DeviceServiceTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/DeviceServiceTest.java index 257eed8232..16bd851641 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/service/DeviceServiceTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/service/DeviceServiceTest.java @@ -19,8 +19,10 @@ import com.datastax.oss.driver.api.core.uuid.Uuids; import com.google.common.util.concurrent.ListeningExecutorService; import com.google.common.util.concurrent.MoreExecutors; import org.junit.After; +import org.junit.AfterClass; import org.junit.Assert; import org.junit.Before; +import org.junit.BeforeClass; import org.junit.Test; import org.junit.jupiter.api.Assertions; import org.mockito.Mockito; @@ -111,12 +113,21 @@ public class DeviceServiceTest extends AbstractServiceTest { private IdComparator idComparator = new IdComparator<>(); private TenantId anotherTenantId; - private ListeningExecutorService executor; + private static ListeningExecutorService executor; + + @BeforeClass + public static void beforeClass() { + executor = MoreExecutors.listeningDecorator(Executors.newFixedThreadPool(10, ThingsBoardThreadFactory.forName("DeviceServiceTestScope"))); + } + + @AfterClass + public static void afterClass() { + executor.shutdownNow(); + } @Before public void before() { anotherTenantId = createTenant().getId(); - executor = MoreExecutors.listeningDecorator(Executors.newFixedThreadPool(10, ThingsBoardThreadFactory.forName(getClass().getSimpleName() + "-test-scope"))); } @After @@ -126,7 +137,6 @@ public class DeviceServiceTest extends AbstractServiceTest { tenantProfileService.deleteTenantProfiles(tenantId); tenantProfileService.deleteTenantProfiles(anotherTenantId); - executor.shutdownNow(); } @Test From c464ba993ff633b0c882bd2aef1d782500f91fad Mon Sep 17 00:00:00 2001 From: Andrii Landiak Date: Tue, 28 Oct 2025 13:12:16 +0200 Subject: [PATCH 07/42] Improve alarm template --- .../AlarmAssignmentTriggerProcessor.java | 1 + .../trigger/AlarmCommentTriggerProcessor.java | 5 +++- .../rule/trigger/AlarmTriggerProcessor.java | 17 ++++++++++++ .../server/controller/AbstractWebTest.java | 11 +++++--- .../notification/NotificationRuleApiTest.java | 24 ++++++++--------- .../server/common/data/alarm/AlarmInfo.java | 8 ++++-- .../common/data/device/profile/AlarmRule.java | 4 +++ .../info/AlarmAssignmentNotificationInfo.java | 4 ++- .../info/AlarmCommentNotificationInfo.java | 4 ++- .../info/AlarmNotificationInfo.java | 26 ++++++++++--------- .../rule/trigger/AlarmCommentTrigger.java | 5 ++++ .../rule/trigger/AlarmTrigger.java | 5 ++++ ...signmentNotificationRuleTriggerConfig.java | 4 +++ ...mCommentNotificationRuleTriggerConfig.java | 4 +++ .../AlarmNotificationRuleTriggerConfig.java | 6 +++++ .../thingsboard/server/common/msg/TbMsg.java | 13 ++++------ .../assets/help/en_US/notification/alarm.md | 2 ++ .../en_US/notification/alarm_assignment.md | 1 + .../help/en_US/notification/alarm_comment.md | 1 + 19 files changed, 105 insertions(+), 40 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/service/notification/rule/trigger/AlarmAssignmentTriggerProcessor.java b/application/src/main/java/org/thingsboard/server/service/notification/rule/trigger/AlarmAssignmentTriggerProcessor.java index ca3667b6ef..f9646409c6 100644 --- a/application/src/main/java/org/thingsboard/server/service/notification/rule/trigger/AlarmAssignmentTriggerProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/notification/rule/trigger/AlarmAssignmentTriggerProcessor.java @@ -62,6 +62,7 @@ public class AlarmAssignmentTriggerProcessor implements NotificationRuleTriggerP .alarmType(alarmInfo.getType()) .alarmOriginator(alarmInfo.getOriginator()) .alarmOriginatorName(alarmInfo.getOriginatorName()) + .alarmOriginatorLabel(alarmInfo.getOriginatorLabel()) .alarmSeverity(alarmInfo.getSeverity()) .alarmStatus(alarmInfo.getStatus()) .alarmCustomerId(alarmInfo.getCustomerId()) diff --git a/application/src/main/java/org/thingsboard/server/service/notification/rule/trigger/AlarmCommentTriggerProcessor.java b/application/src/main/java/org/thingsboard/server/service/notification/rule/trigger/AlarmCommentTriggerProcessor.java index 8cca98f7fd..691be2e894 100644 --- a/application/src/main/java/org/thingsboard/server/service/notification/rule/trigger/AlarmCommentTriggerProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/notification/rule/trigger/AlarmCommentTriggerProcessor.java @@ -57,11 +57,13 @@ public class AlarmCommentTriggerProcessor implements NotificationRuleTriggerProc @Override public RuleOriginatedNotificationInfo constructNotificationInfo(AlarmCommentTrigger trigger) { Alarm alarm = trigger.getAlarm(); - String originatorName; + String originatorName, originatorLabel; if (alarm instanceof AlarmInfo) { originatorName = ((AlarmInfo) alarm).getOriginatorName(); + originatorLabel = ((AlarmInfo) alarm).getOriginatorLabel(); } else { originatorName = entityService.fetchEntityName(trigger.getTenantId(), alarm.getOriginator()).orElse(""); + originatorLabel = entityService.fetchEntityLabel(trigger.getTenantId(), alarm.getOriginator()).orElse(""); } return AlarmCommentNotificationInfo.builder() .comment(trigger.getComment().getComment().get("text").asText()) @@ -73,6 +75,7 @@ public class AlarmCommentTriggerProcessor implements NotificationRuleTriggerProc .alarmType(alarm.getType()) .alarmOriginator(alarm.getOriginator()) .alarmOriginatorName(originatorName) + .alarmOriginatorLabel(originatorLabel) .alarmSeverity(alarm.getSeverity()) .alarmStatus(alarm.getStatus()) .alarmCustomerId(alarm.getCustomerId()) diff --git a/application/src/main/java/org/thingsboard/server/service/notification/rule/trigger/AlarmTriggerProcessor.java b/application/src/main/java/org/thingsboard/server/service/notification/rule/trigger/AlarmTriggerProcessor.java index 7d6959dc4f..af80d7f2ab 100644 --- a/application/src/main/java/org/thingsboard/server/service/notification/rule/trigger/AlarmTriggerProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/notification/rule/trigger/AlarmTriggerProcessor.java @@ -15,7 +15,9 @@ */ package org.thingsboard.server.service.notification.rule.trigger; +import com.fasterxml.jackson.databind.JsonNode; import org.springframework.stereotype.Service; +import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.server.common.data.alarm.Alarm; import org.thingsboard.server.common.data.alarm.AlarmApiCallResult; import org.thingsboard.server.common.data.alarm.AlarmInfo; @@ -28,6 +30,9 @@ import org.thingsboard.server.common.data.notification.rule.trigger.config.Alarm import org.thingsboard.server.common.data.notification.rule.trigger.config.AlarmNotificationRuleTriggerConfig.ClearRule; import org.thingsboard.server.common.data.notification.rule.trigger.config.NotificationRuleTriggerType; +import java.util.HashMap; +import java.util.Map; + import static org.apache.commons.collections4.CollectionUtils.isNotEmpty; import static org.thingsboard.server.common.data.util.CollectionsUtil.emptyOrContains; @@ -106,15 +111,27 @@ public class AlarmTriggerProcessor implements NotificationRuleTriggerProcessor toInfoTemplateMap(JsonNode details) { + Map infoMap = JacksonUtil.toFlatMap(details); + Map result = new HashMap<>(); + for (Map.Entry entry : infoMap.entrySet()) { + String key = "info." + entry.getKey(); + result.put(key, entry.getValue()); + } + return result; + } + @Override public NotificationRuleTriggerType getTriggerType() { return NotificationRuleTriggerType.ALARM; diff --git a/application/src/test/java/org/thingsboard/server/controller/AbstractWebTest.java b/application/src/test/java/org/thingsboard/server/controller/AbstractWebTest.java index fcbe203e79..f04cc049c8 100644 --- a/application/src/test/java/org/thingsboard/server/controller/AbstractWebTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/AbstractWebTest.java @@ -680,9 +680,14 @@ public abstract class AbstractWebTest extends AbstractInMemoryStorageTest { } protected Device createDevice(String name, String accessToken) throws Exception { + return createDevice(name, "default", null, accessToken); + } + + protected Device createDevice(String name, String type, String label, String accessToken) throws Exception { Device device = new Device(); device.setName(name); - device.setType("default"); + device.setType(type); + device.setLabel(label); DeviceData deviceData = new DeviceData(); deviceData.setTransportConfiguration(new DefaultDeviceTransportConfiguration()); deviceData.setConfiguration(new DefaultDeviceConfiguration()); @@ -1117,7 +1122,7 @@ public abstract class AbstractWebTest extends AbstractInMemoryStorageTest { Awaitility.await("CF state for entity actor ready to refresh dynamic arguments").atMost(TIMEOUT, TimeUnit.SECONDS).until(() -> { CalculatedFieldState calculatedFieldState = statesMap.get(cfId); boolean isReady = calculatedFieldState != null && ((GeofencingCalculatedFieldState) calculatedFieldState).getLastDynamicArgumentsRefreshTs() - < System.currentTimeMillis() - TimeUnit.SECONDS.toMillis(scheduledUpdateInterval); + < System.currentTimeMillis() - TimeUnit.SECONDS.toMillis(scheduledUpdateInterval); log.warn("entityId {}, cfId {}, state ready to refresh == {}", entityId, cfId, isReady); return isReady; }); @@ -1308,7 +1313,7 @@ public abstract class AbstractWebTest extends AbstractInMemoryStorageTest { protected List findJobs(List types, List entities) throws Exception { return doGetTypedWithPageLink("/api/jobs?types=" + types.stream().map(Enum::name).collect(Collectors.joining(",")) + - "&entities=" + entities.stream().map(UUID::toString).collect(Collectors.joining(",")) + "&", + "&entities=" + entities.stream().map(UUID::toString).collect(Collectors.joining(",")) + "&", new TypeReference>() {}, new PageLink(100, 0, null, new SortOrder("createdTime", SortOrder.Direction.DESC))).getData(); } diff --git a/application/src/test/java/org/thingsboard/server/service/notification/NotificationRuleApiTest.java b/application/src/test/java/org/thingsboard/server/service/notification/NotificationRuleApiTest.java index bab70ea505..10eff284f8 100644 --- a/application/src/test/java/org/thingsboard/server/service/notification/NotificationRuleApiTest.java +++ b/application/src/test/java/org/thingsboard/server/service/notification/NotificationRuleApiTest.java @@ -22,9 +22,9 @@ import org.junit.Before; import org.junit.Test; import org.junit.function.ThrowingRunnable; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.mock.mockito.SpyBean; import org.springframework.data.util.Pair; import org.springframework.test.context.TestPropertySource; +import org.springframework.test.context.bean.override.mockito.MockitoSpyBean; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.server.cache.limits.RateLimitService; import org.thingsboard.server.common.data.DataConstants; @@ -137,7 +137,7 @@ import static org.thingsboard.server.common.data.notification.rule.trigger.confi }) public class NotificationRuleApiTest extends AbstractNotificationApiTest { - @SpyBean + @MockitoSpyBean private AlarmSubscriptionService alarmSubscriptionService; @Autowired private DefaultSystemInfoService systemInfoService; @@ -193,7 +193,7 @@ public class NotificationRuleApiTest extends AbstractNotificationApiTest { @Test public void testNotificationRuleProcessing_alarmTrigger() throws Exception { String notificationSubject = "Alarm type: ${alarmType}, status: ${alarmStatus}, " + - "severity: ${alarmSeverity}, deviceId: ${alarmOriginatorId}"; + "severity: ${alarmSeverity}, deviceId: ${alarmOriginatorId}, details: ${data}."; String notificationText = "Status: ${alarmStatus}, severity: ${alarmSeverity}"; NotificationTemplate notificationTemplate = createNotificationTemplate(NotificationType.ALARM, notificationSubject, notificationText, NotificationDeliveryMethod.WEB); @@ -221,12 +221,12 @@ public class NotificationRuleApiTest extends AbstractNotificationApiTest { clients.put(delay, userAndClient.getSecond()); } notificationRule.setRecipientsConfig(recipientsConfig); - notificationRule = saveNotificationRule(notificationRule); + saveNotificationRule(notificationRule); String alarmType = "myBoolIsTrue"; DeviceProfile deviceProfile = createDeviceProfileWithAlarmRules(alarmType); - Device device = createDevice("Device 1", deviceProfile.getName(), "1234"); + Device device = createDevice("Device 1", deviceProfile.getName(), "label", "1234"); clients.values().forEach(wsClient -> { wsClient.subscribeForUnreadNotifications(10).waitForReply(true); @@ -250,7 +250,7 @@ public class NotificationRuleApiTest extends AbstractNotificationApiTest { assertThat(actualDelay).isCloseTo(expectedDelay, offset(2.0)); assertThat(notification.getSubject()).isEqualTo("Alarm type: " + alarmType + ", status: " + AlarmStatus.ACTIVE_UNACK + ", " + - "severity: " + AlarmSeverity.CRITICAL.toString().toLowerCase() + ", deviceId: " + device.getId()); + "severity: " + AlarmSeverity.CRITICAL.toString().toLowerCase() + ", deviceId: " + device.getId() + ", details: attribute is true."); assertThat(notification.getText()).isEqualTo("Status: " + AlarmStatus.ACTIVE_UNACK + ", severity: " + AlarmSeverity.CRITICAL.toString().toLowerCase()); assertThat(notification.getType()).isEqualTo(NotificationType.ALARM); @@ -270,7 +270,7 @@ public class NotificationRuleApiTest extends AbstractNotificationApiTest { wsClient.waitForUpdate(true); Notification updatedNotification = wsClient.getLastDataUpdate().getUpdate(); assertThat(updatedNotification.getSubject()).isEqualTo("Alarm type: " + alarmType + ", status: " + expectedStatus + ", " + - "severity: " + expectedSeverity.toString().toLowerCase() + ", deviceId: " + device.getId()); + "severity: " + expectedSeverity.toString().toLowerCase() + ", deviceId: " + device.getId() + ", details: attribute is true."); assertThat(updatedNotification.getText()).isEqualTo("Status: " + expectedStatus + ", severity: " + expectedSeverity.toString().toLowerCase()); wsClient.close(); @@ -516,10 +516,10 @@ public class NotificationRuleApiTest extends AbstractNotificationApiTest { .notifyOn(Set.of(ASSIGNED, UNASSIGNED)) .build(); NotificationTarget target = createNotificationTarget(tenantAdminUserId); - String template = "${userEmail} ${action} alarm on ${alarmOriginatorEntityType} '${alarmOriginatorName}'. Assignee: ${assigneeEmail}"; + String template = "${userEmail} ${action} alarm on ${alarmOriginatorEntityType} '${alarmOriginatorName}' with label '${alarmOriginatorLabel}'. Assignee: ${assigneeEmail}"; createNotificationRule(triggerConfig, "Test", template, target.getId()); - Device device = createDevice("Device A", "123"); + Device device = createDevice("Device A", "default", "test", "123"); Alarm alarm = Alarm.builder() .tenantId(tenantId) .originator(device.getId()) @@ -536,7 +536,7 @@ public class NotificationRuleApiTest extends AbstractNotificationApiTest { doPost("/api/alarm/" + alarmId + "/assign/" + tenantAdminUserId).andExpect(status().isOk()); }, notification -> { assertThat(notification.getText()).isEqualTo( - TENANT_ADMIN_EMAIL + " assigned alarm on Device 'Device A'. Assignee: " + TENANT_ADMIN_EMAIL + TENANT_ADMIN_EMAIL + " assigned alarm on Device 'Device A' with label 'test'. Assignee: " + TENANT_ADMIN_EMAIL ); }); @@ -544,7 +544,7 @@ public class NotificationRuleApiTest extends AbstractNotificationApiTest { doDelete("/api/alarm/" + alarmId + "/assign").andExpect(status().isOk()); }, notification -> { assertThat(notification.getText()).isEqualTo( - TENANT_ADMIN_EMAIL + " unassigned alarm on Device 'Device A'. Assignee: " + TENANT_ADMIN_EMAIL + " unassigned alarm on Device 'Device A' with label 'test'. Assignee: " ); }); } @@ -950,7 +950,7 @@ public class NotificationRuleApiTest extends AbstractNotificationApiTest { alarm.setAlarmType(alarmType); alarm.setId(alarmType); AlarmRule alarmRule = new AlarmRule(); - alarmRule.setAlarmDetails("Details"); + alarmRule.setAlarmDetails("attribute is ${bool}"); AlarmCondition alarmCondition = new AlarmCondition(); alarmCondition.setSpec(new SimpleAlarmConditionSpec()); List condition = new ArrayList<>(); diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/alarm/AlarmInfo.java b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/AlarmInfo.java index b316a2ef50..ee168a2e0b 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/alarm/AlarmInfo.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/AlarmInfo.java @@ -21,11 +21,14 @@ import lombok.Getter; import lombok.Setter; import lombok.ToString; +import java.io.Serial; + @EqualsAndHashCode(callSuper = true) @ToString(callSuper = true) @Schema public class AlarmInfo extends Alarm { + @Serial private static final long serialVersionUID = 2807343093519543363L; @Getter @@ -58,9 +61,10 @@ public class AlarmInfo extends Alarm { public AlarmInfo(AlarmInfo alarmInfo) { super(alarmInfo); - this.originatorName = alarmInfo.originatorName; - this.originatorLabel = alarmInfo.originatorLabel; + this.originatorName = alarmInfo.getOriginatorName(); + this.originatorLabel = alarmInfo.getOriginatorLabel(); this.assignee = alarmInfo.getAssignee(); + this.originatorDisplayName = alarmInfo.getOriginatorDisplayName(); } public AlarmInfo(Alarm alarm, String originatorName, String originatorLabel, AlarmAssignee assignee) { diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/AlarmRule.java b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/AlarmRule.java index 16850e3669..faeecd7eb3 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/AlarmRule.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/AlarmRule.java @@ -21,12 +21,16 @@ import lombok.Data; import org.thingsboard.server.common.data.id.DashboardId; import org.thingsboard.server.common.data.validation.NoXss; +import java.io.Serial; import java.io.Serializable; @Schema @Data public class AlarmRule implements Serializable { + @Serial + private static final long serialVersionUID = -7617427132423304707L; + @Valid @Schema(description = "JSON object representing the alarm rule condition") private AlarmCondition condition; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/notification/info/AlarmAssignmentNotificationInfo.java b/common/data/src/main/java/org/thingsboard/server/common/data/notification/info/AlarmAssignmentNotificationInfo.java index f29a54d17a..086b326ddd 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/notification/info/AlarmAssignmentNotificationInfo.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/notification/info/AlarmAssignmentNotificationInfo.java @@ -53,6 +53,7 @@ public class AlarmAssignmentNotificationInfo implements RuleOriginatedNotificati private UUID alarmId; private EntityId alarmOriginator; private String alarmOriginatorName; + private String alarmOriginatorLabel; private AlarmSeverity alarmSeverity; private AlarmStatus alarmStatus; private CustomerId alarmCustomerId; @@ -77,7 +78,8 @@ public class AlarmAssignmentNotificationInfo implements RuleOriginatedNotificati "alarmStatus", alarmStatus.toString(), "alarmOriginatorEntityType", alarmOriginator.getEntityType().getNormalName(), "alarmOriginatorId", alarmOriginator.getId().toString(), - "alarmOriginatorName", alarmOriginatorName + "alarmOriginatorName", alarmOriginatorName, + "alarmOriginatorLabel", alarmOriginatorLabel ); } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/notification/info/AlarmCommentNotificationInfo.java b/common/data/src/main/java/org/thingsboard/server/common/data/notification/info/AlarmCommentNotificationInfo.java index e46d539399..202812921f 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/notification/info/AlarmCommentNotificationInfo.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/notification/info/AlarmCommentNotificationInfo.java @@ -48,6 +48,7 @@ public class AlarmCommentNotificationInfo implements RuleOriginatedNotificationI private UUID alarmId; private EntityId alarmOriginator; private String alarmOriginatorName; + private String alarmOriginatorLabel; private AlarmSeverity alarmSeverity; private AlarmStatus alarmStatus; private CustomerId alarmCustomerId; @@ -68,7 +69,8 @@ public class AlarmCommentNotificationInfo implements RuleOriginatedNotificationI "alarmStatus", alarmStatus.toString(), "alarmOriginatorEntityType", alarmOriginator.getEntityType().getNormalName(), "alarmOriginatorId", alarmOriginator.getId().toString(), - "alarmOriginatorName", alarmOriginatorName + "alarmOriginatorName", alarmOriginatorName, + "alarmOriginatorLabel", alarmOriginatorLabel ); } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/notification/info/AlarmNotificationInfo.java b/common/data/src/main/java/org/thingsboard/server/common/data/notification/info/AlarmNotificationInfo.java index a1a9a34a36..cfcf66b016 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/notification/info/AlarmNotificationInfo.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/notification/info/AlarmNotificationInfo.java @@ -25,11 +25,10 @@ import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.DashboardId; import org.thingsboard.server.common.data.id.EntityId; +import java.util.HashMap; import java.util.Map; import java.util.UUID; -import static org.thingsboard.server.common.data.util.CollectionsUtil.mapOf; - @Data @NoArgsConstructor @AllArgsConstructor @@ -41,25 +40,28 @@ public class AlarmNotificationInfo implements RuleOriginatedNotificationInfo { private UUID alarmId; private EntityId alarmOriginator; private String alarmOriginatorName; + private String alarmOriginatorLabel; private AlarmSeverity alarmSeverity; private AlarmStatus alarmStatus; private boolean acknowledged; private boolean cleared; private CustomerId alarmCustomerId; private DashboardId dashboardId; + private Map info; @Override public Map getTemplateData() { - return mapOf( - "alarmType", alarmType, - "action", action, - "alarmId", alarmId.toString(), - "alarmSeverity", alarmSeverity.name().toLowerCase(), - "alarmStatus", alarmStatus.toString(), - "alarmOriginatorEntityType", alarmOriginator.getEntityType().getNormalName(), - "alarmOriginatorName", alarmOriginatorName, - "alarmOriginatorId", alarmOriginator.getId().toString() - ); + Map templateData = new HashMap<>(info); + templateData.put("alarmType", alarmType); + templateData.put("action", action); + templateData.put("alarmId", alarmId.toString()); + templateData.put("alarmSeverity", alarmSeverity.name().toLowerCase()); + templateData.put("alarmStatus", alarmStatus.toString()); + templateData.put("alarmOriginatorEntityType", alarmOriginator.getEntityType().getNormalName()); + templateData.put("alarmOriginatorName", alarmOriginatorName); + templateData.put("alarmOriginatorLabel", alarmOriginatorLabel); + templateData.put("alarmOriginatorId", alarmOriginator.getId().toString()); + return templateData; } @Override diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/AlarmCommentTrigger.java b/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/AlarmCommentTrigger.java index a9cee05707..e6298691ea 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/AlarmCommentTrigger.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/AlarmCommentTrigger.java @@ -25,10 +25,15 @@ import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.notification.rule.trigger.config.NotificationRuleTriggerType; +import java.io.Serial; + @Data @Builder public class AlarmCommentTrigger implements NotificationRuleTrigger { + @Serial + private static final long serialVersionUID = -8614770559491757202L; + private final TenantId tenantId; private final AlarmComment comment; private final Alarm alarm; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/AlarmTrigger.java b/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/AlarmTrigger.java index a64bf13266..275c3b1e18 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/AlarmTrigger.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/AlarmTrigger.java @@ -22,10 +22,15 @@ import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.notification.rule.trigger.config.NotificationRuleTriggerType; +import java.io.Serial; + @Data @Builder public class AlarmTrigger implements NotificationRuleTrigger { + @Serial + private static final long serialVersionUID = -466810297904938644L; + private final TenantId tenantId; private final AlarmApiCallResult alarmUpdate; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/config/AlarmAssignmentNotificationRuleTriggerConfig.java b/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/config/AlarmAssignmentNotificationRuleTriggerConfig.java index a63a2d0cdf..cf701b5221 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/config/AlarmAssignmentNotificationRuleTriggerConfig.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/config/AlarmAssignmentNotificationRuleTriggerConfig.java @@ -23,6 +23,7 @@ import lombok.NoArgsConstructor; import org.thingsboard.server.common.data.alarm.AlarmSearchStatus; import org.thingsboard.server.common.data.alarm.AlarmSeverity; +import java.io.Serial; import java.util.Set; @Data @@ -31,6 +32,9 @@ import java.util.Set; @Builder public class AlarmAssignmentNotificationRuleTriggerConfig implements NotificationRuleTriggerConfig { + @Serial + private static final long serialVersionUID = -5313556049809972096L; + private Set alarmTypes; private Set alarmSeverities; private Set alarmStatuses; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/config/AlarmCommentNotificationRuleTriggerConfig.java b/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/config/AlarmCommentNotificationRuleTriggerConfig.java index e2da4f5291..62762cce6e 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/config/AlarmCommentNotificationRuleTriggerConfig.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/config/AlarmCommentNotificationRuleTriggerConfig.java @@ -22,6 +22,7 @@ import lombok.NoArgsConstructor; import org.thingsboard.server.common.data.alarm.AlarmSearchStatus; import org.thingsboard.server.common.data.alarm.AlarmSeverity; +import java.io.Serial; import java.util.Set; @Data @@ -30,6 +31,9 @@ import java.util.Set; @Builder public class AlarmCommentNotificationRuleTriggerConfig implements NotificationRuleTriggerConfig { + @Serial + private static final long serialVersionUID = -9164282098882339645L; + private Set alarmTypes; private Set alarmSeverities; private Set alarmStatuses; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/config/AlarmNotificationRuleTriggerConfig.java b/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/config/AlarmNotificationRuleTriggerConfig.java index 53f160b8ad..5006e63abd 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/config/AlarmNotificationRuleTriggerConfig.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/config/AlarmNotificationRuleTriggerConfig.java @@ -23,6 +23,7 @@ import lombok.NoArgsConstructor; import org.thingsboard.server.common.data.alarm.AlarmSearchStatus; import org.thingsboard.server.common.data.alarm.AlarmSeverity; +import java.io.Serial; import java.io.Serializable; import java.util.Set; @@ -32,6 +33,9 @@ import java.util.Set; @Builder public class AlarmNotificationRuleTriggerConfig implements NotificationRuleTriggerConfig { + @Serial + private static final long serialVersionUID = -7382883720381542344L; + private Set alarmTypes; private Set alarmSeverities; @NotEmpty @@ -46,6 +50,8 @@ public class AlarmNotificationRuleTriggerConfig implements NotificationRuleTrigg @Data public static class ClearRule implements Serializable { + @Serial + private static final long serialVersionUID = 7922533150038105124L; private Set alarmStatuses; } diff --git a/common/message/src/main/java/org/thingsboard/server/common/msg/TbMsg.java b/common/message/src/main/java/org/thingsboard/server/common/msg/TbMsg.java index 1d8d9497a9..7c9f51a3ef 100644 --- a/common/message/src/main/java/org/thingsboard/server/common/msg/TbMsg.java +++ b/common/message/src/main/java/org/thingsboard/server/common/msg/TbMsg.java @@ -41,9 +41,6 @@ import java.util.Objects; import java.util.UUID; import java.util.concurrent.CopyOnWriteArrayList; -/** - * Created by ashvayka on 13.01.18. - */ @Data @Slf4j public final class TbMsg implements Serializable { @@ -500,11 +497,11 @@ public final class TbMsg implements Serializable { public String toString() { return "TbMsg.TbMsgBuilder(queueName=" + this.queueName + ", id=" + this.id + ", ts=" + this.ts + - ", type=" + this.type + ", internalType=" + this.internalType + ", originator=" + this.originator + - ", customerId=" + this.customerId + ", metaData=" + this.metaData + ", dataType=" + this.dataType + - ", data=" + this.data + ", ruleChainId=" + this.ruleChainId + ", ruleNodeId=" + this.ruleNodeId + - ", correlationId=" + this.correlationId + ", partition=" + this.partition + ", previousCalculatedFields=" + this.previousCalculatedFieldIds + - ", ctx=" + this.ctx + ", callback=" + this.callback + ")"; + ", type=" + this.type + ", internalType=" + this.internalType + ", originator=" + this.originator + + ", customerId=" + this.customerId + ", metaData=" + this.metaData + ", dataType=" + this.dataType + + ", data=" + this.data + ", ruleChainId=" + this.ruleChainId + ", ruleNodeId=" + this.ruleNodeId + + ", correlationId=" + this.correlationId + ", partition=" + this.partition + ", previousCalculatedFields=" + this.previousCalculatedFieldIds + + ", ctx=" + this.ctx + ", callback=" + this.callback + ")"; } } diff --git a/ui-ngx/src/assets/help/en_US/notification/alarm.md b/ui-ngx/src/assets/help/en_US/notification/alarm.md index 00ba021e1b..b03addf7a1 100644 --- a/ui-ngx/src/assets/help/en_US/notification/alarm.md +++ b/ui-ngx/src/assets/help/en_US/notification/alarm.md @@ -16,11 +16,13 @@ Available template parameters: * `alarmStatus` - the alarm status; * `alarmOriginatorEntityType` - the entity type of the alarm originator, e.g. 'Device'; * `alarmOriginatorName` - the name of the alarm originator, e.g. 'Sensor T1'; +* `alarmOriginatorLabel` - the label of the alarm originator, e.g. 'Sensor T1'; * `alarmOriginatorId` - the alarm originator entity id as uuid string; * `recipientTitle` - title of the recipient (first and last name if specified, email otherwise); * `recipientEmail` - email of the recipient; * `recipientFirstName` - first name of the recipient; * `recipientLastName` - last name of the recipient; +* `info.` - any key field from the alarm's additional info. Fox example, if additional info is `{"data": "Temperature is 25"}`, use `${info.data}` to access "Temperature is 25"; Parameter names must be wrapped using `${...}`. For example: `${action}`. You may also modify the value of the parameter with one of the suffixes: diff --git a/ui-ngx/src/assets/help/en_US/notification/alarm_assignment.md b/ui-ngx/src/assets/help/en_US/notification/alarm_assignment.md index aa80b13b35..53b2ade36d 100644 --- a/ui-ngx/src/assets/help/en_US/notification/alarm_assignment.md +++ b/ui-ngx/src/assets/help/en_US/notification/alarm_assignment.md @@ -15,6 +15,7 @@ Available template parameters: * `alarmStatus` - the alarm status; * `alarmOriginatorEntityType` - the entity type of the alarm originator, e.g. 'Device'; * `alarmOriginatorName` - the name of the alarm originator, e.g. 'Sensor T1'; +* `alarmOriginatorLabel` - the label of the alarm originator, e.g. 'Sensor T1'; * `alarmOriginatorId` - the alarm originator entity id as uuid string; * `assigneeTitle` - title of the assignee; * `assigneeEmail` - email of the assignee; diff --git a/ui-ngx/src/assets/help/en_US/notification/alarm_comment.md b/ui-ngx/src/assets/help/en_US/notification/alarm_comment.md index 150e76babc..41ada1fba2 100644 --- a/ui-ngx/src/assets/help/en_US/notification/alarm_comment.md +++ b/ui-ngx/src/assets/help/en_US/notification/alarm_comment.md @@ -15,6 +15,7 @@ Available template parameters: * `alarmStatus` - the alarm status; * `alarmOriginatorEntityType` - the entity type of the alarm originator, e.g. 'Device'; * `alarmOriginatorName` - the name of the alarm originator, e.g. 'Sensor T1'; +* `alarmOriginatorLabel` - the label of the alarm originator, e.g. 'Sensor T1'; * `alarmOriginatorId` - the alarm originator entity id as uuid string; * `comment` - text of the comment; * `action` - one of: 'added', 'updated'; From ddfc023da127815e1adc20290bda48825f00f4ca Mon Sep 17 00:00:00 2001 From: Andrii Landiak Date: Tue, 28 Oct 2025 14:22:32 +0200 Subject: [PATCH 08/42] Fix tests --- .../java/org/thingsboard/server/common/data/alarm/AlarmInfo.java | 1 - 1 file changed, 1 deletion(-) diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/alarm/AlarmInfo.java b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/AlarmInfo.java index ee168a2e0b..70032327b9 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/alarm/AlarmInfo.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/AlarmInfo.java @@ -64,7 +64,6 @@ public class AlarmInfo extends Alarm { this.originatorName = alarmInfo.getOriginatorName(); this.originatorLabel = alarmInfo.getOriginatorLabel(); this.assignee = alarmInfo.getAssignee(); - this.originatorDisplayName = alarmInfo.getOriginatorDisplayName(); } public AlarmInfo(Alarm alarm, String originatorName, String originatorLabel, AlarmAssignee assignee) { From 18b0b56cc47a8b46fa83269f2b60b73513ef81aa Mon Sep 17 00:00:00 2001 From: Andrii Landiak Date: Tue, 28 Oct 2025 16:29:27 +0200 Subject: [PATCH 09/42] Fix testNotificationRuleProcessing_alarmTrigger --- .../server/service/notification/NotificationRuleApiTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/application/src/test/java/org/thingsboard/server/service/notification/NotificationRuleApiTest.java b/application/src/test/java/org/thingsboard/server/service/notification/NotificationRuleApiTest.java index 10eff284f8..659f6379e7 100644 --- a/application/src/test/java/org/thingsboard/server/service/notification/NotificationRuleApiTest.java +++ b/application/src/test/java/org/thingsboard/server/service/notification/NotificationRuleApiTest.java @@ -193,7 +193,7 @@ public class NotificationRuleApiTest extends AbstractNotificationApiTest { @Test public void testNotificationRuleProcessing_alarmTrigger() throws Exception { String notificationSubject = "Alarm type: ${alarmType}, status: ${alarmStatus}, " + - "severity: ${alarmSeverity}, deviceId: ${alarmOriginatorId}, details: ${data}."; + "severity: ${alarmSeverity}, deviceId: ${alarmOriginatorId}, details: ${info.data}."; String notificationText = "Status: ${alarmStatus}, severity: ${alarmSeverity}"; NotificationTemplate notificationTemplate = createNotificationTemplate(NotificationType.ALARM, notificationSubject, notificationText, NotificationDeliveryMethod.WEB); From 890f19ea931c13be6d2b548dbda60bf99d7af4e6 Mon Sep 17 00:00:00 2001 From: Andrii Landiak Date: Tue, 28 Oct 2025 17:40:28 +0200 Subject: [PATCH 10/42] Fix VC tests --- .../service/sync/vc/VersionControlTest.java | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/application/src/test/java/org/thingsboard/server/service/sync/vc/VersionControlTest.java b/application/src/test/java/org/thingsboard/server/service/sync/vc/VersionControlTest.java index 2db78871d4..73f67739f3 100644 --- a/application/src/test/java/org/thingsboard/server/service/sync/vc/VersionControlTest.java +++ b/application/src/test/java/org/thingsboard/server/service/sync/vc/VersionControlTest.java @@ -244,7 +244,7 @@ public class VersionControlTest extends AbstractControllerTest { DeviceProfile deviceProfile = createDeviceProfile(null, null, "Device profile v1.0"); OtaPackage firmware = createOtaPackage(tenantId1, deviceProfile.getId(), OtaPackageType.FIRMWARE); OtaPackage software = createOtaPackage(tenantId1, deviceProfile.getId(), OtaPackageType.SOFTWARE); - Device device = createDevice(null, deviceProfile.getId(), "Device v1.0", "test1", newDevice -> { + Device device = createDevice(deviceProfile.getId(), "Device v1.0", "test1", newDevice -> { newDevice.setFirmwareId(firmware.getId()); newDevice.setSoftwareId(software.getId()); }); @@ -267,7 +267,7 @@ public class VersionControlTest extends AbstractControllerTest { createVersion("profiles", EntityType.DEVICE_PROFILE); OtaPackage firmware = createOtaPackage(tenantId1, deviceProfile.getId(), OtaPackageType.FIRMWARE); OtaPackage software = createOtaPackage(tenantId1, deviceProfile.getId(), OtaPackageType.SOFTWARE); - Device device = createDevice(null, deviceProfile.getId(), "Device of tenant 1", "test1", newDevice -> { + Device device = createDevice(deviceProfile.getId(), "Device of tenant 1", "test1", newDevice -> { newDevice.setFirmwareId(firmware.getId()); newDevice.setSoftwareId(software.getId()); }); @@ -528,7 +528,7 @@ public class VersionControlTest extends AbstractControllerTest { @Test public void testVcWithRelations_betweenTenants() throws Exception { Asset asset = createAsset(null, null, "Asset 1"); - Device device = createDevice(null, null, "Device 1", "test1"); + Device device = createDevice("Device 1", "test1"); EntityRelation relation = createRelation(asset.getId(), device.getId()); String versionId = createVersion("assets and devices", EntityType.ASSET, EntityType.DEVICE, EntityType.DEVICE_PROFILE, EntityType.ASSET_PROFILE); @@ -554,11 +554,11 @@ public class VersionControlTest extends AbstractControllerTest { @Test public void testVcWithRelations_sameTenant() throws Exception { Asset asset = createAsset(null, null, "Asset 1"); - Device device1 = createDevice(null, null, "Device 1", "test1"); + Device device1 = createDevice("Device 1", "test1"); EntityRelation relation1 = createRelation(device1.getId(), asset.getId()); String versionId = createVersion("assets", EntityType.ASSET); - Device device2 = createDevice(null, null, "Device 2", "test2"); + Device device2 = createDevice("Device 2", "test2"); EntityRelation relation2 = createRelation(device2.getId(), asset.getId()); List relations = findRelationsByTo(asset.getId()); assertThat(relations).contains(relation1, relation2); @@ -591,7 +591,7 @@ public class VersionControlTest extends AbstractControllerTest { @Test public void testVcWithCalculatedFields_betweenTenants() throws Exception { Asset asset = createAsset(null, null, "Asset 1"); - Device device = createDevice(null, null, "Device 1", "test1"); + Device device = createDevice("Device 1", "test1"); CalculatedField calculatedField = createCalculatedField("CalculatedField1", device.getId(), asset.getId()); String versionId = createVersion("calculated fields of asset and device", EntityType.ASSET, EntityType.DEVICE, EntityType.DEVICE_PROFILE, EntityType.ASSET_PROFILE); @@ -617,7 +617,7 @@ public class VersionControlTest extends AbstractControllerTest { @Test public void testVcWithReferencedCalculatedFields_betweenTenants() throws Exception { Asset asset = createAsset(null, null, "Asset 1"); - Device device = createDevice(null, null, "Device 1", "test1"); + Device device = createDevice("Device 1", "test1"); CalculatedField deviceCalculatedField = createCalculatedField("CalculatedField1", device.getId(), asset.getId()); CalculatedField assetCalculatedField = createCalculatedField("CalculatedField2", asset.getId(), device.getId()); String versionId = createVersion("calculated fields of asset and device", EntityType.ASSET, EntityType.DEVICE, EntityType.DEVICE_PROFILE, EntityType.ASSET_PROFILE); @@ -911,9 +911,8 @@ public class VersionControlTest extends AbstractControllerTest { login(tenantAdmin2.getEmail(), tenantAdmin2.getEmail()); } - private Device createDevice(CustomerId customerId, DeviceProfileId deviceProfileId, String name, String accessToken, Consumer... modifiers) { + private Device createDevice(DeviceProfileId deviceProfileId, String name, String accessToken, Consumer... modifiers) { Device device = new Device(); - device.setCustomerId(customerId); device.setName(name); device.setLabel("lbl"); device.setDeviceProfileId(deviceProfileId); From f427f3f058ecfb0fda9ddf90c7a0638a2bfddd26 Mon Sep 17 00:00:00 2001 From: dashevchenko Date: Thu, 6 Nov 2025 16:55:35 +0200 Subject: [PATCH 11/42] introduced new request param "key" for UI to deal with keys with comma --- .../controller/TelemetryController.java | 76 +++++++++++-------- .../server/controller/AbstractWebTest.java | 15 ++++ .../controller/TelemetryControllerTest.java | 60 +++++++++++++++ ui-ngx/src/app/core/http/attribute.service.ts | 4 +- 4 files changed, 121 insertions(+), 34 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/controller/TelemetryController.java b/application/src/main/java/org/thingsboard/server/controller/TelemetryController.java index c98ae0dcb8..4e4d2256f3 100644 --- a/application/src/main/java/org/thingsboard/server/controller/TelemetryController.java +++ b/application/src/main/java/org/thingsboard/server/controller/TelemetryController.java @@ -36,6 +36,7 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.util.MultiValueMap; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; @@ -208,10 +209,12 @@ public class TelemetryController extends BaseController { public DeferredResult getAttributes( @Parameter(description = ENTITY_TYPE_PARAM_DESCRIPTION, required = true, schema = @Schema(defaultValue = "DEVICE")) @PathVariable("entityType") String entityType, @Parameter(description = ENTITY_ID_PARAM_DESCRIPTION, required = true) @PathVariable("entityId") String entityIdStr, - @Parameter(description = ATTRIBUTES_KEYS_DESCRIPTION) @RequestParam(name = "keys", required = false) String keysStr) throws ThingsboardException { + @Parameter(description = ATTRIBUTES_KEYS_DESCRIPTION) @RequestParam(name = "keys", required = false) String keysStr, + @RequestParam MultiValueMap params) throws ThingsboardException { + List keys = params.get("key") != null ? params.get("key") : toKeysList(keysStr); SecurityUser user = getCurrentUser(); return accessValidator.validateEntityAndCallback(getCurrentUser(), Operation.READ_ATTRIBUTES, entityType, entityIdStr, - (result, tenantId, entityId) -> getAttributeValuesCallback(result, user, entityId, null, keysStr)); + (result, tenantId, entityId) -> getAttributeValuesCallback(result, user, entityId, null, keys)); } @@ -231,10 +234,12 @@ public class TelemetryController extends BaseController { @Parameter(description = ENTITY_TYPE_PARAM_DESCRIPTION, required = true, schema = @Schema(defaultValue = "DEVICE")) @PathVariable("entityType") String entityType, @Parameter(description = ENTITY_ID_PARAM_DESCRIPTION, required = true) @PathVariable("entityId") String entityIdStr, @Parameter(description = ATTRIBUTES_SCOPE_DESCRIPTION, schema = @Schema(allowableValues = {"SERVER_SCOPE", "SHARED_SCOPE", "CLIENT_SCOPE"}, requiredMode = Schema.RequiredMode.REQUIRED)) @PathVariable("scope") AttributeScope scope, - @Parameter(description = ATTRIBUTES_KEYS_DESCRIPTION) @RequestParam(name = "keys", required = false) String keysStr) throws ThingsboardException { + @Parameter(description = ATTRIBUTES_KEYS_DESCRIPTION) @RequestParam(name = "keys", required = false) String keysStr, + @RequestParam MultiValueMap params) throws ThingsboardException { + List keys = params.get("key") != null ? params.get("key") : toKeysList(keysStr); SecurityUser user = getCurrentUser(); return accessValidator.validateEntityAndCallback(getCurrentUser(), Operation.READ_ATTRIBUTES, entityType, entityIdStr, - (result, tenantId, entityId) -> getAttributeValuesCallback(result, user, entityId, scope, keysStr)); + (result, tenantId, entityId) -> getAttributeValuesCallback(result, user, entityId, scope, keys)); } @ApiOperation(value = "Get time series keys (getTimeseriesKeys)", @@ -270,10 +275,12 @@ public class TelemetryController extends BaseController { @Parameter(description = ENTITY_ID_PARAM_DESCRIPTION, required = true) @PathVariable("entityId") String entityIdStr, @Parameter(description = TELEMETRY_KEYS_DESCRIPTION) @RequestParam(name = "keys", required = false) String keysStr, @Parameter(description = STRICT_DATA_TYPES_DESCRIPTION) - @RequestParam(name = "useStrictDataTypes", required = false, defaultValue = "false") Boolean useStrictDataTypes) throws ThingsboardException { + @RequestParam(name = "useStrictDataTypes", required = false, defaultValue = "false") Boolean useStrictDataTypes, + @RequestParam MultiValueMap params) throws ThingsboardException { + List keys = params.get("key") != null ? params.get("key") : toKeysList(keysStr); SecurityUser user = getCurrentUser(); return accessValidator.validateEntityAndCallback(getCurrentUser(), Operation.READ_TELEMETRY, entityType, entityIdStr, - (result, tenantId, entityId) -> getLatestTimeseriesValuesCallback(result, user, entityId, keysStr, useStrictDataTypes)); + (result, tenantId, entityId) -> getLatestTimeseriesValuesCallback(result, user, entityId, keys, useStrictDataTypes)); } @ApiOperation(value = "Get time series data (getTimeseries)", @@ -291,7 +298,7 @@ public class TelemetryController extends BaseController { public DeferredResult getTimeseries( @Parameter(description = ENTITY_TYPE_PARAM_DESCRIPTION, required = true, schema = @Schema(defaultValue = "DEVICE")) @PathVariable("entityType") String entityType, @Parameter(description = ENTITY_ID_PARAM_DESCRIPTION, required = true) @PathVariable("entityId") String entityIdStr, - @Parameter(description = TELEMETRY_KEYS_BASE_DESCRIPTION, required = true) @RequestParam(name = "keys") String keys, + @Parameter(description = TELEMETRY_KEYS_BASE_DESCRIPTION) @RequestParam(name = "keys", required = false) String keysStr, @Parameter(description = "A long value representing the start timestamp of the time range in milliseconds, UTC.") @RequestParam(name = "startTs") Long startTs, @Parameter(description = "A long value representing the end timestamp of the time range in milliseconds, UTC.") @@ -312,9 +319,11 @@ public class TelemetryController extends BaseController { @Parameter(description = SORT_ORDER_DESCRIPTION, schema = @Schema(allowableValues = {"ASC", "DESC"})) @RequestParam(name = "orderBy", defaultValue = "DESC") String orderBy, @Parameter(description = STRICT_DATA_TYPES_DESCRIPTION) - @RequestParam(name = "useStrictDataTypes", required = false, defaultValue = "false") Boolean useStrictDataTypes) throws ThingsboardException { + @RequestParam(name = "useStrictDataTypes", required = false, defaultValue = "false") Boolean useStrictDataTypes, + @RequestParam MultiValueMap params) throws ThingsboardException { + List keys = params.get("key") != null ? params.get("key") : toKeysList(keysStr); DeferredResult response = new DeferredResult<>(); - Futures.addCallback(tbTelemetryService.getTimeseries(EntityIdFactory.getByTypeAndId(entityType, entityIdStr), toKeysList(keys), startTs, endTs, + Futures.addCallback(tbTelemetryService.getTimeseries(EntityIdFactory.getByTypeAndId(entityType, entityIdStr), keys, startTs, endTs, intervalType, interval, timeZone, limit, Aggregation.valueOf(aggStr), orderBy, useStrictDataTypes, getCurrentUser()), getTsKvListCallback(response, useStrictDataTypes), MoreExecutors.directExecutor()); return response; @@ -466,7 +475,7 @@ public class TelemetryController extends BaseController { public DeferredResult deleteEntityTimeseries( @Parameter(description = ENTITY_TYPE_PARAM_DESCRIPTION, required = true, schema = @Schema(defaultValue = "DEVICE")) @PathVariable("entityType") String entityType, @Parameter(description = ENTITY_ID_PARAM_DESCRIPTION, required = true) @PathVariable("entityId") String entityIdStr, - @Parameter(description = TELEMETRY_KEYS_DESCRIPTION, required = true) @RequestParam(name = "keys") String keysStr, + @Parameter(description = TELEMETRY_KEYS_DESCRIPTION) @RequestParam(name = "keys", required = false) String keysStr, @Parameter(description = "A boolean value to specify if should be deleted all data for selected keys or only data that are in the selected time range.") @RequestParam(name = "deleteAllDataForKeys", defaultValue = "false") boolean deleteAllDataForKeys, @Parameter(description = "A long value representing the start timestamp of removal time range in milliseconds.") @@ -476,16 +485,17 @@ public class TelemetryController extends BaseController { @Parameter(description = "If the parameter is set to true, the latest telemetry can be removed, otherwise, in case that parameter is set to false the latest value will not removed.") @RequestParam(name = "deleteLatest", required = false, defaultValue = "true") boolean deleteLatest, @Parameter(description = "If the parameter is set to true, the latest telemetry will be rewritten in case that current latest value was removed, otherwise, in case that parameter is set to false the new latest value will not set.") - @RequestParam(name = "rewriteLatestIfDeleted", defaultValue = "false") boolean rewriteLatestIfDeleted) throws ThingsboardException { + @RequestParam(name = "rewriteLatestIfDeleted", defaultValue = "false") boolean rewriteLatestIfDeleted, + @RequestParam MultiValueMap params) throws ThingsboardException { + List keys = params.get("key") != null ? params.get("key") : toKeysList(keysStr); EntityId entityId = EntityIdFactory.getByTypeAndId(entityType, entityIdStr); - return deleteTimeseries(entityId, keysStr, deleteAllDataForKeys, startTs, endTs, rewriteLatestIfDeleted, deleteLatest); + return deleteTimeseries(entityId, keys, deleteAllDataForKeys, startTs, endTs, rewriteLatestIfDeleted, deleteLatest); } - private DeferredResult deleteTimeseries(EntityId entityIdStr, String keysStr, boolean deleteAllDataForKeys, + private DeferredResult deleteTimeseries(EntityId entityIdStr, List keys, boolean deleteAllDataForKeys, Long startTs, Long endTs, boolean rewriteLatestIfDeleted, boolean deleteLatest) throws ThingsboardException { - List keys = toKeysList(keysStr); if (keys.isEmpty()) { - return getImmediateDeferredResult("Empty keys: " + keysStr, HttpStatus.BAD_REQUEST); + return getImmediateDeferredResult("Empty keys: " + keys, HttpStatus.BAD_REQUEST); } SecurityUser user = getCurrentUser(); @@ -547,9 +557,11 @@ public class TelemetryController extends BaseController { public DeferredResult deleteDeviceAttributes( @Parameter(description = DEVICE_ID_PARAM_DESCRIPTION, required = true) @PathVariable(DEVICE_ID) String deviceIdStr, @Parameter(description = ATTRIBUTES_SCOPE_DESCRIPTION, schema = @Schema(allowableValues = {"SERVER_SCOPE", "SHARED_SCOPE", "CLIENT_SCOPE"}, requiredMode = Schema.RequiredMode.REQUIRED)) @PathVariable("scope") AttributeScope scope, - @Parameter(description = ATTRIBUTES_KEYS_DESCRIPTION, required = true) @RequestParam(name = "keys") String keysStr) throws ThingsboardException { + @Parameter(description = ATTRIBUTES_KEYS_DESCRIPTION) @RequestParam(name = "keys", required = false) String keysStr, + @RequestParam MultiValueMap params) throws ThingsboardException { + List keys = params.get("key") != null ? params.get("key") : toKeysList(keysStr); EntityId entityId = EntityIdFactory.getByTypeAndUuid(EntityType.DEVICE, deviceIdStr); - return deleteAttributes(entityId, scope, keysStr); + return deleteAttributes(entityId, scope, keys); } @ApiOperation(value = "Delete entity attributes (deleteEntityAttributes)", @@ -570,15 +582,16 @@ public class TelemetryController extends BaseController { @Parameter(description = ENTITY_TYPE_PARAM_DESCRIPTION, required = true, schema = @Schema(defaultValue = "DEVICE")) @PathVariable("entityType") String entityType, @Parameter(description = ENTITY_ID_PARAM_DESCRIPTION, required = true) @PathVariable("entityId") String entityIdStr, @Parameter(description = ATTRIBUTES_SCOPE_DESCRIPTION, required = true, schema = @Schema(allowableValues = {"SERVER_SCOPE", "SHARED_SCOPE", "CLIENT_SCOPE"})) @PathVariable("scope") AttributeScope scope, - @Parameter(description = ATTRIBUTES_KEYS_DESCRIPTION, required = true) @RequestParam(name = "keys") String keysStr) throws ThingsboardException { + @Parameter(description = ATTRIBUTES_KEYS_DESCRIPTION) @RequestParam(name = "keys", required = false) String keysStr, + @RequestParam MultiValueMap params) throws ThingsboardException { + List keys = params.get("key") != null ? params.get("key") : toKeysList(keysStr); EntityId entityId = EntityIdFactory.getByTypeAndId(entityType, entityIdStr); - return deleteAttributes(entityId, scope, keysStr); + return deleteAttributes(entityId, scope, keys); } - private DeferredResult deleteAttributes(EntityId entityIdSrc, AttributeScope scope, String keysStr) throws ThingsboardException { - List keys = toKeysList(keysStr); + private DeferredResult deleteAttributes(EntityId entityIdSrc, AttributeScope scope, List keys) throws ThingsboardException { if (keys.isEmpty()) { - return getImmediateDeferredResult("Empty keys: " + keysStr, HttpStatus.BAD_REQUEST); + return getImmediateDeferredResult("Empty keys: " + keys, HttpStatus.BAD_REQUEST); } SecurityUser user = getCurrentUser(); @@ -709,30 +722,29 @@ public class TelemetryController extends BaseController { }); } - private void getLatestTimeseriesValuesCallback(@Nullable DeferredResult result, SecurityUser user, EntityId entityId, String keys, Boolean useStrictDataTypes) { + private void getLatestTimeseriesValuesCallback(@Nullable DeferredResult result, SecurityUser user, EntityId entityId, List keys, Boolean useStrictDataTypes) { ListenableFuture> future; - if (StringUtils.isEmpty(keys)) { + if (keys.isEmpty()) { future = tsService.findAllLatest(user.getTenantId(), entityId); } else { - future = tsService.findLatest(user.getTenantId(), entityId, toKeysList(keys)); + future = tsService.findLatest(user.getTenantId(), entityId, keys); } Futures.addCallback(future, getTsKvListCallback(result, useStrictDataTypes), MoreExecutors.directExecutor()); } - private void getAttributeValuesCallback(@Nullable DeferredResult result, SecurityUser user, EntityId entityId, AttributeScope scope, String keys) { - List keyList = toKeysList(keys); - FutureCallback> callback = getAttributeValuesToResponseCallback(result, user, scope, entityId, keyList); + private void getAttributeValuesCallback(@Nullable DeferredResult result, SecurityUser user, EntityId entityId, AttributeScope scope, List keys) { + FutureCallback> callback = getAttributeValuesToResponseCallback(result, user, scope, entityId, keys); if (scope != null) { - if (keyList != null && !keyList.isEmpty()) { - Futures.addCallback(attributesService.find(user.getTenantId(), entityId, scope, keyList), callback, MoreExecutors.directExecutor()); + if (keys != null && !keys.isEmpty()) { + Futures.addCallback(attributesService.find(user.getTenantId(), entityId, scope, keys), callback, MoreExecutors.directExecutor()); } else { Futures.addCallback(attributesService.findAll(user.getTenantId(), entityId, scope), callback, MoreExecutors.directExecutor()); } } else { List>> futures = new ArrayList<>(); for (AttributeScope tmpScope : AttributeScope.values()) { - if (keyList != null && !keyList.isEmpty()) { - futures.add(attributesService.find(user.getTenantId(), entityId, tmpScope, keyList)); + if (keys != null && !keys.isEmpty()) { + futures.add(attributesService.find(user.getTenantId(), entityId, tmpScope, keys)); } else { futures.add(attributesService.findAll(user.getTenantId(), entityId, tmpScope)); } diff --git a/application/src/test/java/org/thingsboard/server/controller/AbstractWebTest.java b/application/src/test/java/org/thingsboard/server/controller/AbstractWebTest.java index fcbe203e79..56447689c3 100644 --- a/application/src/test/java/org/thingsboard/server/controller/AbstractWebTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/AbstractWebTest.java @@ -738,6 +738,12 @@ public abstract class AbstractWebTest extends AbstractInMemoryStorageTest { return doPost("/api/device-with-credentials", request, Device.class); } + protected ResultActions doGetAsync(String urlTemplate, MultiValueMap params) throws Exception { + MockHttpServletRequestBuilder getRequest = get(urlTemplate).params(params); + setJwtToken(getRequest); + return mockMvc.perform(asyncDispatch(mockMvc.perform(getRequest).andExpect(request().asyncStarted()).andReturn())); + } + protected ResultActions doGet(String urlTemplate, Object... urlVariables) throws Exception { MockHttpServletRequestBuilder getRequest = get(urlTemplate, urlVariables); setJwtToken(getRequest); @@ -937,6 +943,15 @@ public abstract class AbstractWebTest extends AbstractInMemoryStorageTest { return mockMvc.perform(deleteRequest); } + protected ResultActions doDeleteAsync(String urlTemplate, MultiValueMap params) throws Exception { + MockHttpServletRequestBuilder deleteRequest = delete(urlTemplate) + .params(params); + setJwtToken(deleteRequest); + MvcResult result = mockMvc.perform(deleteRequest).andReturn(); + result.getAsyncResult(DEFAULT_TIMEOUT); + return mockMvc.perform(asyncDispatch(result)); + } + protected ResultActions doDeleteAsync(String urlTemplate, Long timeout, String... params) throws Exception { MockHttpServletRequestBuilder deleteRequest = delete(urlTemplate, params); setJwtToken(deleteRequest); diff --git a/application/src/test/java/org/thingsboard/server/controller/TelemetryControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/TelemetryControllerTest.java index ad3c6d5312..21371cc316 100644 --- a/application/src/test/java/org/thingsboard/server/controller/TelemetryControllerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/TelemetryControllerTest.java @@ -19,7 +19,10 @@ import com.fasterxml.jackson.databind.node.ObjectNode; import org.junit.Assert; import org.junit.Test; import org.springframework.test.context.TestPropertySource; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException; +import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.server.common.data.Device; import org.thingsboard.server.common.data.SaveDeviceWithCredentialsRequest; import org.thingsboard.server.common.data.kv.BasicTsKvEntry; @@ -232,6 +235,63 @@ public class TelemetryControllerTest extends AbstractControllerTest { doPostAsync("/api/plugins/telemetry/DEVICE/" + device.getId() + "/timeseries/smth", invalidRequestBody2, String.class, status().isBadRequest()); } + @Test + public void testDeleteTelemetryByKeyWithComma() throws Exception { + loginTenantAdmin(); + Device device = createDevice(); + + String tsKey = "key1,key2"; + String testBody = JacksonUtil.newObjectNode() + .put(tsKey, "value") + .toString(); + doPostAsync("/api/plugins/telemetry/DEVICE/" + device.getId() + "/timeseries/smth", testBody, String.class, status().isOk()); + + MultiValueMap params = new LinkedMultiValueMap<>(); + params.add("key", tsKey); + params.add("deleteAllDataForKeys", "true"); + + ObjectNode tsData = readResponse(doGetAsync("/api/plugins/telemetry/DEVICE/" + device.getId() + "/values/timeseries", params), ObjectNode.class); + assertThat(tsData.get("key1,key2").get(0).get("value").asText()).isEqualTo("value"); + + doDeleteAsync("/api/plugins/telemetry/DEVICE/" + device.getId() + "/timeseries/delete", params); + + ObjectNode tsDataAfterDeletion = readResponse(doGetAsync("/api/plugins/telemetry/DEVICE/" + device.getId() + "/values/timeseries", params), ObjectNode.class); + Assert.assertTrue(tsDataAfterDeletion.get("key1,key2").get(0).get("value").isNull()); + } + + @Test + public void testDeleteTelemetryByKeysWithComma() throws Exception { + loginTenantAdmin(); + Device device = createDevice(); + + String keyWithComma = "key1,key2"; + String testBody = JacksonUtil.newObjectNode() + .put(keyWithComma, "value") + .toString(); + doPostAsync("/api/plugins/telemetry/DEVICE/" + device.getId() + "/timeseries/smth", testBody, String.class, status().isOk()); + + String key = "key3"; + String testBody2 = JacksonUtil.newObjectNode() + .put(key, "value") + .toString(); + MultiValueMap params = new LinkedMultiValueMap<>(); + params.add("key", keyWithComma); + params.add("key", key); + params.add("deleteAllDataForKeys", "true"); + + doPostAsync("/api/plugins/telemetry/DEVICE/" + device.getId() + "/timeseries/smth", testBody2, String.class, status().isOk()); + + ObjectNode tsData = readResponse(doGetAsync("/api/plugins/telemetry/DEVICE/" + device.getId() + "/values/timeseries", params), ObjectNode.class); + assertThat(tsData.get("key1,key2").get(0).get("value").asText()).isEqualTo("value"); + assertThat(tsData.get("key3").get(0).get("value").asText()).isEqualTo("value"); + + doDeleteAsync("/api/plugins/telemetry/DEVICE/" + device.getId() + "/timeseries/delete", params); + + ObjectNode tsDataAfterDeletion = readResponse(doGetAsync("/api/plugins/telemetry/DEVICE/" + device.getId() + "/values/timeseries", params), ObjectNode.class); + Assert.assertTrue(tsDataAfterDeletion.get("key1,key2").get(0).get("value").isNull()); + Assert.assertTrue(tsDataAfterDeletion.get("key3").get(0).get("value").isNull()); + } + private Device createDevice() throws Exception { String testToken = "TEST_TOKEN"; diff --git a/ui-ngx/src/app/core/http/attribute.service.ts b/ui-ngx/src/app/core/http/attribute.service.ts index 8687d3ec0a..589e429180 100644 --- a/ui-ngx/src/app/core/http/attribute.service.ts +++ b/ui-ngx/src/app/core/http/attribute.service.ts @@ -15,7 +15,7 @@ /// import { Injectable } from '@angular/core'; -import { defaultHttpOptionsFromConfig, RequestConfig } from './http-utils'; +import {createDefaultHttpOptions, defaultHttpOptionsFromConfig, RequestConfig} from './http-utils'; import { forkJoin, Observable, of } from 'rxjs'; import { HttpClient } from '@angular/common/http'; import { EntityId } from '@shared/models/id/entity-id'; @@ -75,7 +75,7 @@ export class AttributeService { if (isDefinedAndNotNull(endTs)) { url += `&endTs=${endTs}`; } - return this.http.delete(url, defaultHttpOptionsFromConfig(config)); + return this.http.delete(url, createDefaultHttpOptions( {key: timeseries.map(key => key.key)}, config)); } public saveEntityAttributes(entityId: EntityId, attributeScope: AttributeScope, attributes: Array, From d61e9a0cb35a0da5607084f25dbe17844c92706b Mon Sep 17 00:00:00 2001 From: Maksym Tsymbarov Date: Fri, 7 Nov 2025 16:19:46 +0200 Subject: [PATCH 12/42] fixes parse error --- .../widget/lib/rpc/led-indicator.component.ts | 13 +++++++++---- .../widget/lib/rpc/round-switch.component.ts | 13 +++++++++---- .../components/widget/lib/rpc/switch.component.ts | 13 +++++++++---- 3 files changed, 27 insertions(+), 12 deletions(-) diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/rpc/led-indicator.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/rpc/led-indicator.component.ts index d983a4ebfe..6cd91eadae 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/rpc/led-indicator.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/rpc/led-indicator.component.ts @@ -332,11 +332,16 @@ export class LedIndicatorComponent extends PageComponent implements OnInit, OnDe if (keyData && keyData.data && keyData.data[0]) { const attrValue = keyData.data[0][1]; if (isDefined(attrValue)) { - let parsed = null; + let valueToParse = attrValue; try { - parsed = this.parseValueFunction(JSON.parse(attrValue)); - } catch (e){/**/} - value = !!parsed; + valueToParse = JSON.parse(attrValue); + } catch (e) { } + + try { + value = !!this.parseValueFunction(valueToParse); + } catch (e) { + value = false; + } } } } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/rpc/round-switch.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/rpc/round-switch.component.ts index 6990ff4264..3e483b498a 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/rpc/round-switch.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/rpc/round-switch.component.ts @@ -330,11 +330,16 @@ export class RoundSwitchComponent extends PageComponent implements OnInit, OnDes if (keyData && keyData.data && keyData.data[0]) { const attrValue = keyData.data[0][1]; if (isDefined(attrValue)) { - let parsed = null; + let valueToParse = attrValue; try { - parsed = this.parseValueFunction(JSON.parse(attrValue)); - } catch (e){/**/} - value = !!parsed; + valueToParse = JSON.parse(attrValue); + } catch (e) { } + + try { + value = !!this.parseValueFunction(valueToParse); + } catch (e) { + value = false; + } } } } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/rpc/switch.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/rpc/switch.component.ts index c9807e5635..e9809cb61c 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/rpc/switch.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/rpc/switch.component.ts @@ -373,11 +373,16 @@ export class SwitchComponent extends PageComponent implements AfterViewInit, OnD if (keyData && keyData.data && keyData.data[0]) { const attrValue = keyData.data[0][1]; if (isDefined(attrValue)) { - let parsed = null; + let valueToParse = attrValue; try { - parsed = this.parseValueFunction(JSON.parse(attrValue)); - } catch (e){/**/} - value = !!parsed; + valueToParse = JSON.parse(attrValue); + } catch (e) { } + + try { + value = !!this.parseValueFunction(valueToParse); + } catch (e) { + value = false; + } } } } From bb810d87376903cc7246e92a4aae7dd2a8a79c26 Mon Sep 17 00:00:00 2001 From: Vladyslav_Prykhodko Date: Fri, 7 Nov 2025 17:25:05 +0200 Subject: [PATCH 13/42] UI: Implement delete attributes and telemetry with multi-value Query Param --- ui-ngx/src/app/core/http/attribute.service.ts | 81 ++++++++----------- ui-ngx/src/app/core/http/http-utils.ts | 31 ++++++- 2 files changed, 62 insertions(+), 50 deletions(-) diff --git a/ui-ngx/src/app/core/http/attribute.service.ts b/ui-ngx/src/app/core/http/attribute.service.ts index 589e429180..fc87702628 100644 --- a/ui-ngx/src/app/core/http/attribute.service.ts +++ b/ui-ngx/src/app/core/http/attribute.service.ts @@ -15,7 +15,7 @@ /// import { Injectable } from '@angular/core'; -import {createDefaultHttpOptions, defaultHttpOptionsFromConfig, RequestConfig} from './http-utils'; +import { defaultHttpOptionsFromConfig, defaultHttpOptionsFromParams, RequestConfig } from './http-utils'; import { forkJoin, Observable, of } from 'rxjs'; import { HttpClient } from '@angular/common/http'; import { EntityId } from '@shared/models/id/entity-id'; @@ -35,47 +35,37 @@ export class AttributeService { public getEntityAttributes(entityId: EntityId, attributeScope?: AttributeScope, keys?: Array, config?: RequestConfig): Observable> { let url = `/api/plugins/telemetry/${entityId.entityType}/${entityId.id}/values/attributes`; - + let queryParams: object = null; if (attributeScope) { url += `/${attributeScope}`; } if (keys && keys.length) { - url += `?keys=${keys.join(',')}`; + queryParams = {key: keys}; } - return this.http.get>(url, defaultHttpOptionsFromConfig(config)); + return this.http.get>(url, defaultHttpOptionsFromParams(queryParams, config)); } public deleteEntityAttributes(entityId: EntityId, attributeScope: AttributeScope, attributes: Array, config?: RequestConfig): Observable { - const keys = attributes.map(attribute => encodeURIComponent(attribute.key)).join(','); - return this.http.delete(`/api/plugins/telemetry/${entityId.entityType}/${entityId.id}/${attributeScope}` + - `?keys=${keys}`, - defaultHttpOptionsFromConfig(config)); + return this.http.delete(`/api/plugins/telemetry/${entityId.entityType}/${entityId.id}/${attributeScope}`, + defaultHttpOptionsFromParams({key: attributes.map(attribute => attribute.key)}, config)); } public deleteEntityTimeseries(entityId: EntityId, timeseries: Array, deleteAllDataForKeys = false, startTs?: number, endTs?: number, rewriteLatestIfDeleted = false, deleteLatest = true, config?: RequestConfig): Observable { - const keys = timeseries.map(attribute => encodeURIComponent(attribute.key)).join(','); - let url = `/api/plugins/telemetry/${entityId.entityType}/${entityId.id}/timeseries/delete?keys=${keys}`; - if (isDefinedAndNotNull(deleteAllDataForKeys)) { - url += `&deleteAllDataForKeys=${deleteAllDataForKeys}`; - } - if (isDefinedAndNotNull(rewriteLatestIfDeleted)) { - url += `&rewriteLatestIfDeleted=${rewriteLatestIfDeleted}`; - } - if (isDefinedAndNotNull(deleteLatest)) { - url += `&deleteLatest=${deleteLatest}`; - } - if (isDefinedAndNotNull(startTs)) { - url += `&startTs=${startTs}`; - } - if (isDefinedAndNotNull(endTs)) { - url += `&endTs=${endTs}`; - } - return this.http.delete(url, createDefaultHttpOptions( {key: timeseries.map(key => key.key)}, config)); + const queryParams = { + key: timeseries.map(key => key.key), + deleteAllDataForKeys: deleteAllDataForKeys, + rewriteLatestIfDeleted: rewriteLatestIfDeleted, + deleteLatest: deleteLatest, + startTs: startTs, + endTs: endTs + }; + return this.http.delete(`/api/plugins/telemetry/${entityId.entityType}/${entityId.id}/timeseries/delete`, + defaultHttpOptionsFromParams(queryParams, config)); } public saveEntityAttributes(entityId: EntityId, attributeScope: AttributeScope, attributes: Array, @@ -138,32 +128,29 @@ export class AttributeService { limit: number = 100, agg: AggregationType = AggregationType.NONE, interval?: number, orderBy: DataSortOrder = DataSortOrder.DESC, useStrictDataTypes: boolean = false, config?: RequestConfig): Observable { - let url = `/api/plugins/telemetry/${entityId.entityType}/${entityId.id}/values/timeseries?keys=${keys.join(',')}&startTs=${startTs}&endTs=${endTs}`; - if (isDefinedAndNotNull(limit)) { - url += `&limit=${limit}`; - } - if (isDefinedAndNotNull(agg)) { - url += `&agg=${agg}`; - } - if (isDefinedAndNotNull(interval)) { - url += `&interval=${interval}`; - } - if (isDefinedAndNotNull(orderBy)) { - url += `&orderBy=${orderBy}`; - } - if (isDefinedAndNotNull(useStrictDataTypes)) { - url += `&useStrictDataTypes=${useStrictDataTypes}`; - } - - return this.http.get(url, defaultHttpOptionsFromConfig(config)); + const queryParams = { + key: keys, + startTs: startTs, + endTs: endTs, + limit: limit, + agg: agg, + interval: interval, + orderBy: orderBy, + useStrictDataTypes: useStrictDataTypes + } + return this.http.get(`/api/plugins/telemetry/${entityId.entityType}/${entityId.id}/values/timeseries`, + defaultHttpOptionsFromParams(queryParams, config)); } public getEntityTimeseriesLatest(entityId: EntityId, keys?: Array, useStrictDataTypes = false, config?: RequestConfig): Observable { - let url = `/api/plugins/telemetry/${entityId.entityType}/${entityId.id}/values/timeseries?useStrictDataTypes=${useStrictDataTypes}`; + const queryParams: Record = { + useStrictDataTypes: useStrictDataTypes + } if (isDefinedAndNotNull(keys) && keys.length) { - url += `&keys=${keys.join(',')}`; + queryParams.key = keys; } - return this.http.get(url, defaultHttpOptionsFromConfig(config)); + return this.http.get(`/api/plugins/telemetry/${entityId.entityType}/${entityId.id}/values/timeseries`, + defaultHttpOptionsFromParams(queryParams, config)); } } diff --git a/ui-ngx/src/app/core/http/http-utils.ts b/ui-ngx/src/app/core/http/http-utils.ts index ebd08f13fe..3d28f6ddf4 100644 --- a/ui-ngx/src/app/core/http/http-utils.ts +++ b/ui-ngx/src/app/core/http/http-utils.ts @@ -38,7 +38,10 @@ export function createDefaultHttpOptions(queryParamsOrConfig?: QueryParams | Req if (hasRequestConfig(queryParamsOrConfig)) { return defaultHttpOptionsFromConfig(queryParamsOrConfig as RequestConfig); } - const queryParams = queryParamsOrConfig as QueryParams; + return defaultHttpOptionsFromParams(queryParamsOrConfig as QueryParams, config); +} + +export function defaultHttpOptionsFromParams(queryParams?: QueryParams, config?: RequestConfig) { const finalConfig = { ...config, ...(queryParams && { queryParams }), @@ -57,9 +60,11 @@ export function defaultHttpOptions(ignoreLoading: boolean = false, ignoreErrors: boolean = false, resendRequest: boolean = false, queryParams?: QueryParams) { + const cleanedParams = cleanQueryParams(queryParams); + return { headers: new HttpHeaders({'Content-Type': 'application/json'}), - params: new InterceptorHttpParams(new InterceptorConfig(ignoreLoading, ignoreErrors, resendRequest), queryParams) + params: new InterceptorHttpParams(new InterceptorConfig(ignoreLoading, ignoreErrors, resendRequest), cleanedParams) }; } @@ -67,7 +72,27 @@ export function defaultHttpUploadOptions(ignoreLoading: boolean = false, ignoreErrors: boolean = false, resendRequest: boolean = false, queryParams?: QueryParams) { + const cleanedParams = cleanQueryParams(queryParams); + return { - params: new InterceptorHttpParams(new InterceptorConfig(ignoreLoading, ignoreErrors, resendRequest), queryParams) + params: new InterceptorHttpParams(new InterceptorConfig(ignoreLoading, ignoreErrors, resendRequest), cleanedParams) }; } + +function cleanQueryParams(params?: QueryParams): QueryParams | undefined { + if (!params) { + return undefined; + } + + const entries = Object.entries(params); + + const cleanedEntries = entries.filter( + ([_, value]) => value !== null && value !== undefined + ); + + if (!cleanedEntries.length) { + return undefined; + } + + return Object.fromEntries(cleanedEntries); +} From b5d3d9e05fe0dad30f7bc33cfcea6584ed79ab16 Mon Sep 17 00:00:00 2001 From: Maksym Tsymbarov Date: Sun, 9 Nov 2025 12:30:22 +0200 Subject: [PATCH 14/42] fixed request error --- ui-ngx/src/app/core/http/queue.service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui-ngx/src/app/core/http/queue.service.ts b/ui-ngx/src/app/core/http/queue.service.ts index d9bc19ef62..651d26c9dc 100644 --- a/ui-ngx/src/app/core/http/queue.service.ts +++ b/ui-ngx/src/app/core/http/queue.service.ts @@ -75,7 +75,7 @@ export class QueueService { } public getQueueStatisticsByIds(queueStatIds: Array, config?: RequestConfig): Observable> { - return this.http.get>(`/api/queueStats?QueueStatsIds=${queueStatIds.join(',')}`, + return this.http.get>(`/api/queueStats?queueStatsIds=${queueStatIds.join(',')}`, defaultHttpOptionsFromConfig(config)).pipe( map(queueStats => queueStats.map(queueStat => this.parseQueueStatName(queueStat)) ) From 0905d5d2548c1cc8ea23c55c40af7b88ff0d9f7c Mon Sep 17 00:00:00 2001 From: Maksym Tsymbarov Date: Mon, 10 Nov 2025 13:14:34 +0200 Subject: [PATCH 15/42] fixed lint problems --- .../home/components/widget/lib/rpc/led-indicator.component.ts | 2 +- .../home/components/widget/lib/rpc/round-switch.component.ts | 2 +- .../modules/home/components/widget/lib/rpc/switch.component.ts | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/rpc/led-indicator.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/rpc/led-indicator.component.ts index 6cd91eadae..a59bbca537 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/rpc/led-indicator.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/rpc/led-indicator.component.ts @@ -335,7 +335,7 @@ export class LedIndicatorComponent extends PageComponent implements OnInit, OnDe let valueToParse = attrValue; try { valueToParse = JSON.parse(attrValue); - } catch (e) { } + } catch (e) {/**/} try { value = !!this.parseValueFunction(valueToParse); diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/rpc/round-switch.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/rpc/round-switch.component.ts index 3e483b498a..8e64cc7100 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/rpc/round-switch.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/rpc/round-switch.component.ts @@ -333,7 +333,7 @@ export class RoundSwitchComponent extends PageComponent implements OnInit, OnDes let valueToParse = attrValue; try { valueToParse = JSON.parse(attrValue); - } catch (e) { } + } catch (e) {/**/} try { value = !!this.parseValueFunction(valueToParse); diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/rpc/switch.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/rpc/switch.component.ts index e9809cb61c..7a8f4e0dd6 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/rpc/switch.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/rpc/switch.component.ts @@ -376,7 +376,7 @@ export class SwitchComponent extends PageComponent implements AfterViewInit, OnD let valueToParse = attrValue; try { valueToParse = JSON.parse(attrValue); - } catch (e) { } + } catch (e) {/**/} try { value = !!this.parseValueFunction(valueToParse); From 1e2c127e1cbf47d36c10fe22674a8545357e0ccd Mon Sep 17 00:00:00 2001 From: Dmytro Skarzhynets Date: Mon, 10 Nov 2025 18:43:52 +0200 Subject: [PATCH 16/42] Remove redundant persistence of CF links # Conflicts: # application/src/main/data/upgrade/basic/schema_update.sql # common/dao-api/src/main/java/org/thingsboard/server/dao/cf/CalculatedFieldService.java # common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/BaseCalculatedFieldConfiguration.java # common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/CalculatedFieldConfiguration.java # common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/geofencing/GeofencingCalculatedFieldConfiguration.java # dao/src/main/java/org/thingsboard/server/dao/cf/BaseCalculatedFieldService.java # dao/src/test/java/org/thingsboard/server/dao/service/CalculatedFieldServiceTest.java --- .../main/data/upgrade/basic/schema_update.sql | 7 ++ ...alculatedFieldManagerMessageProcessor.java | 23 +++-- .../controller/CalculatedFieldController.java | 4 +- .../cf/DefaultCalculatedFieldCache.java | 10 +- .../DefaultCalculatedFieldQueueService.java | 2 +- .../server/dao/cf/CalculatedFieldService.java | 14 --- .../server/common/data/EntityType.java | 3 +- .../common/data/cf/CalculatedFieldLink.java | 46 +-------- ...entsBasedCalculatedFieldConfiguration.java | 17 ++-- .../CalculatedFieldConfiguration.java | 12 +-- ...eofencingCalculatedFieldConfiguration.java | 12 ++- .../common/data/id/CalculatedFieldLinkId.java | 45 --------- .../common/data/id/EntityIdFactory.java | 1 - common/proto/src/main/proto/queue.proto | 2 +- .../dao/cf/BaseCalculatedFieldService.java | 62 ++---------- .../server/dao/cf/CalculatedFieldLinkDao.java | 42 --------- .../entity/DefaultEntityServiceRegistry.java | 3 - .../server/dao/model/ModelConstants.java | 12 +-- .../model/sql/CalculatedFieldLinkEntity.java | 79 ---------------- .../CalculatedFieldLinkDataValidator.java | 41 -------- .../sql/cf/CalculatedFieldLinkRepository.java | 36 ------- ...efaultNativeCalculatedFieldRepository.java | 38 -------- .../dao/sql/cf/JpaCalculatedFieldLinkDao.java | 94 ------------------- .../cf/NativeCalculatedFieldRepository.java | 3 - .../main/resources/sql/schema-entities.sql | 10 -- .../service/CalculatedFieldServiceTest.java | 23 +---- .../service/EntityServiceRegistryTest.java | 5 - .../CalculatedFieldDataValidatorTest.java | 10 +- .../CalculatedFieldLinkDataValidatorTest.java | 57 ----------- .../server/msa/cf/CalculatedFieldTest.java | 10 +- .../rule/engine/util/TenantIdLoader.java | 10 -- .../rule/engine/util/TenantIdLoaderTest.java | 11 +-- 32 files changed, 73 insertions(+), 671 deletions(-) delete mode 100644 common/data/src/main/java/org/thingsboard/server/common/data/id/CalculatedFieldLinkId.java delete mode 100644 dao/src/main/java/org/thingsboard/server/dao/cf/CalculatedFieldLinkDao.java delete mode 100644 dao/src/main/java/org/thingsboard/server/dao/model/sql/CalculatedFieldLinkEntity.java delete mode 100644 dao/src/main/java/org/thingsboard/server/dao/service/validator/CalculatedFieldLinkDataValidator.java delete mode 100644 dao/src/main/java/org/thingsboard/server/dao/sql/cf/CalculatedFieldLinkRepository.java delete mode 100644 dao/src/main/java/org/thingsboard/server/dao/sql/cf/JpaCalculatedFieldLinkDao.java delete mode 100644 dao/src/test/java/org/thingsboard/server/dao/service/validator/CalculatedFieldLinkDataValidatorTest.java diff --git a/application/src/main/data/upgrade/basic/schema_update.sql b/application/src/main/data/upgrade/basic/schema_update.sql index e5fdba571b..17512ed64c 100644 --- a/application/src/main/data/upgrade/basic/schema_update.sql +++ b/application/src/main/data/upgrade/basic/schema_update.sql @@ -69,3 +69,10 @@ ALTER TABLE calculated_field DROP CONSTRAINT IF EXISTS calculated_field_unq_key; ALTER TABLE calculated_field ADD CONSTRAINT calculated_field_unq_key UNIQUE (entity_id, type, name); -- CALCULATED FIELD UNIQUE CONSTRAINT UPDATE END + +-- REMOVAL OF CALCULATED FIELD LINKS PERSISTENCE START + +DROP TABLE IF EXISTS calculated_field_link; +ANALYZE calculated_field; + +-- REMOVAL OF CALCULATED FIELD LINKS PERSISTENCE END diff --git a/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldManagerMessageProcessor.java b/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldManagerMessageProcessor.java index 3a19841cc5..ccdf719f6c 100644 --- a/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldManagerMessageProcessor.java +++ b/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldManagerMessageProcessor.java @@ -611,7 +611,7 @@ public class CalculatedFieldManagerMessageProcessor extends AbstractContextAware var proto = msg.getProto(); List result = new ArrayList<>(); for (var link : getCalculatedFieldLinksByEntityId(entityId)) { - CalculatedFieldCtx ctx = calculatedFields.get(link.getCalculatedFieldId()); + CalculatedFieldCtx ctx = calculatedFields.get(link.calculatedFieldId()); if (ctx.linkMatches(entityId, proto)) { result.add(ctx.toCalculatedFieldEntityCtxId()); } @@ -738,13 +738,13 @@ public class CalculatedFieldManagerMessageProcessor extends AbstractContextAware private void addLinks(CalculatedField newCf) { var newLinks = newCf.getConfiguration().buildCalculatedFieldLinks(tenantId, newCf.getEntityId(), newCf.getId()); - newLinks.forEach(link -> entityIdCalculatedFieldLinks.computeIfAbsent(link.getEntityId(), id -> new CopyOnWriteArrayList<>()).add(link)); + newLinks.forEach(link -> entityIdCalculatedFieldLinks.computeIfAbsent(link.entityId(), id -> new CopyOnWriteArrayList<>()).add(link)); } private void deleteLinks(CalculatedFieldCtx cfCtx) { var oldCf = cfCtx.getCalculatedField(); var oldLinks = oldCf.getConfiguration().buildCalculatedFieldLinks(tenantId, oldCf.getEntityId(), oldCf.getId()); - oldLinks.forEach(link -> entityIdCalculatedFieldLinks.computeIfAbsent(link.getEntityId(), id -> new CopyOnWriteArrayList<>()).remove(link)); + oldLinks.forEach(link -> entityIdCalculatedFieldLinks.computeIfAbsent(link.entityId(), id -> new CopyOnWriteArrayList<>()).remove(link)); } public void onPartitionChange(CalculatedFieldPartitionChangeMsg msg) { @@ -757,15 +757,11 @@ public class CalculatedFieldManagerMessageProcessor extends AbstractContextAware log.trace("Processing calculated field record: {}", cf); try { initCalculatedField(cf); + initCalculatedFieldLinks(cf); } catch (CalculatedFieldException e) { log.error("Failed to process calculated field record: {}", cf, e); } }); - PageDataIterable cfls = new PageDataIterable<>(pageLink -> cfDaoService.findAllCalculatedFieldLinksByTenantId(tenantId, pageLink), cfSettings.getInitTenantFetchPackSize()); - cfls.forEach(link -> { - log.trace("Processing calculated field link record: {}", link); - initCalculatedFieldLink(link); - }); } private void initCalculatedField(CalculatedField cf) throws CalculatedFieldException { @@ -782,10 +778,13 @@ public class CalculatedFieldManagerMessageProcessor extends AbstractContextAware } } - private void initCalculatedFieldLink(CalculatedFieldLink link) { - // We use copy on write lists to safely pass the reference to another actor for the iteration. - // Alternative approach would be to use any list but avoid modifications to the list (change the complete map value instead) - entityIdCalculatedFieldLinks.computeIfAbsent(link.getEntityId(), id -> new CopyOnWriteArrayList<>()).add(link); + private void initCalculatedFieldLinks(CalculatedField cf) { + List links = cf.getConfiguration().buildCalculatedFieldLinks(cf.getTenantId(), cf.getEntityId(), cf.getId()); + for (CalculatedFieldLink link : links) { + // We use copy on write lists to safely pass the reference to another actor for the iteration. + // Alternative approach would be to use any list but avoid modifications to the list (change the complete map value instead) + entityIdCalculatedFieldLinks.computeIfAbsent(link.entityId(), id -> new CopyOnWriteArrayList<>()).add(link); + } } private void initEntitiesCache() { diff --git a/application/src/main/java/org/thingsboard/server/controller/CalculatedFieldController.java b/application/src/main/java/org/thingsboard/server/controller/CalculatedFieldController.java index 8c193a5b20..9a24ab91fc 100644 --- a/application/src/main/java/org/thingsboard/server/controller/CalculatedFieldController.java +++ b/application/src/main/java/org/thingsboard/server/controller/CalculatedFieldController.java @@ -63,10 +63,10 @@ import org.thingsboard.server.service.security.permission.Operation; import java.util.ArrayList; import java.util.Collections; -import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Optional; +import java.util.Set; import java.util.concurrent.TimeUnit; import static org.thingsboard.server.controller.ControllerConstants.CF_TEXT_SEARCH_DESCRIPTION; @@ -287,7 +287,7 @@ public class CalculatedFieldController extends BaseController { } private void checkReferencedEntities(CalculatedFieldConfiguration calculatedFieldConfig) throws ThingsboardException { - List referencedEntityIds = calculatedFieldConfig.getReferencedEntities(); + Set referencedEntityIds = calculatedFieldConfig.getReferencedEntities(); for (EntityId referencedEntityId : referencedEntityIds) { EntityType entityType = referencedEntityId.getEntityType(); switch (entityType) { diff --git a/application/src/main/java/org/thingsboard/server/service/cf/DefaultCalculatedFieldCache.java b/application/src/main/java/org/thingsboard/server/service/cf/DefaultCalculatedFieldCache.java index e802bd7677..4466b368b2 100644 --- a/application/src/main/java/org/thingsboard/server/service/cf/DefaultCalculatedFieldCache.java +++ b/application/src/main/java/org/thingsboard/server/service/cf/DefaultCalculatedFieldCache.java @@ -82,19 +82,17 @@ public class DefaultCalculatedFieldCache implements CalculatedFieldCache { cfs.forEach(cf -> { if (cf != null) { calculatedFields.putIfAbsent(cf.getId(), cf); + List links = cf.getConfiguration().buildCalculatedFieldLinks(cf.getTenantId(), cf.getEntityId(), cf.getId()); + calculatedFieldLinks.put(cf.getId(), new CopyOnWriteArrayList<>(links)); } }); calculatedFields.values().forEach(cf -> { entityIdCalculatedFields.computeIfAbsent(cf.getEntityId(), id -> new CopyOnWriteArrayList<>()).add(cf); }); - PageDataIterable cfls = new PageDataIterable<>(calculatedFieldService::findAllCalculatedFieldLinks, initFetchPackSize); - cfls.forEach(link -> { - calculatedFieldLinks.computeIfAbsent(link.getCalculatedFieldId(), id -> new CopyOnWriteArrayList<>()).add(link); - }); calculatedFieldLinks.values().stream() .flatMap(List::stream) .forEach(link -> - entityIdCalculatedFieldLinks.computeIfAbsent(link.getEntityId(), id -> new CopyOnWriteArrayList<>()).add(link) + entityIdCalculatedFieldLinks.computeIfAbsent(link.entityId(), id -> new CopyOnWriteArrayList<>()).add(link) ); } @@ -226,7 +224,7 @@ public class DefaultCalculatedFieldCache implements CalculatedFieldCache { log.debug("[{}] evict calculated field links from cache: {}", calculatedFieldId, oldCalculatedField); calculatedFieldsCtx.remove(calculatedFieldId); log.debug("[{}] evict calculated field ctx from cache: {}", calculatedFieldId, oldCalculatedField); - entityIdCalculatedFieldLinks.forEach((entityId, calculatedFieldLinks) -> calculatedFieldLinks.removeIf(link -> link.getCalculatedFieldId().equals(calculatedFieldId))); + entityIdCalculatedFieldLinks.forEach((entityId, calculatedFieldLinks) -> calculatedFieldLinks.removeIf(link -> link.calculatedFieldId().equals(calculatedFieldId))); log.debug("[{}] evict calculated field links from cached links by entity id: {}", calculatedFieldId, oldCalculatedField); } diff --git a/application/src/main/java/org/thingsboard/server/service/cf/DefaultCalculatedFieldQueueService.java b/application/src/main/java/org/thingsboard/server/service/cf/DefaultCalculatedFieldQueueService.java index 2bf605140a..0236de552f 100644 --- a/application/src/main/java/org/thingsboard/server/service/cf/DefaultCalculatedFieldQueueService.java +++ b/application/src/main/java/org/thingsboard/server/service/cf/DefaultCalculatedFieldQueueService.java @@ -172,7 +172,7 @@ public class DefaultCalculatedFieldQueueService implements CalculatedFieldQueueS List links = calculatedFieldCache.getCalculatedFieldLinksByEntityId(entityId); for (CalculatedFieldLink link : links) { - CalculatedFieldCtx ctx = calculatedFieldCache.getCalculatedFieldCtx(link.getCalculatedFieldId()); + CalculatedFieldCtx ctx = calculatedFieldCache.getCalculatedFieldCtx(link.calculatedFieldId()); if (ctx != null && linkedEntityFilter.test(ctx)) { return true; } diff --git a/common/dao-api/src/main/java/org/thingsboard/server/dao/cf/CalculatedFieldService.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/cf/CalculatedFieldService.java index 57c6df3c7f..bef4675e03 100644 --- a/common/dao-api/src/main/java/org/thingsboard/server/dao/cf/CalculatedFieldService.java +++ b/common/dao-api/src/main/java/org/thingsboard/server/dao/cf/CalculatedFieldService.java @@ -16,10 +16,8 @@ package org.thingsboard.server.dao.cf; import org.thingsboard.server.common.data.cf.CalculatedField; -import org.thingsboard.server.common.data.cf.CalculatedFieldLink; import org.thingsboard.server.common.data.cf.CalculatedFieldType; import org.thingsboard.server.common.data.id.CalculatedFieldId; -import org.thingsboard.server.common.data.id.CalculatedFieldLinkId; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.page.PageData; @@ -52,18 +50,6 @@ public interface CalculatedFieldService extends EntityDaoService { int deleteAllCalculatedFieldsByEntityId(TenantId tenantId, EntityId entityId); - CalculatedFieldLink saveCalculatedFieldLink(TenantId tenantId, CalculatedFieldLink calculatedFieldLink); - - CalculatedFieldLink findCalculatedFieldLinkById(TenantId tenantId, CalculatedFieldLinkId calculatedFieldLinkId); - - List findAllCalculatedFieldLinksById(TenantId tenantId, CalculatedFieldId calculatedFieldId); - - List findAllCalculatedFieldLinksByEntityId(TenantId tenantId, EntityId entityId); - - PageData findAllCalculatedFieldLinksByTenantId(TenantId tenantId, PageLink pageLink); - - PageData findAllCalculatedFieldLinks(PageLink pageLink); - boolean referencedInAnyCalculatedField(TenantId tenantId, EntityId referencedEntityId); } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/EntityType.java b/common/data/src/main/java/org/thingsboard/server/common/data/EntityType.java index 110052b57f..6a37f9fe1b 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/EntityType.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/EntityType.java @@ -23,6 +23,7 @@ import java.util.EnumSet; import java.util.List; public enum EntityType { + TENANT(1), CUSTOMER(2), USER(3, "tb_user"), @@ -61,7 +62,7 @@ public enum EntityType { MOBILE_APP(37), MOBILE_APP_BUNDLE(38), CALCULATED_FIELD(39), - CALCULATED_FIELD_LINK(40), + // CALCULATED_FIELD_LINK(40), - was removed in 4.3 JOB(41), ADMIN_SETTINGS(42), AI_MODEL(43, "ai_model") { diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/cf/CalculatedFieldLink.java b/common/data/src/main/java/org/thingsboard/server/common/data/cf/CalculatedFieldLink.java index 3f048815da..71f5917c34 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/cf/CalculatedFieldLink.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/cf/CalculatedFieldLink.java @@ -15,52 +15,8 @@ */ package org.thingsboard.server.common.data.cf; -import io.swagger.v3.oas.annotations.media.Schema; -import lombok.Data; -import lombok.EqualsAndHashCode; -import org.thingsboard.server.common.data.BaseData; import org.thingsboard.server.common.data.id.CalculatedFieldId; -import org.thingsboard.server.common.data.id.CalculatedFieldLinkId; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.TenantId; -@Schema -@Data -@EqualsAndHashCode(callSuper = true) -public class CalculatedFieldLink extends BaseData { - - private static final long serialVersionUID = 6492846246722091530L; - - private TenantId tenantId; - private EntityId entityId; - - @Schema(description = "JSON object with the Calculated Field Id. ", accessMode = Schema.AccessMode.READ_ONLY) - private CalculatedFieldId calculatedFieldId; - - public CalculatedFieldLink() { - super(); - } - - public CalculatedFieldLink(CalculatedFieldLinkId id) { - super(id); - } - - public CalculatedFieldLink(TenantId tenantId, EntityId entityId, CalculatedFieldId calculatedFieldId) { - this.tenantId = tenantId; - this.entityId = entityId; - this.calculatedFieldId = calculatedFieldId; - } - - @Override - public String toString() { - return new StringBuilder() - .append("CalculatedFieldLink[") - .append("tenantId=").append(tenantId) - .append(", entityId=").append(entityId) - .append(", calculatedFieldId=").append(calculatedFieldId) - .append(", createdTime=").append(createdTime) - .append(", id=").append(id).append(']') - .toString(); - } - -} +public record CalculatedFieldLink(TenantId tenantId, EntityId entityId, CalculatedFieldId calculatedFieldId) {} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/ArgumentsBasedCalculatedFieldConfiguration.java b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/ArgumentsBasedCalculatedFieldConfiguration.java index f422869c95..294335e665 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/ArgumentsBasedCalculatedFieldConfiguration.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/ArgumentsBasedCalculatedFieldConfiguration.java @@ -19,10 +19,12 @@ import jakarta.validation.Valid; import jakarta.validation.constraints.NotEmpty; import org.thingsboard.server.common.data.id.EntityId; -import java.util.List; +import java.util.Collections; import java.util.Map; import java.util.Objects; -import java.util.stream.Collectors; +import java.util.Set; + +import static java.util.stream.Collectors.toSet; public interface ArgumentsBasedCalculatedFieldConfiguration extends CalculatedFieldConfiguration { @@ -30,14 +32,15 @@ public interface ArgumentsBasedCalculatedFieldConfiguration extends CalculatedFi @NotEmpty Map getArguments(); - default List getReferencedEntities() { - if (getArguments() == null) { - return List.of(); + default Set getReferencedEntities() { + var args = getArguments(); + if (args == null) { + return Collections.emptySet(); } - return getArguments().values().stream() + return args.values().stream() .map(Argument::getRefEntityId) .filter(Objects::nonNull) - .collect(Collectors.toList()); + .collect(toSet()); } } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/CalculatedFieldConfiguration.java b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/CalculatedFieldConfiguration.java index 3df9a32dcc..5313dea0bf 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/CalculatedFieldConfiguration.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/CalculatedFieldConfiguration.java @@ -28,7 +28,9 @@ import org.thingsboard.server.common.data.id.CalculatedFieldId; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.TenantId; +import java.util.Collections; import java.util.List; +import java.util.Set; import java.util.stream.Collectors; @JsonTypeInfo( @@ -55,16 +57,12 @@ public interface CalculatedFieldConfiguration { default void validate() {} @JsonIgnore - default List getReferencedEntities() { - return List.of(); + default Set getReferencedEntities() { + return Collections.emptySet(); } default CalculatedFieldLink buildCalculatedFieldLink(TenantId tenantId, EntityId referencedEntityId, CalculatedFieldId calculatedFieldId) { - CalculatedFieldLink link = new CalculatedFieldLink(); - link.setTenantId(tenantId); - link.setEntityId(referencedEntityId); - link.setCalculatedFieldId(calculatedFieldId); - return link; + return new CalculatedFieldLink(tenantId, referencedEntityId, calculatedFieldId); } default List buildCalculatedFieldLinks(TenantId tenantId, EntityId cfEntityId, CalculatedFieldId calculatedFieldId) { diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/geofencing/GeofencingCalculatedFieldConfiguration.java b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/geofencing/GeofencingCalculatedFieldConfiguration.java index 47d344fe1b..acefd5ff26 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/geofencing/GeofencingCalculatedFieldConfiguration.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/geofencing/GeofencingCalculatedFieldConfiguration.java @@ -26,10 +26,13 @@ import org.thingsboard.server.common.data.cf.configuration.Output; import org.thingsboard.server.common.data.cf.configuration.ScheduledUpdateSupportedCalculatedFieldConfiguration; import org.thingsboard.server.common.data.id.EntityId; +import java.util.Collections; import java.util.HashMap; -import java.util.List; import java.util.Map; import java.util.Objects; +import java.util.Set; + +import static java.util.stream.Collectors.toSet; @Data public class GeofencingCalculatedFieldConfiguration implements ArgumentsBasedCalculatedFieldConfiguration, ScheduledUpdateSupportedCalculatedFieldConfiguration { @@ -62,8 +65,11 @@ public class GeofencingCalculatedFieldConfiguration implements ArgumentsBasedCal @Override - public List getReferencedEntities() { - return zoneGroups == null ? List.of() : zoneGroups.values().stream().map(ZoneGroupConfiguration::getRefEntityId).filter(Objects::nonNull).toList(); + public Set getReferencedEntities() { + return zoneGroups == null ? Collections.emptySet() : zoneGroups.values().stream() + .map(ZoneGroupConfiguration::getRefEntityId) + .filter(Objects::nonNull) + .collect(toSet()); } @Override diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/id/CalculatedFieldLinkId.java b/common/data/src/main/java/org/thingsboard/server/common/data/id/CalculatedFieldLinkId.java deleted file mode 100644 index 6a0c680bb6..0000000000 --- a/common/data/src/main/java/org/thingsboard/server/common/data/id/CalculatedFieldLinkId.java +++ /dev/null @@ -1,45 +0,0 @@ -/** - * Copyright © 2016-2025 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.common.data.id; - -import com.fasterxml.jackson.annotation.JsonCreator; -import com.fasterxml.jackson.annotation.JsonProperty; -import io.swagger.v3.oas.annotations.media.Schema; -import org.thingsboard.server.common.data.EntityType; - -import java.util.UUID; - -@Schema -public class CalculatedFieldLinkId extends UUIDBased implements EntityId { - - private static final long serialVersionUID = 1L; - - @JsonCreator - public CalculatedFieldLinkId(@JsonProperty("id") UUID id) { - super(id); - } - - public static CalculatedFieldLinkId fromString(String calculatedFieldLinkId) { - return new CalculatedFieldLinkId(UUID.fromString(calculatedFieldLinkId)); - } - - @Schema(requiredMode = Schema.RequiredMode.REQUIRED, description = "string", example = "CALCULATED_FIELD_LINK", allowableValues = "CALCULATED_FIELD_LINK") - @Override - public EntityType getEntityType() { - return EntityType.CALCULATED_FIELD_LINK; - } - -} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/id/EntityIdFactory.java b/common/data/src/main/java/org/thingsboard/server/common/data/id/EntityIdFactory.java index 09d34ff10b..3b8959624d 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/id/EntityIdFactory.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/id/EntityIdFactory.java @@ -80,7 +80,6 @@ public class EntityIdFactory { case DOMAIN -> new DomainId(uuid); case MOBILE_APP_BUNDLE -> new MobileAppBundleId(uuid); case CALCULATED_FIELD -> new CalculatedFieldId(uuid); - case CALCULATED_FIELD_LINK -> new CalculatedFieldLinkId(uuid); case JOB -> new JobId(uuid); case ADMIN_SETTINGS -> new AdminSettingsId(uuid); case AI_MODEL -> new AiModelId(uuid); diff --git a/common/proto/src/main/proto/queue.proto b/common/proto/src/main/proto/queue.proto index bfdce15dbd..5f33524464 100644 --- a/common/proto/src/main/proto/queue.proto +++ b/common/proto/src/main/proto/queue.proto @@ -62,7 +62,7 @@ enum EntityTypeProto { MOBILE_APP = 37; MOBILE_APP_BUNDLE = 38; CALCULATED_FIELD = 39; - CALCULATED_FIELD_LINK = 40; + // CALCULATED_FIELD_LINK = 40; - was removed in 4.3 JOB = 41; ADMIN_SETTINGS = 42; AI_MODEL = 43; diff --git a/dao/src/main/java/org/thingsboard/server/dao/cf/BaseCalculatedFieldService.java b/dao/src/main/java/org/thingsboard/server/dao/cf/BaseCalculatedFieldService.java index 8e875bfae2..d3ca3ccff3 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/cf/BaseCalculatedFieldService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/cf/BaseCalculatedFieldService.java @@ -21,11 +21,9 @@ import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.cf.CalculatedField; -import org.thingsboard.server.common.data.cf.CalculatedFieldLink; import org.thingsboard.server.common.data.cf.CalculatedFieldType; import org.thingsboard.server.common.data.cf.configuration.CalculatedFieldConfiguration; import org.thingsboard.server.common.data.id.CalculatedFieldId; -import org.thingsboard.server.common.data.id.CalculatedFieldLinkId; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.HasId; import org.thingsboard.server.common.data.id.TenantId; @@ -36,7 +34,6 @@ import org.thingsboard.server.dao.eventsourcing.DeleteEntityEvent; import org.thingsboard.server.dao.eventsourcing.SaveEntityEvent; import org.thingsboard.server.dao.exception.IncorrectParameterException; import org.thingsboard.server.dao.service.validator.CalculatedFieldDataValidator; -import org.thingsboard.server.dao.service.validator.CalculatedFieldLinkDataValidator; import java.util.EnumSet; import java.util.List; @@ -57,9 +54,7 @@ public class BaseCalculatedFieldService extends AbstractEntityService implements public static final String INCORRECT_ENTITY_ID = "Incorrect entityId "; private final CalculatedFieldDao calculatedFieldDao; - private final CalculatedFieldLinkDao calculatedFieldLinkDao; private final CalculatedFieldDataValidator calculatedFieldDataValidator; - private final CalculatedFieldLinkDataValidator calculatedFieldLinkDataValidator; @Override public CalculatedField save(CalculatedField calculatedField) { @@ -84,9 +79,13 @@ public class BaseCalculatedFieldService extends AbstractEntityService implements log.trace("Executing save calculated field, [{}]", calculatedField); updateDebugSettings(tenantId, calculatedField, System.currentTimeMillis()); CalculatedField savedCalculatedField = calculatedFieldDao.save(tenantId, calculatedField); - createOrUpdateCalculatedFieldLink(tenantId, savedCalculatedField); - eventPublisher.publishEvent(SaveEntityEvent.builder().tenantId(savedCalculatedField.getTenantId()).entityId(savedCalculatedField.getId()) - .entity(savedCalculatedField).oldEntity(oldCalculatedField).created(calculatedField.getId() == null).build()); + eventPublisher.publishEvent(SaveEntityEvent.builder() + .tenantId(savedCalculatedField.getTenantId()) + .entityId(savedCalculatedField.getId()) + .entity(savedCalculatedField) + .oldEntity(oldCalculatedField) + .created(calculatedField.getId() == null) + .build()); return savedCalculatedField; } catch (Exception e) { checkConstraintViolation(e, @@ -191,48 +190,6 @@ public class BaseCalculatedFieldService extends AbstractEntityService implements return calculatedFields.size(); } - @Override - public CalculatedFieldLink saveCalculatedFieldLink(TenantId tenantId, CalculatedFieldLink calculatedFieldLink) { - calculatedFieldLinkDataValidator.validate(calculatedFieldLink, CalculatedFieldLink::getTenantId); - log.trace("Executing save calculated field link, [{}]", calculatedFieldLink); - return calculatedFieldLinkDao.save(tenantId, calculatedFieldLink); - } - - @Override - public CalculatedFieldLink findCalculatedFieldLinkById(TenantId tenantId, CalculatedFieldLinkId calculatedFieldLinkId) { - log.trace("Executing findCalculatedFieldLinkById, tenantId [{}], calculatedFieldLinkId [{}]", tenantId, calculatedFieldLinkId); - validateId(tenantId, id -> INCORRECT_TENANT_ID + id); - validateId(calculatedFieldLinkId, id -> "Incorrect calculatedFieldLinkId " + id); - return calculatedFieldLinkDao.findById(tenantId, calculatedFieldLinkId.getId()); - } - - @Override - public List findAllCalculatedFieldLinksById(TenantId tenantId, CalculatedFieldId calculatedFieldId) { - log.trace("Executing findAllCalculatedFieldLinksById, calculatedFieldId [{}]", calculatedFieldId); - return calculatedFieldLinkDao.findCalculatedFieldLinksByCalculatedFieldId(tenantId, calculatedFieldId); - } - - @Override - public List findAllCalculatedFieldLinksByEntityId(TenantId tenantId, EntityId entityId) { - log.trace("Executing findAllCalculatedFieldLinksByEntityId, entityId [{}]", entityId); - return calculatedFieldLinkDao.findCalculatedFieldLinksByEntityId(tenantId, entityId); - } - - @Override - public PageData findAllCalculatedFieldLinksByTenantId(TenantId tenantId, PageLink pageLink) { - log.trace("Executing findAllCalculatedFieldLinksByTenantId, tenantId[{}] pageLink [{}]", tenantId, pageLink); - validateId(tenantId, id -> INCORRECT_TENANT_ID + id); - validatePageLink(pageLink); - return calculatedFieldLinkDao.findAllByTenantId(tenantId, pageLink); - } - - @Override - public PageData findAllCalculatedFieldLinks(PageLink pageLink) { - log.trace("Executing findAllCalculatedFieldLinks, pageLink [{}]", pageLink); - validatePageLink(pageLink); - return calculatedFieldLinkDao.findAll(pageLink); - } - @Override public boolean referencedInAnyCalculatedField(TenantId tenantId, EntityId referencedEntityId) { return calculatedFieldDao.findAllByTenantId(tenantId).stream() @@ -258,9 +215,4 @@ public class BaseCalculatedFieldService extends AbstractEntityService implements return EntityType.CALCULATED_FIELD; } - private void createOrUpdateCalculatedFieldLink(TenantId tenantId, CalculatedField calculatedField) { - List links = calculatedField.getConfiguration().buildCalculatedFieldLinks(tenantId, calculatedField.getEntityId(), calculatedField.getId()); - links.forEach(link -> saveCalculatedFieldLink(tenantId, link)); - } - } diff --git a/dao/src/main/java/org/thingsboard/server/dao/cf/CalculatedFieldLinkDao.java b/dao/src/main/java/org/thingsboard/server/dao/cf/CalculatedFieldLinkDao.java deleted file mode 100644 index dd184289ed..0000000000 --- a/dao/src/main/java/org/thingsboard/server/dao/cf/CalculatedFieldLinkDao.java +++ /dev/null @@ -1,42 +0,0 @@ -/** - * Copyright © 2016-2025 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.dao.cf; - -import org.thingsboard.server.common.data.cf.CalculatedFieldLink; -import org.thingsboard.server.common.data.id.CalculatedFieldId; -import org.thingsboard.server.common.data.id.EntityId; -import org.thingsboard.server.common.data.id.TenantId; -import org.thingsboard.server.common.data.page.PageData; -import org.thingsboard.server.common.data.page.PageLink; -import org.thingsboard.server.dao.Dao; - -import java.util.List; - -public interface CalculatedFieldLinkDao extends Dao { - - List findCalculatedFieldLinksByCalculatedFieldId(TenantId tenantId, CalculatedFieldId calculatedFieldId); - - List findCalculatedFieldLinksByEntityId(TenantId tenantId, EntityId entityId); - - List findCalculatedFieldLinksByTenantId(TenantId tenantId); - - List findAll(); - - PageData findAll(PageLink pageLink); - - PageData findAllByTenantId(TenantId tenantId, PageLink pageLink); - -} diff --git a/dao/src/main/java/org/thingsboard/server/dao/entity/DefaultEntityServiceRegistry.java b/dao/src/main/java/org/thingsboard/server/dao/entity/DefaultEntityServiceRegistry.java index 9c50ad621f..a795bddffa 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/entity/DefaultEntityServiceRegistry.java +++ b/dao/src/main/java/org/thingsboard/server/dao/entity/DefaultEntityServiceRegistry.java @@ -43,9 +43,6 @@ public class DefaultEntityServiceRegistry implements EntityServiceRegistry { if (EntityType.RULE_CHAIN.equals(entityType)) { entityDaoServicesMap.put(EntityType.RULE_NODE, entityDaoService); } - if (EntityType.CALCULATED_FIELD.equals(entityType)) { - entityDaoServicesMap.put(EntityType.CALCULATED_FIELD_LINK, entityDaoService); - } }); log.debug("Initialized EntityServiceRegistry total [{}] entries", entityDaoServicesMap.size()); } diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java b/dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java index 3960c67525..aee8356af4 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java +++ b/dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java @@ -26,8 +26,7 @@ import java.util.UUID; public class ModelConstants { - private ModelConstants() { - } + private ModelConstants() {} public static final UUID NULL_UUID = Uuids.startOf(0); public static final TenantId SYSTEM_TENANT = TenantId.fromUUID(ModelConstants.NULL_UUID); @@ -731,15 +730,6 @@ public class ModelConstants { public static final String CALCULATED_FIELD_CONFIGURATION = "configuration"; public static final String CALCULATED_FIELD_VERSION = "version"; - /** - * Calculated field links constants. - */ - public static final String CALCULATED_FIELD_LINK_TABLE_NAME = "calculated_field_link"; - public static final String CALCULATED_FIELD_LINK_TENANT_ID_COLUMN = TENANT_ID_COLUMN; - public static final String CALCULATED_FIELD_LINK_ENTITY_TYPE = ENTITY_TYPE_COLUMN; - public static final String CALCULATED_FIELD_LINK_ENTITY_ID = ENTITY_ID_COLUMN; - public static final String CALCULATED_FIELD_LINK_CALCULATED_FIELD_ID = "calculated_field_id"; - /** * Tasks constants. */ diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/sql/CalculatedFieldLinkEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/sql/CalculatedFieldLinkEntity.java deleted file mode 100644 index 0f2a6455ec..0000000000 --- a/dao/src/main/java/org/thingsboard/server/dao/model/sql/CalculatedFieldLinkEntity.java +++ /dev/null @@ -1,79 +0,0 @@ -/** - * Copyright © 2016-2025 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.dao.model.sql; - -import jakarta.persistence.Column; -import jakarta.persistence.Entity; -import jakarta.persistence.Table; -import lombok.Data; -import lombok.EqualsAndHashCode; -import org.thingsboard.server.common.data.cf.CalculatedFieldLink; -import org.thingsboard.server.common.data.id.CalculatedFieldId; -import org.thingsboard.server.common.data.id.CalculatedFieldLinkId; -import org.thingsboard.server.common.data.id.EntityIdFactory; -import org.thingsboard.server.common.data.id.TenantId; -import org.thingsboard.server.dao.model.BaseEntity; -import org.thingsboard.server.dao.model.BaseSqlEntity; - -import java.util.UUID; - -import static org.thingsboard.server.dao.model.ModelConstants.CALCULATED_FIELD_LINK_CALCULATED_FIELD_ID; -import static org.thingsboard.server.dao.model.ModelConstants.CALCULATED_FIELD_LINK_ENTITY_ID; -import static org.thingsboard.server.dao.model.ModelConstants.CALCULATED_FIELD_LINK_ENTITY_TYPE; -import static org.thingsboard.server.dao.model.ModelConstants.CALCULATED_FIELD_LINK_TABLE_NAME; -import static org.thingsboard.server.dao.model.ModelConstants.CALCULATED_FIELD_LINK_TENANT_ID_COLUMN; - -@Data -@EqualsAndHashCode(callSuper = true) -@Entity -@Table(name = CALCULATED_FIELD_LINK_TABLE_NAME) -public class CalculatedFieldLinkEntity extends BaseSqlEntity implements BaseEntity { - - @Column(name = CALCULATED_FIELD_LINK_TENANT_ID_COLUMN) - private UUID tenantId; - - @Column(name = CALCULATED_FIELD_LINK_ENTITY_TYPE) - private String entityType; - - @Column(name = CALCULATED_FIELD_LINK_ENTITY_ID) - private UUID entityId; - - @Column(name = CALCULATED_FIELD_LINK_CALCULATED_FIELD_ID) - private UUID calculatedFieldId; - - public CalculatedFieldLinkEntity() { - super(); - } - - public CalculatedFieldLinkEntity(CalculatedFieldLink calculatedFieldLink) { - super(calculatedFieldLink); - this.tenantId = calculatedFieldLink.getTenantId().getId(); - this.entityType = calculatedFieldLink.getEntityId().getEntityType().name(); - this.entityId = calculatedFieldLink.getEntityId().getId(); - this.calculatedFieldId = calculatedFieldLink.getCalculatedFieldId().getId(); - } - - @Override - public CalculatedFieldLink toData() { - CalculatedFieldLink calculatedFieldLink = new CalculatedFieldLink(new CalculatedFieldLinkId(id)); - calculatedFieldLink.setCreatedTime(createdTime); - calculatedFieldLink.setTenantId(TenantId.fromUUID(tenantId)); - calculatedFieldLink.setEntityId(EntityIdFactory.getByTypeAndUuid(entityType, entityId)); - calculatedFieldLink.setCalculatedFieldId(new CalculatedFieldId(calculatedFieldId)); - return calculatedFieldLink; - } - -} diff --git a/dao/src/main/java/org/thingsboard/server/dao/service/validator/CalculatedFieldLinkDataValidator.java b/dao/src/main/java/org/thingsboard/server/dao/service/validator/CalculatedFieldLinkDataValidator.java deleted file mode 100644 index aaba200c92..0000000000 --- a/dao/src/main/java/org/thingsboard/server/dao/service/validator/CalculatedFieldLinkDataValidator.java +++ /dev/null @@ -1,41 +0,0 @@ -/** - * Copyright © 2016-2025 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.dao.service.validator; - -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.stereotype.Component; -import org.thingsboard.server.common.data.cf.CalculatedFieldLink; -import org.thingsboard.server.common.data.id.TenantId; -import org.thingsboard.server.dao.cf.CalculatedFieldLinkDao; -import org.thingsboard.server.dao.exception.DataValidationException; -import org.thingsboard.server.dao.service.DataValidator; - -@Component -public class CalculatedFieldLinkDataValidator extends DataValidator { - - @Autowired - private CalculatedFieldLinkDao calculatedFieldLinkDao; - - @Override - protected CalculatedFieldLink validateUpdate(TenantId tenantId, CalculatedFieldLink calculatedFieldLink) { - CalculatedFieldLink old = calculatedFieldLinkDao.findById(calculatedFieldLink.getTenantId(), calculatedFieldLink.getId().getId()); - if (old == null) { - throw new DataValidationException("Can't update non existing calculated field link!"); - } - return old; - } - -} diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/cf/CalculatedFieldLinkRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/cf/CalculatedFieldLinkRepository.java deleted file mode 100644 index 6f6a0775f3..0000000000 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/cf/CalculatedFieldLinkRepository.java +++ /dev/null @@ -1,36 +0,0 @@ -/** - * Copyright © 2016-2025 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.dao.sql.cf; - -import org.springframework.data.domain.Page; -import org.springframework.data.domain.Pageable; -import org.springframework.data.jpa.repository.JpaRepository; -import org.thingsboard.server.dao.model.sql.CalculatedFieldLinkEntity; - -import java.util.List; -import java.util.UUID; - -public interface CalculatedFieldLinkRepository extends JpaRepository { - - List findAllByTenantIdAndCalculatedFieldId(UUID tenantId, UUID calculatedFieldId); - - List findAllByTenantIdAndEntityId(UUID tenantId, UUID entityId); - - List findAllByTenantId(UUID tenantId); - - Page findAllByTenantId(UUID tenantId, Pageable pageable); - -} diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/cf/DefaultNativeCalculatedFieldRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/cf/DefaultNativeCalculatedFieldRepository.java index bbce4e2721..f2b565131e 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/cf/DefaultNativeCalculatedFieldRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/cf/DefaultNativeCalculatedFieldRepository.java @@ -25,12 +25,10 @@ import org.springframework.transaction.support.TransactionTemplate; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.cf.CalculatedField; -import org.thingsboard.server.common.data.cf.CalculatedFieldLink; import org.thingsboard.server.common.data.cf.CalculatedFieldType; import org.thingsboard.server.common.data.cf.configuration.CalculatedFieldConfiguration; import org.thingsboard.server.common.data.debug.DebugSettings; import org.thingsboard.server.common.data.id.CalculatedFieldId; -import org.thingsboard.server.common.data.id.CalculatedFieldLinkId; import org.thingsboard.server.common.data.id.EntityIdFactory; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.page.PageData; @@ -49,9 +47,6 @@ public class DefaultNativeCalculatedFieldRepository implements NativeCalculatedF private final String CF_COUNT_QUERY = "SELECT count(id) FROM calculated_field;"; private final String CF_QUERY = "SELECT * FROM calculated_field ORDER BY created_time ASC LIMIT %s OFFSET %s"; - private final String CFL_COUNT_QUERY = "SELECT count(id) FROM calculated_field_link;"; - private final String CFL_QUERY = "SELECT * FROM calculated_field_link ORDER BY created_time ASC LIMIT %s OFFSET %s"; - private final NamedParameterJdbcTemplate jdbcTemplate; private final TransactionTemplate transactionTemplate; @@ -103,37 +98,4 @@ public class DefaultNativeCalculatedFieldRepository implements NativeCalculatedF }); } - @Override - public PageData findCalculatedFieldLinks(Pageable pageable) { - return transactionTemplate.execute(status -> { - long startTs = System.currentTimeMillis(); - int totalElements = jdbcTemplate.queryForObject(CFL_COUNT_QUERY, Collections.emptyMap(), Integer.class); - log.debug("Count query took {} ms", System.currentTimeMillis() - startTs); - startTs = System.currentTimeMillis(); - List> rows = jdbcTemplate.queryForList(String.format(CFL_QUERY, pageable.getPageSize(), pageable.getOffset()), Collections.emptyMap()); - log.debug("Main query took {} ms", System.currentTimeMillis() - startTs); - int totalPages = pageable.getPageSize() > 0 ? (int) Math.ceil((float) totalElements / pageable.getPageSize()) : 1; - boolean hasNext = pageable.getPageSize() > 0 && totalElements > pageable.getOffset() + rows.size(); - var data = rows.stream().map(row -> { - - UUID id = (UUID) row.get("id"); - long createdTime = (long) row.get("created_time"); - UUID tenantId = (UUID) row.get("tenant_id"); - EntityType entityType = EntityType.valueOf((String) row.get("entity_type")); - UUID entityId = (UUID) row.get("entity_id"); - UUID calculatedFieldId = (UUID) row.get("calculated_field_id"); - - CalculatedFieldLink calculatedFieldLink = new CalculatedFieldLink(); - calculatedFieldLink.setId(new CalculatedFieldLinkId(id)); - calculatedFieldLink.setCreatedTime(createdTime); - calculatedFieldLink.setTenantId(new TenantId(tenantId)); - calculatedFieldLink.setEntityId(EntityIdFactory.getByTypeAndUuid(entityType, entityId)); - calculatedFieldLink.setCalculatedFieldId(new CalculatedFieldId(calculatedFieldId)); - - return calculatedFieldLink; - }).collect(Collectors.toList()); - return new PageData<>(data, totalPages, totalElements, hasNext); - }); - } - } diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/cf/JpaCalculatedFieldLinkDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/cf/JpaCalculatedFieldLinkDao.java deleted file mode 100644 index 38871f2fb8..0000000000 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/cf/JpaCalculatedFieldLinkDao.java +++ /dev/null @@ -1,94 +0,0 @@ -/** - * Copyright © 2016-2025 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.dao.sql.cf; - -import lombok.AllArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.springframework.data.jpa.repository.JpaRepository; -import org.springframework.stereotype.Component; -import org.thingsboard.server.common.data.EntityType; -import org.thingsboard.server.common.data.cf.CalculatedFieldLink; -import org.thingsboard.server.common.data.id.CalculatedFieldId; -import org.thingsboard.server.common.data.id.EntityId; -import org.thingsboard.server.common.data.id.TenantId; -import org.thingsboard.server.common.data.page.PageData; -import org.thingsboard.server.common.data.page.PageLink; -import org.thingsboard.server.dao.DaoUtil; -import org.thingsboard.server.dao.cf.CalculatedFieldLinkDao; -import org.thingsboard.server.dao.model.sql.CalculatedFieldLinkEntity; -import org.thingsboard.server.dao.sql.JpaAbstractDao; -import org.thingsboard.server.dao.util.SqlDao; - -import java.util.List; -import java.util.UUID; - -@Slf4j -@Component -@AllArgsConstructor -@SqlDao -public class JpaCalculatedFieldLinkDao extends JpaAbstractDao implements CalculatedFieldLinkDao { - - private final CalculatedFieldLinkRepository calculatedFieldLinkRepository; - private final NativeCalculatedFieldRepository nativeCalculatedFieldRepository; - - @Override - public List findCalculatedFieldLinksByCalculatedFieldId(TenantId tenantId, CalculatedFieldId calculatedFieldId) { - return DaoUtil.convertDataList(calculatedFieldLinkRepository.findAllByTenantIdAndCalculatedFieldId(tenantId.getId(), calculatedFieldId.getId())); - } - - @Override - public List findCalculatedFieldLinksByEntityId(TenantId tenantId, EntityId entityId) { - return DaoUtil.convertDataList(calculatedFieldLinkRepository.findAllByTenantIdAndEntityId(tenantId.getId(), entityId.getId())); - } - - @Override - public List findCalculatedFieldLinksByTenantId(TenantId tenantId) { - return DaoUtil.convertDataList(calculatedFieldLinkRepository.findAllByTenantId(tenantId.getId())); - } - - @Override - public List findAll() { - return DaoUtil.convertDataList(calculatedFieldLinkRepository.findAll()); - } - - @Override - public PageData findAll(PageLink pageLink) { - log.debug("Try to find calculated field links by pageLink [{}]", pageLink); - return nativeCalculatedFieldRepository.findCalculatedFieldLinks(DaoUtil.toPageable(pageLink)); - } - - @Override - public PageData findAllByTenantId(TenantId tenantId, PageLink pageLink) { - log.debug("Try to find calculated field links by tenantId [{}], pageLink [{}]", tenantId, pageLink); - return DaoUtil.toPageData(calculatedFieldLinkRepository.findAllByTenantId(tenantId.getId(), DaoUtil.toPageable(pageLink))); - } - - @Override - protected Class getEntityClass() { - return CalculatedFieldLinkEntity.class; - } - - @Override - protected JpaRepository getRepository() { - return calculatedFieldLinkRepository; - } - - @Override - public EntityType getEntityType() { - return EntityType.CALCULATED_FIELD_LINK; - } - -} diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/cf/NativeCalculatedFieldRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/cf/NativeCalculatedFieldRepository.java index f37a5764a0..7cc3507076 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/cf/NativeCalculatedFieldRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/cf/NativeCalculatedFieldRepository.java @@ -17,13 +17,10 @@ package org.thingsboard.server.dao.sql.cf; import org.springframework.data.domain.Pageable; import org.thingsboard.server.common.data.cf.CalculatedField; -import org.thingsboard.server.common.data.cf.CalculatedFieldLink; import org.thingsboard.server.common.data.page.PageData; public interface NativeCalculatedFieldRepository { PageData findCalculatedFields(Pageable pageable); - PageData findCalculatedFieldLinks(Pageable pageable); - } diff --git a/dao/src/main/resources/sql/schema-entities.sql b/dao/src/main/resources/sql/schema-entities.sql index 4ccc5c9a2b..4722d4dafa 100644 --- a/dao/src/main/resources/sql/schema-entities.sql +++ b/dao/src/main/resources/sql/schema-entities.sql @@ -926,16 +926,6 @@ CREATE TABLE IF NOT EXISTS calculated_field ( CONSTRAINT calculated_field_unq_key UNIQUE (entity_id, type, name) ); -CREATE TABLE IF NOT EXISTS calculated_field_link ( - id uuid NOT NULL CONSTRAINT calculated_field_link_pkey PRIMARY KEY, - created_time bigint NOT NULL, - tenant_id uuid NOT NULL, - entity_type VARCHAR(32), - entity_id uuid NOT NULL, - calculated_field_id uuid NOT NULL, - CONSTRAINT fk_calculated_field_id FOREIGN KEY (calculated_field_id) REFERENCES calculated_field(id) ON DELETE CASCADE -); - CREATE TABLE IF NOT EXISTS cf_debug_event ( id uuid NOT NULL, tenant_id uuid NOT NULL , diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/CalculatedFieldServiceTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/CalculatedFieldServiceTest.java index 0e20f188b1..c904852fbe 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/service/CalculatedFieldServiceTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/service/CalculatedFieldServiceTest.java @@ -15,13 +15,8 @@ */ package org.thingsboard.server.dao.service; -import com.google.common.util.concurrent.ListeningExecutorService; -import com.google.common.util.concurrent.MoreExecutors; -import org.junit.After; -import org.junit.Before; import org.junit.Test; import org.springframework.beans.factory.annotation.Autowired; -import org.thingsboard.common.util.ThingsBoardExecutors; import org.thingsboard.server.common.data.Device; import org.thingsboard.server.common.data.cf.CalculatedField; import org.thingsboard.server.common.data.cf.CalculatedFieldType; @@ -64,18 +59,6 @@ public class CalculatedFieldServiceTest extends AbstractServiceTest { @Autowired private TbTenantProfileCache tbTenantProfileCache; - private ListeningExecutorService executor; - - @Before - public void before() { - executor = MoreExecutors.listeningDecorator(ThingsBoardExecutors.newWorkStealingPool(8, getClass())); - } - - @After - public void after() { - executor.shutdownNow(); - } - @Test public void testSaveCalculatedField() { Device device = createTestDevice(); @@ -144,7 +127,7 @@ public class CalculatedFieldServiceTest extends AbstractServiceTest { .isInstanceOf(DataValidationException.class) .hasCauseInstanceOf(IllegalArgumentException.class) .hasMessageStartingWith("Scheduled update interval is less than configured " + - "minimum allowed interval in tenant profile: "); + "minimum allowed interval in tenant profile: "); } @Test @@ -163,7 +146,7 @@ public class CalculatedFieldServiceTest extends AbstractServiceTest { .getMaxRelationLevelPerCfArgument(); // Zone-group argument (ATTRIBUTE) - ZoneGroupConfiguration zoneGroupConfiguration = new ZoneGroupConfiguration( "allowed", REPORT_TRANSITION_EVENTS_AND_PRESENCE_STATUS, false); + ZoneGroupConfiguration zoneGroupConfiguration = new ZoneGroupConfiguration("allowed", REPORT_TRANSITION_EVENTS_AND_PRESENCE_STATUS, false); var dynamicSourceConfiguration = new RelationPathQueryDynamicSourceConfiguration(); List levels = new ArrayList<>(); @@ -203,7 +186,7 @@ public class CalculatedFieldServiceTest extends AbstractServiceTest { cfg.setEntityCoordinates(entityCoordinates); // Zone-group argument (ATTRIBUTE) — make it DYNAMIC so scheduling is enabled - ZoneGroupConfiguration zoneGroupConfiguration = new ZoneGroupConfiguration( "allowed", REPORT_TRANSITION_EVENTS_AND_PRESENCE_STATUS, false); + ZoneGroupConfiguration zoneGroupConfiguration = new ZoneGroupConfiguration("allowed", REPORT_TRANSITION_EVENTS_AND_PRESENCE_STATUS, false); var dynamicSourceConfiguration = new RelationPathQueryDynamicSourceConfiguration(); dynamicSourceConfiguration.setLevels(List.of(new RelationPathLevel(EntitySearchDirection.FROM, EntityRelation.CONTAINS_TYPE))); zoneGroupConfiguration.setRefDynamicSourceConfiguration(dynamicSourceConfiguration); diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/EntityServiceRegistryTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/EntityServiceRegistryTest.java index 9503f4e23f..0e7797ce56 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/service/EntityServiceRegistryTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/service/EntityServiceRegistryTest.java @@ -20,7 +20,6 @@ import org.junit.Assert; import org.junit.Test; import org.springframework.beans.factory.annotation.Autowired; import org.thingsboard.server.common.data.EntityType; -import org.thingsboard.server.dao.cf.CalculatedFieldService; import org.thingsboard.server.dao.entity.EntityDaoService; import org.thingsboard.server.dao.entity.EntityServiceRegistry; import org.thingsboard.server.dao.rule.RuleChainService; @@ -45,8 +44,4 @@ public class EntityServiceRegistryTest extends AbstractServiceTest { Assert.assertTrue(entityServiceRegistry.getServiceByEntityType(EntityType.RULE_NODE) instanceof RuleChainService); } - @Test - public void givenCalculatedFieldLinkEntityType_whenGetServiceByEntityTypeCalled_thenReturnedCalculatedFieldService() { - Assert.assertTrue(entityServiceRegistry.getServiceByEntityType(EntityType.CALCULATED_FIELD_LINK) instanceof CalculatedFieldService); - } } diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/validator/CalculatedFieldDataValidatorTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/validator/CalculatedFieldDataValidatorTest.java index 43fb44431e..0d52f999de 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/service/validator/CalculatedFieldDataValidatorTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/service/validator/CalculatedFieldDataValidatorTest.java @@ -17,8 +17,8 @@ package org.thingsboard.server.dao.service.validator; import org.junit.jupiter.api.Test; import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.boot.test.mock.mockito.MockBean; -import org.springframework.boot.test.mock.mockito.SpyBean; +import org.springframework.test.context.bean.override.mockito.MockitoBean; +import org.springframework.test.context.bean.override.mockito.MockitoSpyBean; import org.thingsboard.server.common.data.cf.CalculatedField; import org.thingsboard.server.common.data.cf.CalculatedFieldType; import org.thingsboard.server.common.data.id.CalculatedFieldId; @@ -38,11 +38,11 @@ public class CalculatedFieldDataValidatorTest { private final TenantId TENANT_ID = TenantId.fromUUID(UUID.fromString("7b5229e9-166e-41a9-a257-3b1dafad1b04")); private final CalculatedFieldId CALCULATED_FIELD_ID = new CalculatedFieldId(UUID.fromString("060fbe45-fbb2-4549-abf3-f72a6be3cb9f")); - @MockBean + @MockitoBean private CalculatedFieldDao calculatedFieldDao; - @MockBean + @MockitoBean private DefaultApiLimitService apiLimitService; - @SpyBean + @MockitoSpyBean private CalculatedFieldDataValidator validator; @Test diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/validator/CalculatedFieldLinkDataValidatorTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/validator/CalculatedFieldLinkDataValidatorTest.java deleted file mode 100644 index c477498602..0000000000 --- a/dao/src/test/java/org/thingsboard/server/dao/service/validator/CalculatedFieldLinkDataValidatorTest.java +++ /dev/null @@ -1,57 +0,0 @@ -/** - * Copyright © 2016-2025 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.dao.service.validator; - -import org.junit.jupiter.api.Test; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.boot.test.mock.mockito.MockBean; -import org.springframework.boot.test.mock.mockito.SpyBean; -import org.thingsboard.server.common.data.cf.CalculatedFieldLink; -import org.thingsboard.server.common.data.id.CalculatedFieldId; -import org.thingsboard.server.common.data.id.CalculatedFieldLinkId; -import org.thingsboard.server.common.data.id.TenantId; -import org.thingsboard.server.dao.cf.CalculatedFieldLinkDao; -import org.thingsboard.server.dao.exception.DataValidationException; - -import java.util.UUID; - -import static org.assertj.core.api.Assertions.assertThatThrownBy; -import static org.mockito.BDDMockito.given; - -@SpringBootTest(classes = CalculatedFieldLinkDataValidator.class) -public class CalculatedFieldLinkDataValidatorTest { - - private final TenantId TENANT_ID = TenantId.fromUUID(UUID.fromString("2ba09d99-6143-43dc-b645-381fc0c43ebe")); - private final CalculatedFieldLinkId CALCULATED_FIELD_LINK_ID = new CalculatedFieldLinkId(UUID.fromString("a5609ef4-cb42-43ce-9b23-e090a4878d1c")); - - @MockBean - private CalculatedFieldLinkDao calculatedFieldLinkDao; - @SpyBean - private CalculatedFieldLinkDataValidator validator; - - @Test - public void testUpdateNonExistingCalculatedField() { - CalculatedFieldLink calculatedFieldLink = new CalculatedFieldLink(CALCULATED_FIELD_LINK_ID); - calculatedFieldLink.setCalculatedFieldId(new CalculatedFieldId(UUID.fromString("136477af-fd07-4498-b9c9-54fe50e82992"))); - - given(calculatedFieldLinkDao.findById(TENANT_ID, CALCULATED_FIELD_LINK_ID.getId())).willReturn(null); - - assertThatThrownBy(() -> validator.validateUpdate(TENANT_ID, calculatedFieldLink)) - .isInstanceOf(DataValidationException.class) - .hasMessage("Can't update non existing calculated field link!"); - } - -} diff --git a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/cf/CalculatedFieldTest.java b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/cf/CalculatedFieldTest.java index 68c2c9462f..9c043eee8d 100644 --- a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/cf/CalculatedFieldTest.java +++ b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/cf/CalculatedFieldTest.java @@ -17,7 +17,7 @@ package org.thingsboard.server.msa.cf; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.node.ArrayNode; -import org.testcontainers.shaded.org.apache.commons.lang3.RandomStringUtils; +import org.apache.commons.lang3.RandomStringUtils; import org.testng.annotations.AfterClass; import org.testng.annotations.BeforeClass; import org.testng.annotations.BeforeMethod; @@ -600,7 +600,7 @@ public class CalculatedFieldTest extends AbstractContainerTest { CalculatedField calculatedField = new CalculatedField(); calculatedField.setEntityId(entityId); calculatedField.setType(CalculatedFieldType.SIMPLE); - calculatedField.setName("C to F" + RandomStringUtils.randomAlphabetic(5)); + calculatedField.setName("C to F" + RandomStringUtils.insecure().nextAlphabetic(5)); calculatedField.setDebugSettings(DebugSettings.all()); SimpleCalculatedFieldConfiguration config = new SimpleCalculatedFieldConfiguration(); @@ -624,15 +624,11 @@ public class CalculatedFieldTest extends AbstractContainerTest { return testRestClient.postCalculatedField(calculatedField); } - private CalculatedField createScriptCalculatedField() { - return createScriptCalculatedField(device.getId(), asset.getId()); - } - private CalculatedField createScriptCalculatedField(EntityId entityId, EntityId refEntityId) { CalculatedField calculatedField = new CalculatedField(); calculatedField.setEntityId(entityId); calculatedField.setType(CalculatedFieldType.SCRIPT); - calculatedField.setName("Air density" + RandomStringUtils.randomAlphabetic(5)); + calculatedField.setName("Air density" + RandomStringUtils.insecure().nextAlphabetic(5)); calculatedField.setDebugSettings(DebugSettings.all()); ScriptCalculatedFieldConfiguration config = new ScriptCalculatedFieldConfiguration(); diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/util/TenantIdLoader.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/util/TenantIdLoader.java index 8ea0f70a3f..cd0b11bb25 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/util/TenantIdLoader.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/util/TenantIdLoader.java @@ -18,14 +18,12 @@ package org.thingsboard.rule.engine.util; import org.thingsboard.rule.engine.api.TbContext; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.HasTenantId; -import org.thingsboard.server.common.data.cf.CalculatedFieldLink; import org.thingsboard.server.common.data.id.AiModelId; import org.thingsboard.server.common.data.id.AlarmId; import org.thingsboard.server.common.data.id.ApiUsageStateId; import org.thingsboard.server.common.data.id.AssetId; import org.thingsboard.server.common.data.id.AssetProfileId; import org.thingsboard.server.common.data.id.CalculatedFieldId; -import org.thingsboard.server.common.data.id.CalculatedFieldLinkId; import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.DashboardId; import org.thingsboard.server.common.data.id.DeviceId; @@ -170,14 +168,6 @@ public class TenantIdLoader { case CALCULATED_FIELD: tenantEntity = ctx.getCalculatedFieldService().findById(ctxTenantId, new CalculatedFieldId(id)); break; - case CALCULATED_FIELD_LINK: - CalculatedFieldLink calculatedFieldLink = ctx.getCalculatedFieldService().findCalculatedFieldLinkById(ctxTenantId, new CalculatedFieldLinkId(id)); - if (calculatedFieldLink != null) { - tenantEntity = ctx.getCalculatedFieldService().findById(ctxTenantId, calculatedFieldLink.getCalculatedFieldId()); - } else { - tenantEntity = null; - } - break; case JOB: tenantEntity = ctx.getJobService().findJobById(ctxTenantId, new JobId(id)); break; diff --git a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/util/TenantIdLoaderTest.java b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/util/TenantIdLoaderTest.java index 16698b2841..15d513790d 100644 --- a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/util/TenantIdLoaderTest.java +++ b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/util/TenantIdLoaderTest.java @@ -29,7 +29,6 @@ import org.thingsboard.rule.engine.api.RuleEngineAssetProfileCache; import org.thingsboard.rule.engine.api.RuleEngineDeviceProfileCache; import org.thingsboard.rule.engine.api.RuleEngineRpcService; import org.thingsboard.rule.engine.api.TbContext; -import org.thingsboard.server.common.data.AdminSettings; import org.thingsboard.server.common.data.ApiUsageState; import org.thingsboard.server.common.data.Customer; import org.thingsboard.server.common.data.Dashboard; @@ -46,14 +45,12 @@ import org.thingsboard.server.common.data.alarm.Alarm; import org.thingsboard.server.common.data.asset.Asset; import org.thingsboard.server.common.data.asset.AssetProfile; import org.thingsboard.server.common.data.cf.CalculatedField; -import org.thingsboard.server.common.data.cf.CalculatedFieldLink; import org.thingsboard.server.common.data.domain.Domain; import org.thingsboard.server.common.data.edge.Edge; import org.thingsboard.server.common.data.id.AssetProfileId; import org.thingsboard.server.common.data.id.DeviceProfileId; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.EntityIdFactory; -import org.thingsboard.server.common.data.id.NotificationId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.TenantProfileId; import org.thingsboard.server.common.data.job.Job; @@ -80,6 +77,7 @@ import org.thingsboard.server.dao.device.DeviceService; import org.thingsboard.server.dao.domain.DomainService; import org.thingsboard.server.dao.edge.EdgeService; import org.thingsboard.server.dao.entityview.EntityViewService; +import org.thingsboard.server.dao.job.JobService; import org.thingsboard.server.dao.mobile.MobileAppBundleService; import org.thingsboard.server.dao.mobile.MobileAppService; import org.thingsboard.server.dao.notification.NotificationRequestService; @@ -92,7 +90,6 @@ import org.thingsboard.server.dao.queue.QueueService; import org.thingsboard.server.dao.queue.QueueStatsService; import org.thingsboard.server.dao.resource.ResourceService; import org.thingsboard.server.dao.rule.RuleChainService; -import org.thingsboard.server.dao.job.JobService; import org.thingsboard.server.dao.user.UserService; import org.thingsboard.server.dao.widget.WidgetTypeService; import org.thingsboard.server.dao.widget.WidgetsBundleService; @@ -422,12 +419,6 @@ public class TenantIdLoaderTest { when(ctx.getCalculatedFieldService()).thenReturn(calculatedFieldService); doReturn(calculatedField).when(calculatedFieldService).findById(eq(tenantId), any()); break; - case CALCULATED_FIELD_LINK: - CalculatedFieldLink calculatedFieldLink = new CalculatedFieldLink(); - calculatedFieldLink.setTenantId(tenantId); - when(ctx.getCalculatedFieldService()).thenReturn(calculatedFieldService); - doReturn(calculatedFieldLink).when(calculatedFieldService).findCalculatedFieldLinkById(eq(tenantId), any()); - break; case JOB: Job job = new Job(); job.setTenantId(tenantId); From f911a6d2d9c950d15b29f9bf282c133d35122663 Mon Sep 17 00:00:00 2001 From: ArtemDzhereleiko Date: Tue, 11 Nov 2025 12:53:13 +0200 Subject: [PATCH 17/42] UI: bug fix calculated fileds --- .../alarm-rules/alarm-rules-table-config.ts | 5 ++++- .../alarm-rules/alarm-rules-table.component.ts | 3 +++ ...le-complex-filter-predicate-dialog.component.html | 1 + ...rule-complex-filter-predicate-dialog.component.ts | 1 + .../filter/alarm-rule-filter-dialog.component.html | 1 + .../alarm-rule-filter-predicate-list.component.html | 1 + .../alarm-rule-filter-predicate-list.component.ts | 3 +++ .../alarm-rule-filter-predicate-value.component.html | 2 +- .../alarm-rule-filter-predicate-value.component.ts | 12 ++++++++---- .../alarm-rule-filter-predicate.component.html | 1 + .../filter/alarm-rule-filter-predicate.component.ts | 4 ++++ .../calculated-fields-table-config.ts | 5 ++++- .../calculated-fields-table.component.ts | 3 +++ 13 files changed, 35 insertions(+), 7 deletions(-) diff --git a/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rules-table-config.ts b/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rules-table-config.ts index 2b70d951cb..5167e32853 100644 --- a/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rules-table-config.ts +++ b/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rules-table-config.ts @@ -54,6 +54,7 @@ import { CalculatedFieldDebugDialogData } from "@home/components/calculated-fields/components/debug-dialog/calculated-field-debug-dialog.component"; import { AlarmSeverity, alarmSeverityTranslations } from "@shared/models/alarm.models"; +import { UtilsService } from "@core/services/utils.service"; export class AlarmRulesTableConfig extends EntityTableConfig { @@ -75,6 +76,7 @@ export class AlarmRulesTableConfig extends EntityTableConfig { private ownerId: EntityId = null, private importExportService: ImportExportService, private entityDebugSettingsService: EntityDebugSettingsService, + private utilsService: UtilsService, ) { super(); this.tableTitle = this.translate.instant('alarm-rule.alarm-rules'); @@ -115,7 +117,8 @@ export class AlarmRulesTableConfig extends EntityTableConfig { this.defaultSortOrder = {property: 'createdTime', direction: Direction.DESC}; this.columns.push(new DateEntityTableColumn('createdTime', 'common.created-time', this.datePipe, '150px')); - this.columns.push(new EntityTableColumn('name', 'alarm-rule.alarm-type', '33%')); + this.columns.push(new EntityTableColumn('name', 'alarm-rule.alarm-type', '33%', + entity => this.utilsService.customTranslation(entity.name, entity.name))); this.columns.push(new EntityTableColumn('createRule', 'alarm-rule.severities', '67%', entity => Object.keys(entity.configuration.createRules).map((severity) => this.translate.instant(alarmSeverityTranslations.get(severity as AlarmSeverity))).join(', '), () => ({}), false)); diff --git a/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rules-table.component.ts b/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rules-table.component.ts index 9a553c1ffa..4ade1c5461 100644 --- a/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rules-table.component.ts +++ b/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rules-table.component.ts @@ -35,6 +35,7 @@ import { ImportExportService } from '@shared/import-export/import-export.service import { EntityDebugSettingsService } from '@home/components/entity/debug/entity-debug-settings.service'; import { DatePipe } from '@angular/common'; import { AlarmRulesTableConfig } from "@home/components/alarm-rules/alarm-rules-table-config"; +import { UtilsService } from "@core/services/utils.service"; @Component({ selector: 'tb-alarm-rules-table', @@ -63,6 +64,7 @@ export class AlarmRulesTableComponent { private renderer: Renderer2, private importExportService: ImportExportService, private entityDebugSettingsService: EntityDebugSettingsService, + private utilsService: UtilsService, private destroyRef: DestroyRef) { effect(() => { @@ -80,6 +82,7 @@ export class AlarmRulesTableComponent { this.ownerId(), this.importExportService, this.entityDebugSettingsService, + this.utilsService, ); this.cd.markForCheck(); } diff --git a/ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-complex-filter-predicate-dialog.component.html b/ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-complex-filter-predicate-dialog.component.html index ed5dc4b9cd..75bce37a80 100644 --- a/ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-complex-filter-predicate-dialog.component.html +++ b/ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-complex-filter-predicate-dialog.component.html @@ -35,6 +35,7 @@ diff --git a/ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-complex-filter-predicate-dialog.component.ts b/ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-complex-filter-predicate-dialog.component.ts index c8766b0b17..941ce326e1 100644 --- a/ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-complex-filter-predicate-dialog.component.ts +++ b/ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-complex-filter-predicate-dialog.component.ts @@ -35,6 +35,7 @@ export interface AlarmRuleComplexFilterPredicateDialogData { isAdd: boolean; valueType: EntityKeyValueType; arguments: Record; + argumentInUse: string; } @Component({ diff --git a/ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-dialog.component.html b/ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-dialog.component.html index e622e9e4f8..abc1dd463d 100644 --- a/ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-dialog.component.html +++ b/ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-dialog.component.html @@ -77,6 +77,7 @@ diff --git a/ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-predicate-list.component.html b/ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-predicate-list.component.html index 4eaa1eafb6..b7450262f6 100644 --- a/ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-predicate-list.component.html +++ b/ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-predicate-list.component.html @@ -42,6 +42,7 @@ diff --git a/ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-predicate-list.component.ts b/ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-predicate-list.component.ts index 646cd720fc..ecb52944b2 100644 --- a/ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-predicate-list.component.ts +++ b/ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-predicate-list.component.ts @@ -78,6 +78,8 @@ export class AlarmRuleFilterPredicateListComponent implements ControlValueAccess @Input() arguments: Record; + @Input() argumentInUse: string; + filterListFormGroup = this.fb.group({ predicates: this.fb.array([]) }); @@ -195,6 +197,7 @@ export class AlarmRuleFilterPredicateListComponent implements ControlValueAccess valueType: this.valueType, isAdd: true, arguments: this.arguments, + argumentInUse: this.argumentInUse } }).afterClosed().pipe( map(result => result) diff --git a/ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-predicate-value.component.html b/ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-predicate-value.component.html index c7bcc92012..10294e851a 100644 --- a/ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-predicate-value.component.html +++ b/ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-predicate-value.component.html @@ -55,7 +55,7 @@ @for (argument of argumentsList; track argument) { - {{ argument }} + {{ argument }} } @if (filterPredicateValueFormGroup.get('dynamicValueArgument').touched && filterPredicateValueFormGroup.get('dynamicValueArgument').hasError('required')) { diff --git a/ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-predicate-value.component.ts b/ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-predicate-value.component.ts index 9675ad5fea..036083c826 100644 --- a/ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-predicate-value.component.ts +++ b/ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-predicate-value.component.ts @@ -57,6 +57,9 @@ export class AlarmRuleFilterPredicateValueComponent implements ControlValueAcces @Input() valueType: EntityKeyValueType; + @Input() + argumentInUse: string; + valueTypeEnum = EntityKeyValueType; filterPredicateValueFormGroup: FormGroup>>; @@ -104,7 +107,7 @@ export class AlarmRuleFilterPredicateValueComponent implements ControlValueAcces }); this.dynamicModeControl.valueChanges.pipe( takeUntilDestroyed(this.destroyRef) - ).subscribe(value => this.updateValueModeValidators(value)) + ).subscribe(value => this.updateValueModeValidators(value)); } setDisabledState(isDisabled: boolean): void { @@ -114,16 +117,17 @@ export class AlarmRuleFilterPredicateValueComponent implements ControlValueAcces } else { this.filterPredicateValueFormGroup.enable({emitEvent: false}); this.dynamicModeControl.enable({emitEvent: false}); + this.updateValueModeValidators(this.dynamicModeControl.value); } } private updateValueModeValidators(isDynamicMode: boolean): void { if (isDynamicMode) { this.filterPredicateValueFormGroup.get('staticValue').disable({emitEvent: false}); - this.filterPredicateValueFormGroup.get('dynamicValueArgument').enable({emitEvent: false}); + this.filterPredicateValueFormGroup.get('dynamicValueArgument').enable(); } else { - this.filterPredicateValueFormGroup.get('staticValue').enable({emitEvent: false}); this.filterPredicateValueFormGroup.get('dynamicValueArgument').disable({emitEvent: false}); + this.filterPredicateValueFormGroup.get('staticValue').enable(); } } @@ -142,7 +146,7 @@ export class AlarmRuleFilterPredicateValueComponent implements ControlValueAcces writeValue(predicateValue: AlarmRuleValue): void { this.filterPredicateValueFormGroup.patchValue(predicateValue, {emitEvent: false}); - this.dynamicModeControl.patchValue(!!predicateValue.dynamicValueArgument?.length); + this.dynamicModeControl.patchValue(!!predicateValue.dynamicValueArgument?.length, {emitEvent: false}); } private updateModel() { diff --git a/ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-predicate.component.html b/ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-predicate.component.html index 6e440eac86..d73c222f9d 100644 --- a/ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-predicate.component.html +++ b/ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-predicate.component.html @@ -71,6 +71,7 @@ @if (type !== filterPredicateType.COMPLEX) { diff --git a/ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-predicate.component.ts b/ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-predicate.component.ts index 30c8ec5d2b..b508be0c79 100644 --- a/ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-predicate.component.ts +++ b/ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-predicate.component.ts @@ -70,6 +70,9 @@ export class AlarmRuleFilterPredicateComponent implements ControlValueAccessor, @Input() arguments: Record; + @Input() + argumentInUse: string; + filterPredicateFormGroup = this.fb.group({ operation: [], ignoreCase: false, @@ -146,6 +149,7 @@ export class AlarmRuleFilterPredicateComponent implements ControlValueAccessor, valueType: this.valueType, isAdd: false, arguments: this.arguments, + argumentInUse: this.argumentInUse, } }).afterClosed().subscribe( (result) => { diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/calculated-fields-table-config.ts b/ui-ngx/src/app/modules/home/components/calculated-fields/calculated-fields-table-config.ts index 8cc5d10fc6..0ac64b428f 100644 --- a/ui-ngx/src/app/modules/home/components/calculated-fields/calculated-fields-table-config.ts +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/calculated-fields-table-config.ts @@ -59,6 +59,7 @@ import { ImportExportService } from '@shared/import-export/import-export.service import { isObject } from '@core/utils'; import { EntityDebugSettingsService } from '@home/components/entity/debug/entity-debug-settings.service'; import { DatePipe } from '@angular/common'; +import { UtilsService } from "@core/services/utils.service"; export class CalculatedFieldsTableConfig extends EntityTableConfig { @@ -80,6 +81,7 @@ export class CalculatedFieldsTableConfig extends EntityTableConfig('createdTime', 'common.created-time', this.datePipe, '150px')); - this.columns.push(new EntityTableColumn('name', 'common.name', '33%')); + this.columns.push(new EntityTableColumn('name', 'common.name', '33%', + entity => this.utilsService.customTranslation(entity.name, entity.name))); this.columns.push(new EntityTableColumn('type', 'common.type', '170px', entity => this.translate.instant(CalculatedFieldTypeTranslations.get(entity.type).name), () => ({whiteSpace: 'nowrap' }))); this.columns.push(expressionColumn); diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/calculated-fields-table.component.ts b/ui-ngx/src/app/modules/home/components/calculated-fields/calculated-fields-table.component.ts index ac7fea9454..ee3d013907 100644 --- a/ui-ngx/src/app/modules/home/components/calculated-fields/calculated-fields-table.component.ts +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/calculated-fields-table.component.ts @@ -35,6 +35,7 @@ import { CalculatedFieldsService } from '@core/http/calculated-fields.service'; import { ImportExportService } from '@shared/import-export/import-export.service'; import { EntityDebugSettingsService } from '@home/components/entity/debug/entity-debug-settings.service'; import { DatePipe } from '@angular/common'; +import { UtilsService } from "@core/services/utils.service"; @Component({ selector: 'tb-calculated-fields-table', @@ -63,6 +64,7 @@ export class CalculatedFieldsTableComponent { private renderer: Renderer2, private importExportService: ImportExportService, private entityDebugSettingsService: EntityDebugSettingsService, + private utilsService: UtilsService, private destroyRef: DestroyRef) { effect(() => { @@ -80,6 +82,7 @@ export class CalculatedFieldsTableComponent { this.ownerId(), this.importExportService, this.entityDebugSettingsService, + this.utilsService, ); this.cd.markForCheck(); } From e1ddc5fd0ef484a860d99b5b53ae94a8d7138a98 Mon Sep 17 00:00:00 2001 From: ArtemDzhereleiko Date: Tue, 11 Nov 2025 13:18:28 +0200 Subject: [PATCH 18/42] UI: Custom schedule align to center --- .../components/alarm-rules/cf-alarm-schedule.component.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-schedule.component.html b/ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-schedule.component.html index 13c158e67d..7fe09d7850 100644 --- a/ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-schedule.component.html +++ b/ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-schedule.component.html @@ -90,7 +90,7 @@ -
From 79b5250944d259352922f50d010a16b9b856cf01 Mon Sep 17 00:00:00 2001 From: ArtemDzhereleiko Date: Wed, 12 Nov 2025 09:41:50 +0200 Subject: [PATCH 19/42] UI: Fixed icon color max level warning hint --- ...ulated-field-geofencing-zone-groups-panel.component.scss | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/components/geofencing-configuration/calculated-field-geofencing-zone-groups-panel.component.scss b/ui-ngx/src/app/modules/home/components/calculated-fields/components/geofencing-configuration/calculated-field-geofencing-zone-groups-panel.component.scss index 3768392229..4963e67a2b 100644 --- a/ui-ngx/src/app/modules/home/components/calculated-fields/components/geofencing-configuration/calculated-field-geofencing-zone-groups-panel.component.scss +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/components/geofencing-configuration/calculated-field-geofencing-zone-groups-panel.component.scss @@ -44,6 +44,12 @@ } } + .max-args-warning { + .mat-icon { + color: #FAA405; + } + } + .limit-field-row { @media screen and (max-width: 520px) { display: flex; From 171da3494688f53a909a2287dd2024fd75734f9f Mon Sep 17 00:00:00 2001 From: ArtemDzhereleiko Date: Wed, 12 Nov 2025 10:23:40 +0200 Subject: [PATCH 20/42] UI: Fixed max limit init value and make aliase required --- .../components/widget/lib/cards/api-usage-widget.component.ts | 2 +- .../lib/settings/cards/api-usage-widget-settings.component.ts | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/cards/api-usage-widget.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/cards/api-usage-widget.component.ts index 69f328a006..b7f48d41f6 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/cards/api-usage-widget.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/cards/api-usage-widget.component.ts @@ -94,7 +94,7 @@ export class ApiUsageWidgetComponent implements OnInit, OnDestroy { const progress = data[0][key.maxLimit.key] !== 0 ? Math.min(100, ((data[0][key.current.key] / data[0][key.maxLimit.key]) * 100)) : 0; key.progress = isFinite(progress) ? progress : 0; key.status.value = data[0][key.status.key] ? data[0][key.status.key].toLowerCase() : 'enabled'; - key.maxLimit.value = isFinite(data[0][key.maxLimit.key]) && data[0][key.maxLimit.key] !== 0 ? this.toShortNumber(data[0][key.maxLimit.key]) : '∞'; + key.maxLimit.value = isFinite(data[0][key.maxLimit.key]) && data[0][key.maxLimit.key] !== 0 && data[0][key.maxLimit.key] !== '' ? this.toShortNumber(data[0][key.maxLimit.key]) : '∞'; key.current.value = isFinite(data[0][key.current.key]) ? this.toShortNumber(data[0][key.current.key]) : 0; }); this.cd.detectChanges(); diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/api-usage-widget-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/api-usage-widget-settings.component.ts index 1d42b1503c..3e909b0901 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/api-usage-widget-settings.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/api-usage-widget-settings.component.ts @@ -29,7 +29,7 @@ import { UntypedFormBuilder, UntypedFormGroup, ValidationErrors, - ValidatorFn + ValidatorFn, Validators } from '@angular/forms'; import { Store } from '@ngrx/store'; import { AppState } from '@core/core.state'; @@ -140,7 +140,7 @@ export class ApiUsageWidgetSettingsComponent extends WidgetSettingsComponent { protected onSettingsSet(settings: WidgetSettings) { this.apiUsageWidgetSettingsForm = this.fb.group({ - dsEntityAliasId: [settings?.dsEntityAliasId], + dsEntityAliasId: [settings?.dsEntityAliasId, Validators.required], apiUsageDataKeys: this.prepareDataKeysFormArray(settings?.apiUsageDataKeys), targetDashboardState: [settings?.targetDashboardState], background: [settings?.background, []], From 7f7ab7bff3e7b8eae0e955938190aba7a8ca3707 Mon Sep 17 00:00:00 2001 From: ArtemDzhereleiko Date: Wed, 12 Nov 2025 10:50:04 +0200 Subject: [PATCH 21/42] UI: Fixed expansion panel for force 2fa settings --- .../home/pages/admin/two-factor-auth-settings.component.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui-ngx/src/app/modules/home/pages/admin/two-factor-auth-settings.component.html b/ui-ngx/src/app/modules/home/pages/admin/two-factor-auth-settings.component.html index faf1904427..d1192cace4 100644 --- a/ui-ngx/src/app/modules/home/pages/admin/two-factor-auth-settings.component.html +++ b/ui-ngx/src/app/modules/home/pages/admin/two-factor-auth-settings.component.html @@ -96,7 +96,7 @@ {{ twoFactorAuthProvidersData.get(provider.value.providerType).name | translate }} From a229e450c316177de32d52873246d4f3bb114d42 Mon Sep 17 00:00:00 2001 From: Andrii Landiak Date: Wed, 12 Nov 2025 10:56:32 +0200 Subject: [PATCH 22/42] Fix notification template files --- .../help/en_US/notification/edge_communication_failure.md | 3 +++ ui-ngx/src/assets/help/en_US/notification/edge_connection.md | 3 +++ .../src/assets/help/en_US/notification/resources_shortage.md | 3 +++ .../assets/help/en_US/notification/task_processing_failure.md | 3 +++ 4 files changed, 12 insertions(+) diff --git a/ui-ngx/src/assets/help/en_US/notification/edge_communication_failure.md b/ui-ngx/src/assets/help/en_US/notification/edge_communication_failure.md index 712f2b45e7..0df4732ab4 100644 --- a/ui-ngx/src/assets/help/en_US/notification/edge_communication_failure.md +++ b/ui-ngx/src/assets/help/en_US/notification/edge_communication_failure.md @@ -12,6 +12,9 @@ Available template parameters: * `edgeId` - the edge id as uuid string; * `edgeName` - the name of the edge; * `failureMsg` - the string representation of the failure, occurred on the Edge; +* `recipientEmail` - email of the recipient; +* `recipientFirstName` - first name of the recipient; +* `recipientLastName` - last name of the recipient; Parameter names must be wrapped using `${...}`. For example: `${edgeName}`. You may also modify the value of the parameter with one of the suffixes: diff --git a/ui-ngx/src/assets/help/en_US/notification/edge_connection.md b/ui-ngx/src/assets/help/en_US/notification/edge_connection.md index 37f0ec7573..4c97f6ccde 100644 --- a/ui-ngx/src/assets/help/en_US/notification/edge_connection.md +++ b/ui-ngx/src/assets/help/en_US/notification/edge_connection.md @@ -12,6 +12,9 @@ Available template parameters: * `edgeId` - the edge id as uuid string; * `edgeName` - the name of the edge; * `eventType` - the string representation of the connectivity status: connected or disconnected; +* `recipientEmail` - email of the recipient; +* `recipientFirstName` - first name of the recipient; +* `recipientLastName` - last name of the recipient; Parameter names must be wrapped using `${...}`. For example: `${edgeName}`. You may also modify the value of the parameter with one of the suffixes: diff --git a/ui-ngx/src/assets/help/en_US/notification/resources_shortage.md b/ui-ngx/src/assets/help/en_US/notification/resources_shortage.md index 6ea03a514c..2c0634a5f0 100644 --- a/ui-ngx/src/assets/help/en_US/notification/resources_shortage.md +++ b/ui-ngx/src/assets/help/en_US/notification/resources_shortage.md @@ -13,6 +13,9 @@ Available template parameters: * `usage` - the current usage value of the resource; * `serviceId` - the service id (convenient in cluster setup); * `serviceType` - the service type (convenient in cluster setup); +* `recipientEmail` - email of the recipient; +* `recipientFirstName` - first name of the recipient; +* `recipientLastName` - last name of the recipient; Parameter names must be wrapped using `${...}`. For example: `${resource}`. You may also modify the value of the parameter with one of the suffixes: diff --git a/ui-ngx/src/assets/help/en_US/notification/task_processing_failure.md b/ui-ngx/src/assets/help/en_US/notification/task_processing_failure.md index 0bcf107298..da46dc11bd 100644 --- a/ui-ngx/src/assets/help/en_US/notification/task_processing_failure.md +++ b/ui-ngx/src/assets/help/en_US/notification/task_processing_failure.md @@ -16,6 +16,9 @@ Available template parameters: * `entityType` - the type of the entity to which the task is related; * `entityId` - the id of the entity to which the task is related; * `attempt` - the number of attempts processing the task +* `recipientEmail` - email of the recipient; +* `recipientFirstName` - first name of the recipient; +* `recipientLastName` - last name of the recipient; Parameter names must be wrapped using `${...}`. For example: `${entityType}`. You may also modify the value of the parameter with one of the suffixes: From 31aaf6e8e21e4a3cd7d743a36f4b45b1636be942 Mon Sep 17 00:00:00 2001 From: Andrii Landiak Date: Wed, 12 Nov 2025 11:35:35 +0200 Subject: [PATCH 23/42] Fix potential issue with details init --- .../common/data/notification/info/AlarmNotificationInfo.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/notification/info/AlarmNotificationInfo.java b/common/data/src/main/java/org/thingsboard/server/common/data/notification/info/AlarmNotificationInfo.java index 00040db7aa..5ad33b1e6f 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/notification/info/AlarmNotificationInfo.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/notification/info/AlarmNotificationInfo.java @@ -51,7 +51,7 @@ public class AlarmNotificationInfo implements RuleOriginatedNotificationInfo { @Override public Map getTemplateData() { - Map templateData = new HashMap<>(details); + Map templateData = details != null ? new HashMap<>(details) : new HashMap<>(); templateData.put("alarmType", alarmType); templateData.put("action", action); templateData.put("alarmId", alarmId.toString()); From b815aa02dca875b1293fe7d9dc9cdd127835e772 Mon Sep 17 00:00:00 2001 From: ArtemDzhereleiko Date: Wed, 12 Nov 2025 11:47:32 +0200 Subject: [PATCH 24/42] UI: Remove button to doc --- .../home/components/alarm-rules/alarm-rule-dialog.component.html | 1 - 1 file changed, 1 deletion(-) diff --git a/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rule-dialog.component.html b/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rule-dialog.component.html index 04423edea6..e0f1a052d5 100644 --- a/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rule-dialog.component.html +++ b/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rule-dialog.component.html @@ -19,7 +19,6 @@

{{ 'alarm-rule.alarm-rule' | translate}}

-
- warning - + {{errorText}} this.updateView(value)), @@ -152,7 +156,7 @@ export class StringAutocompleteComponent implements ControlValueAccessor, OnInit updateView(value: string) { this.searchText = value ? value : ''; if (this.modelValue !== value) { - this.modelValue = value; + this.modelValue = value?.trim(); this.propagateChange(this.modelValue); } } From 6d832f519904e5f91751adf2520e18413954c151 Mon Sep 17 00:00:00 2001 From: dashevchenko Date: Wed, 12 Nov 2025 14:17:07 +0200 Subject: [PATCH 26/42] refactoring --- .../server/controller/ControllerConstants.java | 6 ++++-- .../server/controller/TelemetryController.java | 18 +++++++++++------- 2 files changed, 15 insertions(+), 9 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/controller/ControllerConstants.java b/application/src/main/java/org/thingsboard/server/controller/ControllerConstants.java index 276d2f615c..6a48b2bc7e 100644 --- a/application/src/main/java/org/thingsboard/server/controller/ControllerConstants.java +++ b/application/src/main/java/org/thingsboard/server/controller/ControllerConstants.java @@ -1630,11 +1630,13 @@ public class ControllerConstants { protected static final String ENTITY_VIEW_INFO_DESCRIPTION = "Entity Views Info extends the Entity View with customer title and 'is public' flag. " + ENTITY_VIEW_DESCRIPTION; protected static final String ATTRIBUTES_SCOPE_DESCRIPTION = "A string value representing the attributes scope. For example, 'SERVER_SCOPE'."; - protected static final String ATTRIBUTES_KEYS_DESCRIPTION = "A string value representing the comma-separated list of attributes keys. For example, 'active,inactivityAlarmTime'."; + protected static final String ATTRIBUTES_KEYS_DESCRIPTION = "A string value representing the comma-separated list of attributes keys. For example, 'active,inactivityAlarmTime'. " + + "If attribute keys contain commas duplicate 'key' parameter for each key, for example '?key=my,key&key=my,second,key"; protected static final String ATTRIBUTES_JSON_REQUEST_DESCRIPTION = "A string value representing the json object. For example, '{\"key\":\"value\"}'. See API call description for more details."; protected static final String TELEMETRY_KEYS_BASE_DESCRIPTION = "A string value representing the comma-separated list of telemetry keys."; - protected static final String TELEMETRY_KEYS_DESCRIPTION = TELEMETRY_KEYS_BASE_DESCRIPTION + " If keys are not selected, the result will return all latest time series. For example, 'temperature,humidity'."; + protected static final String TELEMETRY_KEYS_DESCRIPTION = TELEMETRY_KEYS_BASE_DESCRIPTION + " If keys are not selected, the result will return all latest time series. For example, 'temperature,humidity'. " + + "If telemetry keys contain commas duplicate 'key' parameter for each key, for example '?key=my,key&key=my,second,key"; protected static final String TELEMETRY_SCOPE_DESCRIPTION = "Value is deprecated, reserved for backward compatibility and not used in the API call implementation. Specify any scope for compatibility"; protected static final String TELEMETRY_JSON_REQUEST_DESCRIPTION = "A JSON with the telemetry values. See API call description for more details."; diff --git a/application/src/main/java/org/thingsboard/server/controller/TelemetryController.java b/application/src/main/java/org/thingsboard/server/controller/TelemetryController.java index 4e4d2256f3..02e3debda8 100644 --- a/application/src/main/java/org/thingsboard/server/controller/TelemetryController.java +++ b/application/src/main/java/org/thingsboard/server/controller/TelemetryController.java @@ -211,7 +211,7 @@ public class TelemetryController extends BaseController { @Parameter(description = ENTITY_ID_PARAM_DESCRIPTION, required = true) @PathVariable("entityId") String entityIdStr, @Parameter(description = ATTRIBUTES_KEYS_DESCRIPTION) @RequestParam(name = "keys", required = false) String keysStr, @RequestParam MultiValueMap params) throws ThingsboardException { - List keys = params.get("key") != null ? params.get("key") : toKeysList(keysStr); + List keys = getKeys(keysStr, params); SecurityUser user = getCurrentUser(); return accessValidator.validateEntityAndCallback(getCurrentUser(), Operation.READ_ATTRIBUTES, entityType, entityIdStr, (result, tenantId, entityId) -> getAttributeValuesCallback(result, user, entityId, null, keys)); @@ -236,7 +236,7 @@ public class TelemetryController extends BaseController { @Parameter(description = ATTRIBUTES_SCOPE_DESCRIPTION, schema = @Schema(allowableValues = {"SERVER_SCOPE", "SHARED_SCOPE", "CLIENT_SCOPE"}, requiredMode = Schema.RequiredMode.REQUIRED)) @PathVariable("scope") AttributeScope scope, @Parameter(description = ATTRIBUTES_KEYS_DESCRIPTION) @RequestParam(name = "keys", required = false) String keysStr, @RequestParam MultiValueMap params) throws ThingsboardException { - List keys = params.get("key") != null ? params.get("key") : toKeysList(keysStr); + List keys = getKeys(keysStr, params); SecurityUser user = getCurrentUser(); return accessValidator.validateEntityAndCallback(getCurrentUser(), Operation.READ_ATTRIBUTES, entityType, entityIdStr, (result, tenantId, entityId) -> getAttributeValuesCallback(result, user, entityId, scope, keys)); @@ -277,7 +277,7 @@ public class TelemetryController extends BaseController { @Parameter(description = STRICT_DATA_TYPES_DESCRIPTION) @RequestParam(name = "useStrictDataTypes", required = false, defaultValue = "false") Boolean useStrictDataTypes, @RequestParam MultiValueMap params) throws ThingsboardException { - List keys = params.get("key") != null ? params.get("key") : toKeysList(keysStr); + List keys = getKeys(keysStr, params); SecurityUser user = getCurrentUser(); return accessValidator.validateEntityAndCallback(getCurrentUser(), Operation.READ_TELEMETRY, entityType, entityIdStr, (result, tenantId, entityId) -> getLatestTimeseriesValuesCallback(result, user, entityId, keys, useStrictDataTypes)); @@ -321,7 +321,7 @@ public class TelemetryController extends BaseController { @Parameter(description = STRICT_DATA_TYPES_DESCRIPTION) @RequestParam(name = "useStrictDataTypes", required = false, defaultValue = "false") Boolean useStrictDataTypes, @RequestParam MultiValueMap params) throws ThingsboardException { - List keys = params.get("key") != null ? params.get("key") : toKeysList(keysStr); + List keys = getKeys(keysStr, params); DeferredResult response = new DeferredResult<>(); Futures.addCallback(tbTelemetryService.getTimeseries(EntityIdFactory.getByTypeAndId(entityType, entityIdStr), keys, startTs, endTs, intervalType, interval, timeZone, limit, Aggregation.valueOf(aggStr), orderBy, useStrictDataTypes, getCurrentUser()), @@ -487,7 +487,7 @@ public class TelemetryController extends BaseController { @Parameter(description = "If the parameter is set to true, the latest telemetry will be rewritten in case that current latest value was removed, otherwise, in case that parameter is set to false the new latest value will not set.") @RequestParam(name = "rewriteLatestIfDeleted", defaultValue = "false") boolean rewriteLatestIfDeleted, @RequestParam MultiValueMap params) throws ThingsboardException { - List keys = params.get("key") != null ? params.get("key") : toKeysList(keysStr); + List keys = getKeys(keysStr, params); EntityId entityId = EntityIdFactory.getByTypeAndId(entityType, entityIdStr); return deleteTimeseries(entityId, keys, deleteAllDataForKeys, startTs, endTs, rewriteLatestIfDeleted, deleteLatest); } @@ -559,7 +559,7 @@ public class TelemetryController extends BaseController { @Parameter(description = ATTRIBUTES_SCOPE_DESCRIPTION, schema = @Schema(allowableValues = {"SERVER_SCOPE", "SHARED_SCOPE", "CLIENT_SCOPE"}, requiredMode = Schema.RequiredMode.REQUIRED)) @PathVariable("scope") AttributeScope scope, @Parameter(description = ATTRIBUTES_KEYS_DESCRIPTION) @RequestParam(name = "keys", required = false) String keysStr, @RequestParam MultiValueMap params) throws ThingsboardException { - List keys = params.get("key") != null ? params.get("key") : toKeysList(keysStr); + List keys = getKeys(keysStr, params); EntityId entityId = EntityIdFactory.getByTypeAndUuid(EntityType.DEVICE, deviceIdStr); return deleteAttributes(entityId, scope, keys); } @@ -584,11 +584,15 @@ public class TelemetryController extends BaseController { @Parameter(description = ATTRIBUTES_SCOPE_DESCRIPTION, required = true, schema = @Schema(allowableValues = {"SERVER_SCOPE", "SHARED_SCOPE", "CLIENT_SCOPE"})) @PathVariable("scope") AttributeScope scope, @Parameter(description = ATTRIBUTES_KEYS_DESCRIPTION) @RequestParam(name = "keys", required = false) String keysStr, @RequestParam MultiValueMap params) throws ThingsboardException { - List keys = params.get("key") != null ? params.get("key") : toKeysList(keysStr); + List keys = getKeys(keysStr, params); EntityId entityId = EntityIdFactory.getByTypeAndId(entityType, entityIdStr); return deleteAttributes(entityId, scope, keys); } + private List getKeys(String keysStr, MultiValueMap params) { + return params.get("key") != null ? params.get("key") : toKeysList(keysStr); + } + private DeferredResult deleteAttributes(EntityId entityIdSrc, AttributeScope scope, List keys) throws ThingsboardException { if (keys.isEmpty()) { return getImmediateDeferredResult("Empty keys: " + keys, HttpStatus.BAD_REQUEST); From f011eec81d63f9602ff566202c99abe63b2bd2f1 Mon Sep 17 00:00:00 2001 From: Viacheslav Klimov Date: Wed, 12 Nov 2025 14:24:30 +0200 Subject: [PATCH 27/42] Apply suggestions from code review --- .../org/thingsboard/server/controller/ControllerConstants.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/application/src/main/java/org/thingsboard/server/controller/ControllerConstants.java b/application/src/main/java/org/thingsboard/server/controller/ControllerConstants.java index 6a48b2bc7e..9b64853a69 100644 --- a/application/src/main/java/org/thingsboard/server/controller/ControllerConstants.java +++ b/application/src/main/java/org/thingsboard/server/controller/ControllerConstants.java @@ -1636,7 +1636,7 @@ public class ControllerConstants { protected static final String TELEMETRY_KEYS_BASE_DESCRIPTION = "A string value representing the comma-separated list of telemetry keys."; protected static final String TELEMETRY_KEYS_DESCRIPTION = TELEMETRY_KEYS_BASE_DESCRIPTION + " If keys are not selected, the result will return all latest time series. For example, 'temperature,humidity'. " + - "If telemetry keys contain commas duplicate 'key' parameter for each key, for example '?key=my,key&key=my,second,key"; + "If telemetry keys contain comma, duplicate 'key' parameter for each key, for example '?key=my,key&key=my,second,key"; protected static final String TELEMETRY_SCOPE_DESCRIPTION = "Value is deprecated, reserved for backward compatibility and not used in the API call implementation. Specify any scope for compatibility"; protected static final String TELEMETRY_JSON_REQUEST_DESCRIPTION = "A JSON with the telemetry values. See API call description for more details."; From 443aff00712f2b712db79fc687e524006e0adb92 Mon Sep 17 00:00:00 2001 From: Viacheslav Klimov Date: Wed, 12 Nov 2025 14:25:17 +0200 Subject: [PATCH 28/42] Update ATTRIBUTES_KEYS_DESCRIPTION --- .../org/thingsboard/server/controller/ControllerConstants.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/application/src/main/java/org/thingsboard/server/controller/ControllerConstants.java b/application/src/main/java/org/thingsboard/server/controller/ControllerConstants.java index 9b64853a69..7dfa616ab5 100644 --- a/application/src/main/java/org/thingsboard/server/controller/ControllerConstants.java +++ b/application/src/main/java/org/thingsboard/server/controller/ControllerConstants.java @@ -1631,7 +1631,7 @@ public class ControllerConstants { protected static final String ATTRIBUTES_SCOPE_DESCRIPTION = "A string value representing the attributes scope. For example, 'SERVER_SCOPE'."; protected static final String ATTRIBUTES_KEYS_DESCRIPTION = "A string value representing the comma-separated list of attributes keys. For example, 'active,inactivityAlarmTime'. " + - "If attribute keys contain commas duplicate 'key' parameter for each key, for example '?key=my,key&key=my,second,key"; + "If attribute keys contain comma, duplicate 'key' parameter for each key, for example '?key=my,key&key=my,second,key"; protected static final String ATTRIBUTES_JSON_REQUEST_DESCRIPTION = "A string value representing the json object. For example, '{\"key\":\"value\"}'. See API call description for more details."; protected static final String TELEMETRY_KEYS_BASE_DESCRIPTION = "A string value representing the comma-separated list of telemetry keys."; From 5b479e7298176181f012da9a427059364cabfffc Mon Sep 17 00:00:00 2001 From: dashevchenko Date: Wed, 12 Nov 2025 14:27:36 +0200 Subject: [PATCH 29/42] test improvements --- .../thingsboard/server/dao/service/AssetServiceTest.java | 6 +++++- .../thingsboard/server/dao/service/DeviceServiceTest.java | 6 +++++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/AssetServiceTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/AssetServiceTest.java index 9ad8b42c21..b7f208b8c7 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/service/AssetServiceTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/service/AssetServiceTest.java @@ -70,6 +70,7 @@ import java.util.Map; import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; +import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.thingsboard.server.dao.model.ModelConstants.NULL_UUID; @@ -134,7 +135,7 @@ public class AssetServiceTest extends AbstractServiceTest { } @Test - public void testAssetLimitOnTenantProfileLevel() { + public void testAssetLimitOnTenantProfileLevel() throws InterruptedException { TenantProfile tenantProfile = new TenantProfile(); tenantProfile.setName("Test profile"); tenantProfile.setDescription("Test"); @@ -161,6 +162,9 @@ public class AssetServiceTest extends AbstractServiceTest { long countByTenantId = assetService.countByTenantId(anotherTenantId); return countByTenantId == 5; }); + + Thread.sleep(2000); + assertThat(assetService.countByTenantId(anotherTenantId)).isEqualTo(5); } @Test diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/DeviceServiceTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/DeviceServiceTest.java index 16bd851641..1ad1ec082a 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/service/DeviceServiceTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/service/DeviceServiceTest.java @@ -83,6 +83,7 @@ import java.util.Map; import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; +import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.mockito.ArgumentMatchers.any; import static org.thingsboard.server.common.data.ota.OtaPackageType.FIRMWARE; @@ -156,7 +157,7 @@ public class DeviceServiceTest extends AbstractServiceTest { } @Test - public void testDeviceLimitOnTenantProfileLevel() { + public void testDeviceLimitOnTenantProfileLevel() throws InterruptedException { TenantProfile defaultTenantProfile = tenantProfileService.findDefaultTenantProfile(tenantId); defaultTenantProfile.getProfileData().setConfiguration(DefaultTenantProfileConfiguration.builder().maxDevices(5l).build()); tenantProfileService.saveTenantProfile(tenantId, defaultTenantProfile); @@ -175,6 +176,9 @@ public class DeviceServiceTest extends AbstractServiceTest { long countByTenantId = deviceService.countByTenantId(tenantId); return countByTenantId == 5; }); + + Thread.sleep(2000); + assertThat(deviceService.countByTenantId(tenantId)).isEqualTo(5); } @Test From 2165162dd599cebf212eb37095e4f594eba3f781 Mon Sep 17 00:00:00 2001 From: Dmytro Skarzhynets Date: Thu, 7 Aug 2025 13:40:09 +0300 Subject: [PATCH 30/42] Device profile node: fix NPE when evaluation dynamic duration rules --- .../rule/engine/profile/DeviceState.java | 42 ++++++++++++++++++- 1 file changed, 40 insertions(+), 2 deletions(-) diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/DeviceState.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/DeviceState.java index 4bd81050db..63db109d64 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/DeviceState.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/DeviceState.java @@ -17,6 +17,7 @@ package org.thingsboard.rule.engine.profile; import com.google.gson.JsonElement; import com.google.gson.JsonParser; +import lombok.SneakyThrows; import lombok.extern.slf4j.Slf4j; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.rule.engine.api.TbContext; @@ -29,9 +30,14 @@ import org.thingsboard.server.common.data.Device; import org.thingsboard.server.common.data.DeviceProfile; import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.alarm.Alarm; +import org.thingsboard.server.common.data.device.profile.AlarmCondition; import org.thingsboard.server.common.data.device.profile.AlarmConditionFilterKey; import org.thingsboard.server.common.data.device.profile.AlarmConditionKeyType; +import org.thingsboard.server.common.data.device.profile.AlarmConditionSpec; +import org.thingsboard.server.common.data.device.profile.AlarmConditionSpecType; +import org.thingsboard.server.common.data.device.profile.AlarmRule; import org.thingsboard.server.common.data.device.profile.DeviceProfileAlarm; +import org.thingsboard.server.common.data.device.profile.DurationAlarmConditionSpec; import org.thingsboard.server.common.data.exception.ApiUsageLimitsExceededException; import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.id.DeviceProfileId; @@ -39,6 +45,8 @@ import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.kv.AttributeKvEntry; import org.thingsboard.server.common.data.kv.KvEntry; import org.thingsboard.server.common.data.kv.TsKvEntry; +import org.thingsboard.server.common.data.query.DynamicValue; +import org.thingsboard.server.common.data.query.DynamicValueSourceType; import org.thingsboard.server.common.data.query.EntityKey; import org.thingsboard.server.common.data.query.EntityKeyType; import org.thingsboard.server.common.data.rule.RuleNodeState; @@ -56,6 +64,7 @@ import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; import java.util.concurrent.ExecutionException; import java.util.stream.Collectors; +import java.util.stream.Stream; import static org.thingsboard.server.common.data.msg.TbMsgType.ACTIVITY_EVENT; import static org.thingsboard.server.common.data.msg.TbMsgType.ALARM_ACK; @@ -87,6 +96,10 @@ class DeviceState { this.deviceId = deviceId; this.deviceProfile = deviceProfile; + if (hasDurationRulesWithDynamicValueFromCurrentDevice(deviceProfile)) { + latestValues = fetchLatestValues(ctx, deviceId); + } + this.dynamicPredicateValueCtx = new DynamicPredicateValueCtxImpl(ctx.getTenantId(), deviceId, ctx); if (config.isPersistAlarmRulesState()) { @@ -116,7 +129,10 @@ class DeviceState { public void updateProfile(TbContext ctx, DeviceProfile deviceProfile) throws ExecutionException, InterruptedException { Set oldKeys = Set.copyOf(this.deviceProfile.getEntityKeys()); this.deviceProfile.updateDeviceProfile(deviceProfile); - if (latestValues != null) { + + if (latestValues == null && hasDurationRulesWithDynamicValueFromCurrentDevice(this.deviceProfile)) { + latestValues = fetchLatestValues(ctx, deviceId); + } else if (latestValues != null) { Set keysToFetch = new HashSet<>(this.deviceProfile.getEntityKeys()); keysToFetch.removeAll(oldKeys); if (!keysToFetch.isEmpty()) { @@ -134,10 +150,31 @@ class DeviceState { } } + private static boolean hasDurationRulesWithDynamicValueFromCurrentDevice(ProfileState deviceProfile) { + return deviceProfile.getAlarmSettings().stream().anyMatch(DeviceState::isDurationRuleWithDynamicValueFromCurrentDevice); + } + + private static boolean isDurationRuleWithDynamicValueFromCurrentDevice(DeviceProfileAlarm alarm) { + return Stream.concat(alarm.getCreateRules().values().stream(), Stream.ofNullable(alarm.getClearRule())) + .map(AlarmRule::getCondition) + .map(AlarmCondition::getSpec) + .anyMatch(spec -> isDurationRule(spec) && hasDynamicDurationValueFromCurrentDevice((DurationAlarmConditionSpec) spec)); + } + + private static boolean isDurationRule(AlarmConditionSpec spec) { + return spec instanceof DurationAlarmConditionSpec durationSpec && durationSpec.getType() == AlarmConditionSpecType.DURATION; + } + + private static boolean hasDynamicDurationValueFromCurrentDevice(DurationAlarmConditionSpec spec) { + DynamicValue dynamicValue = spec.getPredicate().getDynamicValue(); + return dynamicValue != null && dynamicValue.getSourceType() == DynamicValueSourceType.CURRENT_DEVICE; + } + public void harvestAlarms(TbContext ctx, long ts) throws ExecutionException, InterruptedException { log.debug("[{}] Going to harvest alarms: {}", ctx.getSelfId(), ts); boolean stateChanged = false; for (AlarmState state : alarmStates.values()) { + state.setDataSnapshot(latestValues); stateChanged |= state.process(ctx, ts); } if (persistState && stateChanged) { @@ -347,7 +384,8 @@ class DeviceState { return EntityKeyType.ATTRIBUTE; } - private DataSnapshot fetchLatestValues(TbContext ctx, EntityId originator) throws ExecutionException, InterruptedException { + @SneakyThrows + private DataSnapshot fetchLatestValues(TbContext ctx, EntityId originator) { Set entityKeysToFetch = deviceProfile.getEntityKeys(); DataSnapshot result = new DataSnapshot(entityKeysToFetch); addEntityKeysToSnapshot(ctx, originator, entityKeysToFetch, result); From 038332a836fabd935934639f63435519f490278d Mon Sep 17 00:00:00 2001 From: Dmytro Skarzhynets Date: Wed, 3 Sep 2025 16:40:13 +0300 Subject: [PATCH 31/42] Device profile node: add test for NPE if no device activity before dynamic duration rules evaluation --- .../profile/TbDeviceProfileNodeTest.java | 566 +++++++++++------- 1 file changed, 340 insertions(+), 226 deletions(-) diff --git a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/profile/TbDeviceProfileNodeTest.java b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/profile/TbDeviceProfileNodeTest.java index 90c35e59b1..a3d7fffb5d 100644 --- a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/profile/TbDeviceProfileNodeTest.java +++ b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/profile/TbDeviceProfileNodeTest.java @@ -16,8 +16,8 @@ package org.thingsboard.rule.engine.profile; import com.fasterxml.jackson.databind.node.ObjectNode; -import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.junit.jupiter.params.provider.Arguments; @@ -58,8 +58,11 @@ import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.id.DeviceProfileId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.kv.AttributeKvEntry; +import org.thingsboard.server.common.data.kv.BaseAttributeKvEntry; +import org.thingsboard.server.common.data.kv.BasicTsKvEntry; +import org.thingsboard.server.common.data.kv.DoubleDataEntry; +import org.thingsboard.server.common.data.kv.LongDataEntry; import org.thingsboard.server.common.data.kv.TsKvEntry; -import org.thingsboard.server.common.data.kv.TsKvEntryAggWrapper; import org.thingsboard.server.common.data.msg.TbMsgType; import org.thingsboard.server.common.data.query.BooleanFilterPredicate; import org.thingsboard.server.common.data.query.DynamicValue; @@ -82,19 +85,24 @@ import java.math.BigDecimal; import java.math.RoundingMode; import java.util.ArrayList; import java.util.Arrays; -import java.util.Collections; import java.util.List; import java.util.Optional; -import java.util.Set; import java.util.TreeMap; import java.util.UUID; import java.util.concurrent.TimeUnit; import java.util.stream.Stream; +import static com.google.common.util.concurrent.Futures.immediateFuture; +import static java.util.Collections.emptyList; +import static java.util.Collections.singleton; +import static java.util.Collections.singletonList; +import static java.util.Collections.singletonMap; +import static org.assertj.core.api.Assertions.assertThatNoException; import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyLong; import static org.mockito.ArgumentMatchers.anySet; import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.lenient; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import static org.thingsboard.server.common.data.device.profile.AlarmConditionKeyType.ATTRIBUTE; @@ -126,16 +134,26 @@ public class TbDeviceProfileNodeTest extends AbstractRuleNodeUpgradeTest { private final CustomerId customerId = new CustomerId(UUID.randomUUID()); private final DeviceProfileId deviceProfileId = new DeviceProfileId(UUID.randomUUID()); + @BeforeEach + public void setup() { + lenient().when(ctx.getTenantId()).thenReturn(tenantId); + lenient().when(ctx.getDeviceProfileCache()).thenReturn(cache); + lenient().when(ctx.getTimeseriesService()).thenReturn(timeseriesService); + lenient().when(ctx.getAlarmService()).thenReturn(alarmService); + lenient().when(ctx.getDeviceService()).thenReturn(deviceService); + lenient().when(ctx.getAttributesService()).thenReturn(attributesService); + } + @Test public void testRandomMessageType() throws Exception { init(); DeviceProfile deviceProfile = new DeviceProfile(); DeviceProfileData deviceProfileData = new DeviceProfileData(); - deviceProfileData.setAlarms(Collections.emptyList()); + deviceProfileData.setAlarms(emptyList()); deviceProfile.setProfileData(deviceProfileData); - Mockito.when(cache.get(tenantId, deviceId)).thenReturn(deviceProfile); + when(cache.get(tenantId, deviceId)).thenReturn(deviceProfile); ObjectNode data = JacksonUtil.newObjectNode(); data.put("temperature", 42); TbMsg msg = TbMsg.newMsg() @@ -156,10 +174,10 @@ public class TbDeviceProfileNodeTest extends AbstractRuleNodeUpgradeTest { DeviceProfile deviceProfile = new DeviceProfile(); DeviceProfileData deviceProfileData = new DeviceProfileData(); - deviceProfileData.setAlarms(Collections.emptyList()); + deviceProfileData.setAlarms(emptyList()); deviceProfile.setProfileData(deviceProfileData); - Mockito.when(cache.get(tenantId, deviceId)).thenReturn(deviceProfile); + when(cache.get(tenantId, deviceId)).thenReturn(deviceProfile); ObjectNode data = JacksonUtil.newObjectNode(); data.put("temperature", 42); TbMsg msg = TbMsg.newMsg() @@ -187,20 +205,20 @@ public class TbDeviceProfileNodeTest extends AbstractRuleNodeUpgradeTest { DeviceProfileAlarm dpa = new DeviceProfileAlarm(); dpa.setId("highTemperatureAlarmID"); dpa.setAlarmType("highTemperatureAlarm"); - dpa.setCreateRules(new TreeMap<>(Collections.singletonMap(AlarmSeverity.CRITICAL, alarmRule))); + dpa.setCreateRules(new TreeMap<>(singletonMap(AlarmSeverity.CRITICAL, alarmRule))); AlarmRule clearRule = new AlarmRule(); AlarmCondition clearCondition = getNumericAlarmCondition(TIME_SERIES, "temperature", LESS, 10.0); clearRule.setCondition(clearCondition); dpa.setClearRule(clearRule); - deviceProfileData.setAlarms(Collections.singletonList(dpa)); + deviceProfileData.setAlarms(singletonList(dpa)); deviceProfile.setProfileData(deviceProfileData); - Mockito.when(cache.get(tenantId, deviceId)).thenReturn(deviceProfile); - Mockito.when(timeseriesService.findLatest(tenantId, deviceId, Collections.singleton("temperature"))) - .thenReturn(Futures.immediateFuture(Collections.emptyList())); - Mockito.when(alarmService.findLatestActiveByOriginatorAndType(tenantId, deviceId, "highTemperatureAlarm")).thenReturn(null); + when(cache.get(tenantId, deviceId)).thenReturn(deviceProfile); + when(timeseriesService.findLatest(tenantId, deviceId, singleton("temperature"))) + .thenReturn(immediateFuture(emptyList())); + when(alarmService.findLatestActiveByOriginatorAndType(tenantId, deviceId, "highTemperatureAlarm")).thenReturn(null); registerCreateAlarmMock(alarmService.createAlarm(any()), true); TbMsg theMsg = TbMsg.newMsg() @@ -262,7 +280,7 @@ public class TbDeviceProfileNodeTest extends AbstractRuleNodeUpgradeTest { AlarmConditionFilter highTempFilter = getAlarmConditionFilter(TIME_SERIES, "temperature", GREATER, 50.0); AlarmCondition alarmHighTempCondition = new AlarmCondition(); - alarmHighTempCondition.setCondition(Collections.singletonList(highTempFilter)); + alarmHighTempCondition.setCondition(singletonList(highTempFilter)); AlarmRule alarmHighTempRule = new AlarmRule(); alarmHighTempRule.setCondition(alarmHighTempCondition); DeviceProfileAlarm dpa = new DeviceProfileAlarm(); @@ -276,13 +294,13 @@ public class TbDeviceProfileNodeTest extends AbstractRuleNodeUpgradeTest { dpa.setCreateRules(createRules); - deviceProfileData.setAlarms(Collections.singletonList(dpa)); + deviceProfileData.setAlarms(singletonList(dpa)); deviceProfile.setProfileData(deviceProfileData); - Mockito.when(cache.get(tenantId, deviceId)).thenReturn(deviceProfile); - Mockito.when(timeseriesService.findLatest(tenantId, deviceId, Collections.singleton("temperature"))) - .thenReturn(Futures.immediateFuture(Collections.emptyList())); - Mockito.when(alarmService.findLatestActiveByOriginatorAndType(tenantId, deviceId, "highTemperatureAlarm1")).thenReturn(null); + when(cache.get(tenantId, deviceId)).thenReturn(deviceProfile); + when(timeseriesService.findLatest(tenantId, deviceId, singleton("temperature"))) + .thenReturn(immediateFuture(emptyList())); + when(alarmService.findLatestActiveByOriginatorAndType(tenantId, deviceId, "highTemperatureAlarm1")).thenReturn(null); registerCreateAlarmMock(alarmService.createAlarm(any()), true); TbMsg theMsg = TbMsg.newMsg() @@ -366,7 +384,7 @@ public class TbDeviceProfileNodeTest extends AbstractRuleNodeUpgradeTest { attributeKvEntity.setLastUpdateTs(System.currentTimeMillis()); AttributeKvEntry entry = attributeKvEntity.toData(); - ListenableFuture> attrListListenableFuture = Futures.immediateFuture(Collections.singletonList(entry)); + ListenableFuture> attrListListenableFuture = immediateFuture(singletonList(entry)); AlarmConditionFilter alarmEnabledFilter = new AlarmConditionFilter(); alarmEnabledFilter.setKey(new AlarmConditionFilterKey(AlarmConditionKeyType.CONSTANT, "alarmEnabled")); @@ -396,19 +414,19 @@ public class TbDeviceProfileNodeTest extends AbstractRuleNodeUpgradeTest { DeviceProfileAlarm dpa = new DeviceProfileAlarm(); dpa.setId("alarmEnabledAlarmID"); dpa.setAlarmType("alarmEnabledAlarm"); - dpa.setCreateRules(new TreeMap<>(Collections.singletonMap(AlarmSeverity.CRITICAL, alarmRule))); + dpa.setCreateRules(new TreeMap<>(singletonMap(AlarmSeverity.CRITICAL, alarmRule))); - deviceProfileData.setAlarms(Collections.singletonList(dpa)); + deviceProfileData.setAlarms(singletonList(dpa)); deviceProfile.setProfileData(deviceProfileData); - Mockito.when(cache.get(tenantId, deviceId)).thenReturn(deviceProfile); - Mockito.when(timeseriesService.findLatest(tenantId, deviceId, Collections.singleton("temperature"))) - .thenReturn(Futures.immediateFuture(Collections.emptyList())); - Mockito.when(alarmService.findLatestActiveByOriginatorAndType(tenantId, deviceId, "alarmEnabledAlarm")) + when(cache.get(tenantId, deviceId)).thenReturn(deviceProfile); + when(timeseriesService.findLatest(tenantId, deviceId, singleton("temperature"))) + .thenReturn(immediateFuture(emptyList())); + when(alarmService.findLatestActiveByOriginatorAndType(tenantId, deviceId, "alarmEnabledAlarm")) .thenReturn(null); registerCreateAlarmMock(alarmService.createAlarm(any()), true); - Mockito.when(ctx.getAttributesService()).thenReturn(attributesService); - Mockito.when(attributesService.find(eq(tenantId), eq(deviceId), Mockito.any(AttributeScope.class), Mockito.anySet())) + when(ctx.getAttributesService()).thenReturn(attributesService); + when(attributesService.find(eq(tenantId), eq(deviceId), Mockito.any(AttributeScope.class), Mockito.anySet())) .thenReturn(attrListListenableFuture); TbMsg theMsg = TbMsg.newMsg() @@ -417,7 +435,7 @@ public class TbDeviceProfileNodeTest extends AbstractRuleNodeUpgradeTest { .copyMetaData(TbMsgMetaData.EMPTY) .data(TbMsg.EMPTY_STRING) .build(); - Mockito.when(ctx.newMsg(Mockito.any(), Mockito.any(TbMsgType.class), Mockito.any(), Mockito.any(), Mockito.any(), Mockito.anyString())) + when(ctx.newMsg(Mockito.any(), Mockito.any(TbMsgType.class), Mockito.any(), Mockito.any(), Mockito.any(), Mockito.anyString())) .thenReturn(theMsg); ObjectNode data = JacksonUtil.newObjectNode(); @@ -459,7 +477,7 @@ public class TbDeviceProfileNodeTest extends AbstractRuleNodeUpgradeTest { attributeKvEntity.setLastUpdateTs(System.currentTimeMillis()); AttributeKvEntry entry = attributeKvEntity.toData(); - ListenableFuture> attrListListenableFuture = Futures.immediateFuture(Optional.of(entry)); + ListenableFuture> attrListListenableFuture = immediateFuture(Optional.of(entry)); AlarmConditionFilter alarmEnabledFilter = new AlarmConditionFilter(); alarmEnabledFilter.setKey(new AlarmConditionFilterKey(AlarmConditionKeyType.CONSTANT, "alarmEnabled")); @@ -489,24 +507,24 @@ public class TbDeviceProfileNodeTest extends AbstractRuleNodeUpgradeTest { DeviceProfileAlarm dpa = new DeviceProfileAlarm(); dpa.setId("alarmEnabledAlarmID"); dpa.setAlarmType("alarmEnabledAlarm"); - dpa.setCreateRules(new TreeMap<>(Collections.singletonMap(AlarmSeverity.CRITICAL, alarmRule))); + dpa.setCreateRules(new TreeMap<>(singletonMap(AlarmSeverity.CRITICAL, alarmRule))); - deviceProfileData.setAlarms(Collections.singletonList(dpa)); + deviceProfileData.setAlarms(singletonList(dpa)); deviceProfile.setProfileData(deviceProfileData); - Mockito.when(deviceService.findDeviceById(tenantId, deviceId)).thenReturn(device); - Mockito.when(cache.get(tenantId, deviceId)).thenReturn(deviceProfile); - Mockito.when(timeseriesService.findLatest(tenantId, deviceId, Collections.singleton("temperature"))) - .thenReturn(Futures.immediateFuture(Collections.emptyList())); - Mockito.when(alarmService.findLatestActiveByOriginatorAndType(tenantId, deviceId, "alarmEnabledAlarm")) + when(deviceService.findDeviceById(tenantId, deviceId)).thenReturn(device); + when(cache.get(tenantId, deviceId)).thenReturn(deviceProfile); + when(timeseriesService.findLatest(tenantId, deviceId, singleton("temperature"))) + .thenReturn(immediateFuture(emptyList())); + when(alarmService.findLatestActiveByOriginatorAndType(tenantId, deviceId, "alarmEnabledAlarm")) .thenReturn(null); registerCreateAlarmMock(alarmService.createAlarm(any()), true); - Mockito.when(ctx.getAttributesService()).thenReturn(attributesService); - Mockito.when(attributesService.find(eq(tenantId), eq(deviceId), Mockito.any(AttributeScope.class), Mockito.anySet())) - .thenReturn(Futures.immediateFuture(Collections.emptyList())); - Mockito.when(attributesService.find(eq(tenantId), eq(customerId), Mockito.any(AttributeScope.class), Mockito.anyString())) - .thenReturn(Futures.immediateFuture(Optional.empty())); - Mockito.when(attributesService.find(eq(tenantId), eq(tenantId), Mockito.any(AttributeScope.class), Mockito.anyString())) + when(ctx.getAttributesService()).thenReturn(attributesService); + when(attributesService.find(eq(tenantId), eq(deviceId), Mockito.any(AttributeScope.class), Mockito.anySet())) + .thenReturn(immediateFuture(emptyList())); + when(attributesService.find(eq(tenantId), eq(customerId), Mockito.any(AttributeScope.class), Mockito.anyString())) + .thenReturn(immediateFuture(Optional.empty())); + when(attributesService.find(eq(tenantId), eq(tenantId), Mockito.any(AttributeScope.class), Mockito.anyString())) .thenReturn(attrListListenableFuture); TbMsg theMsg = TbMsg.newMsg() @@ -554,7 +572,7 @@ public class TbDeviceProfileNodeTest extends AbstractRuleNodeUpgradeTest { AttributeKvEntry entry = attributeKvEntity.toData(); ListenableFuture> listListenableFutureWithLess = - Futures.immediateFuture(Collections.singletonList(entry)); + immediateFuture(singletonList(entry)); AlarmConditionFilter highTempFilter = new AlarmConditionFilter(); highTempFilter.setKey(new AlarmConditionFilterKey(TIME_SERIES, "temperature")); @@ -568,25 +586,25 @@ public class TbDeviceProfileNodeTest extends AbstractRuleNodeUpgradeTest { )); highTempFilter.setPredicate(highTemperaturePredicate); AlarmCondition alarmCondition = new AlarmCondition(); - alarmCondition.setCondition(Collections.singletonList(highTempFilter)); + alarmCondition.setCondition(singletonList(highTempFilter)); AlarmRule alarmRule = new AlarmRule(); alarmRule.setCondition(alarmCondition); DeviceProfileAlarm dpa = new DeviceProfileAlarm(); dpa.setId("highTemperatureAlarmID"); dpa.setAlarmType("highTemperatureAlarm"); - dpa.setCreateRules(new TreeMap<>(Collections.singletonMap(AlarmSeverity.CRITICAL, alarmRule))); + dpa.setCreateRules(new TreeMap<>(singletonMap(AlarmSeverity.CRITICAL, alarmRule))); - deviceProfileData.setAlarms(Collections.singletonList(dpa)); + deviceProfileData.setAlarms(singletonList(dpa)); deviceProfile.setProfileData(deviceProfileData); - Mockito.when(cache.get(tenantId, deviceId)).thenReturn(deviceProfile); - Mockito.when(timeseriesService.findLatest(tenantId, deviceId, Collections.singleton("temperature"))) - .thenReturn(Futures.immediateFuture(Collections.emptyList())); - Mockito.when(alarmService.findLatestActiveByOriginatorAndType(tenantId, deviceId, "highTemperatureAlarm")) + when(cache.get(tenantId, deviceId)).thenReturn(deviceProfile); + when(timeseriesService.findLatest(tenantId, deviceId, singleton("temperature"))) + .thenReturn(immediateFuture(emptyList())); + when(alarmService.findLatestActiveByOriginatorAndType(tenantId, deviceId, "highTemperatureAlarm")) .thenReturn(null); registerCreateAlarmMock(alarmService.createAlarm(any()), true); - Mockito.when(ctx.getAttributesService()).thenReturn(attributesService); - Mockito.when(attributesService.find(eq(tenantId), eq(deviceId), Mockito.any(AttributeScope.class), Mockito.anySet())) + when(ctx.getAttributesService()).thenReturn(attributesService); + when(attributesService.find(eq(tenantId), eq(deviceId), Mockito.any(AttributeScope.class), Mockito.anySet())) .thenReturn(listListenableFutureWithLess); TbMsg theMsg = TbMsg.newMsg() @@ -648,7 +666,7 @@ public class TbDeviceProfileNodeTest extends AbstractRuleNodeUpgradeTest { AttributeKvEntry alarmDelayAttributeKvEntry = alarmDelayAttributeKvEntity.toData(); ListenableFuture> listListenableFuture = - Futures.immediateFuture(Arrays.asList(entry, alarmDelayAttributeKvEntry)); + immediateFuture(Arrays.asList(entry, alarmDelayAttributeKvEntry)); AlarmConditionFilter highTempFilter = new AlarmConditionFilter(); highTempFilter.setKey(new AlarmConditionFilterKey(TIME_SERIES, "temperature")); @@ -662,7 +680,7 @@ public class TbDeviceProfileNodeTest extends AbstractRuleNodeUpgradeTest { )); highTempFilter.setPredicate(highTemperaturePredicate); AlarmCondition alarmCondition = new AlarmCondition(); - alarmCondition.setCondition(Collections.singletonList(highTempFilter)); + alarmCondition.setCondition(singletonList(highTempFilter)); FilterPredicateValue filterPredicateValue = new FilterPredicateValue<>( 10L, @@ -680,19 +698,19 @@ public class TbDeviceProfileNodeTest extends AbstractRuleNodeUpgradeTest { DeviceProfileAlarm dpa = new DeviceProfileAlarm(); dpa.setId("highTemperatureAlarmID"); dpa.setAlarmType("highTemperatureAlarm"); - dpa.setCreateRules(new TreeMap<>(Collections.singletonMap(AlarmSeverity.CRITICAL, alarmRule))); + dpa.setCreateRules(new TreeMap<>(singletonMap(AlarmSeverity.CRITICAL, alarmRule))); - deviceProfileData.setAlarms(Collections.singletonList(dpa)); + deviceProfileData.setAlarms(singletonList(dpa)); deviceProfile.setProfileData(deviceProfileData); - Mockito.when(cache.get(tenantId, deviceId)).thenReturn(deviceProfile); - Mockito.when(timeseriesService.findLatest(tenantId, deviceId, Collections.singleton("temperature"))) - .thenReturn(Futures.immediateFuture(Collections.emptyList())); - Mockito.when(alarmService.findLatestActiveByOriginatorAndType(tenantId, deviceId, "highTemperatureAlarm")) + when(cache.get(tenantId, deviceId)).thenReturn(deviceProfile); + when(timeseriesService.findLatest(tenantId, deviceId, singleton("temperature"))) + .thenReturn(immediateFuture(emptyList())); + when(alarmService.findLatestActiveByOriginatorAndType(tenantId, deviceId, "highTemperatureAlarm")) .thenReturn(null); registerCreateAlarmMock(alarmService.createAlarm(any()), true); - Mockito.when(ctx.getAttributesService()).thenReturn(attributesService); - Mockito.when(attributesService.find(eq(tenantId), eq(deviceId), Mockito.any(AttributeScope.class), Mockito.anySet())) + when(ctx.getAttributesService()).thenReturn(attributesService); + when(attributesService.find(eq(tenantId), eq(deviceId), Mockito.any(AttributeScope.class), Mockito.anySet())) .thenReturn(listListenableFuture); TbMsg theMsg = TbMsg.newMsg() @@ -740,6 +758,105 @@ public class TbDeviceProfileNodeTest extends AbstractRuleNodeUpgradeTest { verify(ctx, Mockito.never()).tellFailure(Mockito.any(), Mockito.any()); } + @Test + public void testCurrentDeviceAttributeForDynamicDurationValue_noMessagesReceivedFromDeviceBeforeAlarmHarvesting() throws Exception { + init(); + + // 1. Setup device profile that has no alarm rules + var deviceProfileData = new DeviceProfileData(); + deviceProfileData.setAlarms(emptyList()); + + var deviceProfile = new DeviceProfile(deviceProfileId); + deviceProfile.setTenantId(tenantId); + deviceProfile.setName("default"); + deviceProfile.setProfileData(deviceProfileData); + + given(cache.get(tenantId, deviceId)).willReturn(deviceProfile); + + // 2. Initialize device state by sending ENTITY_CREATED event + var device = new Device(deviceId); + device.setTenantId(tenantId); + device.setName("device"); + device.setDeviceProfileId(deviceProfileId); + device.setType("default"); + + var entityCreatedEvent = TbMsg.newMsg() + .type(TbMsgType.ENTITY_CREATED) + .originator(deviceId) + .data(JacksonUtil.toString(device)) + .metaData(TbMsgMetaData.EMPTY) + .build(); + + node.onMsg(ctx, entityCreatedEvent); + + // 3. Update device profile so it now has dynamic duration rule with value taken from current device + var predicate = new NumericFilterPredicate(); + predicate.setOperation(NumericOperation.GREATER_OR_EQUAL); + predicate.setValue(new FilterPredicateValue<>(100.0)); + + var filter = new AlarmConditionFilter(); + filter.setKey(new AlarmConditionFilterKey(AlarmConditionKeyType.TIME_SERIES, "temperature")); + filter.setValueType(EntityKeyValueType.NUMERIC); + filter.setPredicate(predicate); + + var durationSpec = new DurationAlarmConditionSpec(); + durationSpec.setUnit(TimeUnit.SECONDS); + durationSpec.setPredicate( + new FilterPredicateValue<>(10L, null, new DynamicValue<>(DynamicValueSourceType.CURRENT_DEVICE, "duration", false)) + ); + + var alarmCondition = new AlarmCondition(); + alarmCondition.setCondition(singletonList(filter)); + alarmCondition.setSpec(durationSpec); + + var alarmRule = new AlarmRule(); + alarmRule.setCondition(alarmCondition); + + var dpa = new DeviceProfileAlarm(); + dpa.setId("c4486528-84f2-bd72-589e-2f9a60f89c17"); + dpa.setAlarmType("Test alarm"); + dpa.setCreateRules(new TreeMap<>(singletonMap(AlarmSeverity.CRITICAL, alarmRule))); + + deviceProfileData.setAlarms(singletonList(dpa)); + + // 4. Mock DB calls for keys used in alarm rule + given(timeseriesService.findLatest(tenantId, deviceId, singleton("temperature"))).willReturn(immediateFuture( + List.of(new BasicTsKvEntry(123L, new DoubleDataEntry("temperature", 55.6))) + )); + + given(attributesService.find(tenantId, deviceId, AttributeScope.CLIENT_SCOPE, singleton("duration"))).willReturn(immediateFuture(emptyList())); + given(attributesService.find(tenantId, deviceId, AttributeScope.SHARED_SCOPE, singleton("duration"))).willReturn(immediateFuture(emptyList())); + given(attributesService.find(tenantId, deviceId, AttributeScope.SERVER_SCOPE, singleton("duration"))).willReturn(immediateFuture( + List.of(new BaseAttributeKvEntry(123L, new LongDataEntry("duration", 20L))) + )); + + // 5. Send DEVICE_PROFILE_UPDATE_SELF_MSG so alarm state (inside device state) gets initialized + given(cache.get(tenantId, deviceProfileId)).willReturn(deviceProfile); + + var deviceProfileUpdateMsg = TbMsg.newMsg() + .originator(tenantId) + .type(TbMsgType.DEVICE_PROFILE_UPDATE_SELF_MSG) + .data(deviceProfileId.toString()) + .metaData(TbMsgMetaData.EMPTY) + .build(); + + node.onMsg(ctx, deviceProfileUpdateMsg); + + // 6. Not sending anything else to simulate no activity + + // 7. Simulate periodic alarm harvesting by manually sending DEVICE_PROFILE_PERIODIC_SELF_MSG message + var periodicCheck = TbMsg.newMsg() + .type(TbMsgType.DEVICE_PROFILE_PERIODIC_SELF_MSG) + .originator(tenantId) + .customerId(customerId) + .metaData(TbMsgMetaData.EMPTY) + .data(TbMsg.EMPTY_JSON_OBJECT) + .build(); + + // NPE should NOT happen here: dynamic value of duration condition should be correctly resolved + assertThatNoException().isThrownBy(() -> node.onMsg(ctx, periodicCheck)); + } + @Test public void testInheritTenantAttributeForDuration() throws Exception { init(); @@ -779,11 +896,11 @@ public class TbDeviceProfileNodeTest extends AbstractRuleNodeUpgradeTest { AttributeKvEntry alarmDelayAttributeKvEntry = alarmDelayAttributeKvEntity.toData(); ListenableFuture> optionalDurationAttribute = - Futures.immediateFuture(Optional.of(alarmDelayAttributeKvEntry)); + immediateFuture(Optional.of(alarmDelayAttributeKvEntry)); ListenableFuture> listNoDurationAttribute = - Futures.immediateFuture(Collections.singletonList(entry)); + immediateFuture(singletonList(entry)); ListenableFuture> emptyOptional = - Futures.immediateFuture(Optional.empty()); + immediateFuture(Optional.empty()); AlarmConditionFilter highTempFilter = new AlarmConditionFilter(); highTempFilter.setKey(new AlarmConditionFilterKey(TIME_SERIES, "temperature")); @@ -797,7 +914,7 @@ public class TbDeviceProfileNodeTest extends AbstractRuleNodeUpgradeTest { )); highTempFilter.setPredicate(highTemperaturePredicate); AlarmCondition alarmCondition = new AlarmCondition(); - alarmCondition.setCondition(Collections.singletonList(highTempFilter)); + alarmCondition.setCondition(singletonList(highTempFilter)); FilterPredicateValue filterPredicateValue = new FilterPredicateValue<>( 10L, @@ -815,25 +932,25 @@ public class TbDeviceProfileNodeTest extends AbstractRuleNodeUpgradeTest { DeviceProfileAlarm dpa = new DeviceProfileAlarm(); dpa.setId("highTemperatureAlarmID"); dpa.setAlarmType("highTemperatureAlarm"); - dpa.setCreateRules(new TreeMap<>(Collections.singletonMap(AlarmSeverity.CRITICAL, alarmRule))); + dpa.setCreateRules(new TreeMap<>(singletonMap(AlarmSeverity.CRITICAL, alarmRule))); - deviceProfileData.setAlarms(Collections.singletonList(dpa)); + deviceProfileData.setAlarms(singletonList(dpa)); deviceProfile.setProfileData(deviceProfileData); - Mockito.when(cache.get(tenantId, deviceId)).thenReturn(deviceProfile); - Mockito.when(timeseriesService.findLatest(tenantId, deviceId, Collections.singleton("temperature"))) - .thenReturn(Futures.immediateFuture(Collections.emptyList())); - Mockito.when(alarmService.findLatestActiveByOriginatorAndType(tenantId, deviceId, "highTemperatureAlarm")) + when(cache.get(tenantId, deviceId)).thenReturn(deviceProfile); + when(timeseriesService.findLatest(tenantId, deviceId, singleton("temperature"))) + .thenReturn(immediateFuture(emptyList())); + when(alarmService.findLatestActiveByOriginatorAndType(tenantId, deviceId, "highTemperatureAlarm")) .thenReturn(null); registerCreateAlarmMock(alarmService.createAlarm(any()), true); - Mockito.when(ctx.getAttributesService()).thenReturn(attributesService); - Mockito.when(attributesService.find(eq(tenantId), eq(tenantId), Mockito.any(AttributeScope.class), Mockito.anyString())) + when(ctx.getAttributesService()).thenReturn(attributesService); + when(attributesService.find(eq(tenantId), eq(tenantId), Mockito.any(AttributeScope.class), Mockito.anyString())) .thenReturn(optionalDurationAttribute); - Mockito.when(ctx.getDeviceService().findDeviceById(tenantId, deviceId)) + when(ctx.getDeviceService().findDeviceById(tenantId, deviceId)) .thenReturn(device); - Mockito.when(attributesService.find(eq(tenantId), eq(customerId), eq(AttributeScope.SERVER_SCOPE), Mockito.anyString())) + when(attributesService.find(eq(tenantId), eq(customerId), eq(AttributeScope.SERVER_SCOPE), Mockito.anyString())) .thenReturn(emptyOptional); - Mockito.when(attributesService.find(eq(tenantId), eq(deviceId), Mockito.any(AttributeScope.class), Mockito.anySet())) + when(attributesService.find(eq(tenantId), eq(deviceId), Mockito.any(AttributeScope.class), Mockito.anySet())) .thenReturn(listNoDurationAttribute); TbMsg theMsg = TbMsg.newMsg() @@ -915,7 +1032,7 @@ public class TbDeviceProfileNodeTest extends AbstractRuleNodeUpgradeTest { AttributeKvEntry alarmDelayAttributeKvEntry = alarmDelayAttributeKvEntity.toData(); ListenableFuture> listListenableFuture = - Futures.immediateFuture(Arrays.asList(entry, alarmDelayAttributeKvEntry)); + immediateFuture(Arrays.asList(entry, alarmDelayAttributeKvEntry)); AlarmConditionFilter highTempFilter = new AlarmConditionFilter(); highTempFilter.setKey(new AlarmConditionFilterKey(TIME_SERIES, "temperature")); @@ -929,7 +1046,7 @@ public class TbDeviceProfileNodeTest extends AbstractRuleNodeUpgradeTest { )); highTempFilter.setPredicate(highTemperaturePredicate); AlarmCondition alarmCondition = new AlarmCondition(); - alarmCondition.setCondition(Collections.singletonList(highTempFilter)); + alarmCondition.setCondition(singletonList(highTempFilter)); FilterPredicateValue filterPredicateValue = new FilterPredicateValue<>( 10, @@ -947,19 +1064,19 @@ public class TbDeviceProfileNodeTest extends AbstractRuleNodeUpgradeTest { DeviceProfileAlarm dpa = new DeviceProfileAlarm(); dpa.setId("highTemperatureAlarmID"); dpa.setAlarmType("highTemperatureAlarm"); - dpa.setCreateRules(new TreeMap<>(Collections.singletonMap(AlarmSeverity.CRITICAL, alarmRule))); + dpa.setCreateRules(new TreeMap<>(singletonMap(AlarmSeverity.CRITICAL, alarmRule))); - deviceProfileData.setAlarms(Collections.singletonList(dpa)); + deviceProfileData.setAlarms(singletonList(dpa)); deviceProfile.setProfileData(deviceProfileData); - Mockito.when(cache.get(tenantId, deviceId)).thenReturn(deviceProfile); - Mockito.when(timeseriesService.findLatest(tenantId, deviceId, Collections.singleton("temperature"))) - .thenReturn(Futures.immediateFuture(Collections.emptyList())); - Mockito.when(alarmService.findLatestActiveByOriginatorAndType(tenantId, deviceId, "highTemperatureAlarm")) + when(cache.get(tenantId, deviceId)).thenReturn(deviceProfile); + when(timeseriesService.findLatest(tenantId, deviceId, singleton("temperature"))) + .thenReturn(immediateFuture(emptyList())); + when(alarmService.findLatestActiveByOriginatorAndType(tenantId, deviceId, "highTemperatureAlarm")) .thenReturn(null); registerCreateAlarmMock(alarmService.createAlarm(any()), true); - Mockito.when(ctx.getAttributesService()).thenReturn(attributesService); - Mockito.when(attributesService.find(eq(tenantId), eq(deviceId), Mockito.any(AttributeScope.class), Mockito.anySet())) + when(ctx.getAttributesService()).thenReturn(attributesService); + when(attributesService.find(eq(tenantId), eq(deviceId), Mockito.any(AttributeScope.class), Mockito.anySet())) .thenReturn(listListenableFuture); TbMsg theMsg = TbMsg.newMsg() @@ -1039,11 +1156,11 @@ public class TbDeviceProfileNodeTest extends AbstractRuleNodeUpgradeTest { AttributeKvEntry alarmDelayAttributeKvEntry = alarmDelayAttributeKvEntity.toData(); ListenableFuture> optionalDurationAttribute = - Futures.immediateFuture(Optional.of(alarmDelayAttributeKvEntry)); + immediateFuture(Optional.of(alarmDelayAttributeKvEntry)); ListenableFuture> listNoDurationAttribute = - Futures.immediateFuture(Collections.singletonList(entry)); + immediateFuture(singletonList(entry)); ListenableFuture> emptyOptional = - Futures.immediateFuture(Optional.empty()); + immediateFuture(Optional.empty()); AlarmConditionFilter highTempFilter = new AlarmConditionFilter(); highTempFilter.setKey(new AlarmConditionFilterKey(TIME_SERIES, "temperature")); @@ -1057,7 +1174,7 @@ public class TbDeviceProfileNodeTest extends AbstractRuleNodeUpgradeTest { )); highTempFilter.setPredicate(highTemperaturePredicate); AlarmCondition alarmCondition = new AlarmCondition(); - alarmCondition.setCondition(Collections.singletonList(highTempFilter)); + alarmCondition.setCondition(singletonList(highTempFilter)); FilterPredicateValue filterPredicateValue = new FilterPredicateValue<>( 10, @@ -1074,25 +1191,25 @@ public class TbDeviceProfileNodeTest extends AbstractRuleNodeUpgradeTest { DeviceProfileAlarm dpa = new DeviceProfileAlarm(); dpa.setId("highTemperatureAlarmID"); dpa.setAlarmType("highTemperatureAlarm"); - dpa.setCreateRules(new TreeMap<>(Collections.singletonMap(AlarmSeverity.CRITICAL, alarmRule))); + dpa.setCreateRules(new TreeMap<>(singletonMap(AlarmSeverity.CRITICAL, alarmRule))); - deviceProfileData.setAlarms(Collections.singletonList(dpa)); + deviceProfileData.setAlarms(singletonList(dpa)); deviceProfile.setProfileData(deviceProfileData); - Mockito.when(cache.get(tenantId, deviceId)).thenReturn(deviceProfile); - Mockito.when(timeseriesService.findLatest(tenantId, deviceId, Collections.singleton("temperature"))) - .thenReturn(Futures.immediateFuture(Collections.emptyList())); - Mockito.when(alarmService.findLatestActiveByOriginatorAndType(tenantId, deviceId, "highTemperatureAlarm")) + when(cache.get(tenantId, deviceId)).thenReturn(deviceProfile); + when(timeseriesService.findLatest(tenantId, deviceId, singleton("temperature"))) + .thenReturn(immediateFuture(emptyList())); + when(alarmService.findLatestActiveByOriginatorAndType(tenantId, deviceId, "highTemperatureAlarm")) .thenReturn(null); registerCreateAlarmMock(alarmService.createAlarm(any()), true); - Mockito.when(ctx.getAttributesService()).thenReturn(attributesService); - Mockito.when(attributesService.find(eq(tenantId), eq(tenantId), Mockito.any(AttributeScope.class), Mockito.anyString())) + when(ctx.getAttributesService()).thenReturn(attributesService); + when(attributesService.find(eq(tenantId), eq(tenantId), Mockito.any(AttributeScope.class), Mockito.anyString())) .thenReturn(optionalDurationAttribute); - Mockito.when(ctx.getDeviceService().findDeviceById(tenantId, deviceId)) + when(ctx.getDeviceService().findDeviceById(tenantId, deviceId)) .thenReturn(device); - Mockito.when(attributesService.find(eq(tenantId), eq(customerId), eq(AttributeScope.SERVER_SCOPE), Mockito.anyString())) + when(attributesService.find(eq(tenantId), eq(customerId), eq(AttributeScope.SERVER_SCOPE), Mockito.anyString())) .thenReturn(emptyOptional); - Mockito.when(attributesService.find(eq(tenantId), eq(deviceId), Mockito.any(AttributeScope.class), Mockito.anySet())) + when(attributesService.find(eq(tenantId), eq(deviceId), Mockito.any(AttributeScope.class), Mockito.anySet())) .thenReturn(listNoDurationAttribute); TbMsg theMsg = TbMsg.newMsg() @@ -1160,7 +1277,7 @@ public class TbDeviceProfileNodeTest extends AbstractRuleNodeUpgradeTest { AttributeKvEntry entry = attributeKvEntity.toData(); ListenableFuture> listListenableFuture = - Futures.immediateFuture(Collections.singletonList(entry)); + immediateFuture(singletonList(entry)); AlarmConditionFilter highTempFilter = new AlarmConditionFilter(); highTempFilter.setKey(new AlarmConditionFilterKey(TIME_SERIES, "temperature")); @@ -1174,7 +1291,7 @@ public class TbDeviceProfileNodeTest extends AbstractRuleNodeUpgradeTest { )); highTempFilter.setPredicate(highTemperaturePredicate); AlarmCondition alarmCondition = new AlarmCondition(); - alarmCondition.setCondition(Collections.singletonList(highTempFilter)); + alarmCondition.setCondition(singletonList(highTempFilter)); FilterPredicateValue filterPredicateValue = new FilterPredicateValue<>( alarmDelayInSeconds, @@ -1192,19 +1309,19 @@ public class TbDeviceProfileNodeTest extends AbstractRuleNodeUpgradeTest { DeviceProfileAlarm dpa = new DeviceProfileAlarm(); dpa.setId("highTemperatureAlarmID"); dpa.setAlarmType("highTemperatureAlarm"); - dpa.setCreateRules(new TreeMap<>(Collections.singletonMap(AlarmSeverity.CRITICAL, alarmRule))); + dpa.setCreateRules(new TreeMap<>(singletonMap(AlarmSeverity.CRITICAL, alarmRule))); - deviceProfileData.setAlarms(Collections.singletonList(dpa)); + deviceProfileData.setAlarms(singletonList(dpa)); deviceProfile.setProfileData(deviceProfileData); - Mockito.when(cache.get(tenantId, deviceId)).thenReturn(deviceProfile); - Mockito.when(timeseriesService.findLatest(tenantId, deviceId, Collections.singleton("temperature"))) - .thenReturn(Futures.immediateFuture(Collections.emptyList())); - Mockito.when(alarmService.findLatestActiveByOriginatorAndType(tenantId, deviceId, "highTemperatureAlarm")) + when(cache.get(tenantId, deviceId)).thenReturn(deviceProfile); + when(timeseriesService.findLatest(tenantId, deviceId, singleton("temperature"))) + .thenReturn(immediateFuture(emptyList())); + when(alarmService.findLatestActiveByOriginatorAndType(tenantId, deviceId, "highTemperatureAlarm")) .thenReturn(null); registerCreateAlarmMock(alarmService.createAlarm(any()), true); - Mockito.when(ctx.getAttributesService()).thenReturn(attributesService); - Mockito.when(attributesService.find(eq(tenantId), eq(deviceId), Mockito.any(AttributeScope.class), Mockito.anySet())) + when(ctx.getAttributesService()).thenReturn(attributesService); + when(attributesService.find(eq(tenantId), eq(deviceId), Mockito.any(AttributeScope.class), Mockito.anySet())) .thenReturn(listListenableFuture); TbMsg theMsg = TbMsg.newMsg() @@ -1277,7 +1394,7 @@ public class TbDeviceProfileNodeTest extends AbstractRuleNodeUpgradeTest { AttributeKvEntry entry = attributeKvEntity.toData(); ListenableFuture> listListenableFuture = - Futures.immediateFuture(Collections.singletonList(entry)); + immediateFuture(singletonList(entry)); AlarmConditionFilter highTempFilter = new AlarmConditionFilter(); highTempFilter.setKey(new AlarmConditionFilterKey(TIME_SERIES, "temperature")); @@ -1291,7 +1408,7 @@ public class TbDeviceProfileNodeTest extends AbstractRuleNodeUpgradeTest { )); highTempFilter.setPredicate(highTemperaturePredicate); AlarmCondition alarmCondition = new AlarmCondition(); - alarmCondition.setCondition(Collections.singletonList(highTempFilter)); + alarmCondition.setCondition(singletonList(highTempFilter)); RepeatingAlarmConditionSpec repeating = new RepeatingAlarmConditionSpec(); repeating.setPredicate(new FilterPredicateValue<>( @@ -1306,19 +1423,19 @@ public class TbDeviceProfileNodeTest extends AbstractRuleNodeUpgradeTest { DeviceProfileAlarm dpa = new DeviceProfileAlarm(); dpa.setId("highTemperatureAlarmID"); dpa.setAlarmType("highTemperatureAlarm"); - dpa.setCreateRules(new TreeMap<>(Collections.singletonMap(AlarmSeverity.CRITICAL, alarmRule))); + dpa.setCreateRules(new TreeMap<>(singletonMap(AlarmSeverity.CRITICAL, alarmRule))); - deviceProfileData.setAlarms(Collections.singletonList(dpa)); + deviceProfileData.setAlarms(singletonList(dpa)); deviceProfile.setProfileData(deviceProfileData); - Mockito.when(cache.get(tenantId, deviceId)).thenReturn(deviceProfile); - Mockito.when(timeseriesService.findLatest(tenantId, deviceId, Collections.singleton("temperature"))) - .thenReturn(Futures.immediateFuture(Collections.emptyList())); - Mockito.when(alarmService.findLatestActiveByOriginatorAndType(tenantId, deviceId, "highTemperatureAlarm")) + when(cache.get(tenantId, deviceId)).thenReturn(deviceProfile); + when(timeseriesService.findLatest(tenantId, deviceId, singleton("temperature"))) + .thenReturn(immediateFuture(emptyList())); + when(alarmService.findLatestActiveByOriginatorAndType(tenantId, deviceId, "highTemperatureAlarm")) .thenReturn(null); registerCreateAlarmMock(alarmService.createAlarm(any()), true); - Mockito.when(ctx.getAttributesService()).thenReturn(attributesService); - Mockito.when(attributesService.find(eq(tenantId), eq(deviceId), Mockito.any(AttributeScope.class), Mockito.anySet())) + when(ctx.getAttributesService()).thenReturn(attributesService); + when(attributesService.find(eq(tenantId), eq(deviceId), Mockito.any(AttributeScope.class), Mockito.anySet())) .thenReturn(listListenableFuture); TbMsg theMsg = TbMsg.newMsg() @@ -1373,7 +1490,7 @@ public class TbDeviceProfileNodeTest extends AbstractRuleNodeUpgradeTest { AttributeKvEntry entryActiveSchedule = attributeKvEntityActiveSchedule.toData(); ListenableFuture> listListenableFutureActiveSchedule = - Futures.immediateFuture(Collections.singletonList(entryActiveSchedule)); + immediateFuture(singletonList(entryActiveSchedule)); AlarmConditionFilter highTempFilter = new AlarmConditionFilter(); highTempFilter.setKey(new AlarmConditionFilterKey(TIME_SERIES, "temperature")); @@ -1387,10 +1504,10 @@ public class TbDeviceProfileNodeTest extends AbstractRuleNodeUpgradeTest { )); highTempFilter.setPredicate(highTemperaturePredicate); AlarmCondition alarmCondition = new AlarmCondition(); - alarmCondition.setCondition(Collections.singletonList(highTempFilter)); + alarmCondition.setCondition(singletonList(highTempFilter)); CustomTimeSchedule schedule = new CustomTimeSchedule(); - schedule.setItems(Collections.emptyList()); + schedule.setItems(emptyList()); schedule.setDynamicValue(new DynamicValue<>(DynamicValueSourceType.CURRENT_DEVICE, "dynamicValueActiveSchedule", false)); AlarmRule alarmRule = new AlarmRule(); @@ -1399,19 +1516,19 @@ public class TbDeviceProfileNodeTest extends AbstractRuleNodeUpgradeTest { DeviceProfileAlarm deviceProfileAlarmActiveSchedule = new DeviceProfileAlarm(); deviceProfileAlarmActiveSchedule.setId("highTemperatureAlarmID"); deviceProfileAlarmActiveSchedule.setAlarmType("highTemperatureAlarm"); - deviceProfileAlarmActiveSchedule.setCreateRules(new TreeMap<>(Collections.singletonMap(AlarmSeverity.CRITICAL, alarmRule))); + deviceProfileAlarmActiveSchedule.setCreateRules(new TreeMap<>(singletonMap(AlarmSeverity.CRITICAL, alarmRule))); - deviceProfileData.setAlarms(Collections.singletonList(deviceProfileAlarmActiveSchedule)); + deviceProfileData.setAlarms(singletonList(deviceProfileAlarmActiveSchedule)); deviceProfile.setProfileData(deviceProfileData); - Mockito.when(cache.get(tenantId, deviceId)).thenReturn(deviceProfile); - Mockito.when(timeseriesService.findLatest(tenantId, deviceId, Collections.singleton("temperature"))) - .thenReturn(Futures.immediateFuture(Collections.emptyList())); - Mockito.when(alarmService.findLatestActiveByOriginatorAndType(tenantId, deviceId, "highTemperatureAlarm")) + when(cache.get(tenantId, deviceId)).thenReturn(deviceProfile); + when(timeseriesService.findLatest(tenantId, deviceId, singleton("temperature"))) + .thenReturn(immediateFuture(emptyList())); + when(alarmService.findLatestActiveByOriginatorAndType(tenantId, deviceId, "highTemperatureAlarm")) .thenReturn(null); registerCreateAlarmMock(alarmService.createAlarm(any()), true); - Mockito.when(ctx.getAttributesService()).thenReturn(attributesService); - Mockito.when(attributesService.find(eq(tenantId), eq(deviceId), Mockito.any(AttributeScope.class), Mockito.anySet())) + when(ctx.getAttributesService()).thenReturn(attributesService); + when(attributesService.find(eq(tenantId), eq(deviceId), Mockito.any(AttributeScope.class), Mockito.anySet())) .thenReturn(listListenableFutureActiveSchedule); TbMsg theMsg = TbMsg.newMsg() @@ -1470,7 +1587,7 @@ public class TbDeviceProfileNodeTest extends AbstractRuleNodeUpgradeTest { AttributeKvEntry entryInactiveSchedule = attributeKvEntityInactiveSchedule.toData(); ListenableFuture> listListenableFutureInactiveSchedule = - Futures.immediateFuture(Collections.singletonList(entryInactiveSchedule)); + immediateFuture(singletonList(entryInactiveSchedule)); AlarmConditionFilter highTempFilter = new AlarmConditionFilter(); highTempFilter.setKey(new AlarmConditionFilterKey(TIME_SERIES, "temperature")); @@ -1485,7 +1602,7 @@ public class TbDeviceProfileNodeTest extends AbstractRuleNodeUpgradeTest { highTempFilter.setPredicate(highTemperaturePredicate); AlarmCondition alarmCondition = new AlarmCondition(); - alarmCondition.setCondition(Collections.singletonList(highTempFilter)); + alarmCondition.setCondition(singletonList(highTempFilter)); CustomTimeSchedule schedule = new CustomTimeSchedule(); @@ -1508,18 +1625,18 @@ public class TbDeviceProfileNodeTest extends AbstractRuleNodeUpgradeTest { DeviceProfileAlarm deviceProfileAlarmNonactiveSchedule = new DeviceProfileAlarm(); deviceProfileAlarmNonactiveSchedule.setId("highTemperatureAlarmID"); deviceProfileAlarmNonactiveSchedule.setAlarmType("highTemperatureAlarm"); - deviceProfileAlarmNonactiveSchedule.setCreateRules(new TreeMap<>(Collections.singletonMap(AlarmSeverity.CRITICAL, alarmRule))); + deviceProfileAlarmNonactiveSchedule.setCreateRules(new TreeMap<>(singletonMap(AlarmSeverity.CRITICAL, alarmRule))); - deviceProfileData.setAlarms(Collections.singletonList(deviceProfileAlarmNonactiveSchedule)); + deviceProfileData.setAlarms(singletonList(deviceProfileAlarmNonactiveSchedule)); deviceProfile.setProfileData(deviceProfileData); - Mockito.when(cache.get(tenantId, deviceId)).thenReturn(deviceProfile); - Mockito.when(timeseriesService.findLatest(tenantId, deviceId, Collections.singleton("temperature"))) - .thenReturn(Futures.immediateFuture(Collections.emptyList())); - Mockito.when(alarmService.findLatestActiveByOriginatorAndType(tenantId, deviceId, "highTemperatureAlarm")) + when(cache.get(tenantId, deviceId)).thenReturn(deviceProfile); + when(timeseriesService.findLatest(tenantId, deviceId, singleton("temperature"))) + .thenReturn(immediateFuture(emptyList())); + when(alarmService.findLatestActiveByOriginatorAndType(tenantId, deviceId, "highTemperatureAlarm")) .thenReturn(null); - Mockito.when(ctx.getAttributesService()).thenReturn(attributesService); - Mockito.when(attributesService.find(eq(tenantId), eq(deviceId), Mockito.any(AttributeScope.class), Mockito.anySet())) + when(ctx.getAttributesService()).thenReturn(attributesService); + when(attributesService.find(eq(tenantId), eq(deviceId), Mockito.any(AttributeScope.class), Mockito.anySet())) .thenReturn(listListenableFutureInactiveSchedule); TbMsg theMsg = TbMsg.newMsg() @@ -1569,9 +1686,9 @@ public class TbDeviceProfileNodeTest extends AbstractRuleNodeUpgradeTest { AttributeKvEntry entry = attributeKvEntity.toData(); ListenableFuture> listListenableFutureWithLess = - Futures.immediateFuture(Collections.emptyList()); + immediateFuture(emptyList()); ListenableFuture> optionalListenableFutureWithLess = - Futures.immediateFuture(Optional.of(entry)); + immediateFuture(Optional.of(entry)); AlarmConditionFilter lowTempFilter = new AlarmConditionFilter(); lowTempFilter.setKey(new AlarmConditionFilterKey(TIME_SERIES, "temperature")); @@ -1586,29 +1703,29 @@ public class TbDeviceProfileNodeTest extends AbstractRuleNodeUpgradeTest { ); lowTempFilter.setPredicate(lowTempPredicate); AlarmCondition alarmCondition = new AlarmCondition(); - alarmCondition.setCondition(Collections.singletonList(lowTempFilter)); + alarmCondition.setCondition(singletonList(lowTempFilter)); AlarmRule alarmRule = new AlarmRule(); alarmRule.setCondition(alarmCondition); DeviceProfileAlarm dpa = new DeviceProfileAlarm(); dpa.setId("lesstempID"); dpa.setAlarmType("lessTemperatureAlarm"); - dpa.setCreateRules(new TreeMap<>(Collections.singletonMap(AlarmSeverity.CRITICAL, alarmRule))); + dpa.setCreateRules(new TreeMap<>(singletonMap(AlarmSeverity.CRITICAL, alarmRule))); - deviceProfileData.setAlarms(Collections.singletonList(dpa)); + deviceProfileData.setAlarms(singletonList(dpa)); deviceProfile.setProfileData(deviceProfileData); - Mockito.when(cache.get(tenantId, deviceId)).thenReturn(deviceProfile); - Mockito.when(timeseriesService.findLatest(tenantId, deviceId, Collections.singleton("temperature"))) - .thenReturn(Futures.immediateFuture(Collections.emptyList())); - Mockito.when(alarmService.findLatestActiveByOriginatorAndType(tenantId, deviceId, "lessTemperatureAlarm")) + when(cache.get(tenantId, deviceId)).thenReturn(deviceProfile); + when(timeseriesService.findLatest(tenantId, deviceId, singleton("temperature"))) + .thenReturn(immediateFuture(emptyList())); + when(alarmService.findLatestActiveByOriginatorAndType(tenantId, deviceId, "lessTemperatureAlarm")) .thenReturn(null); registerCreateAlarmMock(alarmService.createAlarm(any()), true); - Mockito.when(ctx.getAttributesService()).thenReturn(attributesService); - Mockito.when(attributesService.find(eq(tenantId), eq(deviceId), Mockito.any(AttributeScope.class), Mockito.anySet())) + when(ctx.getAttributesService()).thenReturn(attributesService); + when(attributesService.find(eq(tenantId), eq(deviceId), Mockito.any(AttributeScope.class), Mockito.anySet())) .thenReturn(listListenableFutureWithLess); - Mockito.when(ctx.getDeviceService().findDeviceById(tenantId, deviceId)) + when(ctx.getDeviceService().findDeviceById(tenantId, deviceId)) .thenReturn(device); - Mockito.when(attributesService.find(eq(tenantId), eq(customerId), eq(AttributeScope.SERVER_SCOPE), Mockito.anyString())) + when(attributesService.find(eq(tenantId), eq(customerId), eq(AttributeScope.SERVER_SCOPE), Mockito.anyString())) .thenReturn(optionalListenableFutureWithLess); TbMsg theMsg = TbMsg.newMsg() @@ -1655,9 +1772,9 @@ public class TbDeviceProfileNodeTest extends AbstractRuleNodeUpgradeTest { AttributeKvEntry entry = attributeKvEntity.toData(); ListenableFuture> listListenableFutureWithLess = - Futures.immediateFuture(Collections.emptyList()); + immediateFuture(emptyList()); ListenableFuture> optionalListenableFutureWithLess = - Futures.immediateFuture(Optional.of(entry)); + immediateFuture(Optional.of(entry)); AlarmConditionFilter lowTempFilter = new AlarmConditionFilter(); lowTempFilter.setKey(new AlarmConditionFilterKey(TIME_SERIES, "temperature")); @@ -1672,27 +1789,27 @@ public class TbDeviceProfileNodeTest extends AbstractRuleNodeUpgradeTest { ); lowTempFilter.setPredicate(lowTempPredicate); AlarmCondition alarmCondition = new AlarmCondition(); - alarmCondition.setCondition(Collections.singletonList(lowTempFilter)); + alarmCondition.setCondition(singletonList(lowTempFilter)); AlarmRule alarmRule = new AlarmRule(); alarmRule.setCondition(alarmCondition); DeviceProfileAlarm dpa = new DeviceProfileAlarm(); dpa.setId("lesstempID"); dpa.setAlarmType("lessTemperatureAlarm"); - dpa.setCreateRules(new TreeMap<>(Collections.singletonMap(AlarmSeverity.CRITICAL, alarmRule))); + dpa.setCreateRules(new TreeMap<>(singletonMap(AlarmSeverity.CRITICAL, alarmRule))); - deviceProfileData.setAlarms(Collections.singletonList(dpa)); + deviceProfileData.setAlarms(singletonList(dpa)); deviceProfile.setProfileData(deviceProfileData); - Mockito.when(cache.get(tenantId, deviceId)).thenReturn(deviceProfile); - Mockito.when(timeseriesService.findLatest(tenantId, deviceId, Collections.singleton("temperature"))) - .thenReturn(Futures.immediateFuture(Collections.emptyList())); - Mockito.when(alarmService.findLatestActiveByOriginatorAndType(tenantId, deviceId, "lessTemperatureAlarm")) + when(cache.get(tenantId, deviceId)).thenReturn(deviceProfile); + when(timeseriesService.findLatest(tenantId, deviceId, singleton("temperature"))) + .thenReturn(immediateFuture(emptyList())); + when(alarmService.findLatestActiveByOriginatorAndType(tenantId, deviceId, "lessTemperatureAlarm")) .thenReturn(null); registerCreateAlarmMock(alarmService.createAlarm(any()), true); - Mockito.when(ctx.getAttributesService()).thenReturn(attributesService); - Mockito.when(attributesService.find(eq(tenantId), eq(deviceId), Mockito.any(AttributeScope.class), Mockito.anySet())) + when(ctx.getAttributesService()).thenReturn(attributesService); + when(attributesService.find(eq(tenantId), eq(deviceId), Mockito.any(AttributeScope.class), Mockito.anySet())) .thenReturn(listListenableFutureWithLess); - Mockito.when(attributesService.find(eq(tenantId), eq(tenantId), eq(AttributeScope.SERVER_SCOPE), Mockito.anyString())) + when(attributesService.find(eq(tenantId), eq(tenantId), eq(AttributeScope.SERVER_SCOPE), Mockito.anyString())) .thenReturn(optionalListenableFutureWithLess); TbMsg theMsg = TbMsg.newMsg() @@ -1743,11 +1860,11 @@ public class TbDeviceProfileNodeTest extends AbstractRuleNodeUpgradeTest { AttributeKvEntry entry = attributeKvEntity.toData(); ListenableFuture> listListenableFutureWithLess = - Futures.immediateFuture(Collections.emptyList()); + immediateFuture(emptyList()); ListenableFuture> emptyOptionalFuture = - Futures.immediateFuture(Optional.empty()); + immediateFuture(Optional.empty()); ListenableFuture> optionalListenableFutureWithLess = - Futures.immediateFuture(Optional.of(entry)); + immediateFuture(Optional.of(entry)); AlarmConditionFilter lowTempFilter = new AlarmConditionFilter(); lowTempFilter.setKey(new AlarmConditionFilterKey(TIME_SERIES, "temperature")); @@ -1762,31 +1879,31 @@ public class TbDeviceProfileNodeTest extends AbstractRuleNodeUpgradeTest { ); lowTempFilter.setPredicate(lowTempPredicate); AlarmCondition alarmCondition = new AlarmCondition(); - alarmCondition.setCondition(Collections.singletonList(lowTempFilter)); + alarmCondition.setCondition(singletonList(lowTempFilter)); AlarmRule alarmRule = new AlarmRule(); alarmRule.setCondition(alarmCondition); DeviceProfileAlarm dpa = new DeviceProfileAlarm(); dpa.setId("lesstempID"); dpa.setAlarmType("lessTemperatureAlarm"); - dpa.setCreateRules(new TreeMap<>(Collections.singletonMap(AlarmSeverity.CRITICAL, alarmRule))); + dpa.setCreateRules(new TreeMap<>(singletonMap(AlarmSeverity.CRITICAL, alarmRule))); - deviceProfileData.setAlarms(Collections.singletonList(dpa)); + deviceProfileData.setAlarms(singletonList(dpa)); deviceProfile.setProfileData(deviceProfileData); - Mockito.when(cache.get(tenantId, deviceId)).thenReturn(deviceProfile); - Mockito.when(timeseriesService.findLatest(tenantId, deviceId, Collections.singleton("temperature"))) - .thenReturn(Futures.immediateFuture(Collections.emptyList())); - Mockito.when(alarmService.findLatestActiveByOriginatorAndType(tenantId, deviceId, "lessTemperatureAlarm")) + when(cache.get(tenantId, deviceId)).thenReturn(deviceProfile); + when(timeseriesService.findLatest(tenantId, deviceId, singleton("temperature"))) + .thenReturn(immediateFuture(emptyList())); + when(alarmService.findLatestActiveByOriginatorAndType(tenantId, deviceId, "lessTemperatureAlarm")) .thenReturn(null); registerCreateAlarmMock(alarmService.createAlarm(any()), true); - Mockito.when(ctx.getAttributesService()).thenReturn(attributesService); - Mockito.when(ctx.getDeviceService().findDeviceById(tenantId, deviceId)) + when(ctx.getAttributesService()).thenReturn(attributesService); + when(ctx.getDeviceService().findDeviceById(tenantId, deviceId)) .thenReturn(device); - Mockito.when(attributesService.find(eq(tenantId), eq(deviceId), Mockito.any(AttributeScope.class), Mockito.anySet())) + when(attributesService.find(eq(tenantId), eq(deviceId), Mockito.any(AttributeScope.class), Mockito.anySet())) .thenReturn(listListenableFutureWithLess); - Mockito.when(attributesService.find(eq(tenantId), eq(customerId), Mockito.any(AttributeScope.class), Mockito.anyString())) + when(attributesService.find(eq(tenantId), eq(customerId), Mockito.any(AttributeScope.class), Mockito.anyString())) .thenReturn(emptyOptionalFuture); - Mockito.when(attributesService.find(eq(tenantId), eq(tenantId), eq(AttributeScope.SERVER_SCOPE), Mockito.anyString())) + when(attributesService.find(eq(tenantId), eq(tenantId), eq(AttributeScope.SERVER_SCOPE), Mockito.anyString())) .thenReturn(optionalListenableFutureWithLess); TbMsg theMsg = TbMsg.newMsg() @@ -1839,11 +1956,11 @@ public class TbDeviceProfileNodeTest extends AbstractRuleNodeUpgradeTest { AttributeKvEntry entry = attributeKvEntity.toData(); ListenableFuture> listListenableFutureWithLess = - Futures.immediateFuture(Collections.emptyList()); + immediateFuture(emptyList()); ListenableFuture> emptyOptionalFuture = - Futures.immediateFuture(Optional.empty()); + immediateFuture(Optional.empty()); ListenableFuture> optionalListenableFutureWithLess = - Futures.immediateFuture(Optional.of(entry)); + immediateFuture(Optional.of(entry)); AlarmConditionFilter lowTempFilter = new AlarmConditionFilter(); lowTempFilter.setKey(new AlarmConditionFilterKey(TIME_SERIES, "temperature")); @@ -1858,31 +1975,31 @@ public class TbDeviceProfileNodeTest extends AbstractRuleNodeUpgradeTest { ); lowTempFilter.setPredicate(lowTempPredicate); AlarmCondition alarmCondition = new AlarmCondition(); - alarmCondition.setCondition(Collections.singletonList(lowTempFilter)); + alarmCondition.setCondition(singletonList(lowTempFilter)); AlarmRule alarmRule = new AlarmRule(); alarmRule.setCondition(alarmCondition); DeviceProfileAlarm dpa = new DeviceProfileAlarm(); dpa.setId("lesstempID"); dpa.setAlarmType("greaterTemperatureAlarm"); - dpa.setCreateRules(new TreeMap<>(Collections.singletonMap(AlarmSeverity.CRITICAL, alarmRule))); + dpa.setCreateRules(new TreeMap<>(singletonMap(AlarmSeverity.CRITICAL, alarmRule))); - deviceProfileData.setAlarms(Collections.singletonList(dpa)); + deviceProfileData.setAlarms(singletonList(dpa)); deviceProfile.setProfileData(deviceProfileData); - Mockito.when(cache.get(tenantId, deviceId)).thenReturn(deviceProfile); - Mockito.when(timeseriesService.findLatest(tenantId, deviceId, Collections.singleton("temperature"))) - .thenReturn(Futures.immediateFuture(Collections.emptyList())); - Mockito.when(alarmService.findLatestActiveByOriginatorAndType(tenantId, deviceId, "greaterTemperatureAlarm")) + when(cache.get(tenantId, deviceId)).thenReturn(deviceProfile); + when(timeseriesService.findLatest(tenantId, deviceId, singleton("temperature"))) + .thenReturn(immediateFuture(emptyList())); + when(alarmService.findLatestActiveByOriginatorAndType(tenantId, deviceId, "greaterTemperatureAlarm")) .thenReturn(null); registerCreateAlarmMock(alarmService.createAlarm(any()), true); - Mockito.when(ctx.getAttributesService()).thenReturn(attributesService); - Mockito.when(ctx.getDeviceService().findDeviceById(tenantId, deviceId)) + when(ctx.getAttributesService()).thenReturn(attributesService); + when(ctx.getDeviceService().findDeviceById(tenantId, deviceId)) .thenReturn(device); - Mockito.when(attributesService.find(eq(tenantId), eq(deviceId), Mockito.any(AttributeScope.class), Mockito.anySet())) + when(attributesService.find(eq(tenantId), eq(deviceId), Mockito.any(AttributeScope.class), Mockito.anySet())) .thenReturn(listListenableFutureWithLess); - Mockito.when(attributesService.find(eq(tenantId), eq(customerId), Mockito.any(AttributeScope.class), Mockito.anyString())) + when(attributesService.find(eq(tenantId), eq(customerId), Mockito.any(AttributeScope.class), Mockito.anyString())) .thenReturn(emptyOptionalFuture); - Mockito.when(attributesService.find(eq(tenantId), eq(tenantId), eq(AttributeScope.SERVER_SCOPE), Mockito.anyString())) + when(attributesService.find(eq(tenantId), eq(tenantId), eq(AttributeScope.SERVER_SCOPE), Mockito.anyString())) .thenReturn(optionalListenableFutureWithLess); TbMsg theMsg = TbMsg.newMsg() @@ -1911,15 +2028,12 @@ public class TbDeviceProfileNodeTest extends AbstractRuleNodeUpgradeTest { } private void init() throws TbNodeException { - Mockito.when(ctx.getTenantId()).thenReturn(tenantId); - Mockito.when(ctx.getDeviceProfileCache()).thenReturn(cache); - Mockito.lenient().when(ctx.getTimeseriesService()).thenReturn(timeseriesService); - Mockito.lenient().when(ctx.getAlarmService()).thenReturn(alarmService); - Mockito.when(ctx.getDeviceService()).thenReturn(deviceService); - Mockito.lenient().when(ctx.getAttributesService()).thenReturn(attributesService); - TbNodeConfiguration nodeConfiguration = new TbNodeConfiguration(JacksonUtil.newObjectNode()); + var config = new TbDeviceProfileNodeConfiguration(); + config.setFetchAlarmRulesStateOnStart(false); + config.setPersistAlarmRulesState(false); + node = new TbDeviceProfileNode(); - node.init(ctx, nodeConfiguration); + node.init(ctx, new TbNodeConfiguration(JacksonUtil.valueToTree(config))); } private void registerCreateAlarmMock(AlarmApiCallResult a, boolean created) { @@ -1991,23 +2105,23 @@ public class TbDeviceProfileNodeTest extends AbstractRuleNodeUpgradeTest { DeviceProfileAlarm dpa = new DeviceProfileAlarm(); dpa.setId("highTemperatureAlarmID"); dpa.setAlarmType("highTemperatureAlarm"); - dpa.setCreateRules(new TreeMap<>(Collections.singletonMap(AlarmSeverity.CRITICAL, createRule))); + dpa.setCreateRules(new TreeMap<>(singletonMap(AlarmSeverity.CRITICAL, createRule))); dpa.setClearRule(clearRule); - deviceProfileData.setAlarms(Collections.singletonList(dpa)); + deviceProfileData.setAlarms(singletonList(dpa)); deviceProfile.setProfileData(deviceProfileData); ListenableFuture> tsKvList = - Futures.immediateFuture(Collections.singletonList(getTsKvEntry("temperature", 35L))); + immediateFuture(singletonList(getTsKvEntry("temperature", 35L))); ListenableFuture> attrList = - Futures.immediateFuture(Collections.emptyList()); + immediateFuture(emptyList()); - Mockito.when(cache.get(tenantId, deviceId)).thenReturn(deviceProfile); - Mockito.when(timeseriesService.findLatest(tenantId, deviceId, Collections.singleton("temperature"))) + when(cache.get(tenantId, deviceId)).thenReturn(deviceProfile); + when(timeseriesService.findLatest(tenantId, deviceId, singleton("temperature"))) .thenReturn(tsKvList); - Mockito.when(attributesService.find(eq(tenantId), eq(deviceId), any(), anySet())) + when(attributesService.find(eq(tenantId), eq(deviceId), any(), anySet())) .thenReturn(attrList); - Mockito.when(alarmService.findLatestActiveByOriginatorAndType(tenantId, deviceId, "highTemperatureAlarm")).thenReturn(null); + when(alarmService.findLatestActiveByOriginatorAndType(tenantId, deviceId, "highTemperatureAlarm")).thenReturn(null); registerCreateAlarmMock(alarmService.createAlarm(any()), true); TbMsg theMsg = TbMsg.newMsg() @@ -2046,7 +2160,7 @@ public class TbDeviceProfileNodeTest extends AbstractRuleNodeUpgradeTest { private AlarmCondition getNumericAlarmCondition(AlarmConditionKeyType alarmConditionKeyType, String key, NumericOperation operation, Double value) { AlarmConditionFilter filter = getAlarmConditionFilter(alarmConditionKeyType, key, operation, value); AlarmCondition alarmCondition = new AlarmCondition(); - alarmCondition.setCondition(Collections.singletonList(filter)); + alarmCondition.setCondition(singletonList(filter)); return alarmCondition; } @@ -2076,13 +2190,13 @@ public class TbDeviceProfileNodeTest extends AbstractRuleNodeUpgradeTest { highTemperaturePredicate.setValue(new FilterPredicateValue<>(30.0)); highTempFilter.setPredicate(highTemperaturePredicate); AlarmCondition alarmCondition = new AlarmCondition(); - alarmCondition.setCondition(Collections.singletonList(highTempFilter)); + alarmCondition.setCondition(singletonList(highTempFilter)); AlarmRule alarmRule = new AlarmRule(); alarmRule.setCondition(alarmCondition); DeviceProfileAlarm dpa = new DeviceProfileAlarm(); dpa.setId("highTemperatureAlarmID"); dpa.setAlarmType("highTemperatureAlarm"); - dpa.setCreateRules(new TreeMap<>(Collections.singletonMap(AlarmSeverity.CRITICAL, alarmRule))); + dpa.setCreateRules(new TreeMap<>(singletonMap(AlarmSeverity.CRITICAL, alarmRule))); AlarmConditionFilter lowTempFilter = new AlarmConditionFilter(); lowTempFilter.setKey(new AlarmConditionFilterKey(AlarmConditionKeyType.TIME_SERIES, "temperature")); @@ -2093,17 +2207,17 @@ public class TbDeviceProfileNodeTest extends AbstractRuleNodeUpgradeTest { lowTempFilter.setPredicate(lowTemperaturePredicate); AlarmRule clearRule = new AlarmRule(); AlarmCondition clearCondition = new AlarmCondition(); - clearCondition.setCondition(Collections.singletonList(lowTempFilter)); + clearCondition.setCondition(singletonList(lowTempFilter)); clearRule.setCondition(clearCondition); dpa.setClearRule(clearRule); - deviceProfileData.setAlarms(Collections.singletonList(dpa)); + deviceProfileData.setAlarms(singletonList(dpa)); deviceProfile.setProfileData(deviceProfileData); - Mockito.when(cache.get(tenantId, deviceId)).thenReturn(deviceProfile); - Mockito.when(timeseriesService.findLatest(tenantId, deviceId, Collections.singleton("temperature"))) - .thenReturn(Futures.immediateFuture(Collections.emptyList())); - Mockito.when(alarmService.findLatestActiveByOriginatorAndType(tenantId, deviceId, "highTemperatureAlarm")).thenReturn(null); + when(cache.get(tenantId, deviceId)).thenReturn(deviceProfile); + when(timeseriesService.findLatest(tenantId, deviceId, singleton("temperature"))) + .thenReturn(immediateFuture(emptyList())); + when(alarmService.findLatestActiveByOriginatorAndType(tenantId, deviceId, "highTemperatureAlarm")).thenReturn(null); registerCreateAlarmMock(alarmService.createAlarm(any()), true); TbMsg theMsg = TbMsg.newMsg() From 2c3fc140d1cdd36ca473a3bac4d68b5dfb852203 Mon Sep 17 00:00:00 2001 From: dashevchenko Date: Wed, 12 Nov 2025 15:34:46 +0200 Subject: [PATCH 32/42] fixes after merge --- .../org/thingsboard/server/dao/asset/BaseAssetService.java | 2 +- .../thingsboard/server/dao/customer/CustomerServiceImpl.java | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/dao/src/main/java/org/thingsboard/server/dao/asset/BaseAssetService.java b/dao/src/main/java/org/thingsboard/server/dao/asset/BaseAssetService.java index 8bbefc59f9..7d8442fd3a 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/asset/BaseAssetService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/asset/BaseAssetService.java @@ -151,7 +151,7 @@ public class BaseAssetService extends AbstractCachedEntityService saveAsset(asset, true, NameConflictStrategy.DEFAULT)); + return saveEntity(asset, () -> saveAsset(asset, true, nameConflictStrategy)); } @Override diff --git a/dao/src/main/java/org/thingsboard/server/dao/customer/CustomerServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/customer/CustomerServiceImpl.java index f8d36fe02f..70d20852bd 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/customer/CustomerServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/customer/CustomerServiceImpl.java @@ -153,6 +153,9 @@ public class CustomerServiceImpl extends AbstractCachedEntityService saveCustomer(customer, true, nameConflictStrategy)); } + private Customer saveCustomer(Customer customer, boolean doValidate) { + return saveCustomer(customer, doValidate, NameConflictStrategy.DEFAULT); + } private Customer saveCustomer(Customer customer, boolean doValidate, NameConflictStrategy nameConflictStrategy) { log.trace("Executing saveCustomer [{}]", customer); From eb189693d592c866d6c393ab967b24d5ef95c741 Mon Sep 17 00:00:00 2001 From: Viacheslav Klimov Date: Wed, 12 Nov 2025 15:36:57 +0200 Subject: [PATCH 33/42] Add test for 2FA enforcement for sysadmin user --- .../server/controller/AbstractWebTest.java | 2 +- .../server/controller/TwoFactorAuthTest.java | 34 ++++++++++++++++++- 2 files changed, 34 insertions(+), 2 deletions(-) diff --git a/application/src/test/java/org/thingsboard/server/controller/AbstractWebTest.java b/application/src/test/java/org/thingsboard/server/controller/AbstractWebTest.java index 6f1446049f..76434b5766 100644 --- a/application/src/test/java/org/thingsboard/server/controller/AbstractWebTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/AbstractWebTest.java @@ -210,7 +210,7 @@ public abstract class AbstractWebTest extends AbstractInMemoryStorageTest { protected static final String TEST_DIFFERENT_TENANT_NAME = "TEST DIFFERENT TENANT"; protected static final String SYS_ADMIN_EMAIL = "sysadmin@thingsboard.org"; - private static final String SYS_ADMIN_PASSWORD = "sysadmin"; + protected static final String SYS_ADMIN_PASSWORD = "sysadmin"; protected static final String TENANT_ADMIN_EMAIL = "testtenant@thingsboard.org"; protected static final String TENANT_ADMIN_PASSWORD = "tenant"; diff --git a/application/src/test/java/org/thingsboard/server/controller/TwoFactorAuthTest.java b/application/src/test/java/org/thingsboard/server/controller/TwoFactorAuthTest.java index 1138846c08..daa213dc6c 100644 --- a/application/src/test/java/org/thingsboard/server/controller/TwoFactorAuthTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/TwoFactorAuthTest.java @@ -34,6 +34,7 @@ import org.thingsboard.server.common.data.audit.ActionType; import org.thingsboard.server.common.data.audit.AuditLog; import org.thingsboard.server.common.data.exception.ThingsboardException; import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.notification.targets.platform.AllUsersFilter; import org.thingsboard.server.common.data.notification.targets.platform.TenantAdministratorsFilter; import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.common.data.page.SortOrder; @@ -415,7 +416,8 @@ public class TwoFactorAuthTest extends AbstractControllerTest { logInWithMfaToken(username, password, Authority.MFA_CONFIGURATION_TOKEN); - TotpTwoFaAccountConfig totpTwoFaAccountConfig = (TotpTwoFaAccountConfig) twoFactorAuthService.generateNewAccountConfig(user, totpTwoFaProviderConfig.getProviderType()); + TotpTwoFaAccountConfig totpTwoFaAccountConfig = doPost("/api/2fa/account/config/generate?providerType=" + totpTwoFaProviderConfig.getProviderType(), TotpTwoFaAccountConfig.class); + String secret = UriComponentsBuilder.fromUriString(totpTwoFaAccountConfig.getAuthUrl()).build() .getQueryParams().getFirst("secret"); String verificationCode = new Totp(secret).now(); @@ -433,6 +435,36 @@ public class TwoFactorAuthTest extends AbstractControllerTest { doGet("/api/user/" + savedDifferentTenantUser.getId()).andExpect(status().isOk()); } + @Test + public void testEnforceTwoFa_sysadmin() throws Exception { + TotpTwoFaProviderConfig totpTwoFaProviderConfig = new TotpTwoFaProviderConfig(); + totpTwoFaProviderConfig.setIssuerName("tb"); + + PlatformTwoFaSettings twoFaSettings = new PlatformTwoFaSettings(); + twoFaSettings.setProviders(Arrays.stream(new TwoFaProviderConfig[]{totpTwoFaProviderConfig}).collect(Collectors.toList())); + twoFaSettings.setMinVerificationCodeSendPeriod(5); + twoFaSettings.setTotalAllowedTimeForVerification(100); + twoFaSettings.setEnforceTwoFa(true); + AllUsersFilter enforcedUsersFilter = new AllUsersFilter(); + twoFaSettings.setEnforcedUsersFilter(enforcedUsersFilter); + twoFaSettings = twoFaConfigManager.savePlatformTwoFaSettings(TenantId.SYS_TENANT_ID, twoFaSettings); + + logInWithMfaToken(SYS_ADMIN_EMAIL, SYS_ADMIN_PASSWORD, Authority.MFA_CONFIGURATION_TOKEN); + + TotpTwoFaAccountConfig totpTwoFaAccountConfig = doPost("/api/2fa/account/config/generate?providerType=" + totpTwoFaProviderConfig.getProviderType(), TotpTwoFaAccountConfig.class); + String secret = UriComponentsBuilder.fromUriString(totpTwoFaAccountConfig.getAuthUrl()).build() + .getQueryParams().getFirst("secret"); + String verificationCode = new Totp(secret).now(); + readResponse(doPost("/api/2fa/account/config?verificationCode=" + verificationCode, totpTwoFaAccountConfig).andExpect(status().isOk()), JsonNode.class); + + JwtPair tokenPair = readResponse(doPost("/api/auth/2fa/login").andExpect(status().isOk()), JwtPair.class); + assertThat(tokenPair.getToken()).isNotEmpty(); + assertThat(tokenPair.getRefreshToken()).isNotEmpty(); + validateAndSetJwtToken(tokenPair, SYS_ADMIN_EMAIL); + + doGet("/api/user/" + user.getId()).andExpect(status().isOk()); + } + private void logInWithMfaToken(String username, String password, Authority expectedScope) throws Exception { LoginRequest loginRequest = new LoginRequest(username, password); From 78c52e3141308787c1fe628854db1e358a803277 Mon Sep 17 00:00:00 2001 From: dashevchenko Date: Wed, 12 Nov 2025 15:42:44 +0200 Subject: [PATCH 34/42] fixes after merge --- .../thingsboard/server/dao/customer/CustomerServiceImpl.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/dao/src/main/java/org/thingsboard/server/dao/customer/CustomerServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/customer/CustomerServiceImpl.java index 70d20852bd..3c3389baf2 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/customer/CustomerServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/customer/CustomerServiceImpl.java @@ -143,8 +143,7 @@ public class CustomerServiceImpl extends AbstractCachedEntityService saveCustomer(customer, true)); } @Override From 2a8f2a8e73e409fb4a831a111d551773193097aa Mon Sep 17 00:00:00 2001 From: ArtemDzhereleiko Date: Wed, 12 Nov 2025 16:05:26 +0200 Subject: [PATCH 35/42] UI: add translate for email description force 2fa --- .../pages/login/force-two-factor-auth-login.component.html | 2 +- ui-ngx/src/assets/locale/locale.constant-en_US.json | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/ui-ngx/src/app/modules/login/pages/login/force-two-factor-auth-login.component.html b/ui-ngx/src/app/modules/login/pages/login/force-two-factor-auth-login.component.html index 30f34b1d63..39236ef8e2 100644 --- a/ui-ngx/src/app/modules/login/pages/login/force-two-factor-auth-login.component.html +++ b/ui-ngx/src/app/modules/login/pages/login/force-two-factor-auth-login.component.html @@ -157,7 +157,7 @@
-
+

login.email-description

Date: Thu, 13 Nov 2025 12:10:04 +0200 Subject: [PATCH 36/42] refactoring --- .../thingsboard/server/dao/customer/CustomerServiceImpl.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dao/src/main/java/org/thingsboard/server/dao/customer/CustomerServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/customer/CustomerServiceImpl.java index 3c3389baf2..4187a96acc 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/customer/CustomerServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/customer/CustomerServiceImpl.java @@ -143,7 +143,7 @@ public class CustomerServiceImpl extends AbstractCachedEntityService saveCustomer(customer, true)); + return saveCustomer(customer, NameConflictStrategy.DEFAULT); } @Override From 20a05bf87ddb8b3b5f060954bb4012b9b05f7b95 Mon Sep 17 00:00:00 2001 From: Andrii Landiak Date: Thu, 13 Nov 2025 15:48:58 +0200 Subject: [PATCH 37/42] Introduce SystemPatchApplier to update system data when patch version increased on startup --- .../DefaultDatabaseSchemaSettingsService.java | 52 ++- .../service/install/InstallScripts.java | 9 +- .../service/system/SystemInfoService.java | 2 + .../service/system/SystemPatchApplier.java | 272 ++++++++++++ .../server/system/BaseHttpDeviceApiTest.java | 3 - .../server/system/BaseRestApiLimitsTest.java | 4 - .../system/RestTemplateConvertersTest.java | 1 - .../server/system/SystemPatchApplierTest.java | 410 ++++++++++++++++++ .../server/system/sql/DeviceApiSqlTest.java | 3 - .../system/sql/RestApiLimitsSqlTest.java | 1 - .../thingsboard/common/util/JacksonUtil.java | 23 + 11 files changed, 749 insertions(+), 31 deletions(-) create mode 100644 application/src/main/java/org/thingsboard/server/service/system/SystemPatchApplier.java create mode 100644 application/src/test/java/org/thingsboard/server/system/SystemPatchApplierTest.java diff --git a/application/src/main/java/org/thingsboard/server/service/install/DefaultDatabaseSchemaSettingsService.java b/application/src/main/java/org/thingsboard/server/service/install/DefaultDatabaseSchemaSettingsService.java index f41a530630..1917b4e22d 100644 --- a/application/src/main/java/org/thingsboard/server/service/install/DefaultDatabaseSchemaSettingsService.java +++ b/application/src/main/java/org/thingsboard/server/service/install/DefaultDatabaseSchemaSettingsService.java @@ -17,7 +17,6 @@ package org.thingsboard.server.service.install; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; -import org.springframework.context.annotation.Profile; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.stereotype.Service; import org.thingsboard.server.service.install.update.DefaultDataUpdateService; @@ -25,14 +24,13 @@ import org.thingsboard.server.service.install.update.DefaultDataUpdateService; import java.util.List; @Service -@Profile("install") @Slf4j @RequiredArgsConstructor public class DefaultDatabaseSchemaSettingsService implements DatabaseSchemaSettingsService { - // This list should include all versions which are compatible for the upgrade. - // The compatibility cycle usually breaks when we have some scripts written in Java that may not work after new release. - private static final List SUPPORTED_VERSIONS_FOR_UPGRADE = List.of("4.2.0"); + // This list should include all versions that are compatible for the upgrade in 4 digits format (like 4.2.0.0, etc.). + // The compatibility cycle usually breaks when we have some scripts written in Java that may not work after a new release. + private static final List SUPPORTED_VERSIONS_FOR_UPGRADE = List.of("4.2.0.0"); private final ProjectInfo projectInfo; private final JdbcTemplate jdbcTemplate; @@ -80,7 +78,7 @@ public class DefaultDatabaseSchemaSettingsService implements DatabaseSchemaSetti @Override public String getPackageSchemaVersion() { if (packageSchemaVersion == null) { - packageSchemaVersion = projectInfo.getProjectVersion(); + packageSchemaVersion = normalizeVersion(projectInfo.getProjectVersion()); } return packageSchemaVersion; } @@ -88,17 +86,28 @@ public class DefaultDatabaseSchemaSettingsService implements DatabaseSchemaSetti @Override public String getDbSchemaVersion() { if (schemaVersionFromDb == null) { - Long version = getSchemaVersionFromDb(); - if (version == null) { + Long dbVersion = getSchemaVersionFromDb(); + if (dbVersion == null) { onSchemaSettingsError("Upgrade failed: the database schema version is missing."); } @SuppressWarnings("DataFlowIssue") - long major = version / 1000000; - long minor = (version % 1000000) / 1000; - long patch = version % 1000; - - schemaVersionFromDb = major + "." + minor + "." + patch; + long version = dbVersion; + + if (version < 1_000_000_000) { + // Old format: MMM mmm ppp (e.g., 4002001 = 4.2.1) + long major = version / 1_000_000; + long minor = (version % 1_000_000) / 1000; + long maintenance = version % 1000; + schemaVersionFromDb = major + "." + minor + "." + maintenance + ".0"; + } else { + // New format: MMM mmm mmm ppp (e.g., 4002001001 = 4.2.1.1) + long major = version / 1_000_000_000; + long minor = (version % 1_000_000_000) / 1_000_000; + long maintenance = (version % 1_000_000) / 1000; + long patch = version % 1000; + schemaVersionFromDb = major + "." + minor + "." + maintenance + "." + patch; + } } return schemaVersionFromDb; } @@ -116,13 +125,26 @@ public class DefaultDatabaseSchemaSettingsService implements DatabaseSchemaSetti long major = Integer.parseInt(versionParts[0]); long minor = Integer.parseInt(versionParts[1]); - long patch = versionParts.length > 2 ? Integer.parseInt(versionParts[2]) : 0; + long maintenance = Integer.parseInt(versionParts[2]); + long patch = Integer.parseInt(versionParts[3]); - return major * 1000000 + minor * 1000 + patch; + return major * 1_000_000_000L + minor * 1_000_000L + maintenance * 1000L + patch; } private void onSchemaSettingsError(String message) { Runtime.getRuntime().addShutdownHook(new Thread(() -> log.error(message))); throw new RuntimeException(message); } + + private String normalizeVersion(String version) { + String[] parts = version.split("\\."); + + int major = Integer.parseInt(parts[0]); + int minor = parts.length > 1 ? Integer.parseInt(parts[1]) : 0; + int maintenance = parts.length > 2 ? Integer.parseInt(parts[2]) : 0; + int patch = parts.length > 3 ? Integer.parseInt(parts[3]) : 0; + + return major + "." + minor + "." + maintenance + "." + patch; + } + } diff --git a/application/src/main/java/org/thingsboard/server/service/install/InstallScripts.java b/application/src/main/java/org/thingsboard/server/service/install/InstallScripts.java index 4e44517ca0..f9b5c282a8 100644 --- a/application/src/main/java/org/thingsboard/server/service/install/InstallScripts.java +++ b/application/src/main/java/org/thingsboard/server/service/install/InstallScripts.java @@ -65,9 +65,6 @@ import java.util.stream.Stream; import static org.thingsboard.server.utils.LwM2mObjectModelUtils.toLwm2mResource; -/** - * Created by ashvayka on 18.04.18. - */ @Component @Slf4j public class InstallScripts { @@ -134,6 +131,10 @@ public class InstallScripts { return Paths.get(getDataDir(), JSON_DIR, EDGE_DIR, RULE_CHAINS_DIR); } + public Path getWidgetTypesDir() { + return Paths.get(getDataDir(), JSON_DIR, SYSTEM_DIR, WIDGET_TYPES_DIR); + } + public String getDataDir() { if (!StringUtils.isEmpty(dataDir)) { if (!Paths.get(this.dataDir).toFile().isDirectory()) { @@ -237,7 +238,7 @@ public class InstallScripts { } ); } - Path widgetTypesDir = Paths.get(getDataDir(), JSON_DIR, SYSTEM_DIR, WIDGET_TYPES_DIR); + Path widgetTypesDir = getWidgetTypesDir(); if (Files.exists(widgetTypesDir)) { try (Stream dirStream = listDir(widgetTypesDir).filter(path -> path.toString().endsWith(JSON_EXT))) { dirStream.forEach( diff --git a/application/src/main/java/org/thingsboard/server/service/system/SystemInfoService.java b/application/src/main/java/org/thingsboard/server/service/system/SystemInfoService.java index faae80a942..e2edd8a9cc 100644 --- a/application/src/main/java/org/thingsboard/server/service/system/SystemInfoService.java +++ b/application/src/main/java/org/thingsboard/server/service/system/SystemInfoService.java @@ -19,7 +19,9 @@ import org.thingsboard.server.common.data.FeaturesInfo; import org.thingsboard.server.common.data.SystemInfo; public interface SystemInfoService { + SystemInfo getSystemInfo(); FeaturesInfo getFeaturesInfo(); + } diff --git a/application/src/main/java/org/thingsboard/server/service/system/SystemPatchApplier.java b/application/src/main/java/org/thingsboard/server/service/system/SystemPatchApplier.java new file mode 100644 index 0000000000..204d2b5d6c --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/system/SystemPatchApplier.java @@ -0,0 +1,272 @@ +/** + * Copyright © 2016-2025 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.system; + +import com.fasterxml.jackson.databind.JsonNode; +import com.google.common.hash.Hashing; +import jakarta.annotation.PostConstruct; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.stereotype.Component; +import org.thingsboard.common.util.JacksonUtil; +import org.thingsboard.common.util.ThingsBoardThreadFactory; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.widget.WidgetTypeDetails; +import org.thingsboard.server.dao.widget.WidgetTypeService; +import org.thingsboard.server.queue.util.TbCoreComponent; +import org.thingsboard.server.service.install.DatabaseSchemaSettingsService; +import org.thingsboard.server.service.install.InstallScripts; +import org.thingsboard.server.service.install.update.DefaultDataUpdateService; + +import java.io.IOException; +import java.io.UncheckedIOException; +import java.nio.file.Files; +import java.nio.file.NoSuchFileException; +import java.nio.file.Path; +import java.util.Objects; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.stream.Stream; + +/** + * Runs at application startup and applies no-downtime data updates + * when the package PATCH version increases (e.g., 4.2.1.0 -> 4.2.1.1). + */ +@Slf4j +@Component +@TbCoreComponent +@RequiredArgsConstructor +public class SystemPatchApplier { + + private static final long ADVISORY_LOCK_ID = 7536891047216478431L; + + private final JdbcTemplate jdbcTemplate; + private final InstallScripts installScripts; + private final DatabaseSchemaSettingsService schemaSettingsService; + private final WidgetTypeService widgetTypeService; + + @PostConstruct + private void init() { + ExecutorService executor = Executors.newSingleThreadExecutor(ThingsBoardThreadFactory.forName("system-patch-applier")); + executor.submit(() -> { + try { + applyPatchIfNeeded(); + } catch (Exception e) { + log.error("Failed to apply system data patch updates", e); + } finally { + executor.shutdown(); + } + }); + } + + private void applyPatchIfNeeded() { + boolean skipVersionCheck = DefaultDataUpdateService.getEnv("SKIP_PATCH_VERSION_CHECK", false); + if (!skipVersionCheck && !isVersionChanged()) { + return; + } + + if (!acquireAdvisoryLock()) { + log.trace("Could not acquire advisory lock. Another node is processing patch updates."); + return; + } + + try { + int updated = updateWidgetTypes(); + log.info("Updated {} widget types", updated); + + schemaSettingsService.updateSchemaVersion(); + log.info("System data patch update completed successfully"); + + } finally { + releaseAdvisoryLock(); + } + } + + private boolean isVersionChanged() { + String packageVersion = schemaSettingsService.getPackageSchemaVersion(); + String dbVersion = schemaSettingsService.getDbSchemaVersion(); + + log.trace("Package version: {}, DB schema version: {}", packageVersion, dbVersion); + + VersionInfo packageVersionInfo = parseVersion(packageVersion); + VersionInfo dbVersionInfo = parseVersion(dbVersion); + + if (packageVersionInfo == null || dbVersionInfo == null) { + log.warn("Unable to parse versions. Package: {}, DB: {}", packageVersion, dbVersion); + return false; + } + + if (!isPatchVersionChanged(packageVersionInfo, dbVersionInfo)) { + return false; + } + + log.info("Patch version increased from {} to {}. Starting system data update.", dbVersion, packageVersion); + return true; + } + + private boolean isPatchVersionChanged(VersionInfo packageVersion, VersionInfo dbVersion) { + return packageVersion.major == dbVersion.major && packageVersion.minor == dbVersion.minor + && packageVersion.maintenance == dbVersion.maintenance && packageVersion.patch > dbVersion.patch; + } + + private int updateWidgetTypes() { + AtomicInteger updated = new AtomicInteger(); + Path widgetTypesDir = installScripts.getWidgetTypesDir(); + + if (!Files.exists(widgetTypesDir)) { + log.trace("Widget types directory does not exist: {}", widgetTypesDir); + return 0; + } + + try (Stream dirStream = listDir(widgetTypesDir).filter(path -> path.toString().endsWith(InstallScripts.JSON_EXT))) { + dirStream.forEach( + path -> { + try { + if (updateWidgetTypeFromFile(path)) { + updated.incrementAndGet(); + } + } catch (Exception e) { + log.error("Unable to update widget type from json: [{}]", path.toString()); + throw new RuntimeException("Unable to update widget type from json", e); + } + } + ); + } + + return updated.get(); + } + + private boolean updateWidgetTypeFromFile(Path filePath) { + JsonNode json = JacksonUtil.toJsonNode(filePath.toFile()); + WidgetTypeDetails fileWidgetType = JacksonUtil.treeToValue(json, WidgetTypeDetails.class); + String fqn = fileWidgetType.getFqn(); + + WidgetTypeDetails existingWidgetType = widgetTypeService.findWidgetTypeDetailsByTenantIdAndFqn(TenantId.SYS_TENANT_ID, fqn); + if (existingWidgetType == null) { + // We expect only update here, so it's probably never happening, but for test purpose leave it like this: + throw new RuntimeException("Widget type not found: " + fqn); + } + if (isWidgetTypeChanged(existingWidgetType, fileWidgetType)) { + existingWidgetType.setDescription(fileWidgetType.getDescription()); + existingWidgetType.setName(fileWidgetType.getName()); + existingWidgetType.setDescriptor(fileWidgetType.getDescriptor()); + widgetTypeService.saveWidgetType(existingWidgetType); + log.trace("Updated widget type: {}", fqn); + return true; + } + + log.trace("Widget type unchanged: {}", fqn); + return false; + } + + private boolean isWidgetTypeChanged(WidgetTypeDetails existing, WidgetTypeDetails file) { + if (!isDescriptorEqual(existing.getDescriptor(), file.getDescriptor())) { + return true; + } + + if (!Objects.equals(existing.getName(), file.getName())) { + return true; + } + + return !Objects.equals(existing.getDescription(), file.getDescription()); + } + + private boolean isDescriptorEqual(JsonNode desc1, JsonNode desc2) { + if (desc1 == null && desc2 == null) { + return true; + } + if (desc1 == null || desc2 == null) { + return false; + } + + try { + String hash1 = computeChecksum(desc1); + String hash2 = computeChecksum(desc2); + return Objects.equals(hash1, hash2); + } catch (Exception e) { + log.warn("Failed to compare descriptors using checksum, falling back to equals", e); + return desc1.equals(desc2); + } + } + + private String computeChecksum(JsonNode node) { + String canonicalString = JacksonUtil.toCanonicalString(node); + if (canonicalString == null) { + return null; + } + return Hashing.sha256().hashBytes(canonicalString.getBytes()).toString(); + } + + private boolean acquireAdvisoryLock() { + try { + Boolean acquired = jdbcTemplate.queryForObject( + "SELECT pg_try_advisory_lock(?)", + Boolean.class, + ADVISORY_LOCK_ID + ); + if (Boolean.TRUE.equals(acquired)) { + log.trace("Acquired advisory lock"); + return true; + } + return false; + } catch (Exception e) { + log.error("Failed to acquire advisory lock", e); + return false; + } + } + + private void releaseAdvisoryLock() { + try { + jdbcTemplate.queryForObject( + "SELECT pg_advisory_unlock(?)", + Boolean.class, + ADVISORY_LOCK_ID + ); + log.debug("Released advisory lock"); + } catch (Exception e) { + log.error("Failed to release advisory lock", e); + } + } + + private VersionInfo parseVersion(String version) { + try { + String[] parts = version.split("\\."); + int major = Integer.parseInt(parts[0]); + int minor = parts.length > 1 ? Integer.parseInt(parts[1]) : 0; + int maintenance = parts.length > 2 ? Integer.parseInt(parts[2]) : 0; + int patch = parts.length > 3 ? Integer.parseInt(parts[3]) : 0; + return new VersionInfo(major, minor, maintenance, patch); + } catch (Exception e) { + log.error("Failed to parse version: {}", version, e); + return null; + } + } + + private Stream listDir(Path dir) { + try { + return Files.list(dir); + } catch (NoSuchFileException e) { + return Stream.empty(); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + } + + public record VersionInfo(int major, int minor, int maintenance, int patch) {} + +} diff --git a/application/src/test/java/org/thingsboard/server/system/BaseHttpDeviceApiTest.java b/application/src/test/java/org/thingsboard/server/system/BaseHttpDeviceApiTest.java index dbfca2e9f1..65633c6bd9 100644 --- a/application/src/test/java/org/thingsboard/server/system/BaseHttpDeviceApiTest.java +++ b/application/src/test/java/org/thingsboard/server/system/BaseHttpDeviceApiTest.java @@ -37,9 +37,6 @@ import static org.springframework.test.web.servlet.request.MockMvcRequestBuilder import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.request; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; -/** - * @author Andrew Shvayka - */ @TestPropertySource(properties = { "transport.http.enabled=true", "transport.http.max_payload_size=/api/v1/*/rpc/**=10000;/api/v1/**=20000" diff --git a/application/src/test/java/org/thingsboard/server/system/BaseRestApiLimitsTest.java b/application/src/test/java/org/thingsboard/server/system/BaseRestApiLimitsTest.java index 84a366b139..fea9d5086c 100644 --- a/application/src/test/java/org/thingsboard/server/system/BaseRestApiLimitsTest.java +++ b/application/src/test/java/org/thingsboard/server/system/BaseRestApiLimitsTest.java @@ -45,10 +45,6 @@ import java.util.concurrent.TimeoutException; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; -/** - * @author Illia Barkov - */ - @Slf4j public abstract class BaseRestApiLimitsTest extends AbstractControllerTest { diff --git a/application/src/test/java/org/thingsboard/server/system/RestTemplateConvertersTest.java b/application/src/test/java/org/thingsboard/server/system/RestTemplateConvertersTest.java index 6ff57a7ee0..f976156a86 100644 --- a/application/src/test/java/org/thingsboard/server/system/RestTemplateConvertersTest.java +++ b/application/src/test/java/org/thingsboard/server/system/RestTemplateConvertersTest.java @@ -29,7 +29,6 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.springframework.test.web.client.match.MockRestRequestMatchers.requestTo; import static org.springframework.test.web.client.response.MockRestResponseCreators.withSuccess; - @Slf4j public class RestTemplateConvertersTest { diff --git a/application/src/test/java/org/thingsboard/server/system/SystemPatchApplierTest.java b/application/src/test/java/org/thingsboard/server/system/SystemPatchApplierTest.java new file mode 100644 index 0000000000..3c65f19d65 --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/system/SystemPatchApplierTest.java @@ -0,0 +1,410 @@ +/** + * Copyright © 2016-2025 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.system; + +import com.fasterxml.jackson.databind.JsonNode; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.api.io.TempDir; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.CsvSource; +import org.junit.jupiter.params.provider.MethodSource; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.test.util.ReflectionTestUtils; +import org.thingsboard.common.util.JacksonUtil; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.id.WidgetTypeId; +import org.thingsboard.server.common.data.widget.WidgetTypeDetails; +import org.thingsboard.server.dao.widget.WidgetTypeService; +import org.thingsboard.server.service.install.InstallScripts; +import org.thingsboard.server.service.system.SystemPatchApplier; + +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.UUID; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.stream.Stream; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.argThat; +import static org.mockito.ArgumentMatchers.contains; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +public class SystemPatchApplierTest { + + @Mock + private JdbcTemplate jdbcTemplate; + + @Mock + private InstallScripts installScripts; + + @Mock + private WidgetTypeService widgetTypeService; + + @InjectMocks + private SystemPatchApplier reconciler; + + @TempDir + Path tempDir; + + @ParameterizedTest(name = "Parse version {0} should return major={1}, minor={2}, patch={3}") + @CsvSource({ + "4.2.1, 4, 2, 1, 0", + "4.2.0, 4, 2, 0, 0", + "4.2, 4, 2, 0, 0", + "4.0.1.2, 4, 0, 1, 2", + "4, 4, 0, 0, 0", + "1.0.5.7, 1, 0, 5, 7", + "10.20.30.40, 10, 20, 30, 40", + "0.0.1, 0, 0, 1, 0" + }) + void testParseVersion(String versionString, int expectedMajor, int expectedMinor, int expectedMaintenance, int expectedPatch) { + SystemPatchApplier.VersionInfo version = ReflectionTestUtils.invokeMethod(reconciler, "parseVersion", versionString); + + assertNotNull(version, "Version should not be null for: " + versionString); + assertEquals(expectedMajor, version.major(), "Major version mismatch"); + assertEquals(expectedMinor, version.minor(), "Minor version mismatch"); + assertEquals(expectedMaintenance, version.maintenance(), "Maintenance version mismatch"); + assertEquals(expectedPatch, version.patch(), "Patch version mismatch"); + } + + @ParameterizedTest(name = "Parse invalid version: {0}") + @CsvSource({ + "invalid", + "a.b.c", + "1.2.y.x", + "''", + "1.x.3" + }) + void testParseInvalidVersion(String invalidVersion) { + SystemPatchApplier.VersionInfo version = ReflectionTestUtils.invokeMethod(reconciler, "parseVersion", invalidVersion); + assertNull(version, "Version should be null for invalid input: " + invalidVersion); + } + + @Test + void whenLockIsNotAcquired_thenAcquiredIsSuccess() { + when(jdbcTemplate.queryForObject(anyString(), eq(Boolean.class), anyLong())).thenReturn(true); + + Boolean acquired = ReflectionTestUtils.invokeMethod(reconciler, "acquireAdvisoryLock"); + + assertEquals(Boolean.TRUE, acquired); + verify(jdbcTemplate).queryForObject(contains("pg_try_advisory_lock"), eq(Boolean.class), anyLong()); + } + + @Test + void whenLockIsAlreadyAcquired_thenAcquiredIsFailed() { + when(jdbcTemplate.queryForObject(anyString(), eq(Boolean.class), anyLong())).thenReturn(false); + + Boolean acquired = ReflectionTestUtils.invokeMethod(reconciler, "acquireAdvisoryLock"); + + assertNotEquals(Boolean.TRUE, acquired); + } + + @Test + void testReleaseAdvisoryLock() { + when(jdbcTemplate.queryForObject(anyString(), eq(Boolean.class), anyLong())) + .thenReturn(true); + + ReflectionTestUtils.invokeMethod(reconciler, "releaseAdvisoryLock"); + + verify(jdbcTemplate).queryForObject( + contains("pg_advisory_unlock"), eq(Boolean.class), anyLong()); + } + + @Test + void whenWidgetNotFound_thenThrowException() throws Exception { + Path widgetTypesDir = tempDir.resolve("widget_types"); + Files.createDirectories(widgetTypesDir); + when(installScripts.getWidgetTypesDir()).thenReturn(widgetTypesDir); + + WidgetTypeDetails testWidget = createTestWidgetType("test_widget", "Test Widget"); + String json = JacksonUtil.toString(testWidget); + assertNotNull(json); + Files.writeString(widgetTypesDir.resolve("test_widget.json"), json); + + when(widgetTypeService.findWidgetTypeDetailsByTenantIdAndFqn(TenantId.SYS_TENANT_ID, "test_widget")).thenReturn(null); + + assertThrows(RuntimeException.class, () -> ReflectionTestUtils.invokeMethod(reconciler, "updateWidgetTypes")); + } + + @Test + void whenDescriptorChanged_thenUpdateTheExistingWidget() throws Exception { + Path widgetTypesDir = tempDir.resolve("widget_types"); + Files.createDirectories(widgetTypesDir); + when(installScripts.getWidgetTypesDir()).thenReturn(widgetTypesDir); + + WidgetTypeDetails fileWidget = createTestWidgetType("test_widget", "Test Widget"); + fileWidget.setDescriptor(JacksonUtil.toJsonNode("{\"type\":\"latest\",\"version\":2}")); + String json = JacksonUtil.toString(fileWidget); + assertNotNull(json); + Files.writeString(widgetTypesDir.resolve("test_widget.json"), json); + + WidgetTypeDetails existingWidget = createTestWidgetType("test_widget", "Test Widget"); + existingWidget.setId(new WidgetTypeId(UUID.randomUUID())); + existingWidget.setDescriptor(JacksonUtil.toJsonNode("{\"type\":\"latest\",\"version\":1}")); + + when(widgetTypeService.findWidgetTypeDetailsByTenantIdAndFqn(TenantId.SYS_TENANT_ID, "test_widget")) + .thenReturn(existingWidget); + + Integer updated = ReflectionTestUtils.invokeMethod(reconciler, "updateWidgetTypes"); + + assertEquals(1, updated); + verify(widgetTypeService).saveWidgetType(argThat(w -> + w.getDescriptor().get("version").asInt() == 2 + )); + } + + @Test + void whenNameChanged_thenUpdateTheExistingWidget() throws Exception { + Path widgetTypesDir = tempDir.resolve("widget_types"); + Files.createDirectories(widgetTypesDir); + when(installScripts.getWidgetTypesDir()).thenReturn(widgetTypesDir); + + WidgetTypeDetails fileWidget = createTestWidgetType("test_widget", "New Name"); + String json = JacksonUtil.toString(fileWidget); + assertNotNull(json); + Files.writeString(widgetTypesDir.resolve("test_widget.json"), json); + + WidgetTypeDetails existingWidget = createTestWidgetType("test_widget", "Old Name"); + existingWidget.setId(new WidgetTypeId(UUID.randomUUID())); + + when(widgetTypeService.findWidgetTypeDetailsByTenantIdAndFqn(TenantId.SYS_TENANT_ID, "test_widget")) + .thenReturn(existingWidget); + + Integer updated = ReflectionTestUtils.invokeMethod(reconciler, "updateWidgetTypes"); + + assertEquals(1, updated); + verify(widgetTypeService).saveWidgetType(argThat(w -> "New Name".equals(w.getName()))); + } + + @Test + void whenNothingChanged_thenSkipTheUpdateOfTheExistingWidget() throws Exception { + Path widgetTypesDir = tempDir.resolve("widget_types"); + Files.createDirectories(widgetTypesDir); + when(installScripts.getWidgetTypesDir()).thenReturn(widgetTypesDir); + + WidgetTypeDetails fileWidget = createTestWidgetType("test_widget", "Test Widget"); + String json = JacksonUtil.toString(fileWidget); + assertNotNull(json); + Files.writeString(widgetTypesDir.resolve("test_widget.json"), json); + + WidgetTypeDetails existingWidget = createTestWidgetType("test_widget", "Test Widget"); + existingWidget.setId(new WidgetTypeId(UUID.randomUUID())); + + when(widgetTypeService.findWidgetTypeDetailsByTenantIdAndFqn(TenantId.SYS_TENANT_ID, "test_widget")) + .thenReturn(existingWidget); + + Integer updated = ReflectionTestUtils.invokeMethod(reconciler, "updateWidgetTypes"); + + assertEquals(0, updated); + verify(widgetTypeService, never()).saveWidgetType(any()); + } + + @ParameterizedTest(name = "{0}") + @MethodSource("provideDescriptorComparisonTestCases") + void testIfDescriptorsAreEqual(String testName, JsonNode desc1, JsonNode desc2, boolean expectedEqual) { + Boolean result = ReflectionTestUtils.invokeMethod(reconciler, "isDescriptorEqual", desc1, desc2); + assertEquals(expectedEqual, result, testName); + } + + @Test + void whenDescriptorChanged_thenReturnWidgetTypeChanged() { + WidgetTypeDetails existing = createTestWidgetType("test", "Test"); + existing.setDescriptor(JacksonUtil.toJsonNode("{\"version\":1}")); + + WidgetTypeDetails file = createTestWidgetType("test", "Test"); + file.setDescriptor(JacksonUtil.toJsonNode("{\"version\":2}")); + + boolean result = Boolean.TRUE.equals(ReflectionTestUtils.invokeMethod(reconciler, "isWidgetTypeChanged", existing, file)); + assertTrue(result); + } + + @Test + void whenNameChanged_thenReturnWidgetTypeChanged() { + WidgetTypeDetails existing = createTestWidgetType("test", "Old Name"); + WidgetTypeDetails file = createTestWidgetType("test", "New Name"); + + boolean result = Boolean.TRUE.equals(ReflectionTestUtils.invokeMethod(reconciler, "isWidgetTypeChanged", existing, file)); + assertTrue(result); + } + + @Test + void whenDescriptionChanged_thenReturnWidgetTypeChanged() { + WidgetTypeDetails existing = createTestWidgetType("test", "Test"); + existing.setDescription("Old description"); + + WidgetTypeDetails file = createTestWidgetType("test", "Test"); + file.setDescription("New description"); + + boolean result = Boolean.TRUE.equals(ReflectionTestUtils.invokeMethod(reconciler, "isWidgetTypeChanged", existing, file)); + assertTrue(result); + } + + @Test + void whenWidgetTypeAreIdentical_thenNoUpdateIsPerformed() { + WidgetTypeDetails existing = createTestWidgetType("test", "Test"); + WidgetTypeDetails file = createTestWidgetType("test", "Test"); + + boolean result = Boolean.TRUE.equals(ReflectionTestUtils.invokeMethod(reconciler, "isWidgetTypeChanged", existing, file)); + assertFalse(result); + } + + @Test + void whenLockIsHeldByOneThread_thenSecondThreadCannotAcquireLock() throws Exception { + CountDownLatch lockAcquired = new CountDownLatch(1); + CountDownLatch startSecondThread = new CountDownLatch(1); + CountDownLatch testComplete = new CountDownLatch(1); + + AtomicBoolean firstThreadAcquiredLock = new AtomicBoolean(false); + AtomicBoolean secondThreadAcquiredLock = new AtomicBoolean(false); + AtomicBoolean firstThreadSavedWidget = new AtomicBoolean(false); + AtomicBoolean secondThreadSavedWidget = new AtomicBoolean(false); + + Path widgetTypesDir = tempDir.resolve("widget_types"); + Files.createDirectories(widgetTypesDir); + when(installScripts.getWidgetTypesDir()).thenReturn(widgetTypesDir); + + WidgetTypeDetails fileWidget = createTestWidgetType("test_widget", "Test Widget"); + fileWidget.setDescriptor(JacksonUtil.toJsonNode("{\"type\":\"latest\",\"version\":2}")); + String toString = JacksonUtil.toCanonicalString(fileWidget); + assertNotNull(toString); + Files.writeString(widgetTypesDir.resolve("test_widget.json"), toString); + + WidgetTypeDetails existingWidget = createTestWidgetType("test_widget", "Test Widget"); + existingWidget.setId(new WidgetTypeId(UUID.randomUUID())); + existingWidget.setDescriptor(JacksonUtil.toJsonNode("{\"type\":\"latest\",\"version\":1}")); + + when(widgetTypeService.findWidgetTypeDetailsByTenantIdAndFqn(TenantId.SYS_TENANT_ID, "test_widget")).thenReturn(existingWidget); + + when(jdbcTemplate.queryForObject(contains("pg_try_advisory_lock"), eq(Boolean.class), anyLong())) + .thenReturn(true) + .thenReturn(false); + + when(jdbcTemplate.queryForObject(contains("pg_advisory_unlock"), eq(Boolean.class), anyLong())) + .thenReturn(true); + + // The first thread-acquires lock and performs update + Thread firstThread = new Thread(() -> { + try { + Boolean acquired = ReflectionTestUtils.invokeMethod(reconciler, "acquireAdvisoryLock"); + firstThreadAcquiredLock.set(Boolean.TRUE.equals(acquired)); + + if (firstThreadAcquiredLock.get()) { + lockAcquired.countDown(); + startSecondThread.await(5, TimeUnit.SECONDS); + + // Simulate work while holding lock + Thread.sleep(100); + + Integer updated = ReflectionTestUtils.invokeMethod(reconciler, "updateWidgetTypes"); + firstThreadSavedWidget.set(updated != null && updated > 0); + + ReflectionTestUtils.invokeMethod(reconciler, "releaseAdvisoryLock"); + } + } catch (Exception ignored) { + } finally { + testComplete.countDown(); + } + }); + + // Second thread - attempts to acquire lock but fails + Thread secondThread = new Thread(() -> { + try { + lockAcquired.await(5, TimeUnit.SECONDS); + startSecondThread.countDown(); + + Boolean acquired = ReflectionTestUtils.invokeMethod(reconciler, "acquireAdvisoryLock"); + secondThreadAcquiredLock.set(Boolean.TRUE.equals(acquired)); + + if (secondThreadAcquiredLock.get()) { + Integer updated = ReflectionTestUtils.invokeMethod(reconciler, "updateWidgetTypes"); + secondThreadSavedWidget.set(updated != null && updated > 0); + + ReflectionTestUtils.invokeMethod(reconciler, "releaseAdvisoryLock"); + } + } catch (Exception ignored) {} + }); + + firstThread.start(); + secondThread.start(); + + assertTrue(testComplete.await(10, TimeUnit.SECONDS), "Test should complete within timeout"); + firstThread.join(1000); + secondThread.join(1000); + + assertTrue(firstThreadAcquiredLock.get(), "First thread should acquire lock"); + assertFalse(secondThreadAcquiredLock.get(), "Second thread should NOT acquire lock"); + assertTrue(firstThreadSavedWidget.get(), "First thread should save widget"); + assertFalse(secondThreadSavedWidget.get(), "Second thread should NOT save widget"); + + verify(widgetTypeService, times(1)).saveWidgetType(any()); + } + + private static Stream provideDescriptorComparisonTestCases() { + return Stream.of( + Arguments.of("Both null", null, null, true), + Arguments.of("First null", null, JacksonUtil.newObjectNode(), false), + Arguments.of("Second null", JacksonUtil.newObjectNode(), null, false), + Arguments.of("Same content", + JacksonUtil.toJsonNode("{\"type\":\"latest\",\"version\":1}"), + JacksonUtil.toJsonNode("{\"type\":\"latest\",\"version\":1}"), + true), + Arguments.of("Different content", + JacksonUtil.toJsonNode("{\"type\":\"latest\",\"version\":1}"), + JacksonUtil.toJsonNode("{\"type\":\"latest\",\"version\":2}"), + false), + Arguments.of("Different key order but same content", + JacksonUtil.toJsonNode("{\"version\":1,\"type\":\"latest\"}"), + JacksonUtil.toJsonNode("{\"type\":\"latest\",\"version\":1}"), + true), + Arguments.of("Empty objects", + JacksonUtil.toJsonNode("{}"), + JacksonUtil.toJsonNode("{}"), + true) + ); + } + + private WidgetTypeDetails createTestWidgetType(String fqn, String name) { + WidgetTypeDetails widget = new WidgetTypeDetails(); + widget.setFqn(fqn); + widget.setName(name); + widget.setDescription("Test description"); + widget.setTenantId(TenantId.SYS_TENANT_ID); + widget.setDescriptor(JacksonUtil.toJsonNode("{\"type\":\"latest\"}")); + return widget; + } + +} diff --git a/application/src/test/java/org/thingsboard/server/system/sql/DeviceApiSqlTest.java b/application/src/test/java/org/thingsboard/server/system/sql/DeviceApiSqlTest.java index 74f3cbafd9..27e5237fd9 100644 --- a/application/src/test/java/org/thingsboard/server/system/sql/DeviceApiSqlTest.java +++ b/application/src/test/java/org/thingsboard/server/system/sql/DeviceApiSqlTest.java @@ -18,9 +18,6 @@ package org.thingsboard.server.system.sql; import org.thingsboard.server.dao.service.DaoSqlTest; import org.thingsboard.server.system.BaseHttpDeviceApiTest; -/** - * Created by Valerii Sosliuk on 6/27/2017. - */ @DaoSqlTest public class DeviceApiSqlTest extends BaseHttpDeviceApiTest { } diff --git a/application/src/test/java/org/thingsboard/server/system/sql/RestApiLimitsSqlTest.java b/application/src/test/java/org/thingsboard/server/system/sql/RestApiLimitsSqlTest.java index 56c26fb3d3..932fb32328 100644 --- a/application/src/test/java/org/thingsboard/server/system/sql/RestApiLimitsSqlTest.java +++ b/application/src/test/java/org/thingsboard/server/system/sql/RestApiLimitsSqlTest.java @@ -18,7 +18,6 @@ package org.thingsboard.server.system.sql; import org.thingsboard.server.dao.service.DaoSqlTest; import org.thingsboard.server.system.BaseRestApiLimitsTest; - @DaoSqlTest public class RestApiLimitsSqlTest extends BaseRestApiLimitsTest { } diff --git a/common/util/src/main/java/org/thingsboard/common/util/JacksonUtil.java b/common/util/src/main/java/org/thingsboard/common/util/JacksonUtil.java index cd61c7ac20..e3ae70c9a5 100644 --- a/common/util/src/main/java/org/thingsboard/common/util/JacksonUtil.java +++ b/common/util/src/main/java/org/thingsboard/common/util/JacksonUtil.java @@ -83,6 +83,12 @@ public class JacksonUtil { .addModule(new Jdk8Module()) .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false) .build(); + public static final ObjectMapper CANONICAL_JSON_MAPPER = JsonMapper.builder() + .addModule(new Jdk8Module()) + .configure(SerializationFeature.ORDER_MAP_ENTRIES_BY_KEYS, true) + .configure(MapperFeature.SORT_PROPERTIES_ALPHABETICALLY, true) + .serializationInclusion(com.fasterxml.jackson.annotation.JsonInclude.Include.NON_NULL) + .build(); public static ObjectMapper getObjectMapperWithJavaTimeModule() { return JsonMapper.builder() @@ -207,6 +213,23 @@ public class JacksonUtil { return data; } + public static String toCanonicalString(Object value) { + try { + if (value == null) { + return null; + } + + if (value instanceof JsonNode) { + Object pojo = CANONICAL_JSON_MAPPER.convertValue(value, Object.class); + return CANONICAL_JSON_MAPPER.writeValueAsString(pojo); + } + + return CANONICAL_JSON_MAPPER.writeValueAsString(value); + } catch (Exception e) { + throw new IllegalArgumentException("The given Json object value cannot be transformed to a canonical String: " + value, e); + } + } + public static T treeToValue(JsonNode node, Class clazz) { try { return OBJECT_MAPPER.treeToValue(node, clazz); From 61c695238daef10c1ffd164804c797a890c6d91e Mon Sep 17 00:00:00 2001 From: Viacheslav Klimov Date: Thu, 13 Nov 2025 16:03:06 +0200 Subject: [PATCH 38/42] Fix alarm details in notification rules test --- .../server/service/notification/NotificationRuleApiTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/application/src/test/java/org/thingsboard/server/service/notification/NotificationRuleApiTest.java b/application/src/test/java/org/thingsboard/server/service/notification/NotificationRuleApiTest.java index c3a2119cbc..ff49a68d66 100644 --- a/application/src/test/java/org/thingsboard/server/service/notification/NotificationRuleApiTest.java +++ b/application/src/test/java/org/thingsboard/server/service/notification/NotificationRuleApiTest.java @@ -952,7 +952,7 @@ public class NotificationRuleApiTest extends AbstractNotificationApiTest { argument.setRefEntityKey(new ReferencedEntityKey("createAlarm", ArgumentType.ATTRIBUTE, AttributeScope.SERVER_SCOPE)); configuration.setArguments(Map.of("createAlarm", argument)); AlarmRule alarmRule = new AlarmRule(); - alarmRule.setAlarmDetails("attribute is ${bool}"); + alarmRule.setAlarmDetails("attribute is ${createAlarm}"); SimpleAlarmCondition condition = new SimpleAlarmCondition(); TbelAlarmConditionExpression expression = new TbelAlarmConditionExpression(); expression.setExpression("return createAlarm == true;"); From 0809cb17095e52683a7ead5feadb28a281cf35db Mon Sep 17 00:00:00 2001 From: Vladyslav_Prykhodko Date: Fri, 14 Nov 2025 14:54:03 +0200 Subject: [PATCH 39/42] UI: Fixed not allow Test function button in propagation cf --- .../debug-dialog/calculated-field-debug-dialog.component.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/components/debug-dialog/calculated-field-debug-dialog.component.ts b/ui-ngx/src/app/modules/home/components/calculated-fields/components/debug-dialog/calculated-field-debug-dialog.component.ts index dccab5cfd5..70bfa43c63 100644 --- a/ui-ngx/src/app/modules/home/components/calculated-fields/components/debug-dialog/calculated-field-debug-dialog.component.ts +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/components/debug-dialog/calculated-field-debug-dialog.component.ts @@ -58,7 +58,11 @@ export class CalculatedFieldDebugDialogComponent extends DialogComponent this.data.value.type === CalculatedFieldType.SCRIPT && !!(event as Event).body.arguments); + this.eventsTable.entitiesTable.cellActionDescriptors[0].isEnabled = (event => { + return (this.data.value.type === CalculatedFieldType.SCRIPT || + (this.data.value.type === CalculatedFieldType.PROPAGATION && this.data.value.configuration.applyExpressionToResolvedArguments) + ) && !!(event as Event).body.arguments + }); this.eventsTable.entitiesTable.updateData(); } From f733e5a61acce54215d14f07144f39c40a0369ed Mon Sep 17 00:00:00 2001 From: Vladyslav_Prykhodko Date: Fri, 14 Nov 2025 16:07:36 +0200 Subject: [PATCH 40/42] UI: Fixed align error in cf; Fixed required mobile bundle --- .../calculated-field-argument-panel.component.html | 2 +- .../mobile-qr-code-widget-settings.component.html | 1 + .../shared/components/entity/entity-autocomplete.component.html | 1 - 3 files changed, 2 insertions(+), 2 deletions(-) diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/components/calculated-field-arguments/calculated-field-argument-panel.component.html b/ui-ngx/src/app/modules/home/components/calculated-fields/components/calculated-field-arguments/calculated-field-argument-panel.component.html index c7eeaa253b..ea922c1aa7 100644 --- a/ui-ngx/src/app/modules/home/components/calculated-fields/components/calculated-field-arguments/calculated-field-argument-panel.component.html +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/components/calculated-field-arguments/calculated-field-argument-panel.component.html @@ -151,7 +151,7 @@ @if (refEntityKeyFormGroup.get('type').value !== ArgumentType.Rolling) {
{{ 'calculated-fields.default-value' | translate }}
- + @if (argumentFormGroup.get('defaultValue').touched && argumentFormGroup.get('defaultValue').hasError('required')) { {{ 'mobile.bundle' | translate }}
Date: Fri, 14 Nov 2025 17:00:20 +0200 Subject: [PATCH 41/42] UI: Fixed propagation CF validation in expression mode --- .../calculated-field-argument-panel.component.ts | 2 +- .../propagate-arguments-table.component.ts | 8 +++++--- .../dialog/calculated-field-dialog.component.html | 1 + ...ulated-field-geofencing-zone-groups-panel.component.ts | 3 +-- .../propagation-configuration.component.html | 1 + .../propagation-configuration.component.ts | 3 +++ ui-ngx/src/assets/locale/locale.constant-en_US.json | 1 + 7 files changed, 13 insertions(+), 6 deletions(-) diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/components/calculated-field-arguments/calculated-field-argument-panel.component.ts b/ui-ngx/src/app/modules/home/components/calculated-fields/components/calculated-field-arguments/calculated-field-argument-panel.component.ts index 4e8abbe841..89db142fac 100644 --- a/ui-ngx/src/app/modules/home/components/calculated-fields/components/calculated-field-arguments/calculated-field-argument-panel.component.ts +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/components/calculated-field-arguments/calculated-field-argument-panel.component.ts @@ -168,7 +168,7 @@ export class CalculatedFieldArgumentPanelComponent implements OnInit, AfterViewI saveArgument(): void { const value = this.argumentFormGroup.value as CalculatedFieldArgumentValue; if (this.entityType === ArgumentEntityType.Owner) { - value.refDynamicSourceConfiguration.type = ArgumentEntityType.Owner; + value.refDynamicSourceConfiguration = {type: ArgumentEntityType.Owner}; } else if (this.entityType === ArgumentEntityType.Tenant) { value.refEntityId = new TenantId(this.tenantId) as any; } diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/components/calculated-field-arguments/propagate-arguments-table.component.ts b/ui-ngx/src/app/modules/home/components/calculated-fields/components/calculated-field-arguments/propagate-arguments-table.component.ts index 04d1dbf91b..75e66afef5 100644 --- a/ui-ngx/src/app/modules/home/components/calculated-fields/components/calculated-field-arguments/propagate-arguments-table.component.ts +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/components/calculated-field-arguments/propagate-arguments-table.component.ts @@ -32,7 +32,7 @@ import { CalculatedFieldArgumentsTableComponent } from '@home/components/calculated-fields/components/calculated-field-arguments/calculated-field-arguments-table.component'; import { ArgumentEntityType, ArgumentType, CalculatedFieldArgumentValue } from '@shared/models/calculated-field.models'; -import { isDefined } from '@core/utils'; +import { isDefined, isUndefinedOrNull } from '@core/utils'; import { NULL_UUID } from '@shared/models/id/has-uuid'; @Component({ @@ -94,14 +94,14 @@ export class PropagateArgumentsTableComponent extends CalculatedFieldArgumentsTa } protected isEditButtonShowBadge(argument: CalculatedFieldArgumentValue): boolean { - if (!this.isScript && isDefined(argument?.refEntityId)) { + if (!this.isScript && (isDefined(argument?.refEntityId) || isDefined(argument?.refDynamicSourceConfiguration))) { return false; } return super.isEditButtonShowBadge(argument); } protected updateErrorText(): void { - if (!this.isScript && this.argumentsFormArray.controls.some(control => isDefined(control.value?.refEntityId))) { + if (!this.isScript && this.argumentsFormArray.controls.some(control => isDefined(control.value?.refEntityId) || isDefined(control.value.refDynamicSourceConfiguration))) { this.errorText = 'calculated-fields.hint.arguments-propagate-argument-entity-type'; } else if (!this.isScript && this.argumentsFormArray.controls.some(control => control.value.refEntityKey.type === ArgumentType.Rolling)) { this.errorText = 'calculated-fields.hint.arguments-propagate-arguments-with-rolling'; @@ -109,6 +109,8 @@ export class PropagateArgumentsTableComponent extends CalculatedFieldArgumentsTa this.errorText = 'calculated-fields.hint.arguments-entity-not-found'; } else if (!this.argumentsFormArray.controls.length) { this.errorText = 'calculated-fields.hint.arguments-empty'; + } if (this.isScript && !this.argumentsFormArray.controls.some(control => isUndefinedOrNull(control.value?.refEntityId) && isUndefinedOrNull(control.value.refDynamicSourceConfiguration))) { + this.errorText = 'calculated-fields.hint.arguments-propagate-argument-must-current-entity'; } else { this.errorText = ''; } diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/components/dialog/calculated-field-dialog.component.html b/ui-ngx/src/app/modules/home/components/calculated-fields/components/dialog/calculated-field-dialog.component.html index 08bf1328d8..46a93ac07b 100644 --- a/ui-ngx/src/app/modules/home/components/calculated-fields/components/dialog/calculated-field-dialog.component.html +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/components/dialog/calculated-field-dialog.component.html @@ -79,6 +79,7 @@ [entityId]="data.entityId" [entityName]="data.entityName" [tenantId]="data.tenantId" + [ownerId]="data.ownerId" [testScript]="onTestScript.bind(this)" > } diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/components/geofencing-configuration/calculated-field-geofencing-zone-groups-panel.component.ts b/ui-ngx/src/app/modules/home/components/calculated-fields/components/geofencing-configuration/calculated-field-geofencing-zone-groups-panel.component.ts index 0e18521572..6698de93b9 100644 --- a/ui-ngx/src/app/modules/home/components/calculated-fields/components/geofencing-configuration/calculated-field-geofencing-zone-groups-panel.component.ts +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/components/geofencing-configuration/calculated-field-geofencing-zone-groups-panel.component.ts @@ -198,8 +198,7 @@ export class CalculatedFieldGeofencingZoneGroupsPanelComponent implements OnInit break; case ArgumentEntityType.Owner: delete value.refEntityId; - value.refDynamicSourceConfiguration ||= { type: ArgumentEntityType.Owner }; - value.refDynamicSourceConfiguration.type = ArgumentEntityType.Owner; + value.refDynamicSourceConfiguration = {type: ArgumentEntityType.Owner}; break; case ArgumentEntityType.RelationQuery: delete value.refEntityId; diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/components/propagation-configuration/propagation-configuration.component.html b/ui-ngx/src/app/modules/home/components/calculated-fields/components/propagation-configuration/propagation-configuration.component.html index b6f0343885..cc8cf7dc4f 100644 --- a/ui-ngx/src/app/modules/home/components/calculated-fields/components/propagation-configuration/propagation-configuration.component.html +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/components/propagation-configuration/propagation-configuration.component.html @@ -54,6 +54,7 @@ [entityId]="entityId" [tenantId]="tenantId" [entityName]="entityName" + [ownerId]="ownerId" [isScript]="this.propagateConfiguration.get('applyExpressionToResolvedArguments').value"/>
diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/components/propagation-configuration/propagation-configuration.component.ts b/ui-ngx/src/app/modules/home/components/calculated-fields/components/propagation-configuration/propagation-configuration.component.ts index 4fb8bb561e..4485ef2032 100644 --- a/ui-ngx/src/app/modules/home/components/calculated-fields/components/propagation-configuration/propagation-configuration.component.ts +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/components/propagation-configuration/propagation-configuration.component.ts @@ -70,6 +70,9 @@ export class PropagationConfigurationComponent implements ControlValueAccessor, @Input({required: true}) entityName: string; + @Input({required: true}) + ownerId: EntityId; + @Input({required: true}) testScript: () => Observable; 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 c0ce4de8d2..a542ca33b6 100644 --- a/ui-ngx/src/assets/locale/locale.constant-en_US.json +++ b/ui-ngx/src/assets/locale/locale.constant-en_US.json @@ -1197,6 +1197,7 @@ "arguments-simple-with-rolling": "Simple type calculated field should not contain keys with time series rolling type.", "arguments-propagate-arguments-with-rolling": "'Time series rolling' type is incompatible with 'Arguments only' propagation.", "arguments-propagate-argument-entity-type": "Entity type is incompatible with 'Arguments only' propagation.", + "arguments-propagate-argument-must-current-entity": "At least one argument must be configured with the 'Current entity' source entity type.", "arguments-empty": "Arguments should not be empty.", "expression-required": "Expression is required.", "expression-invalid": "Expression is invalid", From fdce51d85fa10731a514cadbe674c4d2afb0980e Mon Sep 17 00:00:00 2001 From: Vladyslav_Prykhodko Date: Fri, 14 Nov 2025 17:23:14 +0200 Subject: [PATCH 42/42] UI: Fixed show Perimeter attribute key in Geofencing Calculated field --- ...calculated-field-geofencing-zone-groups-panel.component.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/components/geofencing-configuration/calculated-field-geofencing-zone-groups-panel.component.html b/ui-ngx/src/app/modules/home/components/calculated-fields/components/geofencing-configuration/calculated-field-geofencing-zone-groups-panel.component.html index 250d6bd120..1f5444f8b3 100644 --- a/ui-ngx/src/app/modules/home/components/calculated-fields/components/geofencing-configuration/calculated-field-geofencing-zone-groups-panel.component.html +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/components/geofencing-configuration/calculated-field-geofencing-zone-groups-panel.component.html @@ -176,7 +176,7 @@
- @if (entityFilter.singleEntity?.id) { + @if (entityFilter.singleEntity?.id || entityType === ArgumentEntityType.RelationQuery || entityType === ArgumentEntityType.Current) {
{{ 'calculated-fields.perimeter-attribute-key' | translate }}