From 3abb23780d493b81793144899682ec19c2f6f2b6 Mon Sep 17 00:00:00 2001 From: dshvaika Date: Wed, 3 Sep 2025 17:21:50 +0300 Subject: [PATCH] Added TimeUnit support --- ...alculatedFieldManagerMessageProcessor.java | 3 +- .../cf/ctx/state/CalculatedFieldCtx.java | 5 +- .../cf/CalculatedFieldIntegrationTest.java | 3 +- ...eofencingCalculatedFieldConfiguration.java | 7 ++- ...SupportedCalculatedFieldConfiguration.java | 31 ++++++++++- ...ncingCalculatedFieldConfigurationTest.java | 51 +++++++++++++++++-- .../dao/cf/BaseCalculatedFieldService.java | 10 +++- .../service/CalculatedFieldServiceTest.java | 16 +++--- 8 files changed, 106 insertions(+), 20 deletions(-) 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 6a100d9d50..1d38480b91 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 @@ -60,7 +60,6 @@ import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.ScheduledFuture; -import java.util.concurrent.TimeUnit; import java.util.function.BiConsumer; import java.util.function.Function; @@ -454,7 +453,7 @@ public class CalculatedFieldManagerMessageProcessor extends AbstractContextAware log.debug("[{}][{}] Dynamic arguments refresh task for CF already exists!", tenantId, cf.getId()); return; } - long refreshDynamicSourceInterval = TimeUnit.SECONDS.toMillis(scheduledCfConfig.getScheduledUpdateIntervalSec()); + long refreshDynamicSourceInterval = scheduledCfConfig.getTimeUnit().toMillis(scheduledCfConfig.getScheduledUpdateInterval()); var scheduledMsg = new CalculatedFieldDynamicArgumentsRefreshMsg(tenantId, cfCtx.getCfId()); ScheduledFuture scheduledFuture = systemContext 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 9f6896392e..260cbe447f 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 @@ -325,8 +325,9 @@ public class CalculatedFieldCtx { if (calculatedField.getConfiguration() instanceof ScheduledUpdateSupportedCalculatedFieldConfiguration thisConfig && other.calculatedField.getConfiguration() instanceof ScheduledUpdateSupportedCalculatedFieldConfiguration otherConfig) { boolean refreshTriggerChanged = thisConfig.isScheduledUpdateEnabled() != otherConfig.isScheduledUpdateEnabled(); - boolean refreshIntervalChanged = thisConfig.getScheduledUpdateIntervalSec() != otherConfig.getScheduledUpdateIntervalSec(); - return refreshTriggerChanged || refreshIntervalChanged; + boolean refreshIntervalChanged = thisConfig.getScheduledUpdateInterval() != otherConfig.getScheduledUpdateInterval(); + boolean timeUnitChanged = thisConfig.getTimeUnit() != otherConfig.getTimeUnit(); + return refreshTriggerChanged || refreshIntervalChanged || timeUnitChanged; } return false; } 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 f9e5dec789..8df1d04f2e 100644 --- a/application/src/test/java/org/thingsboard/server/cf/CalculatedFieldIntegrationTest.java +++ b/application/src/test/java/org/thingsboard/server/cf/CalculatedFieldIntegrationTest.java @@ -799,7 +799,8 @@ public class CalculatedFieldIntegrationTest extends CalculatedFieldControllerTes cfg.setOutput(out); // Enable scheduled refresh with a 6-second interval - cfg.setScheduledUpdateIntervalSec(6); + cfg.setScheduledUpdateInterval(6); + cfg.setTimeUnit(TimeUnit.SECONDS); cf.setConfiguration(cfg); CalculatedField savedCalculatedField = doPost("/api/calculatedField", cf, CalculatedField.class); diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/GeofencingCalculatedFieldConfiguration.java b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/GeofencingCalculatedFieldConfiguration.java index ef20ad16bb..b009142f37 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/GeofencingCalculatedFieldConfiguration.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/GeofencingCalculatedFieldConfiguration.java @@ -28,13 +28,15 @@ import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Set; +import java.util.concurrent.TimeUnit; @Data public class GeofencingCalculatedFieldConfiguration implements ArgumentsBasedCalculatedFieldConfiguration, ScheduledUpdateSupportedCalculatedFieldConfiguration { private EntityCoordinates entityCoordinates; private List zoneGroups; - private int scheduledUpdateIntervalSec; + private int scheduledUpdateInterval; + private TimeUnit timeUnit; private Output output; @@ -63,11 +65,12 @@ public class GeofencingCalculatedFieldConfiguration implements ArgumentsBasedCal @Override public boolean isScheduledUpdateEnabled() { - return scheduledUpdateIntervalSec > 0 && zoneGroups.stream().anyMatch(ZoneGroupConfiguration::hasDynamicSource); + return scheduledUpdateInterval > 0 && zoneGroups.stream().anyMatch(ZoneGroupConfiguration::hasDynamicSource); } @Override public void validate() { + ScheduledUpdateSupportedCalculatedFieldConfiguration.super.validate(); if (entityCoordinates == null) { throw new IllegalArgumentException("Geofencing calculated field entity coordinates must be specified!"); } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/ScheduledUpdateSupportedCalculatedFieldConfiguration.java b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/ScheduledUpdateSupportedCalculatedFieldConfiguration.java index 0d386577ab..afc8402722 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/ScheduledUpdateSupportedCalculatedFieldConfiguration.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/ScheduledUpdateSupportedCalculatedFieldConfiguration.java @@ -17,12 +17,39 @@ package org.thingsboard.server.common.data.cf.configuration; import com.fasterxml.jackson.annotation.JsonIgnore; +import java.util.EnumSet; +import java.util.Set; +import java.util.concurrent.TimeUnit; + public interface ScheduledUpdateSupportedCalculatedFieldConfiguration extends CalculatedFieldConfiguration { + Set SUPPORTED_TIME_UNITS = + EnumSet.of(TimeUnit.SECONDS, TimeUnit.MINUTES, TimeUnit.HOURS); + + @JsonIgnore boolean isScheduledUpdateEnabled(); - int getScheduledUpdateIntervalSec(); + int getScheduledUpdateInterval(); + + void setScheduledUpdateInterval(int interval); + + TimeUnit getTimeUnit(); + + void setTimeUnit(TimeUnit timeUnit); - void setScheduledUpdateIntervalSec(int interval); + @Override + default void validate() { + if (!isScheduledUpdateEnabled()) { + return; + } + var timeUnit = getTimeUnit(); + if (timeUnit == null) { + throw new IllegalArgumentException("Scheduled update time unit should be specified!"); + } + if (!SUPPORTED_TIME_UNITS.contains(timeUnit)) { + throw new IllegalArgumentException("Unsupported scheduled update time unit: " + timeUnit + + ". Allowed: " + SUPPORTED_TIME_UNITS); + } + } } diff --git a/common/data/src/test/java/org/thingsboard/server/common/data/cf/configuration/GeofencingCalculatedFieldConfigurationTest.java b/common/data/src/test/java/org/thingsboard/server/common/data/cf/configuration/GeofencingCalculatedFieldConfigurationTest.java index 1b12c37820..90d2768608 100644 --- a/common/data/src/test/java/org/thingsboard/server/common/data/cf/configuration/GeofencingCalculatedFieldConfigurationTest.java +++ b/common/data/src/test/java/org/thingsboard/server/common/data/cf/configuration/GeofencingCalculatedFieldConfigurationTest.java @@ -17,6 +17,8 @@ package org.thingsboard.server.common.data.cf.configuration; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.EnumSource; import org.mockito.junit.jupiter.MockitoExtension; import org.thingsboard.server.common.data.AttributeScope; import org.thingsboard.server.common.data.cf.CalculatedFieldType; @@ -25,6 +27,7 @@ import org.thingsboard.server.common.data.cf.configuration.geofencing.ZoneGroupC import java.util.List; import java.util.Map; +import java.util.concurrent.TimeUnit; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatCode; @@ -33,6 +36,7 @@ import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; +import static org.thingsboard.server.common.data.cf.configuration.ScheduledUpdateSupportedCalculatedFieldConfiguration.SUPPORTED_TIME_UNITS; import static org.thingsboard.server.common.data.cf.configuration.geofencing.EntityCoordinates.ENTITY_ID_LATITUDE_ARGUMENT_KEY; import static org.thingsboard.server.common.data.cf.configuration.geofencing.EntityCoordinates.ENTITY_ID_LONGITUDE_ARGUMENT_KEY; @@ -122,10 +126,50 @@ public class GeofencingCalculatedFieldConfigurationTest { verify(zoneGroupConfigurationB, never()).validate(); } + @Test + void validateShouldThrowWhenScheduledUpdateIntervalIsSetButTimeUnitIsNotSpecified() { + var cfg = new GeofencingCalculatedFieldConfiguration(); + cfg.setScheduledUpdateInterval(60); + var zg = new ZoneGroupConfiguration("allowedZones", "perimeter", GeofencingReportStrategy.REPORT_TRANSITION_EVENTS_AND_PRESENCE_STATUS, false); + zg.setRefDynamicSourceConfiguration(mock(RelationQueryDynamicSourceConfiguration.class)); + cfg.setZoneGroups(List.of(zg)); + cfg.setTimeUnit(null); + + assertThat(cfg.isScheduledUpdateEnabled()).isTrue(); + + assertThatThrownBy(cfg::validate) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Scheduled update time unit should be specified!"); + } + + @ParameterizedTest + @EnumSource(TimeUnit.class) + void validateShouldThrowWhenScheduledUpdateIntervalIsSetButTimeUnitIsNotSupported(TimeUnit timeUnit) { + var cfg = new GeofencingCalculatedFieldConfiguration(); + cfg.setScheduledUpdateInterval(60); + var zg = new ZoneGroupConfiguration("allowedZones", "perimeter", GeofencingReportStrategy.REPORT_TRANSITION_EVENTS_AND_PRESENCE_STATUS, false); + zg.setRefDynamicSourceConfiguration(mock(RelationQueryDynamicSourceConfiguration.class)); + cfg.setZoneGroups(List.of(zg)); + cfg.setEntityCoordinates(mock(EntityCoordinates.class)); + cfg.setTimeUnit(timeUnit); + + assertThat(cfg.isScheduledUpdateEnabled()).isTrue(); + + if (SUPPORTED_TIME_UNITS.contains(timeUnit)) { + assertThatCode(cfg::validate).doesNotThrowAnyException(); + return; + } + assertThatThrownBy(cfg::validate) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Unsupported scheduled update time unit: " + timeUnit + + ". Allowed: " + SUPPORTED_TIME_UNITS); + } + + @Test void scheduledUpdateDisabledWhenIntervalIsZero() { var cfg = new GeofencingCalculatedFieldConfiguration(); - cfg.setScheduledUpdateIntervalSec(0); + cfg.setScheduledUpdateInterval(0); assertThat(cfg.isScheduledUpdateEnabled()).isFalse(); } @@ -135,17 +179,18 @@ public class GeofencingCalculatedFieldConfigurationTest { var zoneGroupConfigurationMock = mock(ZoneGroupConfiguration.class); when(zoneGroupConfigurationMock.hasDynamicSource()).thenReturn(false); cfg.setZoneGroups(List.of(zoneGroupConfigurationMock)); - cfg.setScheduledUpdateIntervalSec(60); + cfg.setScheduledUpdateInterval(60); assertThat(cfg.isScheduledUpdateEnabled()).isFalse(); } @Test void scheduledUpdateEnabledWhenIntervalIsGreaterThanZeroAndDynamicArgumentsPresent() { var cfg = new GeofencingCalculatedFieldConfiguration(); + cfg.setTimeUnit(TimeUnit.SECONDS); var zoneGroupConfigurationMock = mock(ZoneGroupConfiguration.class); when(zoneGroupConfigurationMock.hasDynamicSource()).thenReturn(true); cfg.setZoneGroups(List.of(zoneGroupConfigurationMock)); - cfg.setScheduledUpdateIntervalSec(60); + cfg.setScheduledUpdateInterval(60); assertThat(cfg.isScheduledUpdateEnabled()).isTrue(); } diff --git a/dao/src/main/java/org/thingsboard/server/dao/cf/BaseCalculatedFieldService.java b/dao/src/main/java/org/thingsboard/server/dao/cf/BaseCalculatedFieldService.java index 436f75457c..4e84cdebe1 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/cf/BaseCalculatedFieldService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/cf/BaseCalculatedFieldService.java @@ -38,6 +38,7 @@ import org.thingsboard.server.dao.service.DataValidator; import java.util.List; import java.util.Optional; +import java.util.concurrent.TimeUnit; import static org.thingsboard.server.dao.service.Validator.validateId; import static org.thingsboard.server.dao.service.Validator.validatePageLink; @@ -98,10 +99,15 @@ public class BaseCalculatedFieldService extends AbstractEntityService implements if (!configuration.isScheduledUpdateEnabled()) { return; } - int tenantProfileMinAllowedValue = tbTenantProfileCache.get(calculatedField.getTenantId()) + TimeUnit timeUnit = configuration.getTimeUnit(); + long intervalInSeconds = timeUnit.toSeconds(configuration.getScheduledUpdateInterval()); + int tenantProfileMinAllowedSecValue = tbTenantProfileCache.get(calculatedField.getTenantId()) .getDefaultProfileConfiguration() .getMinAllowedScheduledUpdateIntervalInSecForCF(); - configuration.setScheduledUpdateIntervalSec(Math.max(configuration.getScheduledUpdateIntervalSec(), tenantProfileMinAllowedValue)); + if (intervalInSeconds < tenantProfileMinAllowedSecValue) { + configuration.setScheduledUpdateInterval(tenantProfileMinAllowedSecValue); + configuration.setTimeUnit(TimeUnit.SECONDS); + } } } diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/CalculatedFieldServiceTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/CalculatedFieldServiceTest.java index 81041180c7..0d70e22339 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/service/CalculatedFieldServiceTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/service/CalculatedFieldServiceTest.java @@ -46,6 +46,7 @@ import org.thingsboard.server.dao.tenant.TbTenantProfileCache; import java.util.List; import java.util.Map; +import java.util.concurrent.TimeUnit; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; @@ -117,7 +118,8 @@ public class CalculatedFieldServiceTest extends AbstractServiceTest { cfg.setZoneGroups(List.of(zoneGroupConfiguration)); // Set a scheduled interval to some value - cfg.setScheduledUpdateIntervalSec(600); + cfg.setScheduledUpdateInterval(600); + cfg.setTimeUnit(TimeUnit.SECONDS); // Create & save Calculated Field CalculatedField cf = new CalculatedField(); @@ -136,7 +138,7 @@ public class CalculatedFieldServiceTest extends AbstractServiceTest { var geofencingCalculatedFieldConfiguration = (GeofencingCalculatedFieldConfiguration) saved.getConfiguration(); // Assert: the interval is saved, but scheduling is not enabled - int savedInterval = geofencingCalculatedFieldConfiguration.getScheduledUpdateIntervalSec(); + int savedInterval = geofencingCalculatedFieldConfiguration.getScheduledUpdateInterval(); boolean scheduledUpdateEnabled = geofencingCalculatedFieldConfiguration.isScheduledUpdateEnabled(); assertThat(savedInterval).isEqualTo(600); @@ -167,7 +169,8 @@ public class CalculatedFieldServiceTest extends AbstractServiceTest { cfg.setZoneGroups(List.of(zoneGroupConfiguration)); // Enable scheduling with an interval below tenant min - cfg.setScheduledUpdateIntervalSec(600); + cfg.setScheduledUpdateInterval(600); + cfg.setTimeUnit(TimeUnit.SECONDS); // Create & save Calculated Field CalculatedField cf = new CalculatedField(); @@ -186,7 +189,7 @@ public class CalculatedFieldServiceTest extends AbstractServiceTest { var geofencingCalculatedFieldConfiguration = (GeofencingCalculatedFieldConfiguration) saved.getConfiguration(); // Assert: the interval is clamped up to tenant profile min - int savedInterval = geofencingCalculatedFieldConfiguration.getScheduledUpdateIntervalSec(); + int savedInterval = geofencingCalculatedFieldConfiguration.getScheduledUpdateInterval(); int min = tbTenantProfileCache.get(tenantId) .getDefaultProfileConfiguration() @@ -225,7 +228,8 @@ public class CalculatedFieldServiceTest extends AbstractServiceTest { // Enable scheduling with an interval greater than tenant min int valueFromConfig = min + 100; - cfg.setScheduledUpdateIntervalSec(valueFromConfig); + cfg.setScheduledUpdateInterval(valueFromConfig); + cfg.setTimeUnit(TimeUnit.SECONDS); // Create & save Calculated Field CalculatedField cf = new CalculatedField(); @@ -244,7 +248,7 @@ public class CalculatedFieldServiceTest extends AbstractServiceTest { var geofencingCalculatedFieldConfiguration = (GeofencingCalculatedFieldConfiguration) saved.getConfiguration(); // Assert: the interval is clamped up to tenant profile min (or stays >= original if already >= min) - int savedInterval = geofencingCalculatedFieldConfiguration.getScheduledUpdateIntervalSec(); + int savedInterval = geofencingCalculatedFieldConfiguration.getScheduledUpdateInterval(); assertThat(savedInterval).isEqualTo(valueFromConfig); calculatedFieldService.deleteCalculatedField(tenantId, saved.getId());