From 3518be97a4c27266572681b6d7abfbf570cbe844 Mon Sep 17 00:00:00 2001 From: dashevchenko Date: Tue, 2 Dec 2025 15:19:47 +0200 Subject: [PATCH 01/32] fixed java rest client --- .../msa/connectivity/JavaRestClientTest.java | 318 +++++++++++++++++- .../thingsboard/rest/client/RestClient.java | 58 +++- 2 files changed, 364 insertions(+), 12 deletions(-) diff --git a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/connectivity/JavaRestClientTest.java b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/connectivity/JavaRestClientTest.java index 636c80d5b3..7cee0a6ba6 100644 --- a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/connectivity/JavaRestClientTest.java +++ b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/connectivity/JavaRestClientTest.java @@ -15,10 +15,14 @@ */ package org.thingsboard.server.msa.connectivity; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ArrayNode; +import com.fasterxml.jackson.databind.node.TextNode; +import com.google.gson.JsonObject; 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.io.HttpClientConnectionManager; import org.apache.hc.client5.http.ssl.DefaultClientTlsStrategy; import org.apache.hc.client5.http.ssl.HostnameVerificationPolicy; import org.apache.hc.client5.http.ssl.NoopHostnameVerifier; @@ -30,26 +34,83 @@ import org.testng.annotations.AfterMethod; import org.testng.annotations.BeforeClass; import org.testng.annotations.BeforeMethod; import org.testng.annotations.Test; +import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.rest.client.RestClient; import org.thingsboard.server.common.data.Device; +import org.thingsboard.server.common.data.EntityType; +import org.thingsboard.server.common.data.Tenant; +import org.thingsboard.server.common.data.User; 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.domain.Domain; +import org.thingsboard.server.common.data.domain.DomainInfo; +import org.thingsboard.server.common.data.id.NotificationTargetId; +import org.thingsboard.server.common.data.id.NotificationTemplateId; +import org.thingsboard.server.common.data.id.UUIDBased; +import org.thingsboard.server.common.data.id.UserId; +import org.thingsboard.server.common.data.mobile.app.MobileApp; +import org.thingsboard.server.common.data.mobile.app.MobileAppStatus; +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.NotificationRequestConfig; +import org.thingsboard.server.common.data.notification.NotificationRequestInfo; +import org.thingsboard.server.common.data.notification.NotificationRequestPreview; +import org.thingsboard.server.common.data.notification.NotificationType; +import org.thingsboard.server.common.data.notification.settings.NotificationSettings; +import org.thingsboard.server.common.data.notification.settings.SlackNotificationDeliveryMethodConfig; +import org.thingsboard.server.common.data.notification.settings.UserNotificationSettings; +import org.thingsboard.server.common.data.notification.targets.NotificationTarget; +import org.thingsboard.server.common.data.notification.targets.platform.PlatformUsersNotificationTargetConfig; +import org.thingsboard.server.common.data.notification.targets.platform.UserListFilter; +import org.thingsboard.server.common.data.notification.targets.platform.UsersFilter; +import org.thingsboard.server.common.data.notification.template.DeliveryMethodNotificationTemplate; +import org.thingsboard.server.common.data.notification.template.EmailDeliveryMethodNotificationTemplate; +import org.thingsboard.server.common.data.notification.template.HasSubject; +import org.thingsboard.server.common.data.notification.template.MobileAppDeliveryMethodNotificationTemplate; +import org.thingsboard.server.common.data.notification.template.NotificationTemplate; +import org.thingsboard.server.common.data.notification.template.NotificationTemplateConfig; +import org.thingsboard.server.common.data.notification.template.SmsDeliveryMethodNotificationTemplate; +import org.thingsboard.server.common.data.notification.template.WebDeliveryMethodNotificationTemplate; +import org.thingsboard.server.common.data.oauth2.PlatformType; import org.thingsboard.server.common.data.page.PageData; +import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.common.data.page.TimePageLink; +import org.thingsboard.server.common.data.query.EntityDataPageLink; +import org.thingsboard.server.common.data.query.EntityDataQuery; +import org.thingsboard.server.common.data.query.EntityDataSortOrder; +import org.thingsboard.server.common.data.query.EntityKey; +import org.thingsboard.server.common.data.query.EntityKeyType; +import org.thingsboard.server.common.data.query.EntityTypeFilter; +import org.thingsboard.server.common.data.security.DeviceCredentials; import org.thingsboard.server.msa.AbstractContainerTest; import org.thingsboard.server.msa.TestProperties; import javax.net.ssl.SSLContext; +import java.util.Arrays; +import java.util.HashMap; import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; import static org.assertj.core.api.Assertions.assertThat; +import static org.thingsboard.server.common.data.notification.NotificationDeliveryMethod.EMAIL; +import static org.thingsboard.server.common.data.notification.NotificationDeliveryMethod.MICROSOFT_TEAMS; +import static org.thingsboard.server.common.data.notification.NotificationDeliveryMethod.WEB; import static org.thingsboard.server.msa.prototypes.DevicePrototypes.defaultDevicePrototype; +import static org.thingsboard.server.msa.ui.utils.EntityPrototypes.defaultTenantAdmin; public class JavaRestClientTest extends AbstractContainerTest { + public static final String DEFAULT_NOTIFICATION_SUBJECT = "Just a test"; + public static final NotificationType DEFAULT_NOTIFICATION_TYPE = NotificationType.GENERAL; private RestClient restClient; + private Tenant tenant; + private User user; @BeforeClass public void beforeClass() throws Exception { @@ -77,11 +138,27 @@ public class JavaRestClientTest extends AbstractContainerTest { @BeforeMethod public void setUp() throws Exception { - restClient.login("tenant@thingsboard.org", "tenant"); + restClient.login("sysadmin@thingsboard.org", "sysadmin"); + + // create tenant and tenant admin + tenant = new Tenant(); + tenant.setTitle("Java Rest Client Test Tenant " + RandomStringUtils.randomAlphabetic(5)); + tenant = restClient.saveTenant(tenant); + + String email = RandomStringUtils.randomAlphabetic(5) + "@gmail.com"; + + user = restClient.saveUser(defaultTenantAdmin(tenant.getId(), email), false); + + restClient.activateUser(user.getId(), "password123", false); + restClient.login(email, "password123"); } @AfterMethod public void tearDown() { + restClient.login("sysadmin@thingsboard.org", "sysadmin"); + if (tenant != null) { + restClient.deleteTenant(tenant.getId()); + } } @Test @@ -123,6 +200,243 @@ public class JavaRestClientTest extends AbstractContainerTest { PageData allClearedAlarms = restClient.getAllAlarms(AlarmSearchStatus.CLEARED, null, new TimePageLink(10, 0), null); assertThat(allClearedAlarms.getData()).hasSize(0); + } + + @Test + public void testTimeSeriesByReadTsKvQueries() { + Device device = restClient.saveDevice(defaultDevicePrototype(RandomStringUtils.randomAlphabetic(5))); + assertThat(device).isNotNull(); + + DeviceCredentials deviceCredentials = restClient.getDeviceCredentialsByDeviceId(device.getId()).get(); + for (int i = 0; i < 3; i++) { + JsonObject values = new JsonObject(); + values.addProperty("temperature", i + 25); + testRestClient.postTelemetry(deviceCredentials.getCredentialsId(), JacksonUtil.toJsonNode(createPayload().toString())); + } + + restClient.saveEntityTelemetry(device.getId(), "ts", JacksonUtil.toJsonNode("{\"temperature\": 25, \"humidity\": 60}")); + restClient.saveEntityTelemetry(device.getId(), "ts", JacksonUtil.toJsonNode("{\"temperature\": 27, \"humidity\": 59}")); + restClient.saveEntityTelemetry(device.getId(), "ts", JacksonUtil.toJsonNode("{\"temperature\": 33, \"humidity\": 62}")); + + EntityTypeFilter filter = new EntityTypeFilter(); + filter.setEntityType(EntityType.DEVICE); + var pageLink = new EntityDataPageLink(20, 0, null, new EntityDataSortOrder(new EntityKey(EntityKeyType.ENTITY_FIELD, "createdTime"), EntityDataSortOrder.Direction.DESC), false); + + var entityFields = Arrays.asList(new EntityKey(EntityKeyType.ENTITY_FIELD, "name"), new EntityKey(EntityKeyType.ENTITY_FIELD, "createdTime")); + + EntityDataQuery entityDataQuery = new EntityDataQuery(filter, pageLink, entityFields, null, null); + JsonNode result = restClient.findEntityTimeseriesAndAttributesKeysByQuery(entityDataQuery, true, true, null); + assertThat(result).isNotNull(); + assertThat((ArrayNode)result.get("timeseries")).contains(new TextNode("temperature"), new TextNode("humidity")); + } + + @Test + public void testFindNotifications() { + NotificationTarget notificationTarget = createNotificationTarget(user.getId()); + String notificationText1 = "Notification 1"; + NotificationTemplate notificationTemplate = createNotificationTemplate(DEFAULT_NOTIFICATION_TYPE, DEFAULT_NOTIFICATION_SUBJECT, notificationText1, new NotificationDeliveryMethod[]{WEB}); + NotificationRequest notificationRequest = submitNotificationRequest(notificationTarget.getId(), notificationTemplate.getId()); + + String notificationText2 = "Notification 2"; + NotificationTemplate notificationTemplate2 = createNotificationTemplate(DEFAULT_NOTIFICATION_TYPE, DEFAULT_NOTIFICATION_SUBJECT, notificationText2, new NotificationDeliveryMethod[]{WEB}); + NotificationRequest notificationRequest2 = submitNotificationRequest(notificationTarget.getId(), notificationTemplate2.getId()); + + PageData initialRequests = restClient.getNotificationRequests(new PageLink(30)); + assertThat(initialRequests.getTotalElements()).isGreaterThanOrEqualTo(2); + + NotificationRequestInfo notificationRequestInfo = restClient.getNotificationRequestById(notificationRequest.getId()).get(); + assertThat(notificationRequestInfo.getName()).isEqualTo(notificationRequest.getName()); + assertThat(notificationRequestInfo.getTemplateName()).isEqualTo(notificationTemplate.getName()); + + NotificationRequestPreview requestPreview = restClient.getNotificationRequestPreview(notificationRequest, 10); + assertThat(requestPreview.getTotalRecipientsCount()).isEqualTo(1); + assertThat(requestPreview.getRecipientsPreview()).isEqualTo(List.of(user.getEmail())); + + PageData notifications = restClient.getNotifications(false, WEB, new PageLink(30)); + assertThat(notifications.getTotalElements()).isEqualTo(2); + + Integer unreadCount = restClient.getUnreadNotificationsCount(WEB); + assertThat(unreadCount).isEqualTo(2); + + restClient.markNotificationAsRead(notifications.getData().get(0).getId()); + + Integer unreadCountAfterRead = restClient.getUnreadNotificationsCount(WEB); + assertThat(unreadCountAfterRead).isEqualTo(1); + + restClient.markAllNotificationsAsRead(WEB); + + Integer unreadCountAfterAllRead = restClient.getUnreadNotificationsCount(WEB); + assertThat(unreadCountAfterAllRead).isEqualTo(0); + + restClient.deleteNotification(notifications.getData().get(0).getId()); + notifications = restClient.getNotifications(false, WEB, new PageLink(30)); + assertThat(notifications.getTotalElements()).isEqualTo(1); + + restClient.deleteNotificationRequest(notificationRequest.getId()); + PageData requestsAfterUpdate = restClient.getNotificationRequests(new PageLink(30)); + assertThat(requestsAfterUpdate.getTotalElements()).isEqualTo(initialRequests.getTotalElements() - 1); + List availableDeliveryMethods = restClient.getAvailableDeliveryMethods(); + assertThat(availableDeliveryMethods).contains(WEB, EMAIL, MICROSOFT_TEAMS); + } + + @Test + public void testSaveNotificationSettings() { + NotificationSettings settings = new NotificationSettings(); + SlackNotificationDeliveryMethodConfig slackConfig = new SlackNotificationDeliveryMethodConfig(); + String slackToken = "xoxb-123123123"; + slackConfig.setBotToken(slackToken); + settings.setDeliveryMethodsConfigs(Map.of( + NotificationDeliveryMethod.SLACK, slackConfig + )); + + restClient.saveNotificationSettings(settings); + + NotificationSettings savedSettings = restClient.getNotificationSettings().get(); + assertThat(savedSettings.getDeliveryMethodsConfigs()).hasSize(1); + assertThat(savedSettings.getDeliveryMethodsConfigs().get(slackConfig.getMethod())).isEqualTo(slackConfig); + + // save user notification settings + var entityActionNotificationPref = new UserNotificationSettings.NotificationPref(); + entityActionNotificationPref.setEnabled(true); + entityActionNotificationPref.setEnabledDeliveryMethods(Map.of( + NotificationDeliveryMethod.WEB, true, + NotificationDeliveryMethod.SMS, false, + NotificationDeliveryMethod.EMAIL, false + )); + + var entitiesLimitNotificationPref = new UserNotificationSettings.NotificationPref(); + entitiesLimitNotificationPref.setEnabled(true); + entitiesLimitNotificationPref.setEnabledDeliveryMethods(Map.of( + NotificationDeliveryMethod.SMS, true, + NotificationDeliveryMethod.WEB, false, + NotificationDeliveryMethod.EMAIL, false + )); + + var apiUsageLimitNotificationPref = new UserNotificationSettings.NotificationPref(); + apiUsageLimitNotificationPref.setEnabled(false); + apiUsageLimitNotificationPref.setEnabledDeliveryMethods(Map.of( + NotificationDeliveryMethod.WEB, true, + NotificationDeliveryMethod.SMS, false, + NotificationDeliveryMethod.EMAIL, false + )); + + UserNotificationSettings userNotificationSettings = new UserNotificationSettings(Map.of( + NotificationType.ENTITY_ACTION, entityActionNotificationPref, + NotificationType.ENTITIES_LIMIT, entitiesLimitNotificationPref, + NotificationType.API_USAGE_LIMIT, apiUsageLimitNotificationPref + )); + UserNotificationSettings saved = restClient.saveUserNotificationSettings(userNotificationSettings); + UserNotificationSettings retrieved = restClient.getUserNotificationSettings().get(); + assertThat(retrieved).isEqualTo(saved); + } + + @Test + public void testSaveDomain() { + restClient.login("sysadmin@thingsboard.org", "sysadmin"); + + Domain domain = new Domain(); + String prefix = RandomStringUtils.randomAlphabetic(5).toLowerCase(); + domain.setName(prefix + ".test.com"); + Domain savedDomain = restClient.saveDomain(domain); + assertThat(savedDomain.getName()).isEqualTo(domain.getName()); + + PageData tenantDomainInfos = restClient.getTenantDomainInfos(new PageLink(10)); + List domainInfos = tenantDomainInfos.getData().stream().filter(domainInfo -> domainInfo.getName().startsWith(prefix)).toList(); + assertThat(domainInfos).hasSize(1); + } + + @Test + public void testSaveMobileApp() { + restClient.login("sysadmin@thingsboard.org", "sysadmin"); + + MobileApp mobileApp = new MobileApp(); + String prefix = RandomStringUtils.randomAlphabetic(5).toLowerCase(); + mobileApp.setPkgName(prefix + "test.app.apple"); + mobileApp.setPlatformType(PlatformType.ANDROID); + mobileApp.setAppSecret(RandomStringUtils.randomAlphabetic(20)); + mobileApp.setStatus(MobileAppStatus.DRAFT); + + MobileApp savedMobileApp = restClient.saveMobileApp(mobileApp); + assertThat(savedMobileApp.getName()).isEqualTo(mobileApp.getName()); + + PageData mobileApps = restClient.getTenantMobileApps(new PageLink(10)); + List retrieved = mobileApps.getData().stream().filter(app -> app.getPkgName().startsWith(prefix)).toList(); + assertThat(retrieved).hasSize(1); + + MobileAppBundle mobileAppBundle = new MobileAppBundle(); + String bundlePrefix = RandomStringUtils.randomAlphabetic(5).toLowerCase(); + mobileAppBundle.setTitle(bundlePrefix + "Test Bundle"); + mobileAppBundle.setAndroidAppId(savedMobileApp.getId()); + + MobileAppBundle savedMobileAppBundle = restClient.saveMobileBundle(mobileAppBundle); + PageData mobileBundleInfos = restClient.getTenantMobileBundleInfos(new PageLink(10)); + List bundleInfos = mobileBundleInfos.getData().stream().filter(mobileAppBundleInfo -> mobileAppBundleInfo.getTitle().startsWith(bundlePrefix)).toList(); + assertThat(bundleInfos).hasSize(1); + } + + protected NotificationTarget createNotificationTarget(UserId... usersIds) { + UserListFilter filter = new UserListFilter(); + filter.setUsersIds(Arrays.stream(usersIds).map(UUIDBased::getId).toList()); + return createNotificationTarget(filter); + } + + protected NotificationTarget createNotificationTarget(UsersFilter usersFilter) { + NotificationTarget notificationTarget = new NotificationTarget(); + notificationTarget.setName(usersFilter.toString() + org.apache.commons.lang3.RandomStringUtils.randomNumeric(5)); + PlatformUsersNotificationTargetConfig targetConfig = new PlatformUsersNotificationTargetConfig(); + targetConfig.setUsersFilter(usersFilter); + notificationTarget.setConfiguration(targetConfig); + return restClient.createNotificationTarget(notificationTarget); + } + + protected NotificationTemplate createNotificationTemplate(NotificationType notificationType, String subject, + String text, NotificationDeliveryMethod... deliveryMethods) { + NotificationTemplate notificationTemplate = new NotificationTemplate(); + notificationTemplate.setName("Notification template: " + RandomStringUtils.randomAlphabetic(5)); + notificationTemplate.setNotificationType(notificationType); + NotificationTemplateConfig config = new NotificationTemplateConfig(); + config.setDeliveryMethodsTemplates(new HashMap<>()); + for (NotificationDeliveryMethod deliveryMethod : deliveryMethods) { + DeliveryMethodNotificationTemplate deliveryMethodNotificationTemplate; + switch (deliveryMethod) { + case WEB: { + deliveryMethodNotificationTemplate = new WebDeliveryMethodNotificationTemplate(); + break; + } + case EMAIL: { + deliveryMethodNotificationTemplate = new EmailDeliveryMethodNotificationTemplate(); + break; + } + case SMS: { + deliveryMethodNotificationTemplate = new SmsDeliveryMethodNotificationTemplate(); + break; + } + case MOBILE_APP: + deliveryMethodNotificationTemplate = new MobileAppDeliveryMethodNotificationTemplate(); + break; + default: + throw new IllegalArgumentException("Unsupported delivery method " + deliveryMethod); + } + deliveryMethodNotificationTemplate.setEnabled(true); + deliveryMethodNotificationTemplate.setBody(text); + if (deliveryMethodNotificationTemplate instanceof HasSubject) { + ((HasSubject) deliveryMethodNotificationTemplate).setSubject(subject); + } + config.getDeliveryMethodsTemplates().put(deliveryMethod, deliveryMethodNotificationTemplate); + } + notificationTemplate.setConfiguration(config); + return restClient.createNotificationTemplate(notificationTemplate); + } + + protected NotificationRequest submitNotificationRequest(NotificationTargetId targetId, NotificationTemplateId notificationTemplateId) { + NotificationRequestConfig config = new NotificationRequestConfig(); + config.setSendingDelayInSec(0); + NotificationRequest notificationRequest = NotificationRequest.builder() + .targets(List.of(targetId).stream().map(UUIDBased::getId).collect(Collectors.toList())) + .templateId(notificationTemplateId) + .additionalConfig(config) + .build(); + return restClient.createNotificationRequest(notificationRequest); } } 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 2c8e01d246..28304de049 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 @@ -144,6 +144,8 @@ 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.notification.targets.NotificationTarget; +import org.thingsboard.server.common.data.notification.template.NotificationTemplate; import org.thingsboard.server.common.data.oauth2.OAuth2Client; import org.thingsboard.server.common.data.oauth2.OAuth2ClientInfo; import org.thingsboard.server.common.data.oauth2.OAuth2ClientLoginInfo; @@ -1824,12 +1826,24 @@ public class RestClient implements Closeable { }).getBody(); } - public JsonNode findEntityTimeseriesAndAttributesKeysByQuery(EntityDataQuery query) { + public JsonNode findEntityTimeseriesAndAttributesKeysByQuery(EntityDataQuery query, boolean isTimeseries, boolean isAttributes, String scope) { + Map params = new HashMap<>(); + params.put("timeseries", String.valueOf(isTimeseries)); + params.put("attributes", String.valueOf(isAttributes)); + + StringBuilder urlBuilder = new StringBuilder(baseURL); + urlBuilder.append("/api/entitiesQuery/find/keys?timeseries={timeseries}&attributes={attributes}"); + + if (scope != null) { + urlBuilder.append("&scope={scope}"); + params.put("scope", scope); + } return restTemplate.exchange( - baseURL + "/api/entitiesQuery/find/keys", + urlBuilder.toString(), HttpMethod.POST, new HttpEntity<>(query), new ParameterizedTypeReference() { - }).getBody(); + }, + params).getBody(); } public PageData findAlarmDataByQuery(AlarmDataQuery query) { @@ -2288,7 +2302,8 @@ public class RestClient implements Closeable { HttpMethod.GET, HttpEntity.EMPTY, new ParameterizedTypeReference>() { - }).getBody(); + }, + params).getBody(); } public Optional getDomainInfoById(DomainId domainId) { @@ -2324,7 +2339,8 @@ public class RestClient implements Closeable { HttpMethod.GET, HttpEntity.EMPTY, new ParameterizedTypeReference>() { - }).getBody(); + }, + params).getBody(); } public Optional getMobileAppById(MobileAppId mobileAppId) { @@ -2356,7 +2372,8 @@ public class RestClient implements Closeable { HttpMethod.GET, HttpEntity.EMPTY, new ParameterizedTypeReference>() { - }).getBody(); + }, + params).getBody(); } public Optional getMobileBundleById(MobileAppBundleId mobileAppBundleId) { @@ -4283,11 +4300,23 @@ public class RestClient implements Closeable { } } - public PageData getNotifications(PageLink pageLink) { + public PageData getNotifications(Boolean unreadOnly, NotificationDeliveryMethod deliveryMethod, PageLink pageLink) { Map params = new HashMap<>(); + + StringBuilder urlBuilder = new StringBuilder(); + urlBuilder.append(baseURL).append("/api/notifications?").append(getUrlParams(pageLink)); addPageLinkToParam(params, pageLink); - return restTemplate.exchange( - baseURL + "/api/notifications?" + getUrlParams(pageLink), + + if (unreadOnly != null) { + urlBuilder.append("&unreadOnly={unreadOnly}"); + params.put("unreadOnly", unreadOnly.toString()); + } + if (deliveryMethod != null) { + urlBuilder.append("&deliveryMethod={deliveryMethod}"); + params.put("deliveryMethod", deliveryMethod.name()); + } + + return restTemplate.exchange(urlBuilder.toString(), HttpMethod.GET, HttpEntity.EMPTY, new ParameterizedTypeReference>() { @@ -4328,7 +4357,8 @@ public class RestClient implements Closeable { baseURL + uri, HttpMethod.PUT, HttpEntity.EMPTY, - Void.class); + Void.class, + params); } @@ -4415,6 +4445,14 @@ public class RestClient implements Closeable { } } + public NotificationTarget createNotificationTarget(NotificationTarget notificationTarget) { + return restTemplate.postForEntity(baseURL + "/api/notification/target", notificationTarget, NotificationTarget.class).getBody(); + } + + public NotificationTemplate createNotificationTemplate(NotificationTemplate notificationTemplate) { + return restTemplate.postForEntity(baseURL + "/api/notification/template", notificationTemplate, NotificationTemplate.class).getBody(); + } + public AiModel saveAiModel(AiModel aiModel) { return restTemplate.postForEntity(baseURL + "/api/ai/model", aiModel, AiModel.class).getBody(); } From 9da2b57a6c465b9540ac4c0c940dcfda108776c6 Mon Sep 17 00:00:00 2001 From: dashevchenko Date: Tue, 2 Dec 2025 16:27:14 +0200 Subject: [PATCH 02/32] fixed compilation after merge with master --- .../server/msa/connectivity/JavaRestClientTest.java | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/connectivity/JavaRestClientTest.java b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/connectivity/JavaRestClientTest.java index 7cee0a6ba6..f96f9aa953 100644 --- a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/connectivity/JavaRestClientTest.java +++ b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/connectivity/JavaRestClientTest.java @@ -80,6 +80,7 @@ import org.thingsboard.server.common.data.oauth2.PlatformType; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.common.data.page.TimePageLink; +import org.thingsboard.server.common.data.query.AvailableEntityKeys; import org.thingsboard.server.common.data.query.EntityDataPageLink; import org.thingsboard.server.common.data.query.EntityDataQuery; import org.thingsboard.server.common.data.query.EntityDataSortOrder; @@ -225,9 +226,9 @@ public class JavaRestClientTest extends AbstractContainerTest { var entityFields = Arrays.asList(new EntityKey(EntityKeyType.ENTITY_FIELD, "name"), new EntityKey(EntityKeyType.ENTITY_FIELD, "createdTime")); EntityDataQuery entityDataQuery = new EntityDataQuery(filter, pageLink, entityFields, null, null); - JsonNode result = restClient.findEntityTimeseriesAndAttributesKeysByQuery(entityDataQuery, true, true, null); - assertThat(result).isNotNull(); - assertThat((ArrayNode)result.get("timeseries")).contains(new TextNode("temperature"), new TextNode("humidity")); + AvailableEntityKeys availableEntityKeys = restClient.findAvailableEntityKeysByQuery(entityDataQuery, true, true, null); + assertThat(availableEntityKeys).isNotNull(); + assertThat(availableEntityKeys.timeseries()).contains("temperature", "humidity"); } @Test From bf4ffc8f9c7ae5b60e893cacdb46b3d24c6f9e7b Mon Sep 17 00:00:00 2001 From: dashevchenko Date: Tue, 2 Dec 2025 16:50:18 +0200 Subject: [PATCH 03/32] refactoring --- .../msa/connectivity/JavaRestClientTest.java | 39 +++---------------- 1 file changed, 6 insertions(+), 33 deletions(-) diff --git a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/connectivity/JavaRestClientTest.java b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/connectivity/JavaRestClientTest.java index f96f9aa953..9d0292cc7e 100644 --- a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/connectivity/JavaRestClientTest.java +++ b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/connectivity/JavaRestClientTest.java @@ -15,9 +15,6 @@ */ package org.thingsboard.server.msa.connectivity; -import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.node.ArrayNode; -import com.fasterxml.jackson.databind.node.TextNode; import com.google.gson.JsonObject; import org.apache.hc.client5.http.impl.classic.CloseableHttpClient; import org.apache.hc.client5.http.impl.classic.HttpClients; @@ -67,7 +64,6 @@ import org.thingsboard.server.common.data.notification.settings.UserNotification import org.thingsboard.server.common.data.notification.targets.NotificationTarget; import org.thingsboard.server.common.data.notification.targets.platform.PlatformUsersNotificationTargetConfig; import org.thingsboard.server.common.data.notification.targets.platform.UserListFilter; -import org.thingsboard.server.common.data.notification.targets.platform.UsersFilter; import org.thingsboard.server.common.data.notification.template.DeliveryMethodNotificationTemplate; import org.thingsboard.server.common.data.notification.template.EmailDeliveryMethodNotificationTemplate; import org.thingsboard.server.common.data.notification.template.HasSubject; @@ -147,9 +143,7 @@ public class JavaRestClientTest extends AbstractContainerTest { tenant = restClient.saveTenant(tenant); String email = RandomStringUtils.randomAlphabetic(5) + "@gmail.com"; - user = restClient.saveUser(defaultTenantAdmin(tenant.getId(), email), false); - restClient.activateUser(user.getId(), "password123", false); restClient.login(email, "password123"); } @@ -306,26 +300,8 @@ public class JavaRestClientTest extends AbstractContainerTest { NotificationDeliveryMethod.EMAIL, false )); - var entitiesLimitNotificationPref = new UserNotificationSettings.NotificationPref(); - entitiesLimitNotificationPref.setEnabled(true); - entitiesLimitNotificationPref.setEnabledDeliveryMethods(Map.of( - NotificationDeliveryMethod.SMS, true, - NotificationDeliveryMethod.WEB, false, - NotificationDeliveryMethod.EMAIL, false - )); - - var apiUsageLimitNotificationPref = new UserNotificationSettings.NotificationPref(); - apiUsageLimitNotificationPref.setEnabled(false); - apiUsageLimitNotificationPref.setEnabledDeliveryMethods(Map.of( - NotificationDeliveryMethod.WEB, true, - NotificationDeliveryMethod.SMS, false, - NotificationDeliveryMethod.EMAIL, false - )); - UserNotificationSettings userNotificationSettings = new UserNotificationSettings(Map.of( - NotificationType.ENTITY_ACTION, entityActionNotificationPref, - NotificationType.ENTITIES_LIMIT, entitiesLimitNotificationPref, - NotificationType.API_USAGE_LIMIT, apiUsageLimitNotificationPref + NotificationType.ENTITY_ACTION, entityActionNotificationPref )); UserNotificationSettings saved = restClient.saveUserNotificationSettings(userNotificationSettings); UserNotificationSettings retrieved = restClient.getUserNotificationSettings().get(); @@ -376,22 +352,19 @@ public class JavaRestClientTest extends AbstractContainerTest { assertThat(bundleInfos).hasSize(1); } - protected NotificationTarget createNotificationTarget(UserId... usersIds) { + private NotificationTarget createNotificationTarget(UserId... usersIds) { UserListFilter filter = new UserListFilter(); filter.setUsersIds(Arrays.stream(usersIds).map(UUIDBased::getId).toList()); - return createNotificationTarget(filter); - } - protected NotificationTarget createNotificationTarget(UsersFilter usersFilter) { NotificationTarget notificationTarget = new NotificationTarget(); - notificationTarget.setName(usersFilter.toString() + org.apache.commons.lang3.RandomStringUtils.randomNumeric(5)); + notificationTarget.setName(filter.toString() + org.apache.commons.lang3.RandomStringUtils.randomNumeric(5)); PlatformUsersNotificationTargetConfig targetConfig = new PlatformUsersNotificationTargetConfig(); - targetConfig.setUsersFilter(usersFilter); + targetConfig.setUsersFilter(filter); notificationTarget.setConfiguration(targetConfig); return restClient.createNotificationTarget(notificationTarget); } - protected NotificationTemplate createNotificationTemplate(NotificationType notificationType, String subject, + private NotificationTemplate createNotificationTemplate(NotificationType notificationType, String subject, String text, NotificationDeliveryMethod... deliveryMethods) { NotificationTemplate notificationTemplate = new NotificationTemplate(); notificationTemplate.setName("Notification template: " + RandomStringUtils.randomAlphabetic(5)); @@ -430,7 +403,7 @@ public class JavaRestClientTest extends AbstractContainerTest { return restClient.createNotificationTemplate(notificationTemplate); } - protected NotificationRequest submitNotificationRequest(NotificationTargetId targetId, NotificationTemplateId notificationTemplateId) { + private NotificationRequest submitNotificationRequest(NotificationTargetId targetId, NotificationTemplateId notificationTemplateId) { NotificationRequestConfig config = new NotificationRequestConfig(); config.setSendingDelayInSec(0); NotificationRequest notificationRequest = NotificationRequest.builder() From ccde77036a2ee10f328ee8d3097af9f986e1828a Mon Sep 17 00:00:00 2001 From: dashevchenko Date: Tue, 2 Dec 2025 16:55:32 +0200 Subject: [PATCH 04/32] refactoring --- .../msa/connectivity/JavaRestClientTest.java | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/connectivity/JavaRestClientTest.java b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/connectivity/JavaRestClientTest.java index 9d0292cc7e..d439bdcf48 100644 --- a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/connectivity/JavaRestClientTest.java +++ b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/connectivity/JavaRestClientTest.java @@ -318,9 +318,8 @@ public class JavaRestClientTest extends AbstractContainerTest { Domain savedDomain = restClient.saveDomain(domain); assertThat(savedDomain.getName()).isEqualTo(domain.getName()); - PageData tenantDomainInfos = restClient.getTenantDomainInfos(new PageLink(10)); - List domainInfos = tenantDomainInfos.getData().stream().filter(domainInfo -> domainInfo.getName().startsWith(prefix)).toList(); - assertThat(domainInfos).hasSize(1); + PageData domainInfos = restClient.getTenantDomainInfos(new PageLink(10, 0 , prefix)); + assertThat(domainInfos.getData()).hasSize(1); } @Test @@ -337,9 +336,8 @@ public class JavaRestClientTest extends AbstractContainerTest { MobileApp savedMobileApp = restClient.saveMobileApp(mobileApp); assertThat(savedMobileApp.getName()).isEqualTo(mobileApp.getName()); - PageData mobileApps = restClient.getTenantMobileApps(new PageLink(10)); - List retrieved = mobileApps.getData().stream().filter(app -> app.getPkgName().startsWith(prefix)).toList(); - assertThat(retrieved).hasSize(1); + PageData retrieved = restClient.getTenantMobileApps(new PageLink(10, 0, prefix)); + assertThat(retrieved.getData()).hasSize(1); MobileAppBundle mobileAppBundle = new MobileAppBundle(); String bundlePrefix = RandomStringUtils.randomAlphabetic(5).toLowerCase(); @@ -347,9 +345,8 @@ public class JavaRestClientTest extends AbstractContainerTest { mobileAppBundle.setAndroidAppId(savedMobileApp.getId()); MobileAppBundle savedMobileAppBundle = restClient.saveMobileBundle(mobileAppBundle); - PageData mobileBundleInfos = restClient.getTenantMobileBundleInfos(new PageLink(10)); - List bundleInfos = mobileBundleInfos.getData().stream().filter(mobileAppBundleInfo -> mobileAppBundleInfo.getTitle().startsWith(bundlePrefix)).toList(); - assertThat(bundleInfos).hasSize(1); + PageData bundleInfos = restClient.getTenantMobileBundleInfos(new PageLink(10, 0, bundlePrefix)); + assertThat(bundleInfos.getData()).hasSize(1); } private NotificationTarget createNotificationTarget(UserId... usersIds) { From 3704e75838c3145dcd7eb122699457c93a564a92 Mon Sep 17 00:00:00 2001 From: Vladyslav_Prykhodko Date: Thu, 4 Dec 2025 12:59:27 +0200 Subject: [PATCH 05/32] UI: Fix CVE-2025-66035 --- ui-ngx/patches/@angular+common+18.2.13.patch | 66 ++++++++++++++++++++ 1 file changed, 66 insertions(+) create mode 100644 ui-ngx/patches/@angular+common+18.2.13.patch diff --git a/ui-ngx/patches/@angular+common+18.2.13.patch b/ui-ngx/patches/@angular+common+18.2.13.patch new file mode 100644 index 0000000000..984562138a --- /dev/null +++ b/ui-ngx/patches/@angular+common+18.2.13.patch @@ -0,0 +1,66 @@ +diff --git a/node_modules/@angular/common/esm2022/http/src/xsrf.mjs b/node_modules/@angular/common/esm2022/http/src/xsrf.mjs +index da69c17..d17f6ad 100755 +--- a/node_modules/@angular/common/esm2022/http/src/xsrf.mjs ++++ b/node_modules/@angular/common/esm2022/http/src/xsrf.mjs +@@ -19,6 +19,10 @@ export const XSRF_HEADER_NAME = new InjectionToken(ngDevMode ? 'XSRF_HEADER_NAME + providedIn: 'root', + factory: () => XSRF_DEFAULT_HEADER_NAME, + }); ++/** ++ * Regex to match absolute URLs, including protocol-relative URLs. ++ */ ++const ABSOLUTE_URL_REGEX = /^(?:https?:)?\/\//i; + /** + * Retrieves the current XSRF token to use with the next outgoing request. + * +@@ -69,7 +73,6 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.13", ngImpo + args: [XSRF_COOKIE_NAME] + }] }] }); + export function xsrfInterceptorFn(req, next) { +- const lcUrl = req.url.toLowerCase(); + // Skip both non-mutating requests and absolute URLs. + // Non-mutating requests don't require a token, and absolute URLs require special handling + // anyway as the cookie set +@@ -77,8 +80,7 @@ export function xsrfInterceptorFn(req, next) { + if (!inject(XSRF_ENABLED) || + req.method === 'GET' || + req.method === 'HEAD' || +- lcUrl.startsWith('http://') || +- lcUrl.startsWith('https://')) { ++ ABSOLUTE_URL_REGEX.test(req.url)) { + return next(req); + } + const token = inject(HttpXsrfTokenExtractor).getToken(); +diff --git a/node_modules/@angular/common/fesm2022/http.mjs b/node_modules/@angular/common/fesm2022/http.mjs +index 1655480..d1dbb38 100755 +--- a/node_modules/@angular/common/fesm2022/http.mjs ++++ b/node_modules/@angular/common/fesm2022/http.mjs +@@ -2352,6 +2352,10 @@ const XSRF_HEADER_NAME = new InjectionToken(ngDevMode ? 'XSRF_HEADER_NAME' : '', + providedIn: 'root', + factory: () => XSRF_DEFAULT_HEADER_NAME, + }); ++/** ++ * Regex to match absolute URLs, including protocol-relative URLs. ++ */ ++const ABSOLUTE_URL_REGEX = /^(?:https?:)?\/\//i; + /** + * Retrieves the current XSRF token to use with the next outgoing request. + * +@@ -2402,7 +2406,6 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.13", ngImpo + args: [XSRF_COOKIE_NAME] + }] }] }); + function xsrfInterceptorFn(req, next) { +- const lcUrl = req.url.toLowerCase(); + // Skip both non-mutating requests and absolute URLs. + // Non-mutating requests don't require a token, and absolute URLs require special handling + // anyway as the cookie set +@@ -2410,8 +2413,7 @@ function xsrfInterceptorFn(req, next) { + if (!inject(XSRF_ENABLED) || + req.method === 'GET' || + req.method === 'HEAD' || +- lcUrl.startsWith('http://') || +- lcUrl.startsWith('https://')) { ++ ABSOLUTE_URL_REGEX.test(req.url)) { + return next(req); + } + const token = inject(HttpXsrfTokenExtractor).getToken(); From 2bc8ec0215cd93606a2334d15c27970d40ac0903 Mon Sep 17 00:00:00 2001 From: samuel Date: Thu, 4 Dec 2025 15:35:18 +0800 Subject: [PATCH 06/32] fix: Error 'TypeError: Missing parameter name ...' when running TB_ENABLE_PROXY=true --- msa/web-ui/server.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/msa/web-ui/server.ts b/msa/web-ui/server.ts index bee2e5af31..db0a464499 100644 --- a/msa/web-ui/server.ts +++ b/msa/web-ui/server.ts @@ -81,12 +81,12 @@ let connections: Socket[] = []; } } }); - app.all('/api/*', (req, res) => { + app.all('/api/*splat', (req, res) => { logger.debug(req.method + ' ' + req.originalUrl); apiProxy.web(req, res); }); - app.all('/static/rulenode/*', (req, res) => { + app.all('/static/rulenode/*splat', (req, res) => { apiProxy.web(req, res); }); From f92e21238300ea8204d59f497b6699de67047320 Mon Sep 17 00:00:00 2001 From: Vladyslav_Prykhodko Date: Thu, 4 Dec 2025 16:13:56 +0200 Subject: [PATCH 07/32] UI: Fix CVE-2025-66412 --- .../patches/@angular+compiler+18.2.13.patch | 88 +++++++++++++++++++ 1 file changed, 88 insertions(+) create mode 100644 ui-ngx/patches/@angular+compiler+18.2.13.patch diff --git a/ui-ngx/patches/@angular+compiler+18.2.13.patch b/ui-ngx/patches/@angular+compiler+18.2.13.patch new file mode 100644 index 0000000000..a4245cb0e9 --- /dev/null +++ b/ui-ngx/patches/@angular+compiler+18.2.13.patch @@ -0,0 +1,88 @@ +diff --git a/node_modules/@angular/compiler/fesm2022/compiler.mjs b/node_modules/@angular/compiler/fesm2022/compiler.mjs +index a00b189..260e7be 100755 +--- a/node_modules/@angular/compiler/fesm2022/compiler.mjs ++++ b/node_modules/@angular/compiler/fesm2022/compiler.mjs +@@ -18631,6 +18631,7 @@ function SECURITY_SCHEMA() { + 'area|ping', + 'audio|src', + 'a|href', ++ 'a|xlink:href', + 'a|ping', + 'blockquote|cite', + 'body|background', +@@ -18644,6 +18645,75 @@ function SECURITY_SCHEMA() { + 'track|src', + 'video|poster', + 'video|src', ++ ++ // MathML namespace ++ // https://crsrc.org/c/third_party/blink/renderer/core/sanitizer/sanitizer.cc;l=753-768;drc=b3eb16372dcd3317d65e9e0265015e322494edcd;bpv=1;bpt=1 ++ 'annotation|href', ++ 'annotation|xlink:href', ++ 'annotation-xml|href', ++ 'annotation-xml|xlink:href', ++ 'maction|href', ++ 'maction|xlink:href', ++ 'malignmark|href', ++ 'malignmark|xlink:href', ++ 'math|href', ++ 'math|xlink:href', ++ 'mroot|href', ++ 'mroot|xlink:href', ++ 'msqrt|href', ++ 'msqrt|xlink:href', ++ 'merror|href', ++ 'merror|xlink:href', ++ 'mfrac|href', ++ 'mfrac|xlink:href', ++ 'mglyph|href', ++ 'mglyph|xlink:href', ++ 'msub|href', ++ 'msub|xlink:href', ++ 'msup|href', ++ 'msup|xlink:href', ++ 'msubsup|href', ++ 'msubsup|xlink:href', ++ 'mmultiscripts|href', ++ 'mmultiscripts|xlink:href', ++ 'mprescripts|href', ++ 'mprescripts|xlink:href', ++ 'mi|href', ++ 'mi|xlink:href', ++ 'mn|href', ++ 'mn|xlink:href', ++ 'mo|href', ++ 'mo|xlink:href', ++ 'mpadded|href', ++ 'mpadded|xlink:href', ++ 'mphantom|href', ++ 'mphantom|xlink:href', ++ 'mrow|href', ++ 'mrow|xlink:href', ++ 'ms|href', ++ 'ms|xlink:href', ++ 'mspace|href', ++ 'mspace|xlink:href', ++ 'mstyle|href', ++ 'mstyle|xlink:href', ++ 'mtable|href', ++ 'mtable|xlink:href', ++ 'mtd|href', ++ 'mtd|xlink:href', ++ 'mtr|href', ++ 'mtr|xlink:href', ++ 'mtext|href', ++ 'mtext|xlink:href', ++ 'mover|href', ++ 'mover|xlink:href', ++ 'munder|href', ++ 'munder|xlink:href', ++ 'munderover|href', ++ 'munderover|xlink:href', ++ 'semantics|href', ++ 'semantics|xlink:href', ++ 'none|href', ++ 'none|xlink:href', + ]); + registerContext(SecurityContext.RESOURCE_URL, [ + 'applet|code', From 958e44f7d667ce697561f48509f41e372fc1c505 Mon Sep 17 00:00:00 2001 From: IrynaMatveieva Date: Fri, 5 Dec 2025 14:14:17 +0200 Subject: [PATCH 08/32] set ts -1 when default value used --- ...faultCalculatedFieldProcessingService.java | 68 ++++------------- .../ctx/state/BaseCalculatedFieldState.java | 5 ++ .../ctx/state/SimpleCalculatedFieldState.java | 5 +- .../ctx/state/SingleValueArgumentEntry.java | 6 +- .../utils/CalculatedFieldArgumentUtils.java | 75 +++++++++++++++++++ .../cf/CalculatedFieldIntegrationTest.java | 17 ++++- .../script/api/tbel/TbelCfCtx.java | 2 +- 7 files changed, 112 insertions(+), 66 deletions(-) create mode 100644 application/src/main/java/org/thingsboard/server/utils/CalculatedFieldArgumentUtils.java diff --git a/application/src/main/java/org/thingsboard/server/service/cf/DefaultCalculatedFieldProcessingService.java b/application/src/main/java/org/thingsboard/server/service/cf/DefaultCalculatedFieldProcessingService.java index f2a6916751..9ff185bb52 100644 --- a/application/src/main/java/org/thingsboard/server/service/cf/DefaultCalculatedFieldProcessingService.java +++ b/application/src/main/java/org/thingsboard/server/service/cf/DefaultCalculatedFieldProcessingService.java @@ -23,7 +23,6 @@ import jakarta.annotation.PostConstruct; import jakarta.annotation.PreDestroy; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; -import org.apache.commons.lang3.math.NumberUtils; import org.springframework.stereotype.Service; import org.thingsboard.common.util.ThingsBoardExecutors; import org.thingsboard.server.actors.calculatedField.CalculatedFieldTelemetryMsg; @@ -31,21 +30,14 @@ import org.thingsboard.server.actors.calculatedField.MultipleTbCallback; import org.thingsboard.server.cluster.TbClusterService; import org.thingsboard.server.common.data.DataConstants; import org.thingsboard.server.common.data.EntityType; -import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.cf.configuration.Argument; import org.thingsboard.server.common.data.cf.configuration.OutputType; 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.kv.Aggregation; -import org.thingsboard.server.common.data.kv.BaseAttributeKvEntry; import org.thingsboard.server.common.data.kv.BaseReadTsKvQuery; -import org.thingsboard.server.common.data.kv.BasicTsKvEntry; -import org.thingsboard.server.common.data.kv.BooleanDataEntry; -import org.thingsboard.server.common.data.kv.DoubleDataEntry; -import org.thingsboard.server.common.data.kv.KvEntry; import org.thingsboard.server.common.data.kv.ReadTsKvQuery; -import org.thingsboard.server.common.data.kv.StringDataEntry; import org.thingsboard.server.common.data.kv.TsKvEntry; import org.thingsboard.server.common.data.msg.TbMsgType; import org.thingsboard.server.common.data.tenant.profile.DefaultTenantProfileConfiguration; @@ -70,9 +62,6 @@ import org.thingsboard.server.service.cf.ctx.CalculatedFieldEntityCtxId; import org.thingsboard.server.service.cf.ctx.state.ArgumentEntry; import org.thingsboard.server.service.cf.ctx.state.CalculatedFieldCtx; import org.thingsboard.server.service.cf.ctx.state.CalculatedFieldState; -import org.thingsboard.server.service.cf.ctx.state.ScriptCalculatedFieldState; -import org.thingsboard.server.service.cf.ctx.state.SimpleCalculatedFieldState; -import org.thingsboard.server.service.cf.ctx.state.SingleValueArgumentEntry; import org.thingsboard.server.service.cf.ctx.state.TsRollingArgumentEntry; import java.util.ArrayList; @@ -80,12 +69,16 @@ import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Map.Entry; -import java.util.Optional; import java.util.UUID; import java.util.concurrent.ExecutionException; import java.util.stream.Collectors; import static org.thingsboard.server.common.data.DataConstants.SCOPE; +import static org.thingsboard.server.service.cf.ctx.state.SingleValueArgumentEntry.DEFAULT_TS; +import static org.thingsboard.server.utils.CalculatedFieldArgumentUtils.createDefaultAttributeEntry; +import static org.thingsboard.server.utils.CalculatedFieldArgumentUtils.createDefaultTsKvEntry; +import static org.thingsboard.server.utils.CalculatedFieldArgumentUtils.createStateByType; +import static org.thingsboard.server.utils.CalculatedFieldArgumentUtils.transformSingleValueArgument; import static org.thingsboard.server.utils.CalculatedFieldUtils.toProto; @TbRuleEngineComponent @@ -244,30 +237,17 @@ public class DefaultCalculatedFieldProcessingService implements CalculatedFieldP private ListenableFuture fetchKvEntry(TenantId tenantId, EntityId entityId, Argument argument) { return switch (argument.getRefEntityKey().getType()) { case TS_ROLLING -> fetchTsRolling(tenantId, entityId, argument); - case ATTRIBUTE -> transformSingleValueArgument( - Futures.transform( - attributesService.find(tenantId, entityId, argument.getRefEntityKey().getScope(), argument.getRefEntityKey().getKey()), - result -> result.or(() -> Optional.of(new BaseAttributeKvEntry(createDefaultKvEntry(argument), System.currentTimeMillis(), 0L))), - calculatedFieldCallbackExecutor) - ); - case TS_LATEST -> transformSingleValueArgument( - Futures.transform( - timeseriesService.findLatest(tenantId, entityId, argument.getRefEntityKey().getKey()), - result -> result.or(() -> Optional.of(new BasicTsKvEntry(System.currentTimeMillis(), createDefaultKvEntry(argument), 0L))), - calculatedFieldCallbackExecutor)); + case ATTRIBUTE -> Futures.transform( + attributesService.find(tenantId, entityId, argument.getRefEntityKey().getScope(), argument.getRefEntityKey().getKey()), + result -> transformSingleValueArgument(result.orElseGet(() -> createDefaultAttributeEntry(argument, DEFAULT_TS))), + calculatedFieldCallbackExecutor); + case TS_LATEST -> Futures.transform( + timeseriesService.findLatest(tenantId, entityId, argument.getRefEntityKey().getKey()), + result -> transformSingleValueArgument(result.orElseGet(() -> createDefaultTsKvEntry(argument, DEFAULT_TS))), + calculatedFieldCallbackExecutor); }; } - private ListenableFuture transformSingleValueArgument(ListenableFuture> kvEntryFuture) { - return Futures.transform(kvEntryFuture, kvEntry -> { - if (kvEntry.isPresent() && kvEntry.get().getValue() != null) { - return ArgumentEntry.createSingleValueArgument(kvEntry.get()); - } else { - return new SingleValueArgumentEntry(); - } - }, calculatedFieldCallbackExecutor); - } - private ListenableFuture fetchTsRolling(TenantId tenantId, EntityId entityId, Argument argument) { long currentTime = System.currentTimeMillis(); long timeWindow = argument.getTimeWindow() == 0 ? System.currentTimeMillis() : argument.getTimeWindow(); @@ -282,28 +262,6 @@ public class DefaultCalculatedFieldProcessingService implements CalculatedFieldP return Futures.transform(tsRollingFuture, tsRolling -> tsRolling == null ? new TsRollingArgumentEntry(limit, timeWindow) : ArgumentEntry.createTsRollingArgument(tsRolling, limit, timeWindow), calculatedFieldCallbackExecutor); } - private KvEntry createDefaultKvEntry(Argument argument) { - String key = argument.getRefEntityKey().getKey(); - String defaultValue = argument.getDefaultValue(); - if (StringUtils.isBlank(defaultValue)) { - return new StringDataEntry(key, null); - } - if (NumberUtils.isParsable(defaultValue)) { - return new DoubleDataEntry(key, Double.parseDouble(defaultValue)); - } - if ("true".equalsIgnoreCase(defaultValue) || "false".equalsIgnoreCase(defaultValue)) { - return new BooleanDataEntry(key, Boolean.parseBoolean(defaultValue)); - } - return new StringDataEntry(key, defaultValue); - } - - private CalculatedFieldState createStateByType(CalculatedFieldCtx ctx) { - return switch (ctx.getCfType()) { - case SIMPLE -> new SimpleCalculatedFieldState(ctx.getArgNames()); - case SCRIPT -> new ScriptCalculatedFieldState(ctx.getArgNames()); - }; - } - private static class TbCallbackWrapper implements TbQueueCallback { private final TbCallback callback; diff --git a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/BaseCalculatedFieldState.java b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/BaseCalculatedFieldState.java index 94a18256a3..856081d7c5 100644 --- a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/BaseCalculatedFieldState.java +++ b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/BaseCalculatedFieldState.java @@ -48,6 +48,11 @@ public abstract class BaseCalculatedFieldState implements CalculatedFieldState { this(new ArrayList<>(), new HashMap<>(), false, DEFAULT_LAST_UPDATE_TS); } + + public long getLatestTimestamp() { + return latestTimestamp == DEFAULT_LAST_UPDATE_TS ? System.currentTimeMillis() : latestTimestamp; + } + @Override public boolean updateState(CalculatedFieldCtx ctx, Map argumentValues) { if (arguments == null) { diff --git a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/SimpleCalculatedFieldState.java b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/SimpleCalculatedFieldState.java index 80f5964582..e80939a952 100644 --- a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/SimpleCalculatedFieldState.java +++ b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/SimpleCalculatedFieldState.java @@ -99,10 +99,9 @@ public class SimpleCalculatedFieldState extends BaseCalculatedFieldState { valuesNode.set(outputName, JacksonUtil.valueToTree(result)); } - long latestTs = getLatestTimestamp(); - if (useLatestTs && latestTs != DEFAULT_LAST_UPDATE_TS) { + if (useLatestTs) { ObjectNode resultNode = JacksonUtil.newObjectNode(); - resultNode.put("ts", latestTs); + resultNode.put("ts", getLatestTimestamp()); resultNode.set("values", valuesNode); return resultNode; } else { diff --git a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/SingleValueArgumentEntry.java b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/SingleValueArgumentEntry.java index 0997fd6cbb..b26e53ef3c 100644 --- a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/SingleValueArgumentEntry.java +++ b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/SingleValueArgumentEntry.java @@ -19,7 +19,6 @@ import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.core.type.TypeReference; import lombok.AllArgsConstructor; import lombok.Data; -import lombok.NoArgsConstructor; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.script.api.tbel.TbelCfArg; import org.thingsboard.script.api.tbel.TbelCfSingleValueArg; @@ -32,13 +31,12 @@ import org.thingsboard.server.common.util.ProtoUtils; import org.thingsboard.server.gen.transport.TransportProtos.AttributeValueProto; import org.thingsboard.server.gen.transport.TransportProtos.TsKvProto; -import static org.thingsboard.server.service.cf.ctx.state.BaseCalculatedFieldState.DEFAULT_LAST_UPDATE_TS; - @Data @AllArgsConstructor public class SingleValueArgumentEntry implements ArgumentEntry { public static final Long DEFAULT_VERSION = -1L; + public static final Long DEFAULT_TS = -1L; private long ts; private BasicKvEntry kvEntryValue; @@ -47,7 +45,7 @@ public class SingleValueArgumentEntry implements ArgumentEntry { private boolean forceResetPrevious; public SingleValueArgumentEntry() { - this.ts = DEFAULT_LAST_UPDATE_TS; + this.ts = DEFAULT_TS; this.version = DEFAULT_VERSION; } diff --git a/application/src/main/java/org/thingsboard/server/utils/CalculatedFieldArgumentUtils.java b/application/src/main/java/org/thingsboard/server/utils/CalculatedFieldArgumentUtils.java new file mode 100644 index 0000000000..1296df6e87 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/utils/CalculatedFieldArgumentUtils.java @@ -0,0 +1,75 @@ +/** + * 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.utils; + +import lombok.NonNull; +import org.apache.commons.lang3.math.NumberUtils; +import org.thingsboard.server.common.data.StringUtils; +import org.thingsboard.server.common.data.cf.configuration.Argument; +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.BooleanDataEntry; +import org.thingsboard.server.common.data.kv.DoubleDataEntry; +import org.thingsboard.server.common.data.kv.KvEntry; +import org.thingsboard.server.common.data.kv.StringDataEntry; +import org.thingsboard.server.common.data.kv.TsKvEntry; +import org.thingsboard.server.service.cf.ctx.state.ArgumentEntry; +import org.thingsboard.server.service.cf.ctx.state.CalculatedFieldCtx; +import org.thingsboard.server.service.cf.ctx.state.CalculatedFieldState; +import org.thingsboard.server.service.cf.ctx.state.ScriptCalculatedFieldState; +import org.thingsboard.server.service.cf.ctx.state.SimpleCalculatedFieldState; +import org.thingsboard.server.service.cf.ctx.state.SingleValueArgumentEntry; + +import static org.thingsboard.server.service.cf.ctx.state.SingleValueArgumentEntry.DEFAULT_VERSION; + +public class CalculatedFieldArgumentUtils { + + public static ArgumentEntry transformSingleValueArgument(@NonNull KvEntry kvEntry) { + return kvEntry.getValue() != null ? ArgumentEntry.createSingleValueArgument(kvEntry) : new SingleValueArgumentEntry(); + } + + public static TsKvEntry createDefaultTsKvEntry(Argument argument, long ts) { + return new BasicTsKvEntry(ts, createDefaultKvEntry(argument), DEFAULT_VERSION); + } + + public static AttributeKvEntry createDefaultAttributeEntry(Argument argument, long ts) { + return new BaseAttributeKvEntry(createDefaultKvEntry(argument), ts, DEFAULT_VERSION); + } + + private static KvEntry createDefaultKvEntry(Argument argument) { + String key = argument.getRefEntityKey().getKey(); + String defaultValue = argument.getDefaultValue(); + if (StringUtils.isBlank(defaultValue)) { + return new StringDataEntry(key, null); + } + if (NumberUtils.isParsable(defaultValue)) { + return new DoubleDataEntry(key, Double.parseDouble(defaultValue)); + } + if ("true".equalsIgnoreCase(defaultValue) || "false".equalsIgnoreCase(defaultValue)) { + return new BooleanDataEntry(key, Boolean.parseBoolean(defaultValue)); + } + return new StringDataEntry(key, defaultValue); + } + + public static CalculatedFieldState createStateByType(CalculatedFieldCtx ctx) { + return switch (ctx.getCfType()) { + case SIMPLE -> new SimpleCalculatedFieldState(ctx.getArgNames()); + case SCRIPT -> new ScriptCalculatedFieldState(ctx.getArgNames()); + }; + } + +} diff --git a/application/src/test/java/org/thingsboard/server/cf/CalculatedFieldIntegrationTest.java b/application/src/test/java/org/thingsboard/server/cf/CalculatedFieldIntegrationTest.java index b500f95d45..03052382f0 100644 --- a/application/src/test/java/org/thingsboard/server/cf/CalculatedFieldIntegrationTest.java +++ b/application/src/test/java/org/thingsboard/server/cf/CalculatedFieldIntegrationTest.java @@ -570,8 +570,6 @@ public class CalculatedFieldIntegrationTest extends CalculatedFieldControllerTes @Test public void testScriptCalculatedFieldWhenUsedLatestTsInScript() throws Exception { Device testDevice = createDevice("Test device", "1234567890"); - long ts = System.currentTimeMillis() - 300000L; - doPost("/api/plugins/telemetry/DEVICE/" + testDevice.getUuidId() + "/timeseries/" + DataConstants.SERVER_SCOPE, JacksonUtil.toJsonNode(String.format("{\"ts\": %s, \"values\": {\"temperature\":30}}", ts))); CalculatedField calculatedField = new CalculatedField(); calculatedField.setEntityId(testDevice.getId()); @@ -585,6 +583,7 @@ public class CalculatedFieldIntegrationTest extends CalculatedFieldControllerTes Argument argument = new Argument(); ReferencedEntityKey refEntityKey = new ReferencedEntityKey("temperature", ArgumentType.TS_LATEST, null); argument.setRefEntityKey(refEntityKey); + argument.setDefaultValue("20"); config.setArguments(Map.of("T", argument)); config.setExpression("return {\"ts\": ctx.latestTs, \"values\": {\"fahrenheitTemp\": (T * 1.8) + 32}};"); @@ -596,7 +595,19 @@ public class CalculatedFieldIntegrationTest extends CalculatedFieldControllerTes CalculatedField savedCalculatedField = doPost("/api/calculatedField", calculatedField, CalculatedField.class); - await().alias("create CF -> perform initial calculation").atMost(TIMEOUT, TimeUnit.SECONDS) + await().alias("create CF -> perform initial calculation with default value").atMost(TIMEOUT, TimeUnit.SECONDS) + .pollInterval(POLL_INTERVAL, TimeUnit.SECONDS) + .untilAsserted(() -> { + ObjectNode fahrenheitTemp = getLatestTelemetry(testDevice.getId(), "fahrenheitTemp"); + assertThat(fahrenheitTemp).isNotNull(); + assertThat(fahrenheitTemp.get("fahrenheitTemp").get(0).get("ts").asText()).isNotEqualTo("-1"); + assertThat(fahrenheitTemp.get("fahrenheitTemp").get(0).get("value").asText()).isEqualTo("68.0"); + }); + + long ts = System.currentTimeMillis() - 10L; + doPost("/api/plugins/telemetry/DEVICE/" + testDevice.getUuidId() + "/timeseries/" + DataConstants.SERVER_SCOPE, JacksonUtil.toJsonNode(String.format("{\"ts\": %s, \"values\": {\"temperature\":30}}", ts))); + + await().alias("update telemetry -> perform calculation").atMost(TIMEOUT, TimeUnit.SECONDS) .pollInterval(POLL_INTERVAL, TimeUnit.SECONDS) .untilAsserted(() -> { ObjectNode fahrenheitTemp = getLatestTelemetry(testDevice.getId(), "fahrenheitTemp"); diff --git a/common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/TbelCfCtx.java b/common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/TbelCfCtx.java index c6023154ea..2fe861ba81 100644 --- a/common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/TbelCfCtx.java +++ b/common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/TbelCfCtx.java @@ -29,7 +29,7 @@ public class TbelCfCtx implements TbelCfObject { public TbelCfCtx(Map args, long latestTs) { this.args = Collections.unmodifiableMap(args); - this.latestTs = latestTs != -1 ? latestTs : System.currentTimeMillis(); + this.latestTs = latestTs; } @Override From e37332a288b532c8f2cdfaae16d578122e9658a2 Mon Sep 17 00:00:00 2001 From: IrynaMatveieva Date: Fri, 5 Dec 2025 17:00:02 +0200 Subject: [PATCH 09/32] handle output update to avoid processing when minor strategy properties updated --- ...CalculatedFieldEntityMessageProcessor.java | 10 +- ...alculatedFieldManagerMessageProcessor.java | 2 + .../EntityInitCalculatedFieldMsg.java | 3 +- .../cf/ctx/state/CalculatedFieldCtx.java | 94 ++++++++++++++++--- 4 files changed, 91 insertions(+), 18 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldEntityMessageProcessor.java b/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldEntityMessageProcessor.java index 2685e7f434..93fd7498e5 100644 --- a/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldEntityMessageProcessor.java +++ b/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldEntityMessageProcessor.java @@ -159,10 +159,14 @@ public class CalculatedFieldEntityMessageProcessor extends AbstractContextAwareM } else { state.setCtx(ctx, actorCtx); } - if (state.isSizeOk()) { - processStateIfReady(state, Collections.emptyMap(), ctx, Collections.singletonList(ctx.getCfId()), null, null, msg.getCallback()); + if (msg.getStateAction() != StateAction.REFRESH_CTX) { + if (state.isSizeOk()) { + processStateIfReady(state, Collections.emptyMap(), ctx, Collections.singletonList(ctx.getCfId()), null, null, msg.getCallback()); + } else { + throw new RuntimeException(ctx.getSizeExceedsLimitMessage()); + } } else { - throw new RuntimeException(ctx.getSizeExceedsLimitMessage()); + msg.getCallback().onSuccess(); } } catch (Exception e) { log.debug("[{}][{}] Failed to initialize CF state", entityId, ctx.getCfId(), e); 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 b19ad1a8b4..97d5d0dcea 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 @@ -455,6 +455,8 @@ public class CalculatedFieldManagerMessageProcessor extends AbstractContextAware stateAction = StateAction.REINIT; // refetch arguments, call state.init, then calculate } else if (newCfCtx.hasContextOnlyChanges(oldCfCtx)) { stateAction = StateAction.REPROCESS; // call state.setCtx, then calculate + } else if (newCfCtx.hasRefreshContextOnlyChanges(oldCfCtx)) { + stateAction = StateAction.REFRESH_CTX; } else { callback.onSuccess(); return; diff --git a/application/src/main/java/org/thingsboard/server/actors/calculatedField/EntityInitCalculatedFieldMsg.java b/application/src/main/java/org/thingsboard/server/actors/calculatedField/EntityInitCalculatedFieldMsg.java index 1e0025988d..49f2c691d3 100644 --- a/application/src/main/java/org/thingsboard/server/actors/calculatedField/EntityInitCalculatedFieldMsg.java +++ b/application/src/main/java/org/thingsboard/server/actors/calculatedField/EntityInitCalculatedFieldMsg.java @@ -39,6 +39,7 @@ public class EntityInitCalculatedFieldMsg implements ToCalculatedFieldSystemMsg INIT, REINIT, RECREATE, - REPROCESS + REPROCESS, + REFRESH_CTX } } diff --git a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/CalculatedFieldCtx.java b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/CalculatedFieldCtx.java index c2c65d3327..489632b9b9 100644 --- a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/CalculatedFieldCtx.java +++ b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/CalculatedFieldCtx.java @@ -37,12 +37,14 @@ import org.thingsboard.server.common.data.cf.configuration.AlarmCalculatedFieldC import org.thingsboard.server.common.data.cf.configuration.Argument; import org.thingsboard.server.common.data.cf.configuration.ArgumentType; import org.thingsboard.server.common.data.cf.configuration.ArgumentsBasedCalculatedFieldConfiguration; +import org.thingsboard.server.common.data.cf.configuration.AttributesImmediateOutputStrategy; import org.thingsboard.server.common.data.cf.configuration.ExpressionBasedCalculatedFieldConfiguration; import org.thingsboard.server.common.data.cf.configuration.Output; import org.thingsboard.server.common.data.cf.configuration.PropagationCalculatedFieldConfiguration; import org.thingsboard.server.common.data.cf.configuration.ReferencedEntityKey; import org.thingsboard.server.common.data.cf.configuration.ScheduledUpdateSupportedCalculatedFieldConfiguration; import org.thingsboard.server.common.data.cf.configuration.SimpleCalculatedFieldConfiguration; +import org.thingsboard.server.common.data.cf.configuration.TimeSeriesImmediateOutputStrategy; import org.thingsboard.server.common.data.cf.configuration.aggregation.AggFunctionInput; import org.thingsboard.server.common.data.cf.configuration.aggregation.RelatedEntitiesAggregationCalculatedFieldConfiguration; import org.thingsboard.server.common.data.cf.configuration.aggregation.single.EntityAggregationCalculatedFieldConfiguration; @@ -623,16 +625,42 @@ public class CalculatedFieldCtx implements Closeable { return new CalculatedFieldEntityCtxId(tenantId, cfId, entityId); } + public boolean hasRefreshContextOnlyChanges(CalculatedFieldCtx other) { // has changes that do not require state recalculation + var thisConfig = calculatedField.getConfiguration(); + var otherConfig = other.getCalculatedField().getConfiguration(); + + var thisOutputStrategy = thisConfig.getOutput().getStrategy(); + var otherOutputStrategy = otherConfig.getOutput().getStrategy(); + + if (!thisOutputStrategy.getType().equals(otherOutputStrategy.getType())) { + return true; + } + + if (thisOutputStrategy instanceof TimeSeriesImmediateOutputStrategy thisTimeSeriesImmediateOutputStrategy + && otherOutputStrategy instanceof TimeSeriesImmediateOutputStrategy otherTimeSeriesImmediateOutputStrategy) { + return thisTimeSeriesImmediateOutputStrategy.getTtl() != otherTimeSeriesImmediateOutputStrategy.getTtl(); + } + + if (thisOutputStrategy instanceof AttributesImmediateOutputStrategy thisAttributesImmediateOutputStrategy + && otherOutputStrategy instanceof AttributesImmediateOutputStrategy otherAttributesImmediateOutputStrategy) { + boolean updateAttributesOnlyOnValueChangeChanged = thisAttributesImmediateOutputStrategy.isUpdateAttributesOnlyOnValueChange() != otherAttributesImmediateOutputStrategy.isUpdateAttributesOnlyOnValueChange(); + boolean sendAttributesUpdatedNotificationUpdated = thisAttributesImmediateOutputStrategy.isSendAttributesUpdatedNotification() != otherAttributesImmediateOutputStrategy.isSendAttributesUpdatedNotification(); + return updateAttributesOnlyOnValueChangeChanged || sendAttributesUpdatedNotificationUpdated; + } + + return false; + } + public boolean hasContextOnlyChanges(CalculatedFieldCtx other) { // has changes that do not require state reinit and will be picked up by the state on the fly if (calculatedField.getConfiguration() instanceof ExpressionBasedCalculatedFieldConfiguration && !Objects.equals(expression, other.expression)) { return true; } - if (!Objects.equals(output, other.output)) { + if (hasOutputChanges(other.output)) { return true; } if (calculatedField.getConfiguration() instanceof SimpleCalculatedFieldConfiguration thisConfig - && other.calculatedField.getConfiguration() instanceof SimpleCalculatedFieldConfiguration otherConfig - && thisConfig.isUseLatestTs() != otherConfig.isUseLatestTs()) { + && other.calculatedField.getConfiguration() instanceof SimpleCalculatedFieldConfiguration otherConfig + && thisConfig.isUseLatestTs() != otherConfig.isUseLatestTs()) { return true; } if (cfType == CalculatedFieldType.ALARM) { @@ -654,14 +682,14 @@ public class CalculatedFieldCtx implements Closeable { return true; } if (calculatedField.getConfiguration() instanceof RelatedEntitiesAggregationCalculatedFieldConfiguration thisConfig - && other.getCalculatedField().getConfiguration() instanceof RelatedEntitiesAggregationCalculatedFieldConfiguration otherConfig - && (thisConfig.getDeduplicationIntervalInSec() != otherConfig.getDeduplicationIntervalInSec() + && other.getCalculatedField().getConfiguration() instanceof RelatedEntitiesAggregationCalculatedFieldConfiguration otherConfig + && (thisConfig.getDeduplicationIntervalInSec() != otherConfig.getDeduplicationIntervalInSec() || !thisConfig.getMetrics().equals(otherConfig.getMetrics()) || thisConfig.isUseLatestTs() != otherConfig.isUseLatestTs())) { return true; } if (calculatedField.getConfiguration() instanceof EntityAggregationCalculatedFieldConfiguration thisConfig - && other.getCalculatedField().getConfiguration() instanceof EntityAggregationCalculatedFieldConfiguration otherConfig) { + && other.getCalculatedField().getConfiguration() instanceof EntityAggregationCalculatedFieldConfiguration otherConfig) { boolean metricsChanged = !Objects.equals(thisConfig.getMetrics(), otherConfig.getMetrics()); boolean watermarkChanged = !Objects.equals(thisConfig.getWatermark(), otherConfig.getWatermark()); return metricsChanged || watermarkChanged; @@ -695,9 +723,47 @@ public class CalculatedFieldCtx implements Closeable { return false; } + private boolean hasOutputChanges(Output otherOutput) { + if (!output.getType().equals(otherOutput.getType())) { + return true; + } + if (!output.getName().equals(otherOutput.getName())) { + return true; + } + if (output.getScope() != (otherOutput.getScope())) { + return true; + } + if (!Objects.equals(output.getDecimalsByDefault(), otherOutput.getDecimalsByDefault())) { + return true; + } + + var thisOutputStrategy = output.getStrategy(); + var otherOutputStrategy = otherOutput.getStrategy(); + + if (thisOutputStrategy instanceof TimeSeriesImmediateOutputStrategy thisTimeSeriesImmediateOutputStrategy + && otherOutputStrategy instanceof TimeSeriesImmediateOutputStrategy otherTimeSeriesImmediateOutputStrategy) { + boolean saveTimeSeriesUpdated = thisTimeSeriesImmediateOutputStrategy.isSaveTimeSeries() != otherTimeSeriesImmediateOutputStrategy.isSaveTimeSeries(); + boolean saveLatestUpdated = thisTimeSeriesImmediateOutputStrategy.isSaveLatest() != otherTimeSeriesImmediateOutputStrategy.isSaveLatest(); + boolean sendWsUpdateUpdated = thisTimeSeriesImmediateOutputStrategy.isSendWsUpdate() != otherTimeSeriesImmediateOutputStrategy.isSendWsUpdate(); + boolean processCfsUpdated = thisTimeSeriesImmediateOutputStrategy.isProcessCfs() != otherTimeSeriesImmediateOutputStrategy.isProcessCfs(); + return saveTimeSeriesUpdated || saveLatestUpdated || sendWsUpdateUpdated || processCfsUpdated; + } + + if (thisOutputStrategy instanceof AttributesImmediateOutputStrategy thisAttributesImmediateOutputStrategy + && otherOutputStrategy instanceof AttributesImmediateOutputStrategy otherAttributesImmediateOutputStrategy) { + + boolean saveTimeSeriesUpdated = thisAttributesImmediateOutputStrategy.isSaveAttribute() != otherAttributesImmediateOutputStrategy.isSaveAttribute(); + boolean sendWsUpdateUpdated = thisAttributesImmediateOutputStrategy.isSendWsUpdate() != otherAttributesImmediateOutputStrategy.isSendWsUpdate(); + boolean processCfsUpdated = thisAttributesImmediateOutputStrategy.isProcessCfs() != otherAttributesImmediateOutputStrategy.isProcessCfs(); + return saveTimeSeriesUpdated || sendWsUpdateUpdated || processCfsUpdated; + } + + return false; + } + private boolean hasGeofencingZoneGroupConfigurationChanges(CalculatedFieldCtx other) { if (calculatedField.getConfiguration() instanceof GeofencingCalculatedFieldConfiguration thisConfig - && other.calculatedField.getConfiguration() instanceof GeofencingCalculatedFieldConfiguration otherConfig) { + && other.calculatedField.getConfiguration() instanceof GeofencingCalculatedFieldConfiguration otherConfig) { return !thisConfig.getZoneGroups().equals(otherConfig.getZoneGroups()); } return false; @@ -705,7 +771,7 @@ public class CalculatedFieldCtx implements Closeable { private boolean hasRelatedEntitiesAggregationConfigurationChanges(CalculatedFieldCtx other) { if (calculatedField.getConfiguration() instanceof RelatedEntitiesAggregationCalculatedFieldConfiguration thisConfig - && other.calculatedField.getConfiguration() instanceof RelatedEntitiesAggregationCalculatedFieldConfiguration otherConfig) { + && other.calculatedField.getConfiguration() instanceof RelatedEntitiesAggregationCalculatedFieldConfiguration otherConfig) { return !thisConfig.getRelation().equals(otherConfig.getRelation()); } return false; @@ -713,7 +779,7 @@ public class CalculatedFieldCtx implements Closeable { private boolean hasEntityAggregationConfigurationChanges(CalculatedFieldCtx other) { if (calculatedField.getConfiguration() instanceof EntityAggregationCalculatedFieldConfiguration thisConfig - && other.calculatedField.getConfiguration() instanceof EntityAggregationCalculatedFieldConfiguration otherConfig) { + && other.calculatedField.getConfiguration() instanceof EntityAggregationCalculatedFieldConfiguration otherConfig) { return !thisConfig.getInterval().equals(otherConfig.getInterval()); } return false; @@ -738,7 +804,7 @@ public class CalculatedFieldCtx implements Closeable { yield true; } yield geofencingState.getLastDynamicArgumentsRefreshTs() < - System.currentTimeMillis() - scheduledUpdateIntervalMillis; + System.currentTimeMillis() - scheduledUpdateIntervalMillis; } default -> false; }; @@ -782,10 +848,10 @@ public class CalculatedFieldCtx implements Closeable { @Override public String toString() { return "CalculatedFieldCtx{" + - "cfId=" + cfId + - ", cfType=" + cfType + - ", entityId=" + entityId + - '}'; + "cfId=" + cfId + + ", cfType=" + cfType + + ", entityId=" + entityId + + '}'; } } From 4e2b4fc9216b554343704fa5b4ddb9d5113b5710 Mon Sep 17 00:00:00 2001 From: IrynaMatveieva Date: Mon, 8 Dec 2025 10:08:18 +0200 Subject: [PATCH 10/32] added getLatestTs method and added tests --- ...faultCalculatedFieldProcessingService.java | 5 +- .../service/cf/ctx/state/ArgumentEntry.java | 2 + .../ctx/state/BaseCalculatedFieldState.java | 22 ++--- .../ctx/state/SimpleCalculatedFieldState.java | 5 +- .../ctx/state/SingleValueArgumentEntry.java | 17 +++- .../cf/ctx/state/TsRollingArgumentEntry.java | 8 ++ .../cf/CalculatedFieldIntegrationTest.java | 98 ++++++++++++++++--- .../script/api/tbel/TbelCfCtx.java | 2 +- 8 files changed, 124 insertions(+), 35 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/service/cf/DefaultCalculatedFieldProcessingService.java b/application/src/main/java/org/thingsboard/server/service/cf/DefaultCalculatedFieldProcessingService.java index 9ff185bb52..b6e1193cfb 100644 --- a/application/src/main/java/org/thingsboard/server/service/cf/DefaultCalculatedFieldProcessingService.java +++ b/application/src/main/java/org/thingsboard/server/service/cf/DefaultCalculatedFieldProcessingService.java @@ -74,7 +74,6 @@ import java.util.concurrent.ExecutionException; import java.util.stream.Collectors; import static org.thingsboard.server.common.data.DataConstants.SCOPE; -import static org.thingsboard.server.service.cf.ctx.state.SingleValueArgumentEntry.DEFAULT_TS; import static org.thingsboard.server.utils.CalculatedFieldArgumentUtils.createDefaultAttributeEntry; import static org.thingsboard.server.utils.CalculatedFieldArgumentUtils.createDefaultTsKvEntry; import static org.thingsboard.server.utils.CalculatedFieldArgumentUtils.createStateByType; @@ -239,11 +238,11 @@ public class DefaultCalculatedFieldProcessingService implements CalculatedFieldP case TS_ROLLING -> fetchTsRolling(tenantId, entityId, argument); case ATTRIBUTE -> Futures.transform( attributesService.find(tenantId, entityId, argument.getRefEntityKey().getScope(), argument.getRefEntityKey().getKey()), - result -> transformSingleValueArgument(result.orElseGet(() -> createDefaultAttributeEntry(argument, DEFAULT_TS))), + result -> transformSingleValueArgument(result.orElseGet(() -> createDefaultAttributeEntry(argument, System.currentTimeMillis()))), calculatedFieldCallbackExecutor); case TS_LATEST -> Futures.transform( timeseriesService.findLatest(tenantId, entityId, argument.getRefEntityKey().getKey()), - result -> transformSingleValueArgument(result.orElseGet(() -> createDefaultTsKvEntry(argument, DEFAULT_TS))), + result -> transformSingleValueArgument(result.orElseGet(() -> createDefaultTsKvEntry(argument, System.currentTimeMillis()))), calculatedFieldCallbackExecutor); }; } diff --git a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/ArgumentEntry.java b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/ArgumentEntry.java index 83e10b8194..3df43d8c2b 100644 --- a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/ArgumentEntry.java +++ b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/ArgumentEntry.java @@ -40,6 +40,8 @@ public interface ArgumentEntry { Object getValue(); + long getLatestTs(); + boolean updateEntry(ArgumentEntry entry); boolean isEmpty(); diff --git a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/BaseCalculatedFieldState.java b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/BaseCalculatedFieldState.java index 856081d7c5..b6d4ad0fac 100644 --- a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/BaseCalculatedFieldState.java +++ b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/BaseCalculatedFieldState.java @@ -48,11 +48,6 @@ public abstract class BaseCalculatedFieldState implements CalculatedFieldState { this(new ArrayList<>(), new HashMap<>(), false, DEFAULT_LAST_UPDATE_TS); } - - public long getLatestTimestamp() { - return latestTimestamp == DEFAULT_LAST_UPDATE_TS ? System.currentTimeMillis() : latestTimestamp; - } - @Override public boolean updateState(CalculatedFieldCtx ctx, Map argumentValues) { if (arguments == null) { @@ -80,7 +75,6 @@ public abstract class BaseCalculatedFieldState implements CalculatedFieldState { if (entryUpdated) { stateUpdated = true; - updateLastUpdateTimestamp(newEntry); } } @@ -116,15 +110,13 @@ public abstract class BaseCalculatedFieldState implements CalculatedFieldState { protected abstract void validateNewEntry(ArgumentEntry newEntry); - private void updateLastUpdateTimestamp(ArgumentEntry entry) { - long newTs = this.latestTimestamp; - if (entry instanceof SingleValueArgumentEntry singleValueArgumentEntry) { - newTs = singleValueArgumentEntry.getTs(); - } else if (entry instanceof TsRollingArgumentEntry tsRollingArgumentEntry) { - Map.Entry lastEntry = tsRollingArgumentEntry.getTsRecords().lastEntry(); - newTs = (lastEntry != null) ? lastEntry.getKey() : DEFAULT_LAST_UPDATE_TS; - } - this.latestTimestamp = Math.max(this.latestTimestamp, newTs); + public long getLatestTimestamp() { + long currentLatestTs = arguments.values().stream() + .mapToLong(ArgumentEntry::getLatestTs) + .max() + .orElse(DEFAULT_LAST_UPDATE_TS); + latestTimestamp = Math.max(currentLatestTs, latestTimestamp); + return latestTimestamp; } } diff --git a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/SimpleCalculatedFieldState.java b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/SimpleCalculatedFieldState.java index e80939a952..80f5964582 100644 --- a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/SimpleCalculatedFieldState.java +++ b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/SimpleCalculatedFieldState.java @@ -99,9 +99,10 @@ public class SimpleCalculatedFieldState extends BaseCalculatedFieldState { valuesNode.set(outputName, JacksonUtil.valueToTree(result)); } - if (useLatestTs) { + long latestTs = getLatestTimestamp(); + if (useLatestTs && latestTs != DEFAULT_LAST_UPDATE_TS) { ObjectNode resultNode = JacksonUtil.newObjectNode(); - resultNode.put("ts", getLatestTimestamp()); + resultNode.put("ts", latestTs); resultNode.set("values", valuesNode); return resultNode; } else { diff --git a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/SingleValueArgumentEntry.java b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/SingleValueArgumentEntry.java index b26e53ef3c..186af92f38 100644 --- a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/SingleValueArgumentEntry.java +++ b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/SingleValueArgumentEntry.java @@ -31,12 +31,13 @@ import org.thingsboard.server.common.util.ProtoUtils; import org.thingsboard.server.gen.transport.TransportProtos.AttributeValueProto; import org.thingsboard.server.gen.transport.TransportProtos.TsKvProto; +import static org.thingsboard.server.service.cf.ctx.state.BaseCalculatedFieldState.DEFAULT_LAST_UPDATE_TS; + @Data @AllArgsConstructor public class SingleValueArgumentEntry implements ArgumentEntry { public static final Long DEFAULT_VERSION = -1L; - public static final Long DEFAULT_TS = -1L; private long ts; private BasicKvEntry kvEntryValue; @@ -45,7 +46,7 @@ public class SingleValueArgumentEntry implements ArgumentEntry { private boolean forceResetPrevious; public SingleValueArgumentEntry() { - this.ts = DEFAULT_TS; + this.ts = DEFAULT_LAST_UPDATE_TS; this.version = DEFAULT_VERSION; } @@ -97,6 +98,11 @@ public class SingleValueArgumentEntry implements ArgumentEntry { return isEmpty() ? null : kvEntryValue.getValue(); } + @Override + public long getLatestTs() { + return !isDefaultValue() ? ts : DEFAULT_LAST_UPDATE_TS; + } + @Override public TbelCfArg toTbelCfArg() { Object value = kvEntryValue.getValue(); @@ -118,7 +124,7 @@ public class SingleValueArgumentEntry implements ArgumentEntry { @Override public boolean updateEntry(ArgumentEntry entry) { if (entry instanceof SingleValueArgumentEntry singleValueEntry) { - if (singleValueEntry.getTs() <= this.ts) { + if (singleValueEntry.getTs() < this.ts) { return false; } @@ -134,4 +140,9 @@ public class SingleValueArgumentEntry implements ArgumentEntry { } return false; } + + public boolean isDefaultValue() { + return DEFAULT_VERSION.equals(this.version); + } + } diff --git a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/TsRollingArgumentEntry.java b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/TsRollingArgumentEntry.java index b5a680a072..ada46a841e 100644 --- a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/TsRollingArgumentEntry.java +++ b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/TsRollingArgumentEntry.java @@ -31,6 +31,8 @@ import java.util.List; import java.util.Map; import java.util.TreeMap; +import static org.thingsboard.server.service.cf.ctx.state.BaseCalculatedFieldState.DEFAULT_LAST_UPDATE_TS; + @Data @NoArgsConstructor @AllArgsConstructor @@ -83,6 +85,12 @@ public class TsRollingArgumentEntry implements ArgumentEntry { return tsRecords; } + @Override + public long getLatestTs() { + var lastEntry = tsRecords.lastEntry(); + return (lastEntry != null) ? lastEntry.getKey() : DEFAULT_LAST_UPDATE_TS; + } + @Override public TbelCfArg toTbelCfArg() { List values = new ArrayList<>(tsRecords.size()); diff --git a/application/src/test/java/org/thingsboard/server/cf/CalculatedFieldIntegrationTest.java b/application/src/test/java/org/thingsboard/server/cf/CalculatedFieldIntegrationTest.java index 03052382f0..c83e8c5ee3 100644 --- a/application/src/test/java/org/thingsboard/server/cf/CalculatedFieldIntegrationTest.java +++ b/application/src/test/java/org/thingsboard/server/cf/CalculatedFieldIntegrationTest.java @@ -45,6 +45,7 @@ import java.util.concurrent.TimeUnit; import static org.assertj.core.api.Assertions.assertThat; import static org.awaitility.Awaitility.await; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; @DaoSqlTest public class CalculatedFieldIntegrationTest extends CalculatedFieldControllerTest { @@ -571,6 +572,9 @@ public class CalculatedFieldIntegrationTest extends CalculatedFieldControllerTes public void testScriptCalculatedFieldWhenUsedLatestTsInScript() throws Exception { Device testDevice = createDevice("Test device", "1234567890"); + long ts = System.currentTimeMillis() - 300000L; + doPost("/api/plugins/telemetry/DEVICE/" + testDevice.getUuidId() + "/timeseries/" + DataConstants.SERVER_SCOPE, JacksonUtil.toJsonNode(String.format("{\"ts\": %s, \"values\": {\"temperature\":30}}", ts))); + CalculatedField calculatedField = new CalculatedField(); calculatedField.setEntityId(testDevice.getId()); calculatedField.setType(CalculatedFieldType.SCRIPT); @@ -583,7 +587,6 @@ public class CalculatedFieldIntegrationTest extends CalculatedFieldControllerTes Argument argument = new Argument(); ReferencedEntityKey refEntityKey = new ReferencedEntityKey("temperature", ArgumentType.TS_LATEST, null); argument.setRefEntityKey(refEntityKey); - argument.setDefaultValue("20"); config.setArguments(Map.of("T", argument)); config.setExpression("return {\"ts\": ctx.latestTs, \"values\": {\"fahrenheitTemp\": (T * 1.8) + 32}};"); @@ -595,25 +598,98 @@ public class CalculatedFieldIntegrationTest extends CalculatedFieldControllerTes CalculatedField savedCalculatedField = doPost("/api/calculatedField", calculatedField, CalculatedField.class); - await().alias("create CF -> perform initial calculation with default value").atMost(TIMEOUT, TimeUnit.SECONDS) + await().alias("create CF -> perform initial calculation").atMost(TIMEOUT, TimeUnit.SECONDS) .pollInterval(POLL_INTERVAL, TimeUnit.SECONDS) .untilAsserted(() -> { ObjectNode fahrenheitTemp = getLatestTelemetry(testDevice.getId(), "fahrenheitTemp"); assertThat(fahrenheitTemp).isNotNull(); - assertThat(fahrenheitTemp.get("fahrenheitTemp").get(0).get("ts").asText()).isNotEqualTo("-1"); - assertThat(fahrenheitTemp.get("fahrenheitTemp").get(0).get("value").asText()).isEqualTo("68.0"); + assertThat(fahrenheitTemp.get("fahrenheitTemp").get(0).get("ts").asText()).isEqualTo(Long.toString(ts)); + assertThat(fahrenheitTemp.get("fahrenheitTemp").get(0).get("value").asText()).isEqualTo("86.0"); }); + } - long ts = System.currentTimeMillis() - 10L; - doPost("/api/plugins/telemetry/DEVICE/" + testDevice.getUuidId() + "/timeseries/" + DataConstants.SERVER_SCOPE, JacksonUtil.toJsonNode(String.format("{\"ts\": %s, \"values\": {\"temperature\":30}}", ts))); + @Test + public void testSimpleCalculatedFieldWhenUseLatestTsIsTrueAndDefaultArguments() throws Exception { + Device testDevice = createDevice("Test device", "1234567890"); - await().alias("update telemetry -> perform calculation").atMost(TIMEOUT, TimeUnit.SECONDS) + CalculatedField calculatedField = new CalculatedField(); + calculatedField.setEntityId(testDevice.getId()); + calculatedField.setType(CalculatedFieldType.SIMPLE); + calculatedField.setName("a + b + c"); + calculatedField.setDebugSettings(DebugSettings.all()); + calculatedField.setConfigurationVersion(1); + + SimpleCalculatedFieldConfiguration config = new SimpleCalculatedFieldConfiguration(); + + Argument argument1 = new Argument(); + ReferencedEntityKey refEntityKey1 = new ReferencedEntityKey("a", ArgumentType.TS_LATEST, null); + argument1.setRefEntityKey(refEntityKey1); + argument1.setDefaultValue("100"); + Argument argument2 = new Argument(); + ReferencedEntityKey refEntityKey2 = new ReferencedEntityKey("b", ArgumentType.TS_LATEST, null); + argument2.setRefEntityKey(refEntityKey2); + argument2.setDefaultValue("200"); + Argument argument3 = new Argument(); + ReferencedEntityKey refEntityKey3 = new ReferencedEntityKey("c", ArgumentType.TS_LATEST, null); + argument3.setRefEntityKey(refEntityKey3); + argument3.setDefaultValue("300"); + config.setArguments(Map.of("a", argument1, "b", argument2, "c", argument3)); + config.setExpression("a + b + c"); + + Output output = new Output(); + output.setName("d"); + output.setType(OutputType.TIME_SERIES); + output.setDecimalsByDefault(0); + config.setOutput(output); + + config.setUseLatestTs(true); + + calculatedField.setConfiguration(config); + + CalculatedField savedCalculatedField = doPost("/api/calculatedField", calculatedField, CalculatedField.class); + + await().alias("create CF -> perform initial calculation with default arguments").atMost(TIMEOUT, TimeUnit.SECONDS) .pollInterval(POLL_INTERVAL, TimeUnit.SECONDS) .untilAsserted(() -> { - ObjectNode fahrenheitTemp = getLatestTelemetry(testDevice.getId(), "fahrenheitTemp"); - assertThat(fahrenheitTemp).isNotNull(); - assertThat(fahrenheitTemp.get("fahrenheitTemp").get(0).get("ts").asText()).isEqualTo(Long.toString(ts)); - assertThat(fahrenheitTemp.get("fahrenheitTemp").get(0).get("value").asText()).isEqualTo("86.0"); + ObjectNode d = getLatestTelemetry(testDevice.getId(), "d"); + assertThat(d).isNotNull(); + assertThat(d.get("d").get(0).get("value").asText()).isEqualTo("600"); + }); + + doPost("/api/plugins/telemetry/DEVICE/" + testDevice.getUuidId() + "/timeseries/" + DataConstants.SERVER_SCOPE, JacksonUtil.toJsonNode("{\"a\":10}")); + + await().alias("update telemetry -> save result with ts of 'a' argument").atMost(TIMEOUT, TimeUnit.SECONDS) + .pollInterval(POLL_INTERVAL, TimeUnit.SECONDS) + .untilAsserted(() -> { + ObjectNode keys = getLatestTelemetry(testDevice.getId(), "d", "a"); + assertThat(keys).isNotNull(); + String aTs = keys.get("a").get(0).get("ts").asText(); + assertThat(keys.get("d").get(0).get("ts").asText()).isEqualTo(aTs); + assertThat(keys.get("d").get(0).get("value").asText()).isEqualTo("510"); + }); + + doPost("/api/plugins/telemetry/DEVICE/" + testDevice.getUuidId() + "/timeseries/" + DataConstants.SERVER_SCOPE, JacksonUtil.toJsonNode("{\"b\":20}")); + doPost("/api/plugins/telemetry/DEVICE/" + testDevice.getUuidId() + "/timeseries/" + DataConstants.SERVER_SCOPE, JacksonUtil.toJsonNode("{\"c\":30}")); + + await().alias("update telemetry -> save result with latest ts of updated arguments").atMost(TIMEOUT, TimeUnit.SECONDS) + .pollInterval(POLL_INTERVAL, TimeUnit.SECONDS) + .untilAsserted(() -> { + ObjectNode keys = getLatestTelemetry(testDevice.getId(), "d"); + assertThat(keys).isNotNull(); + assertThat(keys.get("d").get(0).get("value").asText()).isEqualTo("60"); + }); + + String latestTs = getLatestTelemetry(testDevice.getId(), "d").get("d").get(0).get("ts").asText(); + + doDelete("/api/plugins/telemetry/DEVICE/" + testDevice.getId() + "/timeseries/delete?keys=b&deleteAllDataForKeys=true").andExpect(status().isOk()); + + await().alias("delete telemetry -> save result with previous latest ts and default argument").atMost(TIMEOUT, TimeUnit.SECONDS) + .pollInterval(POLL_INTERVAL, TimeUnit.SECONDS) + .untilAsserted(() -> { + ObjectNode keys = getLatestTelemetry(testDevice.getId(), "d"); + assertThat(keys).isNotNull(); + assertThat(keys.get("d").get(0).get("ts").asText()).isEqualTo(latestTs); + assertThat(keys.get("d").get(0).get("value").asText()).isEqualTo("240"); }); } diff --git a/common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/TbelCfCtx.java b/common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/TbelCfCtx.java index 2fe861ba81..c6023154ea 100644 --- a/common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/TbelCfCtx.java +++ b/common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/TbelCfCtx.java @@ -29,7 +29,7 @@ public class TbelCfCtx implements TbelCfObject { public TbelCfCtx(Map args, long latestTs) { this.args = Collections.unmodifiableMap(args); - this.latestTs = latestTs; + this.latestTs = latestTs != -1 ? latestTs : System.currentTimeMillis(); } @Override From 6f17dd1503ba2c221e5eab424f55c4897a6ae8c0 Mon Sep 17 00:00:00 2001 From: dashevchenko Date: Mon, 8 Dec 2025 11:10:41 +0200 Subject: [PATCH 11/32] RestClient: renamed methods --- .../server/msa/connectivity/JavaRestClientTest.java | 6 +++--- .../main/java/org/thingsboard/rest/client/RestClient.java | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/connectivity/JavaRestClientTest.java b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/connectivity/JavaRestClientTest.java index d439bdcf48..fb41cdf77f 100644 --- a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/connectivity/JavaRestClientTest.java +++ b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/connectivity/JavaRestClientTest.java @@ -358,7 +358,7 @@ public class JavaRestClientTest extends AbstractContainerTest { PlatformUsersNotificationTargetConfig targetConfig = new PlatformUsersNotificationTargetConfig(); targetConfig.setUsersFilter(filter); notificationTarget.setConfiguration(targetConfig); - return restClient.createNotificationTarget(notificationTarget); + return restClient.saveNotificationTarget(notificationTarget); } private NotificationTemplate createNotificationTemplate(NotificationType notificationType, String subject, @@ -397,7 +397,7 @@ public class JavaRestClientTest extends AbstractContainerTest { config.getDeliveryMethodsTemplates().put(deliveryMethod, deliveryMethodNotificationTemplate); } notificationTemplate.setConfiguration(config); - return restClient.createNotificationTemplate(notificationTemplate); + return restClient.saveNotificationTemplate(notificationTemplate); } private NotificationRequest submitNotificationRequest(NotificationTargetId targetId, NotificationTemplateId notificationTemplateId) { @@ -408,6 +408,6 @@ public class JavaRestClientTest extends AbstractContainerTest { .templateId(notificationTemplateId) .additionalConfig(config) .build(); - return restClient.createNotificationRequest(notificationRequest); + return restClient.saveNotificationRequest(notificationRequest); } } 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 64fd891f2c..c82a80e7cc 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 @@ -4360,7 +4360,7 @@ public class RestClient implements Closeable { restTemplate.delete(baseURL + "/api/notification/{id}", notificationId.getId()); } - public NotificationRequest createNotificationRequest(NotificationRequest notificationRequest) { + public NotificationRequest saveNotificationRequest(NotificationRequest notificationRequest) { return restTemplate.postForEntity(baseURL + "/api/notification/request", notificationRequest, NotificationRequest.class).getBody(); } @@ -4439,11 +4439,11 @@ public class RestClient implements Closeable { } } - public NotificationTarget createNotificationTarget(NotificationTarget notificationTarget) { + public NotificationTarget saveNotificationTarget(NotificationTarget notificationTarget) { return restTemplate.postForEntity(baseURL + "/api/notification/target", notificationTarget, NotificationTarget.class).getBody(); } - public NotificationTemplate createNotificationTemplate(NotificationTemplate notificationTemplate) { + public NotificationTemplate saveNotificationTemplate(NotificationTemplate notificationTemplate) { return restTemplate.postForEntity(baseURL + "/api/notification/template", notificationTemplate, NotificationTemplate.class).getBody(); } From 0577d66dcad395f3c82a1e8a547e54df1f5a5b9b Mon Sep 17 00:00:00 2001 From: IrynaMatveieva Date: Mon, 8 Dec 2025 12:37:20 +0200 Subject: [PATCH 12/32] return the most recent ts of default args if all are default --- .../service/cf/ctx/state/ArgumentEntry.java | 2 -- .../ctx/state/BaseCalculatedFieldState.java | 32 +++++++++++++------ .../ctx/state/SingleValueArgumentEntry.java | 5 --- .../cf/ctx/state/TsRollingArgumentEntry.java | 1 - 4 files changed, 23 insertions(+), 17 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/ArgumentEntry.java b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/ArgumentEntry.java index 3df43d8c2b..83e10b8194 100644 --- a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/ArgumentEntry.java +++ b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/ArgumentEntry.java @@ -40,8 +40,6 @@ public interface ArgumentEntry { Object getValue(); - long getLatestTs(); - boolean updateEntry(ArgumentEntry entry); boolean isEmpty(); diff --git a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/BaseCalculatedFieldState.java b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/BaseCalculatedFieldState.java index b6d4ad0fac..14370aa68c 100644 --- a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/BaseCalculatedFieldState.java +++ b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/BaseCalculatedFieldState.java @@ -37,15 +37,13 @@ public abstract class BaseCalculatedFieldState implements CalculatedFieldState { protected Map arguments; protected boolean sizeExceedsLimit; - protected long latestTimestamp = DEFAULT_LAST_UPDATE_TS; - public BaseCalculatedFieldState(List requiredArguments) { this.requiredArguments = requiredArguments; this.arguments = new HashMap<>(); } public BaseCalculatedFieldState() { - this(new ArrayList<>(), new HashMap<>(), false, DEFAULT_LAST_UPDATE_TS); + this(new ArrayList<>(), new HashMap<>(), false); } @Override @@ -111,12 +109,28 @@ public abstract class BaseCalculatedFieldState implements CalculatedFieldState { protected abstract void validateNewEntry(ArgumentEntry newEntry); public long getLatestTimestamp() { - long currentLatestTs = arguments.values().stream() - .mapToLong(ArgumentEntry::getLatestTs) - .max() - .orElse(DEFAULT_LAST_UPDATE_TS); - latestTimestamp = Math.max(currentLatestTs, latestTimestamp); - return latestTimestamp; + long latestTs = DEFAULT_LAST_UPDATE_TS; + + boolean allDefault = arguments.values().stream().allMatch(entry -> { + if (entry instanceof SingleValueArgumentEntry single) { + return single.isDefaultValue(); + } + return false; + }); + + for (ArgumentEntry entry : arguments.values()) { + if (entry instanceof SingleValueArgumentEntry single) { + if (allDefault) { + latestTs = Math.max(latestTs, single.getTs()); + } else if (!single.isDefaultValue()) { + latestTs = Math.max(latestTs, single.getTs()); + } + } else if (entry instanceof TsRollingArgumentEntry rolling) { + latestTs = Math.max(latestTs, rolling.getLatestTs()); + } + } + + return latestTs; } } diff --git a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/SingleValueArgumentEntry.java b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/SingleValueArgumentEntry.java index 186af92f38..2f9a7de940 100644 --- a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/SingleValueArgumentEntry.java +++ b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/SingleValueArgumentEntry.java @@ -98,11 +98,6 @@ public class SingleValueArgumentEntry implements ArgumentEntry { return isEmpty() ? null : kvEntryValue.getValue(); } - @Override - public long getLatestTs() { - return !isDefaultValue() ? ts : DEFAULT_LAST_UPDATE_TS; - } - @Override public TbelCfArg toTbelCfArg() { Object value = kvEntryValue.getValue(); diff --git a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/TsRollingArgumentEntry.java b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/TsRollingArgumentEntry.java index ada46a841e..e01d8b7369 100644 --- a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/TsRollingArgumentEntry.java +++ b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/TsRollingArgumentEntry.java @@ -85,7 +85,6 @@ public class TsRollingArgumentEntry implements ArgumentEntry { return tsRecords; } - @Override public long getLatestTs() { var lastEntry = tsRecords.lastEntry(); return (lastEntry != null) ? lastEntry.getKey() : DEFAULT_LAST_UPDATE_TS; From 7bc7e7e53ca893b7bffda6b6b75548fff246f28f Mon Sep 17 00:00:00 2001 From: Viacheslav Klimov Date: Mon, 8 Dec 2025 14:15:21 +0200 Subject: [PATCH 13/32] Fix CVE-2025-59419 --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 8432c8a464..bfa21134f0 100755 --- a/pom.xml +++ b/pom.xml @@ -147,7 +147,7 @@ 9.2.0 1.1.10.5 9.10.0 - 4.1.125.Final + 4.1.128.Final From e498d346ad9994531db541bf7946d363a926178c Mon Sep 17 00:00:00 2001 From: Viacheslav Klimov Date: Mon, 8 Dec 2025 14:38:25 +0200 Subject: [PATCH 14/32] Fix CVE-2025-66566 --- common/queue/pom.xml | 2 +- pom.xml | 10 ++++++++-- rule-engine/rule-engine-components/pom.xml | 2 +- tools/pom.xml | 4 ++++ 4 files changed, 14 insertions(+), 4 deletions(-) diff --git a/common/queue/pom.xml b/common/queue/pom.xml index b98388d897..e6ab3a4704 100644 --- a/common/queue/pom.xml +++ b/common/queue/pom.xml @@ -69,7 +69,7 @@ kafka-clients - org.lz4 + at.yawk.lz4 lz4-java diff --git a/pom.xml b/pom.xml index bfa21134f0..a15f32d8b0 100755 --- a/pom.xml +++ b/pom.xml @@ -86,7 +86,7 @@ org.apache.kafka.common.network.NetworkReceive class in the application module. It addresses the issue https://issues.apache.org/jira/browse/KAFKA-4090. Here is the source to track https://github.com/apache/kafka/tree/trunk/clients/src/main/java/org/apache/kafka/common/network --> 3.9.1 - 1.8.1 + 1.10.1 8.10.1 3.5.3 2.2 @@ -1162,7 +1162,7 @@ - org.lz4 + at.yawk.lz4 lz4-java ${lz4.version} @@ -1445,6 +1445,12 @@ org.apache.cassandra cassandra-all ${cassandra-all.version} + + + org.lz4 + lz4-java + + org.testng diff --git a/rule-engine/rule-engine-components/pom.xml b/rule-engine/rule-engine-components/pom.xml index d65832013a..90e6a87e52 100644 --- a/rule-engine/rule-engine-components/pom.xml +++ b/rule-engine/rule-engine-components/pom.xml @@ -97,7 +97,7 @@ kafka-clients - org.lz4 + at.yawk.lz4 lz4-java diff --git a/tools/pom.xml b/tools/pom.xml index cce1466971..981c4804c8 100644 --- a/tools/pom.xml +++ b/tools/pom.xml @@ -55,6 +55,10 @@ org.apache.cassandra cassandra-all + + at.yawk.lz4 + lz4-java + commons-io commons-io From 13d6f41eb75b197e4b0833d2a02e54d447a6f68a Mon Sep 17 00:00:00 2001 From: Viacheslav Klimov Date: Mon, 8 Dec 2025 14:40:39 +0200 Subject: [PATCH 15/32] Fix CVE-2025-48734 --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index a15f32d8b0..4c5b2291be 100755 --- a/pom.xml +++ b/pom.xml @@ -102,7 +102,7 @@ 1.6.4 1.6.1 - 1.9.4 + 1.11.0 4.4 1.0.4TB 3.7.1 From 12a0163f80d6ddb0bb4cd6bea53e78786d8e0c5f Mon Sep 17 00:00:00 2001 From: Vladyslav_Prykhodko Date: Mon, 8 Dec 2025 14:58:22 +0200 Subject: [PATCH 16/32] UI: Fixed CVE-2025-59343 --- msa/js-executor/package.json | 2 +- msa/js-executor/yarn.lock | 14 +++++++------- msa/web-ui/package.json | 2 +- msa/web-ui/yarn.lock | 14 +++++++------- 4 files changed, 16 insertions(+), 16 deletions(-) diff --git a/msa/js-executor/package.json b/msa/js-executor/package.json index 220561f6e7..88fc7924bf 100644 --- a/msa/js-executor/package.json +++ b/msa/js-executor/package.json @@ -15,7 +15,7 @@ "dependencies": { "config": "^4.1.1", "express": "^5.1.0", - "js-yaml": "^4.1.0", + "js-yaml": "^4.1.1", "kafkajs": "^2.2.4", "long": "^5.3.2", "uuid-parse": "^1.1.0", diff --git a/msa/js-executor/yarn.lock b/msa/js-executor/yarn.lock index 25ed229141..1d42a9cd96 100644 --- a/msa/js-executor/yarn.lock +++ b/msa/js-executor/yarn.lock @@ -945,10 +945,10 @@ isarray@~1.0.0: resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11" integrity sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ== -js-yaml@^4.1.0: - version "4.1.0" - resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-4.1.0.tgz#c1fb65f8f5017901cdd2c951864ba18458a10602" - integrity sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA== +js-yaml@^4.1.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-4.1.1.tgz#854c292467705b699476e1a2decc0c8a3458806b" + integrity sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA== dependencies: argparse "^2.0.1" @@ -1533,9 +1533,9 @@ supports-preserve-symlinks-flag@^1.0.0: integrity sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w== tar-fs@^2.0.0, tar-fs@^2.1.1: - version "2.1.3" - resolved "https://registry.yarnpkg.com/tar-fs/-/tar-fs-2.1.3.tgz#fb3b8843a26b6f13a08e606f7922875eb1fbbf92" - integrity sha512-090nwYJDmlhwFwEW3QQl+vaNnxsO2yVsd45eTKRBzSzu+hlb1w2K9inVq5b0ngXuLVqQ4ApvsUHHnu/zQNkWAg== + version "2.1.4" + resolved "https://registry.yarnpkg.com/tar-fs/-/tar-fs-2.1.4.tgz#800824dbf4ef06ded9afea4acafe71c67c76b930" + integrity sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ== dependencies: chownr "^1.1.1" mkdirp-classic "^0.5.2" diff --git a/msa/web-ui/package.json b/msa/web-ui/package.json index 2fcf6ab914..e8d4eedd5d 100644 --- a/msa/web-ui/package.json +++ b/msa/web-ui/package.json @@ -19,7 +19,7 @@ "express": "^5.1.0", "http": "0.0.0", "http-proxy": "^1.18.1", - "js-yaml": "^4.1.0", + "js-yaml": "^4.1.1", "winston": "^3.17.0", "winston-daily-rotate-file": "^5.0.0" }, diff --git a/msa/web-ui/yarn.lock b/msa/web-ui/yarn.lock index 7af758435e..0f8b6f3eed 100644 --- a/msa/web-ui/yarn.lock +++ b/msa/web-ui/yarn.lock @@ -1017,10 +1017,10 @@ isarray@~1.0.0: resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11" integrity sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ== -js-yaml@^4.1.0: - version "4.1.0" - resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-4.1.0.tgz#c1fb65f8f5017901cdd2c951864ba18458a10602" - integrity sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA== +js-yaml@^4.1.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-4.1.1.tgz#854c292467705b699476e1a2decc0c8a3458806b" + integrity sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA== dependencies: argparse "^2.0.1" @@ -1615,9 +1615,9 @@ supports-preserve-symlinks-flag@^1.0.0: integrity sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w== tar-fs@^2.0.0, tar-fs@^2.1.1: - version "2.1.3" - resolved "https://registry.yarnpkg.com/tar-fs/-/tar-fs-2.1.3.tgz#fb3b8843a26b6f13a08e606f7922875eb1fbbf92" - integrity sha512-090nwYJDmlhwFwEW3QQl+vaNnxsO2yVsd45eTKRBzSzu+hlb1w2K9inVq5b0ngXuLVqQ4ApvsUHHnu/zQNkWAg== + version "2.1.4" + resolved "https://registry.yarnpkg.com/tar-fs/-/tar-fs-2.1.4.tgz#800824dbf4ef06ded9afea4acafe71c67c76b930" + integrity sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ== dependencies: chownr "^1.1.1" mkdirp-classic "^0.5.2" From 607287b3ac815d472a5b24e5e824660139e6f365 Mon Sep 17 00:00:00 2001 From: IrynaMatveieva Date: Mon, 8 Dec 2025 16:29:31 +0200 Subject: [PATCH 17/32] moved check for intermediate result to hasRefreshContextOnlyChanges --- .../cf/ctx/state/CalculatedFieldCtx.java | 41 +++++++++++-------- 1 file changed, 25 insertions(+), 16 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/CalculatedFieldCtx.java b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/CalculatedFieldCtx.java index f6c577545e..d2bc8d30cf 100644 --- a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/CalculatedFieldCtx.java +++ b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/CalculatedFieldCtx.java @@ -639,26 +639,34 @@ public class CalculatedFieldCtx implements Closeable { } public boolean hasRefreshContextOnlyChanges(CalculatedFieldCtx other) { // has changes that do not require state recalculation - var thisConfig = calculatedField.getConfiguration(); - var otherConfig = other.getCalculatedField().getConfiguration(); - - var thisOutputStrategy = thisConfig.getOutput().getStrategy(); - var otherOutputStrategy = otherConfig.getOutput().getStrategy(); + var thisOutputStrategy = calculatedField.getConfiguration().getOutput().getStrategy(); + var otherOutputStrategy = other.getCalculatedField().getConfiguration().getOutput().getStrategy(); if (!thisOutputStrategy.getType().equals(otherOutputStrategy.getType())) { return true; } - if (thisOutputStrategy instanceof TimeSeriesImmediateOutputStrategy thisTimeSeriesImmediateOutputStrategy - && otherOutputStrategy instanceof TimeSeriesImmediateOutputStrategy otherTimeSeriesImmediateOutputStrategy) { - return thisTimeSeriesImmediateOutputStrategy.getTtl() != otherTimeSeriesImmediateOutputStrategy.getTtl(); + if (thisOutputStrategy instanceof TimeSeriesImmediateOutputStrategy thisTsOutputStrategy + && otherOutputStrategy instanceof TimeSeriesImmediateOutputStrategy otherTsOutputStrategy) { + if (thisTsOutputStrategy.getTtl() != otherTsOutputStrategy.getTtl()) { + return true; + } } - if (thisOutputStrategy instanceof AttributesImmediateOutputStrategy thisAttributesImmediateOutputStrategy - && otherOutputStrategy instanceof AttributesImmediateOutputStrategy otherAttributesImmediateOutputStrategy) { - boolean updateAttributesOnlyOnValueChangeChanged = thisAttributesImmediateOutputStrategy.isUpdateAttributesOnlyOnValueChange() != otherAttributesImmediateOutputStrategy.isUpdateAttributesOnlyOnValueChange(); - boolean sendAttributesUpdatedNotificationUpdated = thisAttributesImmediateOutputStrategy.isSendAttributesUpdatedNotification() != otherAttributesImmediateOutputStrategy.isSendAttributesUpdatedNotification(); - return updateAttributesOnlyOnValueChangeChanged || sendAttributesUpdatedNotificationUpdated; + if (thisOutputStrategy instanceof AttributesImmediateOutputStrategy thisAttrOutputStrategy + && otherOutputStrategy instanceof AttributesImmediateOutputStrategy otherAttrOutputStrategy) { + boolean updateAttrOnValueChangedChanged = thisAttrOutputStrategy.isUpdateAttributesOnlyOnValueChange() != otherAttrOutputStrategy.isUpdateAttributesOnlyOnValueChange(); + boolean sendAttrUpdatedNotificationChanged = thisAttrOutputStrategy.isSendAttributesUpdatedNotification() != otherAttrOutputStrategy.isSendAttributesUpdatedNotification(); + if (updateAttrOnValueChangedChanged || sendAttrUpdatedNotificationChanged) { + return true; + } + } + + if (calculatedField.getConfiguration() instanceof EntityAggregationCalculatedFieldConfiguration thisConfig + && other.getCalculatedField().getConfiguration() instanceof EntityAggregationCalculatedFieldConfiguration otherConfig) { + if (thisConfig.isProduceIntermediateResult() != otherConfig.isProduceIntermediateResult()) { + return true; + } } return false; @@ -705,8 +713,9 @@ public class CalculatedFieldCtx implements Closeable { && other.getCalculatedField().getConfiguration() instanceof EntityAggregationCalculatedFieldConfiguration otherConfig) { boolean metricsChanged = !Objects.equals(thisConfig.getMetrics(), otherConfig.getMetrics()); boolean watermarkChanged = !Objects.equals(thisConfig.getWatermark(), otherConfig.getWatermark()); - boolean produceIntermediateResultChanged = thisConfig.isProduceIntermediateResult() != otherConfig.isProduceIntermediateResult(); - return metricsChanged || watermarkChanged || produceIntermediateResultChanged; + if (metricsChanged || watermarkChanged) { + return true; + } } return false; } @@ -741,7 +750,7 @@ public class CalculatedFieldCtx implements Closeable { if (!output.getType().equals(otherOutput.getType())) { return true; } - if (!output.getName().equals(otherOutput.getName())) { + if (!Objects.equals(output.getName(), otherOutput.getName())) { return true; } if (output.getScope() != (otherOutput.getScope())) { From ba19ebd6acf86e749c5da1fbe2156b0683e3a731 Mon Sep 17 00:00:00 2001 From: Maksym Tsymbarov Date: Fri, 5 Dec 2025 12:11:57 +0200 Subject: [PATCH 18/32] added debug events modal to rule chain elements --- .../rule-node-debug-dialog.component.html | 47 ++++++++++++++ .../rule-node-debug-dialog.component.scss | 26 ++++++++ .../rule-node-debug-dialog.component.ts | 65 +++++++++++++++++++ .../rule-node-details.component.html | 1 + .../rulechain/rule-node-details.component.ts | 4 ++ .../rulechain/rulechain-page.component.html | 1 + .../rulechain/rulechain-page.component.ts | 35 +++++++++- .../pages/rulechain/rulechain-page.module.ts | 4 +- 8 files changed, 181 insertions(+), 2 deletions(-) create mode 100644 ui-ngx/src/app/modules/home/pages/rulechain/rule-node-debug-dialog.component.html create mode 100644 ui-ngx/src/app/modules/home/pages/rulechain/rule-node-debug-dialog.component.scss create mode 100644 ui-ngx/src/app/modules/home/pages/rulechain/rule-node-debug-dialog.component.ts diff --git a/ui-ngx/src/app/modules/home/pages/rulechain/rule-node-debug-dialog.component.html b/ui-ngx/src/app/modules/home/pages/rulechain/rule-node-debug-dialog.component.html new file mode 100644 index 0000000000..b2bd40aa1a --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/rulechain/rule-node-debug-dialog.component.html @@ -0,0 +1,47 @@ + +
+ +

{{ 'rulenode.events' | translate}}

+ + +
+
+ +
+
+ +
+
diff --git a/ui-ngx/src/app/modules/home/pages/rulechain/rule-node-debug-dialog.component.scss b/ui-ngx/src/app/modules/home/pages/rulechain/rule-node-debug-dialog.component.scss new file mode 100644 index 0000000000..c4798d06f9 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/rulechain/rule-node-debug-dialog.component.scss @@ -0,0 +1,26 @@ +/** + * 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. + */ +:host { + .debug-dialog-container { + width: 1080px; + max-width: 100%; + + .debug-dialog-content { + height: 65vh; + border-radius: 0; + } + } +} diff --git a/ui-ngx/src/app/modules/home/pages/rulechain/rule-node-debug-dialog.component.ts b/ui-ngx/src/app/modules/home/pages/rulechain/rule-node-debug-dialog.component.ts new file mode 100644 index 0000000000..e7a7947009 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/rulechain/rule-node-debug-dialog.component.ts @@ -0,0 +1,65 @@ +/// +/// 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. +/// + +import { AfterViewInit, Component, Inject, ViewChild } from '@angular/core'; +import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; +import { DialogComponent } from '@shared/components/dialog.component'; +import { EventTableComponent } from '@home/components/event/event-table.component'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { Router } from '@angular/router'; +import { DebugEventType, EventType } from '@shared/models/event.models'; +import { TenantId } from '@shared/models/id/tenant-id'; +import { EntityId } from '@shared/models/id/entity-id'; + +export interface RuleNodeDebugDialogData { + isActive: boolean; + tenantId: TenantId; + ruleNodeId: EntityId; + ruleNodeTestButtonLabel: string; + onDebugEventSelected: ()=> void; +} + +@Component({ + selector: 'tb-rule-node-debug-dialog', + templateUrl: './rule-node-debug-dialog.component.html', + styleUrl: './rule-node-debug-dialog.component.scss' +}) +export class RuleNodeDebugDialogComponent extends DialogComponent implements AfterViewInit{ + + @ViewChild(EventTableComponent, {static: true}) eventsTable: EventTableComponent; + + readonly DebugEventType = DebugEventType; + readonly debugEventTypes = DebugEventType; + readonly EventType = EventType; + + dialogTitle: string; + + constructor(protected store: Store, + protected router: Router, + @Inject(MAT_DIALOG_DATA) public data: RuleNodeDebugDialogData, + protected dialogRef: MatDialogRef) { + super(store, router, dialogRef); + } + + ngAfterViewInit() { + this.eventsTable.entitiesTable.updateData(); + } + + cancel(): void { + this.dialogRef.close(null); + } +} diff --git a/ui-ngx/src/app/modules/home/pages/rulechain/rule-node-details.component.html b/ui-ngx/src/app/modules/home/pages/rulechain/rule-node-details.component.html index b55dcdaa73..a66819a49b 100644 --- a/ui-ngx/src/app/modules/home/pages/rulechain/rule-node-details.component.html +++ b/ui-ngx/src/app/modules/home/pages/rulechain/rule-node-details.component.html @@ -36,6 +36,7 @@
(); diff --git a/ui-ngx/src/app/modules/home/pages/rulechain/rulechain-page.component.html b/ui-ngx/src/app/modules/home/pages/rulechain/rulechain-page.component.html index 423bde7da3..c31f24c0ea 100644 --- a/ui-ngx/src/app/modules/home/pages/rulechain/rulechain-page.component.html +++ b/ui-ngx/src/app/modules/home/pages/rulechain/rulechain-page.component.html @@ -106,6 +106,7 @@ (RuleNodeDebugDialogComponent, { + disableClose: true, + panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], + data: { + active: true, + tenantId: this.ruleChain.tenantId, + ruleNodeId: this.editingRuleNode.ruleNodeId, + functionTestButtonLabel: this.ruleNodeTestButtonLabel, + debugEventSelected: this.onDebugEventSelected.bind(this) + } + }) + .afterClosed() + .subscribe(); + } + private updateNodeErrorTooltip(node: FcRuleNode) { if (node.error) { const element = $('#' + node.id); diff --git a/ui-ngx/src/app/modules/home/pages/rulechain/rulechain-page.module.ts b/ui-ngx/src/app/modules/home/pages/rulechain/rulechain-page.module.ts index 34518e2b70..51197e1073 100644 --- a/ui-ngx/src/app/modules/home/pages/rulechain/rulechain-page.module.ts +++ b/ui-ngx/src/app/modules/home/pages/rulechain/rulechain-page.module.ts @@ -33,6 +33,7 @@ import { RuleNodeDetailsComponent } from '@home/pages/rulechain/rule-node-detail import { RuleNodeConfigComponent } from '@home/pages/rulechain/rule-node-config.component'; import { LinkLabelsComponent } from '@home/pages/rulechain/link-labels.component'; import { RuleNodeLinkComponent } from '@home/pages/rulechain/rule-node-link.component'; +import { RuleNodeDebugDialogComponent } from '@home/pages/rulechain/rule-node-debug-dialog.component'; @NgModule({ declarations: [ @@ -43,7 +44,8 @@ import { RuleNodeLinkComponent } from '@home/pages/rulechain/rule-node-link.comp RuleNodeConfigComponent, AddRuleNodeLinkDialogComponent, AddRuleNodeDialogComponent, - CreateNestedRuleChainDialogComponent + CreateNestedRuleChainDialogComponent, + RuleNodeDebugDialogComponent ], imports: [ CommonModule, From 185ef9b53b31de8632d5d34105ada22b3fcbe6d4 Mon Sep 17 00:00:00 2001 From: Maksym Tsymbarov Date: Fri, 5 Dec 2025 12:46:27 +0200 Subject: [PATCH 19/32] move events modal to reusable component --- .../event/events-dialog.component.html} | 8 ++++---- .../event/events-dialog.component.scss} | 0 .../event/events-dialog.component.ts} | 19 ++++++++----------- .../home/components/home-components.module.ts | 5 ++++- .../rulechain/rulechain-page.component.ts | 8 ++++---- .../pages/rulechain/rulechain-page.module.ts | 4 +--- 6 files changed, 21 insertions(+), 23 deletions(-) rename ui-ngx/src/app/modules/home/{pages/rulechain/rule-node-debug-dialog.component.html => components/event/events-dialog.component.html} (89%) rename ui-ngx/src/app/modules/home/{pages/rulechain/rule-node-debug-dialog.component.scss => components/event/events-dialog.component.scss} (100%) rename ui-ngx/src/app/modules/home/{pages/rulechain/rule-node-debug-dialog.component.ts => components/event/events-dialog.component.ts} (78%) diff --git a/ui-ngx/src/app/modules/home/pages/rulechain/rule-node-debug-dialog.component.html b/ui-ngx/src/app/modules/home/components/event/events-dialog.component.html similarity index 89% rename from ui-ngx/src/app/modules/home/pages/rulechain/rule-node-debug-dialog.component.html rename to ui-ngx/src/app/modules/home/components/event/events-dialog.component.html index b2bd40aa1a..65040bcf60 100644 --- a/ui-ngx/src/app/modules/home/pages/rulechain/rule-node-debug-dialog.component.html +++ b/ui-ngx/src/app/modules/home/components/event/events-dialog.component.html @@ -17,7 +17,7 @@ -->
-

{{ 'rulenode.events' | translate}}

+

{{ data.title | translate}}

diff --git a/ui-ngx/src/app/modules/home/pages/rulechain/rule-node-debug-dialog.component.scss b/ui-ngx/src/app/modules/home/components/event/events-dialog.component.scss similarity index 100% rename from ui-ngx/src/app/modules/home/pages/rulechain/rule-node-debug-dialog.component.scss rename to ui-ngx/src/app/modules/home/components/event/events-dialog.component.scss diff --git a/ui-ngx/src/app/modules/home/pages/rulechain/rule-node-debug-dialog.component.ts b/ui-ngx/src/app/modules/home/components/event/events-dialog.component.ts similarity index 78% rename from ui-ngx/src/app/modules/home/pages/rulechain/rule-node-debug-dialog.component.ts rename to ui-ngx/src/app/modules/home/components/event/events-dialog.component.ts index e7a7947009..5a21241868 100644 --- a/ui-ngx/src/app/modules/home/pages/rulechain/rule-node-debug-dialog.component.ts +++ b/ui-ngx/src/app/modules/home/components/event/events-dialog.component.ts @@ -26,32 +26,29 @@ import { TenantId } from '@shared/models/id/tenant-id'; import { EntityId } from '@shared/models/id/entity-id'; export interface RuleNodeDebugDialogData { - isActive: boolean; + title: string; tenantId: TenantId; - ruleNodeId: EntityId; - ruleNodeTestButtonLabel: string; + entityId: EntityId; + functionTestButtonLabel: string; onDebugEventSelected: ()=> void; } @Component({ - selector: 'tb-rule-node-debug-dialog', - templateUrl: './rule-node-debug-dialog.component.html', - styleUrl: './rule-node-debug-dialog.component.scss' + selector: 'tb-debug-dialog', + templateUrl: './events-dialog.component.html', + styleUrl: './events-dialog.component.scss' }) -export class RuleNodeDebugDialogComponent extends DialogComponent implements AfterViewInit{ +export class EventsDialogComponent extends DialogComponent implements AfterViewInit{ @ViewChild(EventTableComponent, {static: true}) eventsTable: EventTableComponent; - readonly DebugEventType = DebugEventType; readonly debugEventTypes = DebugEventType; readonly EventType = EventType; - dialogTitle: string; - constructor(protected store: Store, protected router: Router, @Inject(MAT_DIALOG_DATA) public data: RuleNodeDebugDialogData, - protected dialogRef: MatDialogRef) { + protected dialogRef: MatDialogRef) { super(store, router, dialogRef); } diff --git a/ui-ngx/src/app/modules/home/components/home-components.module.ts b/ui-ngx/src/app/modules/home/components/home-components.module.ts index 6da26fb2c3..8a90c28878 100644 --- a/ui-ngx/src/app/modules/home/components/home-components.module.ts +++ b/ui-ngx/src/app/modules/home/components/home-components.module.ts @@ -204,6 +204,7 @@ import { EditApiKeyDescriptionPanelComponent } from '@home/components/api-key/ed import { ApiKeyGeneratedDialogComponent } from '@home/components/api-key/api-key-generated-dialog.component'; import { ApiKeysTableDialogComponent } from '@home/components/api-key/api-keys-table-dialog.component'; import { AuditLogFilterComponent } from "@home/components/audit-log/audit-log-filter.component"; +import { EventsDialogComponent } from '@home/components/event/events-dialog.component'; @NgModule({ declarations: @@ -361,7 +362,8 @@ import { AuditLogFilterComponent } from "@home/components/audit-log/audit-log-fi EditApiKeyDescriptionPanelComponent, ApiKeyGeneratedDialogComponent, AuditLogHeaderComponent, - AuditLogFilterComponent + AuditLogFilterComponent, + EventsDialogComponent ], imports: [ CommonModule, @@ -510,6 +512,7 @@ import { AuditLogFilterComponent } from "@home/components/audit-log/audit-log-fi ResourcesLibraryComponent, ApiKeysTableComponent, ApiKeysTableDialogComponent, + EventsDialogComponent ], providers: [ WidgetComponentService, diff --git a/ui-ngx/src/app/modules/home/pages/rulechain/rulechain-page.component.ts b/ui-ngx/src/app/modules/home/pages/rulechain/rulechain-page.component.ts index 53e2563ee2..3123719f22 100644 --- a/ui-ngx/src/app/modules/home/pages/rulechain/rulechain-page.component.ts +++ b/ui-ngx/src/app/modules/home/pages/rulechain/rulechain-page.component.ts @@ -100,7 +100,7 @@ import { EntityDebugSettings } from '@shared/models/entity.models'; import Timeout = NodeJS.Timeout; import { DomSanitizer } from '@angular/platform-browser'; import { AdditionalDebugActionConfig } from '@home/components/entity/debug/entity-debug-settings.model'; -import { RuleNodeDebugDialogComponent } from '@home/pages/rulechain/rule-node-debug-dialog.component'; +import { EventsDialogComponent } from '@home/components/event/events-dialog.component'; @Component({ selector: 'tb-rulechain-page', @@ -1674,13 +1674,13 @@ export class RuleChainPageComponent extends PageComponent } private openDebugEventsDialog(): void { - this.dialog.open(RuleNodeDebugDialogComponent, { + this.dialog.open(EventsDialogComponent, { disableClose: true, panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], data: { - active: true, + title: 'rulenode.events', tenantId: this.ruleChain.tenantId, - ruleNodeId: this.editingRuleNode.ruleNodeId, + entityId: this.editingRuleNode.ruleNodeId, functionTestButtonLabel: this.ruleNodeTestButtonLabel, debugEventSelected: this.onDebugEventSelected.bind(this) } diff --git a/ui-ngx/src/app/modules/home/pages/rulechain/rulechain-page.module.ts b/ui-ngx/src/app/modules/home/pages/rulechain/rulechain-page.module.ts index 51197e1073..34518e2b70 100644 --- a/ui-ngx/src/app/modules/home/pages/rulechain/rulechain-page.module.ts +++ b/ui-ngx/src/app/modules/home/pages/rulechain/rulechain-page.module.ts @@ -33,7 +33,6 @@ import { RuleNodeDetailsComponent } from '@home/pages/rulechain/rule-node-detail import { RuleNodeConfigComponent } from '@home/pages/rulechain/rule-node-config.component'; import { LinkLabelsComponent } from '@home/pages/rulechain/link-labels.component'; import { RuleNodeLinkComponent } from '@home/pages/rulechain/rule-node-link.component'; -import { RuleNodeDebugDialogComponent } from '@home/pages/rulechain/rule-node-debug-dialog.component'; @NgModule({ declarations: [ @@ -44,8 +43,7 @@ import { RuleNodeDebugDialogComponent } from '@home/pages/rulechain/rule-node-de RuleNodeConfigComponent, AddRuleNodeLinkDialogComponent, AddRuleNodeDialogComponent, - CreateNestedRuleChainDialogComponent, - RuleNodeDebugDialogComponent + CreateNestedRuleChainDialogComponent ], imports: [ CommonModule, From 0d66c6b33e72877270a5aece61e850ac5c4e396b Mon Sep 17 00:00:00 2001 From: Maksym Tsymbarov Date: Fri, 5 Dec 2025 12:51:08 +0200 Subject: [PATCH 20/32] adjustments --- .../home/components/event/events-dialog.component.html | 4 ++-- .../home/components/event/events-dialog.component.ts | 6 ++---- .../home/pages/rulechain/rulechain-page.component.ts | 1 + 3 files changed, 5 insertions(+), 6 deletions(-) diff --git a/ui-ngx/src/app/modules/home/components/event/events-dialog.component.html b/ui-ngx/src/app/modules/home/components/event/events-dialog.component.html index 65040bcf60..7aa1f3dd7f 100644 --- a/ui-ngx/src/app/modules/home/components/event/events-dialog.component.html +++ b/ui-ngx/src/app/modules/home/components/event/events-dialog.component.html @@ -27,8 +27,8 @@
void; } @@ -42,9 +43,6 @@ export class EventsDialogComponent extends DialogComponent, protected router: Router, @Inject(MAT_DIALOG_DATA) public data: RuleNodeDebugDialogData, diff --git a/ui-ngx/src/app/modules/home/pages/rulechain/rulechain-page.component.ts b/ui-ngx/src/app/modules/home/pages/rulechain/rulechain-page.component.ts index 3123719f22..163b4ff1f6 100644 --- a/ui-ngx/src/app/modules/home/pages/rulechain/rulechain-page.component.ts +++ b/ui-ngx/src/app/modules/home/pages/rulechain/rulechain-page.component.ts @@ -1679,6 +1679,7 @@ export class RuleChainPageComponent extends PageComponent panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], data: { title: 'rulenode.events', + debugEventType: DebugEventType.DEBUG_RULE_CHAIN, tenantId: this.ruleChain.tenantId, entityId: this.editingRuleNode.ruleNodeId, functionTestButtonLabel: this.ruleNodeTestButtonLabel, From 30e3c447f37d6072a8d445aae5c9fa76cc6bfa22 Mon Sep 17 00:00:00 2001 From: Maksym Tsymbarov Date: Mon, 8 Dec 2025 15:03:13 +0200 Subject: [PATCH 21/32] moved calculated-fields-table to use debug event modal --- .../alarm-rules/alarm-rules-table-config.ts | 24 ++++-- .../calculated-fields-table-config.ts | 29 +++++-- ...lculated-field-debug-dialog.component.html | 47 ----------- ...lculated-field-debug-dialog.component.scss | 26 ------- ...calculated-field-debug-dialog.component.ts | 77 ------------------- .../components/public-api.ts | 1 - .../home/components/home-components.module.ts | 6 +- .../events-dialog.component.html | 13 ++-- .../events-dialog.component.scss | 0 .../events-dialog.component.ts | 30 +++++--- .../rulechain/rulechain-page.component.ts | 11 +-- 11 files changed, 75 insertions(+), 189 deletions(-) delete mode 100644 ui-ngx/src/app/modules/home/components/calculated-fields/components/debug-dialog/calculated-field-debug-dialog.component.html delete mode 100644 ui-ngx/src/app/modules/home/components/calculated-fields/components/debug-dialog/calculated-field-debug-dialog.component.scss delete mode 100644 ui-ngx/src/app/modules/home/components/calculated-fields/components/debug-dialog/calculated-field-debug-dialog.component.ts rename ui-ngx/src/app/modules/home/{components/event => dialogs}/events-dialog.component.html (79%) rename ui-ngx/src/app/modules/home/{components/event => dialogs}/events-dialog.component.scss (100%) rename ui-ngx/src/app/modules/home/{components/event => dialogs}/events-dialog.component.ts (64%) 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 f73633ad8e..15c7cdaeb1 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 @@ -55,14 +55,12 @@ import { AlarmRuleDialogComponent, AlarmRuleDialogData } from "@home/components/alarm-rules/alarm-rule-dialog.component"; -import { - CalculatedFieldDebugDialogComponent, - 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"; import { deepClone, getEntityDetailsPageURL, isObject } from "@core/utils"; import { AlarmRuleTableHeaderComponent } from "@home/components/alarm-rules/alarm-rule-table-header.component"; +import { EventsDialogComponent, EventsDialogData } from '@home/dialogs/events-dialog.component'; +import { DebugEventType, Event as DebugEvent, EventType } from '@shared/models/event.models'; import { ActionNotificationShow } from "@core/notification/notification.actions"; import { CalculatedFieldScriptTestDialogComponent, @@ -266,13 +264,25 @@ export class AlarmRulesTableConfig extends EntityTableConfig { } private openDebugEventsDialog(calculatedField: CalculatedField): void { - this.dialog.open(CalculatedFieldDebugDialogComponent, { + const customCellActionEnabledFn = (event: DebugEvent) => { + return (calculatedField.type === CalculatedFieldType.SCRIPT || + (calculatedField.type === CalculatedFieldType.PROPAGATION && + calculatedField.configuration.applyExpressionToResolvedArguments) + ) && !!(event as DebugEvent).body.arguments; + }; + + this.dialog.open(EventsDialogComponent, { disableClose: true, panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], data: { + title: 'alarm-rule.debugging', tenantId: this.tenantId, - value: calculatedField, - getTestScriptDialogFn: null, + value: calculatedField.id, + debugEventTypes:[DebugEventType.DEBUG_CALCULATED_FIELD], + disabledEventTypes:[EventType.LC_EVENT, EventType.ERROR, EventType.STATS], + defaultEventType: DebugEventType.DEBUG_CALCULATED_FIELD, + onDebugEventSelected: null, + customCellActionEnabledFn: customCellActionEnabledFn } }) .afterClosed() 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 98e4a29549..f024e1c88c 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 @@ -22,7 +22,7 @@ import { import { EntityType, entityTypeTranslations } from '@shared/models/entity-type.models'; import { TranslateService } from '@ngx-translate/core'; import { Direction } from '@shared/models/page/sort-order'; -import { MatDialog } from '@angular/material/dialog'; +import { MatDialog, MatDialogRef } from '@angular/material/dialog'; import { PageLink } from '@shared/models/page/page-link'; import { EMPTY, Observable, of } from 'rxjs'; import { PageData } from '@shared/models/page/page-data'; @@ -48,8 +48,6 @@ import { PropagationWithExpression, } from '@shared/models/calculated-field.models'; import { - CalculatedFieldDebugDialogComponent, - CalculatedFieldDebugDialogData, CalculatedFieldDialogComponent, CalculatedFieldDialogData, CalculatedFieldScriptTestDialogComponent, @@ -61,6 +59,8 @@ import { EntityDebugSettingsService } from '@home/components/entity/debug/entity import { DatePipe } from '@angular/common'; import { UtilsService } from "@core/services/utils.service"; import { ActionNotificationShow } from "@core/notification/notification.actions"; +import { CalculatedFieldEventBody, DebugEventType, Event as DebugEvent, EventType } from '@shared/models/event.models'; +import { EventsDialogComponent, EventsDialogData } from '@home/dialogs/events-dialog.component'; export class CalculatedFieldsTableConfig extends EntityTableConfig { @@ -233,13 +233,30 @@ export class CalculatedFieldsTableConfig extends EntityTableConfig(CalculatedFieldDebugDialogComponent, { + const customCellActionEnabledFn = (event: DebugEvent) => { + return (calculatedField.type === CalculatedFieldType.SCRIPT || + (calculatedField.type === CalculatedFieldType.PROPAGATION && + calculatedField.configuration.applyExpressionToResolvedArguments) + ) && !!(event as DebugEvent).body.arguments; + }; + + const onDebugEventSelected = (event: CalculatedFieldEventBody, dialogRef: MatDialogRef) => { + this.getTestScriptDialog(calculatedField, JSON.parse(event.arguments)) + .subscribe(expression => dialogRef.close(expression)); + }; + + this.dialog.open(EventsDialogComponent, { disableClose: true, panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], data: { + title: 'alarm-rule.debugging', tenantId: this.tenantId, - value: calculatedField, - getTestScriptDialogFn: this.getTestScriptDialog.bind(this), + value: calculatedField.id, + debugEventTypes:[DebugEventType.DEBUG_CALCULATED_FIELD], + disabledEventTypes:[EventType.LC_EVENT, EventType.ERROR, EventType.STATS], + defaultEventType: DebugEventType.DEBUG_CALCULATED_FIELD, + onDebugEventSelected: onDebugEventSelected, + customCellActionEnabledFn: customCellActionEnabledFn } }) .afterClosed() diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/components/debug-dialog/calculated-field-debug-dialog.component.html b/ui-ngx/src/app/modules/home/components/calculated-fields/components/debug-dialog/calculated-field-debug-dialog.component.html deleted file mode 100644 index 168657b8cb..0000000000 --- a/ui-ngx/src/app/modules/home/components/calculated-fields/components/debug-dialog/calculated-field-debug-dialog.component.html +++ /dev/null @@ -1,47 +0,0 @@ - -
- -

{{ dialogTitle | translate}}

- - -
-
- -
-
- -
-
diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/components/debug-dialog/calculated-field-debug-dialog.component.scss b/ui-ngx/src/app/modules/home/components/calculated-fields/components/debug-dialog/calculated-field-debug-dialog.component.scss deleted file mode 100644 index c4798d06f9..0000000000 --- a/ui-ngx/src/app/modules/home/components/calculated-fields/components/debug-dialog/calculated-field-debug-dialog.component.scss +++ /dev/null @@ -1,26 +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. - */ -:host { - .debug-dialog-container { - width: 1080px; - max-width: 100%; - - .debug-dialog-content { - height: 65vh; - border-radius: 0; - } - } -} 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 deleted file mode 100644 index 70bfa43c63..0000000000 --- a/ui-ngx/src/app/modules/home/components/calculated-fields/components/debug-dialog/calculated-field-debug-dialog.component.ts +++ /dev/null @@ -1,77 +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. -/// - -import { AfterViewInit, Component, Inject, ViewChild } from '@angular/core'; -import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; -import { Store } from '@ngrx/store'; -import { AppState } from '@core/core.state'; -import { Router } from '@angular/router'; -import { DialogComponent } from '@shared/components/dialog.component'; -import { CalculatedFieldEventBody, DebugEventType, Event, EventType } from '@shared/models/event.models'; -import { EventTableComponent } from '@home/components/event/event-table.component'; -import { - CalculatedField, - CalculatedFieldTestScriptFn, - CalculatedFieldType -} from '@shared/models/calculated-field.models'; - -export interface CalculatedFieldDebugDialogData { - tenantId: string; - value: CalculatedField; - getTestScriptDialogFn: CalculatedFieldTestScriptFn; -} - -@Component({ - selector: 'tb-calculated-field-debug-dialog', - styleUrls: ['calculated-field-debug-dialog.component.scss'], - templateUrl: './calculated-field-debug-dialog.component.html', -}) -export class CalculatedFieldDebugDialogComponent extends DialogComponent implements AfterViewInit { - - @ViewChild(EventTableComponent, {static: true}) eventsTable: EventTableComponent; - - readonly DebugEventType = DebugEventType; - readonly debugEventTypes = DebugEventType; - readonly EventType = EventType; - - dialogTitle: string; - - constructor(protected store: Store, - protected router: Router, - @Inject(MAT_DIALOG_DATA) public data: CalculatedFieldDebugDialogData, - protected dialogRef: MatDialogRef) { - super(store, router, dialogRef); - this.dialogTitle = this.data.value.type === CalculatedFieldType.ALARM ? 'alarm-rule.debugging' : 'calculated-fields.debugging'; - } - - ngAfterViewInit(): void { - 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(); - } - - cancel(): void { - this.dialogRef.close(null); - } - - onDebugEventSelected(event: CalculatedFieldEventBody): void { - this.data.getTestScriptDialogFn(this.data.value, JSON.parse(event.arguments)) - .subscribe(expression => this.dialogRef.close(expression)); - } -} diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/components/public-api.ts b/ui-ngx/src/app/modules/home/components/calculated-fields/components/public-api.ts index d4d4f9d1da..f114fd4d67 100644 --- a/ui-ngx/src/app/modules/home/components/calculated-fields/components/public-api.ts +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/components/public-api.ts @@ -15,5 +15,4 @@ /// export * from './dialog/calculated-field-dialog.component'; -export * from './debug-dialog/calculated-field-debug-dialog.component'; export * from './test-dialog/calculated-field-script-test-dialog.component'; diff --git a/ui-ngx/src/app/modules/home/components/home-components.module.ts b/ui-ngx/src/app/modules/home/components/home-components.module.ts index 8a90c28878..b159b9240c 100644 --- a/ui-ngx/src/app/modules/home/components/home-components.module.ts +++ b/ui-ngx/src/app/modules/home/components/home-components.module.ts @@ -192,9 +192,6 @@ import { AIModelDialogComponent } from '@home/components/ai-model/ai-model-dialo import { ResourcesDialogComponent } from "@home/components/resources/resources-dialog.component"; import { ResourcesLibraryComponent } from "@home/components/resources/resources-library.component"; import { CalculatedFieldsTableComponent } from '@home/components/calculated-fields/calculated-fields-table.component'; -import { - CalculatedFieldDebugDialogComponent -} from '@home/components/calculated-fields/components/debug-dialog/calculated-field-debug-dialog.component'; import { CalculatedFieldsModule } from '@home/components/calculated-fields/calculated-field.module'; import { AlarmRuleModule } from "@home/components/alarm-rules/alarm-rule.module"; import { AlarmRulesTableComponent } from "@home/components/alarm-rules/alarm-rules-table.component"; @@ -204,7 +201,7 @@ import { EditApiKeyDescriptionPanelComponent } from '@home/components/api-key/ed import { ApiKeyGeneratedDialogComponent } from '@home/components/api-key/api-key-generated-dialog.component'; import { ApiKeysTableDialogComponent } from '@home/components/api-key/api-keys-table-dialog.component'; import { AuditLogFilterComponent } from "@home/components/audit-log/audit-log-filter.component"; -import { EventsDialogComponent } from '@home/components/event/events-dialog.component'; +import { EventsDialogComponent } from '@home/dialogs/events-dialog.component'; @NgModule({ declarations: @@ -218,7 +215,6 @@ import { EventsDialogComponent } from '@home/components/event/events-dialog.comp AuditLogTableComponent, AuditLogDetailsDialogComponent, CalculatedFieldsTableComponent, - CalculatedFieldDebugDialogComponent, AlarmRulesTableComponent, EventContentDialogComponent, EventTableHeaderComponent, diff --git a/ui-ngx/src/app/modules/home/components/event/events-dialog.component.html b/ui-ngx/src/app/modules/home/dialogs/events-dialog.component.html similarity index 79% rename from ui-ngx/src/app/modules/home/components/event/events-dialog.component.html rename to ui-ngx/src/app/modules/home/dialogs/events-dialog.component.html index 7aa1f3dd7f..5197f6bb4e 100644 --- a/ui-ngx/src/app/modules/home/components/event/events-dialog.component.html +++ b/ui-ngx/src/app/modules/home/dialogs/events-dialog.component.html @@ -27,13 +27,14 @@
diff --git a/ui-ngx/src/app/modules/home/components/event/events-dialog.component.scss b/ui-ngx/src/app/modules/home/dialogs/events-dialog.component.scss similarity index 100% rename from ui-ngx/src/app/modules/home/components/event/events-dialog.component.scss rename to ui-ngx/src/app/modules/home/dialogs/events-dialog.component.scss diff --git a/ui-ngx/src/app/modules/home/components/event/events-dialog.component.ts b/ui-ngx/src/app/modules/home/dialogs/events-dialog.component.ts similarity index 64% rename from ui-ngx/src/app/modules/home/components/event/events-dialog.component.ts rename to ui-ngx/src/app/modules/home/dialogs/events-dialog.component.ts index 30e8a0f4f1..613f4e9c15 100644 --- a/ui-ngx/src/app/modules/home/components/event/events-dialog.component.ts +++ b/ui-ngx/src/app/modules/home/dialogs/events-dialog.component.ts @@ -21,17 +21,20 @@ import { EventTableComponent } from '@home/components/event/event-table.componen import { Store } from '@ngrx/store'; import { AppState } from '@core/core.state'; import { Router } from '@angular/router'; -import { DebugEventType } from '@shared/models/event.models'; -import { TenantId } from '@shared/models/id/tenant-id'; +import { DebugEventType, EventType } from '@shared/models/event.models'; import { EntityId } from '@shared/models/id/entity-id'; +import { BaseData, HasId } from '@shared/models/base-data'; -export interface RuleNodeDebugDialogData { +export interface EventsDialogData { title: string; - tenantId: TenantId; - entityId: EntityId; - debugEventType: DebugEventType; - functionTestButtonLabel: string; - onDebugEventSelected: ()=> void; + tenantId: string; + value: EntityId; + debugEventTypes: Array; + defaultEventType: DebugEventType; + disabledEventTypes?: Array; + functionTestButtonLabel?: string; + onDebugEventSelected?: (event: any, dialogRef: MatDialogRef) => void; + customCellActionEnabledFn?: (event: BaseData) => boolean; } @Component({ @@ -45,16 +48,25 @@ export class EventsDialogComponent extends DialogComponent, protected router: Router, - @Inject(MAT_DIALOG_DATA) public data: RuleNodeDebugDialogData, + @Inject(MAT_DIALOG_DATA) public data: EventsDialogData, protected dialogRef: MatDialogRef) { super(store, router, dialogRef); } ngAfterViewInit() { + if (this.data.customCellActionEnabledFn && this.eventsTable.entitiesTable.cellActionDescriptors?.length > 0) { + this.eventsTable.entitiesTable.cellActionDescriptors[0].isEnabled = this.data.customCellActionEnabledFn; + } this.eventsTable.entitiesTable.updateData(); } cancel(): void { this.dialogRef.close(null); } + + onDebugEventSelected(event: any): void { + if(this.data.onDebugEventSelected) { + this.data.onDebugEventSelected(event, this.dialogRef); + } + } } diff --git a/ui-ngx/src/app/modules/home/pages/rulechain/rulechain-page.component.ts b/ui-ngx/src/app/modules/home/pages/rulechain/rulechain-page.component.ts index 163b4ff1f6..aa2b4fd3eb 100644 --- a/ui-ngx/src/app/modules/home/pages/rulechain/rulechain-page.component.ts +++ b/ui-ngx/src/app/modules/home/pages/rulechain/rulechain-page.component.ts @@ -100,7 +100,7 @@ import { EntityDebugSettings } from '@shared/models/entity.models'; import Timeout = NodeJS.Timeout; import { DomSanitizer } from '@angular/platform-browser'; import { AdditionalDebugActionConfig } from '@home/components/entity/debug/entity-debug-settings.model'; -import { EventsDialogComponent } from '@home/components/event/events-dialog.component'; +import { EventsDialogComponent } from '@home/dialogs/events-dialog.component'; @Component({ selector: 'tb-rulechain-page', @@ -1679,11 +1679,12 @@ export class RuleChainPageComponent extends PageComponent panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], data: { title: 'rulenode.events', - debugEventType: DebugEventType.DEBUG_RULE_CHAIN, - tenantId: this.ruleChain.tenantId, - entityId: this.editingRuleNode.ruleNodeId, + debugEventTypes: [DebugEventType.DEBUG_RULE_CHAIN], + defaultEventType: DebugEventType.DEBUG_RULE_CHAIN, + tenantId: this.ruleChain.tenantId.id, + value: this.editingRuleNode.ruleNodeId, functionTestButtonLabel: this.ruleNodeTestButtonLabel, - debugEventSelected: this.onDebugEventSelected.bind(this) + onDebugEventSelected: this.onDebugEventSelected.bind(this) } }) .afterClosed() From ece81a14770c2028d75182a4af666a3685cd1c67 Mon Sep 17 00:00:00 2001 From: Maksym Tsymbarov Date: Mon, 8 Dec 2025 17:01:22 +0200 Subject: [PATCH 22/32] moved fixed rulenode DebugEventType --- .../calculated-fields/calculated-fields-table-config.ts | 2 +- .../modules/home/pages/rulechain/rulechain-page.component.ts | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) 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 f024e1c88c..18dcb28a2e 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 @@ -249,7 +249,7 @@ export class CalculatedFieldsTableConfig extends EntityTableConfig Date: Mon, 8 Dec 2025 17:04:16 +0200 Subject: [PATCH 23/32] refactoring --- .../cf/ctx/state/CalculatedFieldCtx.java | 44 ++----------------- .../AttributesImmediateOutputStrategy.java | 22 ++++++++++ .../AttributesRuleChainOutputStrategy.java | 11 +++++ .../data/cf/configuration/OutputStrategy.java | 6 +++ .../TimeSeriesImmediateOutputStrategy.java | 21 +++++++++ .../TimeSeriesRuleChainOutputStrategy.java | 11 +++++ 6 files changed, 74 insertions(+), 41 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/CalculatedFieldCtx.java b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/CalculatedFieldCtx.java index d2bc8d30cf..7e5d666ca5 100644 --- a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/CalculatedFieldCtx.java +++ b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/CalculatedFieldCtx.java @@ -37,14 +37,12 @@ import org.thingsboard.server.common.data.cf.configuration.AlarmCalculatedFieldC import org.thingsboard.server.common.data.cf.configuration.Argument; import org.thingsboard.server.common.data.cf.configuration.ArgumentType; import org.thingsboard.server.common.data.cf.configuration.ArgumentsBasedCalculatedFieldConfiguration; -import org.thingsboard.server.common.data.cf.configuration.AttributesImmediateOutputStrategy; import org.thingsboard.server.common.data.cf.configuration.ExpressionBasedCalculatedFieldConfiguration; import org.thingsboard.server.common.data.cf.configuration.Output; import org.thingsboard.server.common.data.cf.configuration.PropagationCalculatedFieldConfiguration; import org.thingsboard.server.common.data.cf.configuration.ReferencedEntityKey; import org.thingsboard.server.common.data.cf.configuration.ScheduledUpdateSupportedCalculatedFieldConfiguration; import org.thingsboard.server.common.data.cf.configuration.SimpleCalculatedFieldConfiguration; -import org.thingsboard.server.common.data.cf.configuration.TimeSeriesImmediateOutputStrategy; import org.thingsboard.server.common.data.cf.configuration.aggregation.AggFunctionInput; import org.thingsboard.server.common.data.cf.configuration.aggregation.RelatedEntitiesAggregationCalculatedFieldConfiguration; import org.thingsboard.server.common.data.cf.configuration.aggregation.single.EntityAggregationCalculatedFieldConfiguration; @@ -641,27 +639,10 @@ public class CalculatedFieldCtx implements Closeable { public boolean hasRefreshContextOnlyChanges(CalculatedFieldCtx other) { // has changes that do not require state recalculation var thisOutputStrategy = calculatedField.getConfiguration().getOutput().getStrategy(); var otherOutputStrategy = other.getCalculatedField().getConfiguration().getOutput().getStrategy(); - - if (!thisOutputStrategy.getType().equals(otherOutputStrategy.getType())) { + if (thisOutputStrategy.hasRefreshContextOnlyChanges(otherOutputStrategy)) { return true; } - if (thisOutputStrategy instanceof TimeSeriesImmediateOutputStrategy thisTsOutputStrategy - && otherOutputStrategy instanceof TimeSeriesImmediateOutputStrategy otherTsOutputStrategy) { - if (thisTsOutputStrategy.getTtl() != otherTsOutputStrategy.getTtl()) { - return true; - } - } - - if (thisOutputStrategy instanceof AttributesImmediateOutputStrategy thisAttrOutputStrategy - && otherOutputStrategy instanceof AttributesImmediateOutputStrategy otherAttrOutputStrategy) { - boolean updateAttrOnValueChangedChanged = thisAttrOutputStrategy.isUpdateAttributesOnlyOnValueChange() != otherAttrOutputStrategy.isUpdateAttributesOnlyOnValueChange(); - boolean sendAttrUpdatedNotificationChanged = thisAttrOutputStrategy.isSendAttributesUpdatedNotification() != otherAttrOutputStrategy.isSendAttributesUpdatedNotification(); - if (updateAttrOnValueChangedChanged || sendAttrUpdatedNotificationChanged) { - return true; - } - } - if (calculatedField.getConfiguration() instanceof EntityAggregationCalculatedFieldConfiguration thisConfig && other.getCalculatedField().getConfiguration() instanceof EntityAggregationCalculatedFieldConfiguration otherConfig) { if (thisConfig.isProduceIntermediateResult() != otherConfig.isProduceIntermediateResult()) { @@ -759,28 +740,9 @@ public class CalculatedFieldCtx implements Closeable { if (!Objects.equals(output.getDecimalsByDefault(), otherOutput.getDecimalsByDefault())) { return true; } - - var thisOutputStrategy = output.getStrategy(); - var otherOutputStrategy = otherOutput.getStrategy(); - - if (thisOutputStrategy instanceof TimeSeriesImmediateOutputStrategy thisTimeSeriesImmediateOutputStrategy - && otherOutputStrategy instanceof TimeSeriesImmediateOutputStrategy otherTimeSeriesImmediateOutputStrategy) { - boolean saveTimeSeriesUpdated = thisTimeSeriesImmediateOutputStrategy.isSaveTimeSeries() != otherTimeSeriesImmediateOutputStrategy.isSaveTimeSeries(); - boolean saveLatestUpdated = thisTimeSeriesImmediateOutputStrategy.isSaveLatest() != otherTimeSeriesImmediateOutputStrategy.isSaveLatest(); - boolean sendWsUpdateUpdated = thisTimeSeriesImmediateOutputStrategy.isSendWsUpdate() != otherTimeSeriesImmediateOutputStrategy.isSendWsUpdate(); - boolean processCfsUpdated = thisTimeSeriesImmediateOutputStrategy.isProcessCfs() != otherTimeSeriesImmediateOutputStrategy.isProcessCfs(); - return saveTimeSeriesUpdated || saveLatestUpdated || sendWsUpdateUpdated || processCfsUpdated; - } - - if (thisOutputStrategy instanceof AttributesImmediateOutputStrategy thisAttributesImmediateOutputStrategy - && otherOutputStrategy instanceof AttributesImmediateOutputStrategy otherAttributesImmediateOutputStrategy) { - - boolean saveTimeSeriesUpdated = thisAttributesImmediateOutputStrategy.isSaveAttribute() != otherAttributesImmediateOutputStrategy.isSaveAttribute(); - boolean sendWsUpdateUpdated = thisAttributesImmediateOutputStrategy.isSendWsUpdate() != otherAttributesImmediateOutputStrategy.isSendWsUpdate(); - boolean processCfsUpdated = thisAttributesImmediateOutputStrategy.isProcessCfs() != otherAttributesImmediateOutputStrategy.isProcessCfs(); - return saveTimeSeriesUpdated || sendWsUpdateUpdated || processCfsUpdated; + if (output.getStrategy().hasContextOnlyChanges(otherOutput.getStrategy())) { + return true; } - return false; } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/AttributesImmediateOutputStrategy.java b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/AttributesImmediateOutputStrategy.java index 714180930d..81874002cb 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/AttributesImmediateOutputStrategy.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/AttributesImmediateOutputStrategy.java @@ -35,4 +35,26 @@ public class AttributesImmediateOutputStrategy implements AttributesOutputStrate public OutputStrategyType getType() { return OutputStrategyType.IMMEDIATE; } + + @Override + public boolean hasContextOnlyChanges(OutputStrategy other) { + if (!(other instanceof AttributesImmediateOutputStrategy otherStrategy)) { + return true; + } + boolean saveTimeSeriesUpdated = saveAttribute != otherStrategy.isSaveAttribute(); + boolean sendWsUpdateUpdated = sendWsUpdate != otherStrategy.isSendWsUpdate(); + boolean processCfsUpdated = processCfs != otherStrategy.isProcessCfs(); + return saveTimeSeriesUpdated || sendWsUpdateUpdated || processCfsUpdated; + } + + @Override + public boolean hasRefreshContextOnlyChanges(OutputStrategy other) { + if (!(other instanceof AttributesImmediateOutputStrategy otherStrategy)) { + return true; + } + boolean updateAttrOnValueChangedChanged = updateAttributesOnlyOnValueChange != otherStrategy.isUpdateAttributesOnlyOnValueChange(); + boolean sendAttrUpdatedNotificationChanged = sendAttributesUpdatedNotification != otherStrategy.isSendAttributesUpdatedNotification(); + return updateAttrOnValueChangedChanged || sendAttrUpdatedNotificationChanged; + } + } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/AttributesRuleChainOutputStrategy.java b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/AttributesRuleChainOutputStrategy.java index 1a3348ce74..f39efadb86 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/AttributesRuleChainOutputStrategy.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/AttributesRuleChainOutputStrategy.java @@ -26,4 +26,15 @@ public class AttributesRuleChainOutputStrategy implements AttributesOutputStrate public OutputStrategyType getType() { return OutputStrategyType.RULE_CHAIN; } + + @Override + public boolean hasContextOnlyChanges(OutputStrategy other) { + return !(other instanceof AttributesRuleChainOutputStrategy); + } + + @Override + public boolean hasRefreshContextOnlyChanges(OutputStrategy other) { + return false; + } + } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/OutputStrategy.java b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/OutputStrategy.java index b635047ae3..252af82ef7 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/OutputStrategy.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/OutputStrategy.java @@ -22,4 +22,10 @@ public interface OutputStrategy { @JsonIgnore OutputStrategyType getType(); + @JsonIgnore + boolean hasContextOnlyChanges(OutputStrategy other); + + @JsonIgnore + boolean hasRefreshContextOnlyChanges(OutputStrategy other); + } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/TimeSeriesImmediateOutputStrategy.java b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/TimeSeriesImmediateOutputStrategy.java index 31b15095d4..a24bfc8683 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/TimeSeriesImmediateOutputStrategy.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/TimeSeriesImmediateOutputStrategy.java @@ -35,4 +35,25 @@ public class TimeSeriesImmediateOutputStrategy implements TimeSeriesOutputStrate public OutputStrategyType getType() { return OutputStrategyType.IMMEDIATE; } + + @Override + public boolean hasContextOnlyChanges(OutputStrategy other) { + if (!(other instanceof TimeSeriesImmediateOutputStrategy otherStrategy)) { + return true; + } + boolean saveTimeSeriesUpdated = saveTimeSeries != otherStrategy.isSaveTimeSeries(); + boolean saveLatestUpdated = saveLatest != otherStrategy.isSaveLatest(); + boolean sendWsUpdateUpdated = sendWsUpdate != otherStrategy.isSendWsUpdate(); + boolean processCfsUpdated = processCfs != otherStrategy.isProcessCfs(); + return saveTimeSeriesUpdated || saveLatestUpdated || sendWsUpdateUpdated || processCfsUpdated; + } + + @Override + public boolean hasRefreshContextOnlyChanges(OutputStrategy other) { + if (!(other instanceof TimeSeriesImmediateOutputStrategy otherStrategy)) { + return true; + } + return ttl != otherStrategy.getTtl(); + } + } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/TimeSeriesRuleChainOutputStrategy.java b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/TimeSeriesRuleChainOutputStrategy.java index 7786efff1a..0b17594869 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/TimeSeriesRuleChainOutputStrategy.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/TimeSeriesRuleChainOutputStrategy.java @@ -26,4 +26,15 @@ public class TimeSeriesRuleChainOutputStrategy implements TimeSeriesOutputStrate public OutputStrategyType getType() { return OutputStrategyType.RULE_CHAIN; } + + @Override + public boolean hasContextOnlyChanges(OutputStrategy other) { + return !(other instanceof TimeSeriesRuleChainOutputStrategy); + } + + @Override + public boolean hasRefreshContextOnlyChanges(OutputStrategy other) { + return false; + } + } From 09eb511599225583100fe2eb2ab7c1f79bc98f48 Mon Sep 17 00:00:00 2001 From: dshvaika Date: Mon, 8 Dec 2025 16:51:40 +0200 Subject: [PATCH 24/32] Fix key_dictionary race causing cached keyId 0 --- .../dictionary/KeyDictionaryCompositeKey.java | 2 +- .../sqlts/dictionary/KeyDictionaryEntry.java | 5 +- .../sqlts/dictionary/JpaKeyDictionaryDao.java | 59 ++++------ .../dictionary/KeyDictionaryRepository.java | 4 + .../dictionary/KeyDictionaryDaoTest.java | 111 ++++++++++++++++++ 5 files changed, 141 insertions(+), 40 deletions(-) create mode 100644 dao/src/test/java/org/thingsboard/server/dao/sqlts/dictionary/KeyDictionaryDaoTest.java diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/sqlts/dictionary/KeyDictionaryCompositeKey.java b/dao/src/main/java/org/thingsboard/server/dao/model/sqlts/dictionary/KeyDictionaryCompositeKey.java index 4f3285b9bf..00e49ea703 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/model/sqlts/dictionary/KeyDictionaryCompositeKey.java +++ b/dao/src/main/java/org/thingsboard/server/dao/model/sqlts/dictionary/KeyDictionaryCompositeKey.java @@ -25,7 +25,7 @@ import java.io.Serializable; @Data @NoArgsConstructor @AllArgsConstructor -public class KeyDictionaryCompositeKey implements Serializable{ +public class KeyDictionaryCompositeKey implements Serializable { @Transient private static final long serialVersionUID = -4089175869616037523L; diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/sqlts/dictionary/KeyDictionaryEntry.java b/dao/src/main/java/org/thingsboard/server/dao/model/sqlts/dictionary/KeyDictionaryEntry.java index a95c7a2bc6..d98105c0bc 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/model/sqlts/dictionary/KeyDictionaryEntry.java +++ b/dao/src/main/java/org/thingsboard/server/dao/model/sqlts/dictionary/KeyDictionaryEntry.java @@ -36,8 +36,7 @@ public final class KeyDictionaryEntry { @Column(name = KEY_COLUMN) private String key; - @Column(name = KEY_ID_COLUMN, unique = true, columnDefinition = "int") - @Generated - private int keyId; + @Column(name = KEY_ID_COLUMN, unique = true, columnDefinition = "int", insertable = false, updatable = false) + private Integer keyId; } \ No newline at end of file diff --git a/dao/src/main/java/org/thingsboard/server/dao/sqlts/dictionary/JpaKeyDictionaryDao.java b/dao/src/main/java/org/thingsboard/server/dao/sqlts/dictionary/JpaKeyDictionaryDao.java index c14c069f23..1ef2c2d6aa 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sqlts/dictionary/JpaKeyDictionaryDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sqlts/dictionary/JpaKeyDictionaryDao.java @@ -17,8 +17,6 @@ package org.thingsboard.server.dao.sqlts.dictionary; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; -import org.hibernate.exception.ConstraintViolationException; -import org.springframework.dao.DataIntegrityViolationException; import org.springframework.stereotype.Component; import org.springframework.transaction.annotation.Propagation; import org.springframework.transaction.annotation.Transactional; @@ -48,43 +46,32 @@ public class JpaKeyDictionaryDao extends JpaAbstractDaoListeningExecutorService @Transactional(propagation = Propagation.NOT_SUPPORTED) @Override public Integer getOrSaveKeyId(String strKey) { - Integer keyId = keyDictionaryMap.get(strKey); - if (keyId == null) { - Optional tsKvDictionaryOptional; - tsKvDictionaryOptional = keyDictionaryRepository.findById(new KeyDictionaryCompositeKey(strKey)); - if (tsKvDictionaryOptional.isEmpty()) { - creationLock.lock(); - try { - keyId = keyDictionaryMap.get(strKey); - if (keyId != null) { - return keyId; - } - tsKvDictionaryOptional = keyDictionaryRepository.findById(new KeyDictionaryCompositeKey(strKey)); - if (tsKvDictionaryOptional.isEmpty()) { - KeyDictionaryEntry keyDictionaryEntry = new KeyDictionaryEntry(); - keyDictionaryEntry.setKey(strKey); - try { - KeyDictionaryEntry saved = keyDictionaryRepository.save(keyDictionaryEntry); - keyDictionaryMap.put(saved.getKey(), saved.getKeyId()); - keyId = saved.getKeyId(); - } catch (DataIntegrityViolationException | ConstraintViolationException e) { - tsKvDictionaryOptional = keyDictionaryRepository.findById(new KeyDictionaryCompositeKey(strKey)); - KeyDictionaryEntry dictionary = tsKvDictionaryOptional.orElseThrow(() -> new RuntimeException("Failed to get KeyDictionaryEntry entity from DB!")); - keyDictionaryMap.put(dictionary.getKey(), dictionary.getKeyId()); - keyId = dictionary.getKeyId(); - } - } else { - keyId = tsKvDictionaryOptional.get().getKeyId(); - } - } finally { - creationLock.unlock(); + Integer cached = keyDictionaryMap.get(strKey); + if (cached != null) { + return cached; + } + creationLock.lock(); + try { + Integer keyId = keyDictionaryMap.get(strKey); + if (keyId != null) { + return keyId; + } + keyId = keyDictionaryRepository.upsertAndGetKeyId(strKey); + if (keyId == null || keyId == 0) { + log.warn("upsertAndGetKeyId returned: [{}] for key: [{}], falling back to findById", keyId, strKey); + KeyDictionaryCompositeKey id = new KeyDictionaryCompositeKey(strKey); + Optional entryOpt = keyDictionaryRepository.findById(id); + if (entryOpt.isEmpty() || + entryOpt.get().getKeyId() == null || + entryOpt.get().getKeyId() == 0) { + throw new IllegalStateException("Failed to resolve keyId for string key: " + strKey + " after fallback. keyId: " + keyId); } - } else { - keyId = tsKvDictionaryOptional.get().getKeyId(); - keyDictionaryMap.put(strKey, keyId); } + keyDictionaryMap.put(strKey, keyId); + return keyId; + } finally { + creationLock.unlock(); } - return keyId; } @Override diff --git a/dao/src/main/java/org/thingsboard/server/dao/sqlts/dictionary/KeyDictionaryRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sqlts/dictionary/KeyDictionaryRepository.java index d264cd9966..a1141b62ff 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sqlts/dictionary/KeyDictionaryRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sqlts/dictionary/KeyDictionaryRepository.java @@ -19,6 +19,7 @@ import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; import org.thingsboard.server.dao.model.sqlts.dictionary.KeyDictionaryCompositeKey; import org.thingsboard.server.dao.model.sqlts.dictionary.KeyDictionaryEntry; @@ -31,4 +32,7 @@ public interface KeyDictionaryRepository extends JpaRepository findAll(Pageable pageable); + @Query(value = "INSERT INTO key_dictionary (key) VALUES (:key) ON CONFLICT (key) DO UPDATE SET key = EXCLUDED.key RETURNING key_id", nativeQuery = true) + Integer upsertAndGetKeyId(@Param("key") String key); + } \ No newline at end of file diff --git a/dao/src/test/java/org/thingsboard/server/dao/sqlts/dictionary/KeyDictionaryDaoTest.java b/dao/src/test/java/org/thingsboard/server/dao/sqlts/dictionary/KeyDictionaryDaoTest.java new file mode 100644 index 0000000000..11c14c99f4 --- /dev/null +++ b/dao/src/test/java/org/thingsboard/server/dao/sqlts/dictionary/KeyDictionaryDaoTest.java @@ -0,0 +1,111 @@ +/** + * 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.sqlts.dictionary; + +import org.junit.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.thingsboard.server.dao.dictionary.KeyDictionaryDao; +import org.thingsboard.server.dao.model.sqlts.dictionary.KeyDictionaryCompositeKey; +import org.thingsboard.server.dao.model.sqlts.dictionary.KeyDictionaryEntry; +import org.thingsboard.server.dao.service.AbstractServiceTest; +import org.thingsboard.server.dao.service.DaoSqlTest; + +import java.util.Arrays; +import java.util.Optional; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; + +import static org.assertj.core.api.Assertions.assertThat; + +@DaoSqlTest +public class KeyDictionaryDaoTest extends AbstractServiceTest { + + @Autowired + private KeyDictionaryDao keyDictionaryDao; + + @Autowired + private KeyDictionaryRepository keyDictionaryRepository; + + private static final String KEY = "testKeyDictionaryDaoTestKey"; + + @Test + public void testGetOrSaveKeyId_concurrent() throws Exception { + int threads = 8; + ExecutorService executor = Executors.newFixedThreadPool(threads); + + CountDownLatch allReady = new CountDownLatch(threads); + CountDownLatch start = new CountDownLatch(1); + CountDownLatch allDone = new CountDownLatch(threads); + + Integer[] keyIds = new Integer[threads]; + + try { + for (int i = 0; i < threads; i++) { + final int idx = i; + executor.submit(() -> { + allReady.countDown(); + try { + // wait until all threads are ready + start.await(); + // concurrent call + Integer id = keyDictionaryDao.getOrSaveKeyId(KEY); + keyIds[idx] = id; + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } finally { + allDone.countDown(); + } + }); + } + + // ensure all threads are queued + allReady.await(5, TimeUnit.SECONDS); + // fire the start gun + start.countDown(); + // wait for all to finish + allDone.await(10, TimeUnit.SECONDS); + } finally { + executor.shutdownNow(); + } + + // basic sanity + for (int i = 0; i < threads; i++) { + assertThat(keyIds[i]) + .as("keyId[%s]", i) + .isNotNull() + .isGreaterThan(0); + } + + // all threads must see the same keyId + int first = keyIds[0]; + assertThat(first).isGreaterThan(0); + assertThat(Arrays.stream(keyIds).distinct().count()) + .as("all threads should get the same keyId") + .isEqualTo(1); + + // DB must have exactly one row for this key and the same id + KeyDictionaryCompositeKey id = new KeyDictionaryCompositeKey(KEY); + Optional entry = keyDictionaryRepository.findById(id); + + assertThat(entry.isPresent()).isTrue(); + assertThat(entry.get().getKeyId()).isEqualTo(first); + + keyDictionaryRepository.deleteById(id); + } + +} From d66e9ecf74b430ac6ab9bee4fa6b863157fbb419 Mon Sep 17 00:00:00 2001 From: dshvaika Date: Mon, 8 Dec 2025 17:45:00 +0200 Subject: [PATCH 25/32] Added new lines to the end of files --- .../server/dao/model/sqlts/dictionary/KeyDictionaryEntry.java | 2 +- .../server/dao/sqlts/dictionary/KeyDictionaryRepository.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/sqlts/dictionary/KeyDictionaryEntry.java b/dao/src/main/java/org/thingsboard/server/dao/model/sqlts/dictionary/KeyDictionaryEntry.java index d98105c0bc..8365e8facc 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/model/sqlts/dictionary/KeyDictionaryEntry.java +++ b/dao/src/main/java/org/thingsboard/server/dao/model/sqlts/dictionary/KeyDictionaryEntry.java @@ -39,4 +39,4 @@ public final class KeyDictionaryEntry { @Column(name = KEY_ID_COLUMN, unique = true, columnDefinition = "int", insertable = false, updatable = false) private Integer keyId; -} \ No newline at end of file +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/sqlts/dictionary/KeyDictionaryRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sqlts/dictionary/KeyDictionaryRepository.java index a1141b62ff..e836cedb19 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sqlts/dictionary/KeyDictionaryRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sqlts/dictionary/KeyDictionaryRepository.java @@ -35,4 +35,4 @@ public interface KeyDictionaryRepository extends JpaRepository Date: Mon, 8 Dec 2025 18:13:30 +0200 Subject: [PATCH 26/32] fixes --- .../alarm-rules/alarm-rules-table-config.ts | 12 ++---------- .../calculated-fields-table-config.ts | 8 ++++---- .../home/dialogs/events-dialog.component.html | 2 +- .../modules/home/dialogs/events-dialog.component.ts | 8 ++++---- .../home/pages/rulechain/rulechain-page.component.ts | 2 +- 5 files changed, 12 insertions(+), 20 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 15c7cdaeb1..0cb6069ee9 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 @@ -264,25 +264,17 @@ export class AlarmRulesTableConfig extends EntityTableConfig { } private openDebugEventsDialog(calculatedField: CalculatedField): void { - const customCellActionEnabledFn = (event: DebugEvent) => { - return (calculatedField.type === CalculatedFieldType.SCRIPT || - (calculatedField.type === CalculatedFieldType.PROPAGATION && - calculatedField.configuration.applyExpressionToResolvedArguments) - ) && !!(event as DebugEvent).body.arguments; - }; - this.dialog.open(EventsDialogComponent, { disableClose: true, panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], data: { title: 'alarm-rule.debugging', tenantId: this.tenantId, - value: calculatedField.id, + entityId: calculatedField.id, debugEventTypes:[DebugEventType.DEBUG_CALCULATED_FIELD], disabledEventTypes:[EventType.LC_EVENT, EventType.ERROR, EventType.STATS], defaultEventType: DebugEventType.DEBUG_CALCULATED_FIELD, - onDebugEventSelected: null, - customCellActionEnabledFn: customCellActionEnabledFn + debugActionEnabledFn: () => false } }) .afterClosed() 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 18dcb28a2e..60c2878802 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 @@ -233,7 +233,7 @@ export class CalculatedFieldsTableConfig extends EntityTableConfig { + const debugActionEnabledFn = (event: DebugEvent) => { return (calculatedField.type === CalculatedFieldType.SCRIPT || (calculatedField.type === CalculatedFieldType.PROPAGATION && calculatedField.configuration.applyExpressionToResolvedArguments) @@ -251,12 +251,12 @@ export class CalculatedFieldsTableConfig extends EntityTableConfig diff --git a/ui-ngx/src/app/modules/home/dialogs/events-dialog.component.ts b/ui-ngx/src/app/modules/home/dialogs/events-dialog.component.ts index 613f4e9c15..d5339de3b2 100644 --- a/ui-ngx/src/app/modules/home/dialogs/events-dialog.component.ts +++ b/ui-ngx/src/app/modules/home/dialogs/events-dialog.component.ts @@ -28,13 +28,13 @@ import { BaseData, HasId } from '@shared/models/base-data'; export interface EventsDialogData { title: string; tenantId: string; - value: EntityId; + entityId: EntityId; debugEventTypes: Array; defaultEventType: DebugEventType; disabledEventTypes?: Array; functionTestButtonLabel?: string; onDebugEventSelected?: (event: any, dialogRef: MatDialogRef) => void; - customCellActionEnabledFn?: (event: BaseData) => boolean; + debugActionEnabledFn?: (event: BaseData) => boolean; } @Component({ @@ -54,8 +54,8 @@ export class EventsDialogComponent extends DialogComponent 0) { - this.eventsTable.entitiesTable.cellActionDescriptors[0].isEnabled = this.data.customCellActionEnabledFn; + if (this.data.debugActionEnabledFn && this.eventsTable.entitiesTable.cellActionDescriptors?.length > 0) { + this.eventsTable.entitiesTable.cellActionDescriptors[0].isEnabled = this.data.debugActionEnabledFn; } this.eventsTable.entitiesTable.updateData(); } diff --git a/ui-ngx/src/app/modules/home/pages/rulechain/rulechain-page.component.ts b/ui-ngx/src/app/modules/home/pages/rulechain/rulechain-page.component.ts index b1cf2eddf2..7f751a0bfe 100644 --- a/ui-ngx/src/app/modules/home/pages/rulechain/rulechain-page.component.ts +++ b/ui-ngx/src/app/modules/home/pages/rulechain/rulechain-page.component.ts @@ -1682,7 +1682,7 @@ export class RuleChainPageComponent extends PageComponent debugEventTypes: [DebugEventType.DEBUG_RULE_NODE], defaultEventType: DebugEventType.DEBUG_RULE_NODE, tenantId: this.ruleChain.tenantId.id, - value: this.editingRuleNode.ruleNodeId, + entityId: this.editingRuleNode.ruleNodeId, functionTestButtonLabel: this.ruleNodeTestButtonLabel, onDebugEventSelected: this.onDebugEventSelected.bind(this) } From 3243c2a2f2555d5a2a0a68dffce36fd6c239a611 Mon Sep 17 00:00:00 2001 From: dshvaika Date: Mon, 8 Dec 2025 18:36:01 +0200 Subject: [PATCH 27/32] Added find before lock for the startup of application --- .../dao/sqlts/dictionary/JpaKeyDictionaryDao.java | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/dao/src/main/java/org/thingsboard/server/dao/sqlts/dictionary/JpaKeyDictionaryDao.java b/dao/src/main/java/org/thingsboard/server/dao/sqlts/dictionary/JpaKeyDictionaryDao.java index 1ef2c2d6aa..bcb9284371 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sqlts/dictionary/JpaKeyDictionaryDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sqlts/dictionary/JpaKeyDictionaryDao.java @@ -50,6 +50,15 @@ public class JpaKeyDictionaryDao extends JpaAbstractDaoListeningExecutorService if (cached != null) { return cached; } + var compositeKey = new KeyDictionaryCompositeKey(strKey); + Optional entryOpt = keyDictionaryRepository.findById(compositeKey); + if (entryOpt.isPresent()) { + Integer keyId = entryOpt.get().getKeyId(); + if (keyId != null) { + keyDictionaryMap.put(strKey, keyId); + return keyId; + } + } creationLock.lock(); try { Integer keyId = keyDictionaryMap.get(strKey); @@ -59,8 +68,7 @@ public class JpaKeyDictionaryDao extends JpaAbstractDaoListeningExecutorService keyId = keyDictionaryRepository.upsertAndGetKeyId(strKey); if (keyId == null || keyId == 0) { log.warn("upsertAndGetKeyId returned: [{}] for key: [{}], falling back to findById", keyId, strKey); - KeyDictionaryCompositeKey id = new KeyDictionaryCompositeKey(strKey); - Optional entryOpt = keyDictionaryRepository.findById(id); + entryOpt = keyDictionaryRepository.findById(compositeKey); if (entryOpt.isEmpty() || entryOpt.get().getKeyId() == null || entryOpt.get().getKeyId() == 0) { From 6253cc699d13475b6e3e210d2b224835b8d6202f Mon Sep 17 00:00:00 2001 From: IrynaMatveieva Date: Tue, 9 Dec 2025 08:52:44 +0200 Subject: [PATCH 28/32] fixed tests --- .../cf/ctx/state/GeofencingCalculatedFieldStateTest.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/GeofencingCalculatedFieldStateTest.java b/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/GeofencingCalculatedFieldStateTest.java index b3ce7bbb44..e717392979 100644 --- a/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/GeofencingCalculatedFieldStateTest.java +++ b/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/GeofencingCalculatedFieldStateTest.java @@ -247,7 +247,7 @@ public class GeofencingCalculatedFieldStateTest { assertThat(result).isNotNull(); assertThat(result.getType()).isEqualTo(output.getType()); assertThat(result.getScope()).isEqualTo(output.getScope()); - assertThat(result.getResult()).isEqualTo( + assertThat(result.getResult().get("values")).isEqualTo( JacksonUtil.newObjectNode() .put("allowedZonesEvent", "ENTERED") .put("allowedZonesStatus", "INSIDE") @@ -329,7 +329,7 @@ public class GeofencingCalculatedFieldStateTest { assertThat(result).isNotNull(); assertThat(result.getType()).isEqualTo(output.getType()); assertThat(result.getScope()).isEqualTo(output.getScope()); - assertThat(result.getResult()).isEqualTo( + assertThat(result.getResult().get("values")).isEqualTo( JacksonUtil.newObjectNode().put("allowedZonesEvent", "ENTERED") ); @@ -406,7 +406,7 @@ public class GeofencingCalculatedFieldStateTest { assertThat(result).isNotNull(); assertThat(result.getType()).isEqualTo(output.getType()); assertThat(result.getScope()).isEqualTo(output.getScope()); - assertThat(result.getResult()).isEqualTo( + assertThat(result.getResult().get("values")).isEqualTo( JacksonUtil.newObjectNode() .put("allowedZonesStatus", "INSIDE") .put("restrictedZonesStatus", "OUTSIDE") From c532cbaa5e0fe0bb94e07016a144dfdfc3e722e4 Mon Sep 17 00:00:00 2001 From: IrynaMatveieva Date: Tue, 9 Dec 2025 10:04:41 +0200 Subject: [PATCH 29/32] added tests --- .../cf/ctx/state/CalculatedFieldCtx.java | 21 +- .../common/data/cf/configuration/Output.java | 21 ++ .../data/cf/configuration/OutputStrategy.java | 2 - .../CalculatedFieldOutputTest.java | 202 ++++++++++++++++++ 4 files changed, 224 insertions(+), 22 deletions(-) create mode 100644 common/data/src/test/java/org/thingsboard/server/common/data/cf/configuration/CalculatedFieldOutputTest.java diff --git a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/CalculatedFieldCtx.java b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/CalculatedFieldCtx.java index 7e5d666ca5..7f515a5c9a 100644 --- a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/CalculatedFieldCtx.java +++ b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/CalculatedFieldCtx.java @@ -657,7 +657,7 @@ public class CalculatedFieldCtx implements Closeable { if (calculatedField.getConfiguration() instanceof ExpressionBasedCalculatedFieldConfiguration && !Objects.equals(expression, other.expression)) { return true; } - if (hasOutputChanges(other.output)) { + if (output.hasContextOnlyChanges(other.output)) { return true; } if (calculatedField.getConfiguration() instanceof SimpleCalculatedFieldConfiguration thisConfig @@ -727,25 +727,6 @@ public class CalculatedFieldCtx implements Closeable { return false; } - private boolean hasOutputChanges(Output otherOutput) { - if (!output.getType().equals(otherOutput.getType())) { - return true; - } - if (!Objects.equals(output.getName(), otherOutput.getName())) { - return true; - } - if (output.getScope() != (otherOutput.getScope())) { - return true; - } - if (!Objects.equals(output.getDecimalsByDefault(), otherOutput.getDecimalsByDefault())) { - return true; - } - if (output.getStrategy().hasContextOnlyChanges(otherOutput.getStrategy())) { - return true; - } - return false; - } - private boolean hasGeofencingZoneGroupConfigurationChanges(CalculatedFieldCtx other) { if (calculatedField.getConfiguration() instanceof GeofencingCalculatedFieldConfiguration thisConfig && other.calculatedField.getConfiguration() instanceof GeofencingCalculatedFieldConfiguration otherConfig) { diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/Output.java b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/Output.java index 848ad759e8..fe85ff03e3 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/Output.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/Output.java @@ -22,6 +22,8 @@ import com.fasterxml.jackson.annotation.JsonSubTypes; import com.fasterxml.jackson.annotation.JsonTypeInfo; import org.thingsboard.server.common.data.AttributeScope; +import java.util.Objects; + @JsonTypeInfo( use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY, @@ -50,4 +52,23 @@ public interface Output { void setDecimalsByDefault(Integer decimalsByDefault); + default boolean hasContextOnlyChanges(Output other) { + if (!getType().equals(other.getType())) { + return true; + } + if (!Objects.equals(getName(), other.getName())) { + return true; + } + if (getScope() != (other.getScope())) { + return true; + } + if (!Objects.equals(getDecimalsByDefault(), other.getDecimalsByDefault())) { + return true; + } + if (getStrategy().hasContextOnlyChanges(other.getStrategy())) { + return true; + } + return false; + } + } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/OutputStrategy.java b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/OutputStrategy.java index 252af82ef7..66b156ca8a 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/OutputStrategy.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/OutputStrategy.java @@ -22,10 +22,8 @@ public interface OutputStrategy { @JsonIgnore OutputStrategyType getType(); - @JsonIgnore boolean hasContextOnlyChanges(OutputStrategy other); - @JsonIgnore boolean hasRefreshContextOnlyChanges(OutputStrategy other); } diff --git a/common/data/src/test/java/org/thingsboard/server/common/data/cf/configuration/CalculatedFieldOutputTest.java b/common/data/src/test/java/org/thingsboard/server/common/data/cf/configuration/CalculatedFieldOutputTest.java new file mode 100644 index 0000000000..52afa47fbd --- /dev/null +++ b/common/data/src/test/java/org/thingsboard/server/common/data/cf/configuration/CalculatedFieldOutputTest.java @@ -0,0 +1,202 @@ +/** + * 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.cf.configuration; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.junit.jupiter.MockitoExtension; +import org.thingsboard.server.common.data.AttributeScope; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; + +@ExtendWith(MockitoExtension.class) +public class CalculatedFieldOutputTest { + + @Test + public void testHasContextOnlyChanges_whenTypeChanged_shouldReturnTrue() { + TimeSeriesOutput output = new TimeSeriesOutput(); + AttributesOutput newOutput = new AttributesOutput(); + + assertThat(output.hasContextOnlyChanges(newOutput)).isTrue(); + } + + @Test + public void testHasContextOnlyChanges_whenNameChanged_shouldReturnTrue() { + TimeSeriesOutput output = new TimeSeriesOutput(); + TimeSeriesOutput newOutput = new TimeSeriesOutput(); + newOutput.setName("new"); + + assertThat(output.hasContextOnlyChanges(newOutput)).isTrue(); + } + + @Test + public void testHasContextOnlyChanges_whenScopeChanged_shouldReturnTrue() { + AttributesOutput output = new AttributesOutput(); + output.setScope(AttributeScope.SHARED_SCOPE); + AttributesOutput newOutput = new AttributesOutput(); + newOutput.setScope(AttributeScope.SERVER_SCOPE); + + assertThat(output.hasContextOnlyChanges(newOutput)).isTrue(); + } + + @Test + public void testHasContextOnlyChanges_whenDecimalsByDefaultChanged_shouldReturnTrue() { + AttributesOutput output = new AttributesOutput(); + AttributesOutput newOutput = new AttributesOutput(); + newOutput.setDecimalsByDefault(2); + + assertThat(output.hasContextOnlyChanges(newOutput)).isTrue(); + } + + @Test + public void testHasContextOnlyChanges_whenStrategyHasContextOnlyChanges_shouldReturnTrue() { + AttributesOutputStrategy outputStrategy = mock(AttributesRuleChainOutputStrategy.class); + given(outputStrategy.hasContextOnlyChanges(any())).willReturn(true); + + AttributesOutput output = new AttributesOutput(); + output.setStrategy(outputStrategy); + AttributesOutput newOutput = new AttributesOutput(); + + assertThat(output.hasContextOnlyChanges(newOutput)).isTrue(); + } + + @Test + public void testHasContextOnlyChanges_whenStrategyDoesNotHaveContextOnlyChanges_shouldReturnTrue() { + AttributesOutputStrategy outputStrategy = mock(AttributesRuleChainOutputStrategy.class); + given(outputStrategy.hasContextOnlyChanges(any())).willReturn(false); + + AttributesOutput output = new AttributesOutput(); + output.setStrategy(outputStrategy); + AttributesOutput newOutput = new AttributesOutput(); + + assertThat(output.hasContextOnlyChanges(newOutput)).isFalse(); + } + + /* .hasContextOnlyChanges() tests*/ + + @Test + public void testAttributesImmediateOutputStrategyHasContextOnlyChanges_whenTypeChanged_shouldReturnTrue() { + AttributesImmediateOutputStrategy strategy = new AttributesImmediateOutputStrategy(true, true, false, true, true); + AttributesRuleChainOutputStrategy newStrategy = new AttributesRuleChainOutputStrategy(); + + assertThat(strategy.hasContextOnlyChanges(newStrategy)).isTrue(); + } + + @Test + public void testAttributesImmediateOutputStrategyHasContextOnlyChanges_whenSaveAttributesChanged_shouldReturnTrue() { + AttributesImmediateOutputStrategy strategy = new AttributesImmediateOutputStrategy(true, true, false, true, true); + AttributesImmediateOutputStrategy newStrategy = new AttributesImmediateOutputStrategy(true, true, true, true, true); + + assertThat(strategy.hasContextOnlyChanges(newStrategy)).isTrue(); + } + + @Test + public void testAttributesImmediateOutputStrategyHasContextOnlyChanges_whenSendWsUpdateChanged_shouldReturnTrue() { + AttributesImmediateOutputStrategy strategy = new AttributesImmediateOutputStrategy(true, true, true, false, true); + AttributesImmediateOutputStrategy newStrategy = new AttributesImmediateOutputStrategy(true, true, true, true, true); + + assertThat(strategy.hasContextOnlyChanges(newStrategy)).isTrue(); + } + + @Test + public void testAttributesImmediateOutputStrategyHasContextOnlyChanges_whenProcessCfsChanged_shouldReturnTrue() { + AttributesImmediateOutputStrategy strategy = new AttributesImmediateOutputStrategy(true, true, true, false, false); + AttributesImmediateOutputStrategy newStrategy = new AttributesImmediateOutputStrategy(true, true, true, false, true); + + assertThat(strategy.hasContextOnlyChanges(newStrategy)).isTrue(); + } + + @Test + public void testAttributesRuleChainOutputStrategyHasContextOnlyChanges_whenTypeChanged_shouldReturnTrue() { + AttributesRuleChainOutputStrategy strategy = new AttributesRuleChainOutputStrategy(); + AttributesImmediateOutputStrategy newStrategy = new AttributesImmediateOutputStrategy(true, true, false, true, true); + + assertThat(strategy.hasContextOnlyChanges(newStrategy)).isTrue(); + } + + @Test + public void testTimeSeriesImmediateOutputStrategyHasContextOnlyChanges_whenTypeChanged_shouldReturnTrue() { + TimeSeriesImmediateOutputStrategy strategy = new TimeSeriesImmediateOutputStrategy(0, false, true, true, true); + TimeSeriesRuleChainOutputStrategy newStrategy = new TimeSeriesRuleChainOutputStrategy(); + + assertThat(strategy.hasContextOnlyChanges(newStrategy)).isTrue(); + } + + @Test + public void testTimeSeriesImmediateOutputStrategyHasContextOnlyChanges_whenSaveLatestChanged_shouldReturnTrue() { + TimeSeriesImmediateOutputStrategy strategy = new TimeSeriesImmediateOutputStrategy(0, false, false, true, true); + TimeSeriesImmediateOutputStrategy newStrategy = new TimeSeriesImmediateOutputStrategy(0, false, true, true, true); + + assertThat(strategy.hasContextOnlyChanges(newStrategy)).isTrue(); + } + + @Test + public void testTimeSeriesImmediateOutputStrategyHasContextOnlyChanges_whenSendWsUpdateChanged_shouldReturnTrue() { + TimeSeriesImmediateOutputStrategy strategy = new TimeSeriesImmediateOutputStrategy(0, true, true, false, true); + TimeSeriesImmediateOutputStrategy newStrategy = new TimeSeriesImmediateOutputStrategy(0, true, true, true, true); + + assertThat(strategy.hasContextOnlyChanges(newStrategy)).isTrue(); + } + + @Test + public void testTimeSeriesImmediateOutputStrategyHasContextOnlyChanges_whenProcessCfsChanged_shouldReturnTrue() { + TimeSeriesImmediateOutputStrategy strategy = new TimeSeriesImmediateOutputStrategy(0, true, true, true, false); + TimeSeriesImmediateOutputStrategy newStrategy = new TimeSeriesImmediateOutputStrategy(0, true, true, true, true); + + assertThat(strategy.hasContextOnlyChanges(newStrategy)).isTrue(); + } + + @Test + public void testTimeSeriesRuleChainOutputStrategyHasContextOnlyChanges_whenProcessCfsChanged_shouldReturnTrue() { + TimeSeriesRuleChainOutputStrategy strategy = new TimeSeriesRuleChainOutputStrategy(); + TimeSeriesImmediateOutputStrategy newStrategy = new TimeSeriesImmediateOutputStrategy(0, true, true, true, false); + + assertThat(strategy.hasContextOnlyChanges(newStrategy)).isTrue(); + } + + /* .hasRefreshContextOnlyChanges() tests*/ + + @Test + public void testAttributesImmediateOutputStrategyHasRefreshContextOnlyChanges_whenUpdateAttrOnValueChangedChanged_shouldReturnTrue() { + AttributesImmediateOutputStrategy strategy = new AttributesImmediateOutputStrategy(true, false, true, false, true); + AttributesImmediateOutputStrategy newStrategy = new AttributesImmediateOutputStrategy(true, true, true, false, true); + + assertThat(strategy.hasContextOnlyChanges(newStrategy)).isFalse(); + assertThat(strategy.hasRefreshContextOnlyChanges(newStrategy)).isTrue(); + } + + @Test + public void testAttributesImmediateOutputStrategyHasRefreshContextOnlyChanges_whenSendAttrUpdatedNotificationChanged_shouldReturnTrue() { + AttributesImmediateOutputStrategy strategy = new AttributesImmediateOutputStrategy(false, true, true, false, true); + AttributesImmediateOutputStrategy newStrategy = new AttributesImmediateOutputStrategy(true, true, true, false, true); + + assertThat(strategy.hasContextOnlyChanges(newStrategy)).isFalse(); + assertThat(strategy.hasRefreshContextOnlyChanges(newStrategy)).isTrue(); + } + + @Test + public void testTimeSeriesImmediateOutputStrategyHasRefreshContextOnlyChanges_whenTtlChanged_shouldReturnTrue() { + TimeSeriesImmediateOutputStrategy strategy = new TimeSeriesImmediateOutputStrategy(0, true, true, true, true); + TimeSeriesImmediateOutputStrategy newStrategy = new TimeSeriesImmediateOutputStrategy(300, true, true, true, true); + + assertThat(strategy.hasContextOnlyChanges(newStrategy)).isFalse(); + assertThat(strategy.hasRefreshContextOnlyChanges(newStrategy)).isTrue(); + } + +} From bde19743622f5fa34f0eb77049c36262587e2a87 Mon Sep 17 00:00:00 2001 From: ArtemDzhereleiko Date: Tue, 9 Dec 2025 10:21:16 +0200 Subject: [PATCH 30/32] UI: Fixed calculated field bugs and redesign alarm rule --- .../alarm-rule-dialog.component.html | 10 +- .../alarm-rule-dialog.component.scss | 4 + .../alarm-rule-dialog.component.ts | 40 +--- ...alarm-rule-condition-dialog.component.html | 205 +++++++++--------- .../cf-alarm-rule-condition.component.html | 2 +- .../cf-alarm-schedule-dialog.component.html | 23 +- .../create-cf-alarm-rules.component.html | 2 +- .../alarm-rule-filter-list.component.scss | 1 + ...-rule-filter-predicate-list.component.scss | 1 + .../alarm-rule-filter-predicate.component.ts | 11 +- ...lculated-field-argument-panel.component.ts | 1 + ...lated-field-arguments-table.component.html | 2 +- ...lated-field-arguments-table.component.scss | 4 + ...-geofencing-zone-groups-panel.component.ts | 2 +- ...eofencing-zone-groups-table.component.html | 2 +- ...culated-field-metrics-table.component.html | 2 +- .../assets/locale/locale.constant-en_US.json | 8 +- 17 files changed, 166 insertions(+), 154 deletions(-) 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 343c3123ab..7e706bfca9 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 @@ -84,7 +84,7 @@
-
{{ 'calculated-fields.arguments' | translate }}
+
{{ 'calculated-fields.arguments' | translate }}
-
{{ 'alarm-rule.create-conditions' | translate }}
+
{{ 'alarm-rule.create-conditions' | translate }}
-
{{ 'alarm-rule.clear-condition' | translate }}
+
{{ 'alarm-rule.clear-condition' | translate }}
@@ -118,7 +118,7 @@
- alarm-rule.no-clear-alarm-rule + alarm-rule.no-clear-alarm-rule
- -
- -
+ } @else { +
+
+ {{ 'alarm-rule.script' | translate }} +
+ +
{{ 'alarm-rule.tbel' | translate }} +
+ +
+
+ +
+
+ }
- } -
- @if (conditionFormGroup.get('expression.type').value === AlarmRuleExpressionType.SIMPLE) { -
- - - {{ 'alarm-rule.filter-preview' | translate }} - - -
- @if (specText) { - {{ specText }} - } - @if (conditionFormGroup.get('expression.filters').value?.length) { - - - } @else { - {{ 'alarm-rule.no-filter-preview' | translate }} - } -
-
-
-
- } + @if (conditionFormGroup.get('expression.type').value === AlarmRuleExpressionType.SIMPLE) { +
+ + + {{ 'alarm-rule.filter-preview' | translate }} + + +
+ @if (specText) { + {{ specText }} + } + @if (conditionFormGroup.get('expression.filters').value?.length) { + + + } @else { + {{ 'alarm-rule.no-filter-preview' | translate }} + } +
+
+
+
+ } +
+
-
-
{{ 'alarm-rule.condition-settings' | translate }}
- - alarm-rule.condition-type - - +
+
+
{{ 'alarm-rule.type' | translate }}
+ + {{ alarmConditionTypeTranslation.get(alarmConditionType) | translate }} - - - @if (isNoData) { - alarm-rule.condition-type-hint - } - + + +
+ @if (isNoData) { +
+ {{ 'alarm-rule.condition-type-hint' | translate }} +
+ } @if (conditionFormGroup.get('type').value == AlarmConditionType.DURATION) {
diff --git a/ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rule-condition.component.html b/ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rule-condition.component.html index 4892fcdecd..22fc01f9de 100644 --- a/ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rule-condition.component.html +++ b/ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rule-condition.component.html @@ -31,7 +31,7 @@ [nowrap]="true" [specText]="specText" required - addFilterPrompt="{{ (isClearCondition ? 'alarm-rule.enter-alarm-rule-clear-condition-prompt' :'alarm-rule.enter-alarm-rule-condition-prompt') | translate }}"> + addFilterPrompt="{{ 'alarm-rule.enter-alarm-rule-condition-prompt' | translate }}"> diff --git a/ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-schedule-dialog.component.html b/ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-schedule-dialog.component.html index 2d12355f48..4caecbcc34 100644 --- a/ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-schedule-dialog.component.html +++ b/ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-schedule-dialog.component.html @@ -19,10 +19,6 @@

{{ (readonly ? 'alarm-rule.schedule-title' : 'alarm-rule.edit-schedule') | translate }}

- - {{ 'alarm-rule.static-schedule' | translate }} - {{ 'alarm-rule.dynamic-schedule' | translate }} -
- - +
+
+
+
{{ 'alarm-rule.mode' | translate }}
+ + {{ 'alarm-rule.static-schedule' | translate }} + {{ 'alarm-rule.dynamic-schedule' | translate }} + +
+ + +
+
+ class="tb-prompt flex flex-1 items-end justify-center text-base required"> {{ 'calculated-fields.no-arguments' | translate }}
diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/components/calculated-field-arguments/calculated-field-arguments-table.component.scss b/ui-ngx/src/app/modules/home/components/calculated-fields/components/calculated-field-arguments/calculated-field-arguments-table.component.scss index 77ced9edef..f4f42f1b54 100644 --- a/ui-ngx/src/app/modules/home/components/calculated-fields/components/calculated-field-arguments/calculated-field-arguments-table.component.scss +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/components/calculated-field-arguments/calculated-field-arguments-table.component.scss @@ -41,6 +41,10 @@ } } + .no-data-found { + font-size: 16px; + } + .max-args-warning { .mat-icon { color: #FAA405; 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 0bbb72efb5..3484e717cb 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 @@ -278,7 +278,7 @@ export class CalculatedFieldGeofencingZoneGroupsPanelComponent implements OnInit this.enableAutocomplete = (this.entityId.entityType === EntityType.DEVICE_PROFILE || this.entityId.entityType === EntityType.ASSET_PROFILE) && type === ArgumentEntityType.Owner; this.geofencingFormGroup.get('refEntityId').get('id').setValue(null); this.geofencingFormGroup.get('perimeterKeyName').reset(''); - const isEntityWithId = !!type && ![ArgumentEntityType.Tenant, ArgumentEntityType.Current, ArgumentEntityType.Owner].includes(type); + const isEntityWithId = !!type && ![ArgumentEntityType.Tenant, ArgumentEntityType.Current, ArgumentEntityType.Owner, ArgumentEntityType.RelationQuery].includes(type); this.geofencingFormGroup.get('refEntityId').get('id')[isEntityWithId ? 'enable' : 'disable'](); if (!isEntityWithId) { this.entityNameSubject.next(null); diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/components/geofencing-configuration/calculated-field-geofencing-zone-groups-table.component.html b/ui-ngx/src/app/modules/home/components/calculated-fields/components/geofencing-configuration/calculated-field-geofencing-zone-groups-table.component.html index 67ec7f097b..20db5c732d 100644 --- a/ui-ngx/src/app/modules/home/components/calculated-fields/components/geofencing-configuration/calculated-field-geofencing-zone-groups-table.component.html +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/components/geofencing-configuration/calculated-field-geofencing-zone-groups-table.component.html @@ -128,7 +128,7 @@ }
+ class="tb-prompt flex flex-1 items-end justify-center text-base required"> {{ 'calculated-fields.no-zone-configured' | translate }}
diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/components/metrics/calculated-field-metrics-table.component.html b/ui-ngx/src/app/modules/home/components/calculated-fields/components/metrics/calculated-field-metrics-table.component.html index 209971f6e7..ff969775d5 100644 --- a/ui-ngx/src/app/modules/home/components/calculated-fields/components/metrics/calculated-field-metrics-table.component.html +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/components/metrics/calculated-field-metrics-table.component.html @@ -102,7 +102,7 @@
+ class="tb-prompt flex flex-1 items-end justify-center text-base required"> {{ 'calculated-fields.metrics.no-metrics-configured' | translate }}
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 ad6a779e02..06296ed045 100644 --- a/ui-ngx/src/assets/locale/locale.constant-en_US.json +++ b/ui-ngx/src/assets/locale/locale.constant-en_US.json @@ -1403,7 +1403,7 @@ "target-entity": "Target entity", "target-entities": "Target entities", "alarm-type": "Alarm type", - "alarm-type-hint": "Unique identifier (e.g., HighTemperatureAlarm) across the scope of the alarm originator (Device, Asset, etc.) to prevent conflicts.", + "alarm-type-hint": "Unique identifier (e.g., HighTempAlarm) across the scope of the alarm originator (Device, Asset, etc.) to prevent conflicts.", "alarm-type-required": "Alarm type is required.", "alarm-type-pattern": "Alarm type is invalid.", "alarm-type-max-length": "Alarm type should be less than 256 characters.", @@ -1513,8 +1513,8 @@ "condition-repeating-value-range": "Count of events should be in a range from 1 to 2147483647.", "condition-repeating-value-pattern": "Count of events should be integers.", "condition-repeating-value-required": "Count of events is required.", - "create-conditions": "Alarm Trigger Conditions", - "clear-condition": "Alarm Clear Condition", + "create-conditions": "Trigger conditions", + "clear-condition": "Clear condition", "no-clear-alarm-rule": "No clear condition configured.", "advanced-settings": "Advanced settings", "propagate-alarm": "Propagate alarm to related entities", @@ -1532,6 +1532,8 @@ "alarm-rule-entity-list": "Entity list", "missing-for": "missing for", "time-unit": "Unit", + "mode": "Mode", + "type": "Type", "value-required": "Value is required.", "min-value": "Value must be 0 or greater.", "argument-in-use": "Argument is used as general argument.", From c0af057590273148da4e65fdb147ba3a9c38a23b Mon Sep 17 00:00:00 2001 From: dshvaika Date: Tue, 9 Dec 2025 11:31:02 +0200 Subject: [PATCH 31/32] refactoring logic in getOrSaveKeyId method --- .../sqlts/dictionary/JpaKeyDictionaryDao.java | 44 ++++++++++--------- 1 file changed, 23 insertions(+), 21 deletions(-) diff --git a/dao/src/main/java/org/thingsboard/server/dao/sqlts/dictionary/JpaKeyDictionaryDao.java b/dao/src/main/java/org/thingsboard/server/dao/sqlts/dictionary/JpaKeyDictionaryDao.java index bcb9284371..9d23bdbcb5 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sqlts/dictionary/JpaKeyDictionaryDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sqlts/dictionary/JpaKeyDictionaryDao.java @@ -51,32 +51,29 @@ public class JpaKeyDictionaryDao extends JpaAbstractDaoListeningExecutorService return cached; } var compositeKey = new KeyDictionaryCompositeKey(strKey); - Optional entryOpt = keyDictionaryRepository.findById(compositeKey); - if (entryOpt.isPresent()) { - Integer keyId = entryOpt.get().getKeyId(); - if (keyId != null) { - keyDictionaryMap.put(strKey, keyId); - return keyId; - } + Optional existingId = keyDictionaryRepository.findById(compositeKey) + .map(KeyDictionaryEntry::getKeyId) + .filter(id -> id != 0); + if (existingId.isPresent()) { + return cacheAndReturn(strKey, existingId.get()); } creationLock.lock(); try { - Integer keyId = keyDictionaryMap.get(strKey); - if (keyId != null) { - return keyId; + Integer fromCache = keyDictionaryMap.get(strKey); + if (fromCache != null) { + return fromCache; } - keyId = keyDictionaryRepository.upsertAndGetKeyId(strKey); - if (keyId == null || keyId == 0) { - log.warn("upsertAndGetKeyId returned: [{}] for key: [{}], falling back to findById", keyId, strKey); - entryOpt = keyDictionaryRepository.findById(compositeKey); - if (entryOpt.isEmpty() || - entryOpt.get().getKeyId() == null || - entryOpt.get().getKeyId() == 0) { - throw new IllegalStateException("Failed to resolve keyId for string key: " + strKey + " after fallback. keyId: " + keyId); - } + Integer keyId = keyDictionaryRepository.upsertAndGetKeyId(strKey); + if (keyId != null && keyId != 0) { + return cacheAndReturn(strKey, keyId); } - keyDictionaryMap.put(strKey, keyId); - return keyId; + log.warn("upsertAndGetKeyId returned: [{}] for key: [{}], falling back to findById", keyId, strKey); + keyId = keyDictionaryRepository.findById(compositeKey) + .map(KeyDictionaryEntry::getKeyId) + .filter(id -> id != 0) + .orElseThrow(() -> new IllegalStateException( + "Failed to resolve keyId for string key: " + strKey + " after fallback.")); + return cacheAndReturn(strKey, keyId); } finally { creationLock.unlock(); } @@ -93,4 +90,9 @@ public class JpaKeyDictionaryDao extends JpaAbstractDaoListeningExecutorService return DaoUtil.pageToPageData(keyDictionaryRepository.findAll(DaoUtil.toPageable(pageLink))); } + private Integer cacheAndReturn(String key, Integer keyId) { + keyDictionaryMap.put(key, keyId); + return keyId; + } + } From 2c04943984abebdd7fe07772046e20d2173e8531 Mon Sep 17 00:00:00 2001 From: dshvaika Date: Tue, 9 Dec 2025 12:11:14 +0200 Subject: [PATCH 32/32] removed checks for 0 value --- .../server/dao/sqlts/dictionary/JpaKeyDictionaryDao.java | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/dao/src/main/java/org/thingsboard/server/dao/sqlts/dictionary/JpaKeyDictionaryDao.java b/dao/src/main/java/org/thingsboard/server/dao/sqlts/dictionary/JpaKeyDictionaryDao.java index 9d23bdbcb5..46bc10010e 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sqlts/dictionary/JpaKeyDictionaryDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sqlts/dictionary/JpaKeyDictionaryDao.java @@ -51,9 +51,7 @@ public class JpaKeyDictionaryDao extends JpaAbstractDaoListeningExecutorService return cached; } var compositeKey = new KeyDictionaryCompositeKey(strKey); - Optional existingId = keyDictionaryRepository.findById(compositeKey) - .map(KeyDictionaryEntry::getKeyId) - .filter(id -> id != 0); + Optional existingId = keyDictionaryRepository.findById(compositeKey).map(KeyDictionaryEntry::getKeyId); if (existingId.isPresent()) { return cacheAndReturn(strKey, existingId.get()); } @@ -64,13 +62,12 @@ public class JpaKeyDictionaryDao extends JpaAbstractDaoListeningExecutorService return fromCache; } Integer keyId = keyDictionaryRepository.upsertAndGetKeyId(strKey); - if (keyId != null && keyId != 0) { + if (keyId != null) { return cacheAndReturn(strKey, keyId); } log.warn("upsertAndGetKeyId returned: [{}] for key: [{}], falling back to findById", keyId, strKey); keyId = keyDictionaryRepository.findById(compositeKey) .map(KeyDictionaryEntry::getKeyId) - .filter(id -> id != 0) .orElseThrow(() -> new IllegalStateException( "Failed to resolve keyId for string key: " + strKey + " after fallback.")); return cacheAndReturn(strKey, keyId);