geofencing cf bugfixes
This commit is contained in:
parent
ba52b1b74a
commit
2c06aa475f
@ -27,7 +27,7 @@ SET profile_data = jsonb_set(
|
||||
CASE
|
||||
WHEN (profile_data -> 'configuration') ? 'minAllowedScheduledUpdateIntervalInSecForCF'
|
||||
THEN NULL
|
||||
ELSE to_jsonb(3600)
|
||||
ELSE to_jsonb(60)
|
||||
END,
|
||||
'maxRelationLevelPerCfArgument',
|
||||
CASE
|
||||
|
||||
@ -1,35 +0,0 @@
|
||||
/**
|
||||
* Copyright © 2016-2025 The Thingsboard Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.thingsboard.server.actors.calculatedField;
|
||||
|
||||
import lombok.Data;
|
||||
import org.thingsboard.server.common.data.id.CalculatedFieldId;
|
||||
import org.thingsboard.server.common.data.id.TenantId;
|
||||
import org.thingsboard.server.common.msg.MsgType;
|
||||
import org.thingsboard.server.common.msg.ToCalculatedFieldSystemMsg;
|
||||
|
||||
@Data
|
||||
public class CalculatedFieldDynamicArgumentsRefreshMsg implements ToCalculatedFieldSystemMsg {
|
||||
|
||||
private final TenantId tenantId;
|
||||
private final CalculatedFieldId cfId;
|
||||
|
||||
@Override
|
||||
public MsgType getMsgType() {
|
||||
return MsgType.CF_DYNAMIC_ARGUMENTS_REFRESH_MSG;
|
||||
}
|
||||
|
||||
}
|
||||
@ -75,9 +75,6 @@ public class CalculatedFieldEntityActor extends AbstractCalculatedFieldActor {
|
||||
case CF_LINKED_TELEMETRY_MSG:
|
||||
processor.process((EntityCalculatedFieldLinkedTelemetryMsg) msg);
|
||||
break;
|
||||
case CF_ENTITY_DYNAMIC_ARGUMENTS_REFRESH_MSG:
|
||||
processor.process((EntityCalculatedFieldDynamicArgumentsRefreshMsg) msg);
|
||||
break;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
|
||||
@ -49,6 +49,7 @@ 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.SingleValueArgumentEntry;
|
||||
import org.thingsboard.server.service.cf.ctx.state.geofencing.GeofencingArgumentEntry;
|
||||
import org.thingsboard.server.service.cf.ctx.state.geofencing.GeofencingCalculatedFieldState;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
@ -227,18 +228,6 @@ public class CalculatedFieldEntityMessageProcessor extends AbstractContextAwareM
|
||||
}
|
||||
}
|
||||
|
||||
public void process(EntityCalculatedFieldDynamicArgumentsRefreshMsg msg) throws CalculatedFieldException {
|
||||
log.debug("[{}][{}] Processing CF dynamic arguments refresh msg.", entityId, msg.getCfId());
|
||||
CalculatedFieldState currentState = states.get(msg.getCfId());
|
||||
if (currentState == null) {
|
||||
log.debug("[{}][{}] Failed to find CF state for entity.", entityId, msg.getCfId());
|
||||
} else {
|
||||
currentState.setDirty(true);
|
||||
log.debug("[{}][{}] CF state marked as dirty.", entityId, msg.getCfId());
|
||||
}
|
||||
msg.getCallback().onSuccess();
|
||||
}
|
||||
|
||||
private void processTelemetry(CalculatedFieldCtx ctx, CalculatedFieldTelemetryMsgProto proto, List<CalculatedFieldId> cfIdList, MultipleTbCallback callback) throws CalculatedFieldException {
|
||||
processArgumentValuesUpdate(ctx, cfIdList, callback, mapToArguments(ctx, proto.getTsDataList()), toTbMsgId(proto), toTbMsgType(proto));
|
||||
}
|
||||
@ -266,12 +255,13 @@ public class CalculatedFieldEntityMessageProcessor extends AbstractContextAwareM
|
||||
if (state == null) {
|
||||
state = getOrInitState(ctx);
|
||||
justRestored = true;
|
||||
} else if (state.isDirty()) {
|
||||
log.debug("[{}][{}] Going to update dirty CF state.", entityId, ctx.getCfId());
|
||||
} else if (ctx.shouldFetchDynamicArgumentsFromDb(state)) {
|
||||
log.debug("[{}][{}] Going to update dynamic arguments for CF.", entityId, ctx.getCfId());
|
||||
try {
|
||||
Map<String, ArgumentEntry> dynamicArgsFromDb = cfService.fetchDynamicArgsFromDb(ctx, entityId);
|
||||
dynamicArgsFromDb.forEach(newArgValues::putIfAbsent);
|
||||
state.setDirty(false);
|
||||
var geofencingState = (GeofencingCalculatedFieldState) state;
|
||||
geofencingState.setLastDynamicArgumentsRefreshTs(System.currentTimeMillis());
|
||||
} catch (Exception e) {
|
||||
throw CalculatedFieldException.builder().ctx(ctx).eventEntity(entityId).cause(e).build();
|
||||
}
|
||||
@ -403,7 +393,7 @@ public class CalculatedFieldEntityMessageProcessor extends AbstractContextAwareM
|
||||
return mapToArguments(entityId, argNames, geofencingArgumentNames, scope, attrDataList);
|
||||
}
|
||||
|
||||
private Map<String, ArgumentEntry> mapToArguments(EntityId entityId, Map<ReferencedEntityKey, String> argNames, List<String> geoArgNames, AttributeScopeProto scope, List<AttributeValueProto> attrDataList) {
|
||||
private Map<String, ArgumentEntry> mapToArguments(EntityId entityId, Map<ReferencedEntityKey, String> argNames, List<String> geofencingArgNames, AttributeScopeProto scope, List<AttributeValueProto> attrDataList) {
|
||||
Map<String, ArgumentEntry> arguments = new HashMap<>();
|
||||
for (AttributeValueProto item : attrDataList) {
|
||||
ReferencedEntityKey key = new ReferencedEntityKey(item.getKey(), ArgumentType.ATTRIBUTE, AttributeScope.valueOf(scope.name()));
|
||||
@ -411,7 +401,7 @@ public class CalculatedFieldEntityMessageProcessor extends AbstractContextAwareM
|
||||
if (argName == null) {
|
||||
continue;
|
||||
}
|
||||
if (geoArgNames.contains(argName)) {
|
||||
if (geofencingArgNames.contains(argName)) {
|
||||
arguments.put(argName, new GeofencingArgumentEntry(entityId, item));
|
||||
continue;
|
||||
}
|
||||
@ -425,26 +415,32 @@ public class CalculatedFieldEntityMessageProcessor extends AbstractContextAwareM
|
||||
if (argNames.isEmpty()) {
|
||||
return Collections.emptyMap();
|
||||
}
|
||||
return mapToArgumentsWithDefaultValue(argNames, ctx.getArguments(), scope, removedAttrKeys);
|
||||
List<String> geofencingArgumentNames = ctx.getLinkedEntityGeofencingArgumentNames();
|
||||
return mapToArgumentsWithDefaultValue(argNames, ctx.getArguments(), geofencingArgumentNames, scope, removedAttrKeys);
|
||||
}
|
||||
|
||||
private Map<String, ArgumentEntry> mapToArgumentsWithDefaultValue(CalculatedFieldCtx ctx, AttributeScopeProto scope, List<String> removedAttrKeys) {
|
||||
return mapToArgumentsWithDefaultValue(ctx.getMainEntityArguments(), ctx.getArguments(), scope, removedAttrKeys);
|
||||
return mapToArgumentsWithDefaultValue(ctx.getMainEntityArguments(), ctx.getArguments(), ctx.getMainEntityGeofencingArgumentNames(), scope, removedAttrKeys);
|
||||
}
|
||||
|
||||
private Map<String, ArgumentEntry> mapToArgumentsWithDefaultValue(Map<ReferencedEntityKey, String> argNames, Map<String, Argument> configArguments, AttributeScopeProto scope, List<String> removedAttrKeys) {
|
||||
private Map<String, ArgumentEntry> mapToArgumentsWithDefaultValue(Map<ReferencedEntityKey, String> argNames, Map<String, Argument> configArguments, List<String> geofencingArgNames, AttributeScopeProto scope, List<String> removedAttrKeys) {
|
||||
Map<String, ArgumentEntry> arguments = new HashMap<>();
|
||||
for (String removedKey : removedAttrKeys) {
|
||||
ReferencedEntityKey key = new ReferencedEntityKey(removedKey, ArgumentType.ATTRIBUTE, AttributeScope.valueOf(scope.name()));
|
||||
String argName = argNames.get(key);
|
||||
if (argName != null) {
|
||||
Argument argument = configArguments.get(argName);
|
||||
String defaultValue = (argument != null) ? argument.getDefaultValue() : null;
|
||||
arguments.put(argName, StringUtils.isNotEmpty(defaultValue)
|
||||
? new SingleValueArgumentEntry(System.currentTimeMillis(), new StringDataEntry(removedKey, defaultValue), null)
|
||||
: new SingleValueArgumentEntry());
|
||||
|
||||
if (argName == null) {
|
||||
continue;
|
||||
}
|
||||
if (geofencingArgNames.contains(argName)) {
|
||||
arguments.put(argName, new GeofencingArgumentEntry());
|
||||
continue;
|
||||
}
|
||||
Argument argument = configArguments.get(argName);
|
||||
String defaultValue = (argument != null) ? argument.getDefaultValue() : null;
|
||||
arguments.put(argName, StringUtils.isNotEmpty(defaultValue)
|
||||
? new SingleValueArgumentEntry(System.currentTimeMillis(), new StringDataEntry(removedKey, defaultValue), null)
|
||||
: new SingleValueArgumentEntry());
|
||||
|
||||
}
|
||||
return arguments;
|
||||
}
|
||||
|
||||
@ -79,9 +79,6 @@ public class CalculatedFieldManagerActor extends AbstractCalculatedFieldActor {
|
||||
case CF_LINKED_TELEMETRY_MSG:
|
||||
processor.onLinkedTelemetryMsg((CalculatedFieldLinkedTelemetryMsg) msg);
|
||||
break;
|
||||
case CF_DYNAMIC_ARGUMENTS_REFRESH_MSG:
|
||||
processor.onDynamicArgumentsRefreshMsg((CalculatedFieldDynamicArgumentsRefreshMsg) msg);
|
||||
break;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
|
||||
@ -27,7 +27,6 @@ import org.thingsboard.server.common.data.EntityType;
|
||||
import org.thingsboard.server.common.data.ProfileEntityIdInfo;
|
||||
import org.thingsboard.server.common.data.cf.CalculatedField;
|
||||
import org.thingsboard.server.common.data.cf.CalculatedFieldLink;
|
||||
import org.thingsboard.server.common.data.cf.configuration.ScheduledUpdateSupportedCalculatedFieldConfiguration;
|
||||
import org.thingsboard.server.common.data.id.AssetId;
|
||||
import org.thingsboard.server.common.data.id.CalculatedFieldId;
|
||||
import org.thingsboard.server.common.data.id.DeviceId;
|
||||
@ -57,10 +56,7 @@ import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
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 static org.thingsboard.server.utils.CalculatedFieldUtils.fromProto;
|
||||
@ -74,7 +70,6 @@ public class CalculatedFieldManagerMessageProcessor extends AbstractContextAware
|
||||
private final Map<CalculatedFieldId, CalculatedFieldCtx> calculatedFields = new HashMap<>();
|
||||
private final Map<EntityId, List<CalculatedFieldCtx>> entityIdCalculatedFields = new HashMap<>();
|
||||
private final Map<EntityId, List<CalculatedFieldLink>> entityIdCalculatedFieldLinks = new HashMap<>();
|
||||
private final Map<CalculatedFieldId, ScheduledFuture<?>> cfDynamicArgumentsRefreshTasks = new ConcurrentHashMap<>();
|
||||
|
||||
private final CalculatedFieldProcessingService cfExecService;
|
||||
private final CalculatedFieldStateService cfStateService;
|
||||
@ -113,8 +108,6 @@ public class CalculatedFieldManagerMessageProcessor extends AbstractContextAware
|
||||
calculatedFields.clear();
|
||||
entityIdCalculatedFields.clear();
|
||||
entityIdCalculatedFieldLinks.clear();
|
||||
cfDynamicArgumentsRefreshTasks.values().forEach(future -> future.cancel(true));
|
||||
cfDynamicArgumentsRefreshTasks.clear();
|
||||
ctx.stop(ctx.getSelf());
|
||||
}
|
||||
|
||||
@ -274,7 +267,6 @@ public class CalculatedFieldManagerMessageProcessor extends AbstractContextAware
|
||||
// Alternative approach would be to use any list but avoid modifications to the list (change the complete map value instead)
|
||||
entityIdCalculatedFields.computeIfAbsent(cf.getEntityId(), id -> new CopyOnWriteArrayList<>()).add(cfCtx);
|
||||
addLinks(cf);
|
||||
scheduleDynamicArgumentsRefreshTaskForCfIfNeeded(cfCtx);
|
||||
applyToTargetCfEntityActors(cfCtx, callback, (id, cb) -> initCfForEntity(id, cfCtx, false, cb));
|
||||
}
|
||||
}
|
||||
@ -304,12 +296,6 @@ public class CalculatedFieldManagerMessageProcessor extends AbstractContextAware
|
||||
calculatedFields.put(newCf.getId(), newCfCtx);
|
||||
List<CalculatedFieldCtx> oldCfList = entityIdCalculatedFields.get(newCf.getEntityId());
|
||||
|
||||
boolean hasSchedulingConfigChanges = newCfCtx.hasSchedulingConfigChanges(oldCfCtx);
|
||||
if (hasSchedulingConfigChanges) {
|
||||
cancelCfDynamicArgumentsRefreshTaskIfExists(cfId, false);
|
||||
scheduleDynamicArgumentsRefreshTaskForCfIfNeeded(newCfCtx);
|
||||
}
|
||||
|
||||
List<CalculatedFieldCtx> newCfList = new CopyOnWriteArrayList<>();
|
||||
boolean found = false;
|
||||
for (CalculatedFieldCtx oldCtx : oldCfList) {
|
||||
@ -350,19 +336,9 @@ public class CalculatedFieldManagerMessageProcessor extends AbstractContextAware
|
||||
}
|
||||
entityIdCalculatedFields.get(cfCtx.getEntityId()).remove(cfCtx);
|
||||
deleteLinks(cfCtx);
|
||||
cancelCfDynamicArgumentsRefreshTaskIfExists(cfId, true);
|
||||
applyToTargetCfEntityActors(cfCtx, callback, (id, cb) -> deleteCfForEntity(id, cfId, cb));
|
||||
}
|
||||
|
||||
private void cancelCfDynamicArgumentsRefreshTaskIfExists(CalculatedFieldId cfId, boolean cfDeleted) {
|
||||
var existingTask = cfDynamicArgumentsRefreshTasks.remove(cfId);
|
||||
if (existingTask != null) {
|
||||
existingTask.cancel(false);
|
||||
String reason = cfDeleted ? "deletion" : "update";
|
||||
log.debug("[{}][{}] Cancelled dynamic arguments refresh task due to CF {}!", tenantId, cfId, reason);
|
||||
}
|
||||
}
|
||||
|
||||
public void onTelemetryMsg(CalculatedFieldTelemetryMsg msg) {
|
||||
EntityId entityId = msg.getEntityId();
|
||||
log.debug("Received telemetry msg from entity [{}]", entityId);
|
||||
@ -442,43 +418,6 @@ public class CalculatedFieldManagerMessageProcessor extends AbstractContextAware
|
||||
return result;
|
||||
}
|
||||
|
||||
private void scheduleDynamicArgumentsRefreshTaskForCfIfNeeded(CalculatedFieldCtx cfCtx) {
|
||||
CalculatedField cf = cfCtx.getCalculatedField();
|
||||
if (!(cf.getConfiguration() instanceof ScheduledUpdateSupportedCalculatedFieldConfiguration scheduledCfConfig)) {
|
||||
return;
|
||||
}
|
||||
if (!scheduledCfConfig.isScheduledUpdateEnabled()) {
|
||||
return;
|
||||
}
|
||||
if (cfDynamicArgumentsRefreshTasks.containsKey(cf.getId())) {
|
||||
log.debug("[{}][{}] Dynamic arguments refresh task for CF already exists!", tenantId, cf.getId());
|
||||
return;
|
||||
}
|
||||
long refreshDynamicSourceInterval = TimeUnit.SECONDS.toMillis(scheduledCfConfig.getScheduledUpdateInterval());
|
||||
var scheduledMsg = new CalculatedFieldDynamicArgumentsRefreshMsg(tenantId, cfCtx.getCfId());
|
||||
|
||||
ScheduledFuture<?> scheduledFuture = systemContext
|
||||
.schedulePeriodicMsgWithDelay(ctx, scheduledMsg, refreshDynamicSourceInterval, refreshDynamicSourceInterval);
|
||||
cfDynamicArgumentsRefreshTasks.put(cf.getId(), scheduledFuture);
|
||||
log.debug("[{}][{}] Scheduled dynamic arguments refresh task for CF!", tenantId, cf.getId());
|
||||
}
|
||||
|
||||
public void onDynamicArgumentsRefreshMsg(CalculatedFieldDynamicArgumentsRefreshMsg msg) {
|
||||
log.debug("[{}] [{}] Processing CF dynamic arguments refresh task.", tenantId, msg.getCfId());
|
||||
CalculatedFieldCtx cfCtx = calculatedFields.get(msg.getCfId());
|
||||
if (cfCtx == null) {
|
||||
log.debug("[{}][{}] Failed to find CF context, going to stop dynamic arguments refresh task for CF.", tenantId, msg.getCfId());
|
||||
cancelCfDynamicArgumentsRefreshTaskIfExists(msg.getCfId(), true);
|
||||
return;
|
||||
}
|
||||
applyToTargetCfEntityActors(cfCtx, msg.getCallback(), (id, cb) -> refreshDynamicArgumentsForEntity(id, msg.getCfId(), cb));
|
||||
}
|
||||
|
||||
private void refreshDynamicArgumentsForEntity(EntityId entityId, CalculatedFieldId cfId, TbCallback callback) {
|
||||
log.debug("Pushing CF dynamic arguments refresh msg to specific actor [{}]", entityId);
|
||||
getOrCreateActor(entityId).tell(new EntityCalculatedFieldDynamicArgumentsRefreshMsg(tenantId, cfId, callback));
|
||||
}
|
||||
|
||||
private void linkedTelemetryMsgForEntity(EntityId entityId, EntityCalculatedFieldLinkedTelemetryMsg msg) {
|
||||
log.debug("Pushing linked telemetry msg to specific actor [{}]", entityId);
|
||||
getOrCreateActor(entityId).tell(msg);
|
||||
@ -565,7 +504,6 @@ public class CalculatedFieldManagerMessageProcessor extends AbstractContextAware
|
||||
// We use copy on write lists to safely pass the reference to another actor for the iteration.
|
||||
// Alternative approach would be to use any list but avoid modifications to the list (change the complete map value instead)
|
||||
entityIdCalculatedFields.computeIfAbsent(cf.getEntityId(), id -> new CopyOnWriteArrayList<>()).add(cfCtx);
|
||||
scheduleDynamicArgumentsRefreshTaskForCfIfNeeded(cfCtx);
|
||||
}
|
||||
|
||||
private void initCalculatedFieldLink(CalculatedFieldLink link) {
|
||||
|
||||
@ -1,37 +0,0 @@
|
||||
/**
|
||||
* Copyright © 2016-2025 The Thingsboard Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.thingsboard.server.actors.calculatedField;
|
||||
|
||||
import lombok.Data;
|
||||
import org.thingsboard.server.common.data.id.CalculatedFieldId;
|
||||
import org.thingsboard.server.common.data.id.TenantId;
|
||||
import org.thingsboard.server.common.msg.MsgType;
|
||||
import org.thingsboard.server.common.msg.ToCalculatedFieldSystemMsg;
|
||||
import org.thingsboard.server.common.msg.queue.TbCallback;
|
||||
|
||||
@Data
|
||||
public class EntityCalculatedFieldDynamicArgumentsRefreshMsg implements ToCalculatedFieldSystemMsg {
|
||||
|
||||
private final TenantId tenantId;
|
||||
private final CalculatedFieldId cfId;
|
||||
private final TbCallback callback;
|
||||
|
||||
@Override
|
||||
public MsgType getMsgType() {
|
||||
return MsgType.CF_ENTITY_DYNAMIC_ARGUMENTS_REFRESH_MSG;
|
||||
}
|
||||
|
||||
}
|
||||
@ -45,6 +45,7 @@ import org.thingsboard.server.dao.usagerecord.ApiLimitService;
|
||||
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.geofencing.GeofencingCalculatedFieldState;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
@ -102,6 +103,9 @@ public abstract class AbstractCalculatedFieldProcessingService {
|
||||
return Futures.whenAllComplete(argFutures.values()).call(() -> {
|
||||
var result = createStateByType(ctx);
|
||||
result.updateState(ctx, resolveArgumentFutures(argFutures));
|
||||
if (ctx.hasRelationQueryDynamicArguments() && result instanceof GeofencingCalculatedFieldState geofencingCalculatedFieldState) {
|
||||
geofencingCalculatedFieldState.setLastDynamicArgumentsRefreshTs(System.currentTimeMillis());
|
||||
}
|
||||
return result;
|
||||
}, MoreExecutors.directExecutor());
|
||||
}
|
||||
|
||||
@ -62,7 +62,7 @@ public abstract class BaseCalculatedFieldState implements CalculatedFieldState {
|
||||
boolean entryUpdated;
|
||||
|
||||
if (existingEntry == null || newEntry.isForceResetPrevious()) {
|
||||
validateNewEntry(newEntry);
|
||||
validateNewEntry(key, newEntry);
|
||||
arguments.put(key, newEntry);
|
||||
entryUpdated = true;
|
||||
} else {
|
||||
@ -93,7 +93,7 @@ public abstract class BaseCalculatedFieldState implements CalculatedFieldState {
|
||||
}
|
||||
}
|
||||
|
||||
protected void validateNewEntry(ArgumentEntry newEntry) {}
|
||||
protected void validateNewEntry(String key, ArgumentEntry newEntry) {}
|
||||
|
||||
private void updateLastUpdateTimestamp(ArgumentEntry entry) {
|
||||
long newTs = this.latestTimestamp;
|
||||
|
||||
@ -43,11 +43,13 @@ import org.thingsboard.server.dao.relation.RelationService;
|
||||
import org.thingsboard.server.dao.usagerecord.ApiLimitService;
|
||||
import org.thingsboard.server.gen.transport.TransportProtos.CalculatedFieldTelemetryMsgProto;
|
||||
import org.thingsboard.server.service.cf.ctx.CalculatedFieldEntityCtxId;
|
||||
import org.thingsboard.server.service.cf.ctx.state.geofencing.GeofencingCalculatedFieldState;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import static org.thingsboard.common.util.ExpressionFunctionsUtil.userDefinedFunctions;
|
||||
|
||||
@ -78,9 +80,12 @@ public class CalculatedFieldCtx {
|
||||
private long maxStateSize;
|
||||
private long maxSingleValueArgumentSize;
|
||||
|
||||
private boolean relationQueryDynamicArguments;
|
||||
private List<String> mainEntityGeofencingArgumentNames;
|
||||
private List<String> linkedEntityGeofencingArgumentNames;
|
||||
|
||||
private long scheduledUpdateIntervalMillis;
|
||||
|
||||
public CalculatedFieldCtx(CalculatedField calculatedField, TbelInvokeService tbelInvokeService, ApiLimitService apiLimitService, RelationService relationService) {
|
||||
this.calculatedField = calculatedField;
|
||||
|
||||
@ -101,6 +106,7 @@ public class CalculatedFieldCtx {
|
||||
var refId = entry.getValue().getRefEntityId();
|
||||
var refKey = entry.getValue().getRefEntityKey();
|
||||
if (refId == null && entry.getValue().hasDynamicSource()) {
|
||||
relationQueryDynamicArguments = true;
|
||||
continue;
|
||||
}
|
||||
if (refId == null || refId.equals(calculatedField.getEntityId())) {
|
||||
@ -126,6 +132,9 @@ public class CalculatedFieldCtx {
|
||||
});
|
||||
}
|
||||
}
|
||||
if (calculatedField.getConfiguration() instanceof ScheduledUpdateSupportedCalculatedFieldConfiguration scheduledConfig) {
|
||||
this.scheduledUpdateIntervalMillis = scheduledConfig.isScheduledUpdateEnabled() ? TimeUnit.SECONDS.toMillis(scheduledConfig.getScheduledUpdateInterval()) : -1L;
|
||||
}
|
||||
this.tbelInvokeService = tbelInvokeService;
|
||||
this.relationService = relationService;
|
||||
|
||||
@ -329,25 +338,42 @@ public class CalculatedFieldCtx {
|
||||
public boolean hasOtherSignificantChanges(CalculatedFieldCtx other) {
|
||||
boolean expressionChanged = calculatedField.getConfiguration() instanceof ExpressionBasedCalculatedFieldConfiguration && !expression.equals(other.expression);
|
||||
boolean outputChanged = !output.equals(other.output);
|
||||
return expressionChanged || outputChanged;
|
||||
boolean scheduledUpdatesConfigChanged = scheduledUpdateIntervalMillis != other.scheduledUpdateIntervalMillis;
|
||||
return expressionChanged || outputChanged || scheduledUpdatesConfigChanged;
|
||||
}
|
||||
|
||||
public boolean hasStateChanges(CalculatedFieldCtx other) {
|
||||
boolean typeChanged = !cfType.equals(other.cfType);
|
||||
boolean argumentsChanged = !arguments.equals(other.arguments);
|
||||
return typeChanged || argumentsChanged;
|
||||
boolean geoZoneGroupsConfigChanged = hasGeofencingZoneGroupConfigurationChanges(other);
|
||||
return typeChanged || argumentsChanged || geoZoneGroupsConfigChanged;
|
||||
}
|
||||
|
||||
public boolean hasSchedulingConfigChanges(CalculatedFieldCtx other) {
|
||||
if (calculatedField.getConfiguration() instanceof ScheduledUpdateSupportedCalculatedFieldConfiguration thisConfig
|
||||
&& other.calculatedField.getConfiguration() instanceof ScheduledUpdateSupportedCalculatedFieldConfiguration otherConfig) {
|
||||
boolean refreshTriggerChanged = thisConfig.isScheduledUpdateEnabled() != otherConfig.isScheduledUpdateEnabled();
|
||||
boolean refreshIntervalChanged = thisConfig.getScheduledUpdateInterval() != otherConfig.getScheduledUpdateInterval();
|
||||
return refreshTriggerChanged || refreshIntervalChanged;
|
||||
private boolean hasGeofencingZoneGroupConfigurationChanges(CalculatedFieldCtx other) {
|
||||
if (calculatedField.getConfiguration() instanceof GeofencingCalculatedFieldConfiguration thisConfig
|
||||
&& other.calculatedField.getConfiguration() instanceof GeofencingCalculatedFieldConfiguration otherConfig) {
|
||||
return !thisConfig.getZoneGroups().equals(otherConfig.getZoneGroups());
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public boolean hasRelationQueryDynamicArguments() {
|
||||
return relationQueryDynamicArguments && scheduledUpdateIntervalMillis != -1;
|
||||
}
|
||||
|
||||
public boolean shouldFetchDynamicArgumentsFromDb(CalculatedFieldState state) {
|
||||
if (!hasRelationQueryDynamicArguments()) {
|
||||
return false;
|
||||
}
|
||||
if (!(state instanceof GeofencingCalculatedFieldState geofencingState)) {
|
||||
return false;
|
||||
}
|
||||
if (geofencingState.getLastDynamicArgumentsRefreshTs() == -1L) {
|
||||
return true;
|
||||
}
|
||||
return geofencingState.getLastDynamicArgumentsRefreshTs() < System.currentTimeMillis() - scheduledUpdateIntervalMillis;
|
||||
}
|
||||
|
||||
public String getSizeExceedsLimitMessage() {
|
||||
return "Failed to init CF state. State size exceeds limit of " + (maxStateSize / 1024) + "Kb!";
|
||||
}
|
||||
|
||||
@ -50,13 +50,6 @@ public interface CalculatedFieldState {
|
||||
|
||||
long getLatestTimestamp();
|
||||
|
||||
default void setDirty(boolean dirty) {
|
||||
}
|
||||
|
||||
default boolean isDirty() {
|
||||
return false;
|
||||
}
|
||||
|
||||
void setRequiredArguments(List<String> requiredArguments);
|
||||
|
||||
boolean updateState(CalculatedFieldCtx ctx, Map<String, ArgumentEntry> argumentValues);
|
||||
|
||||
@ -48,9 +48,10 @@ public class SimpleCalculatedFieldState extends BaseCalculatedFieldState {
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void validateNewEntry(ArgumentEntry newEntry) {
|
||||
protected void validateNewEntry(String key, ArgumentEntry newEntry) {
|
||||
if (newEntry instanceof TsRollingArgumentEntry) {
|
||||
throw new IllegalArgumentException("Rolling argument entry is not supported for simple calculated fields.");
|
||||
throw new IllegalArgumentException("Unsupported argument type detected for argument: " + key + ". " +
|
||||
"Rolling argument entry is not supported for simple calculated fields.");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -41,7 +41,7 @@ public class GeofencingArgumentEntry implements ArgumentEntry {
|
||||
}
|
||||
|
||||
public GeofencingArgumentEntry(EntityId entityId, TransportProtos.AttributeValueProto entry) {
|
||||
this.zoneStates = toZones(Map.of(entityId, ProtoUtils.fromProto(entry)));
|
||||
this(Map.of(entityId, ProtoUtils.fromProto(entry)));
|
||||
}
|
||||
|
||||
public GeofencingArgumentEntry(Map<EntityId, KvEntry> entityIdkvEntryMap) {
|
||||
@ -63,6 +63,10 @@ public class GeofencingArgumentEntry implements ArgumentEntry {
|
||||
if (!(entry instanceof GeofencingArgumentEntry geofencingArgumentEntry)) {
|
||||
throw new IllegalArgumentException("Unsupported argument entry type for geofencing argument entry: " + entry.getType());
|
||||
}
|
||||
if (geofencingArgumentEntry.isEmpty()) {
|
||||
zoneStates.clear();
|
||||
return true;
|
||||
}
|
||||
boolean updated = false;
|
||||
for (var zoneEntry : geofencingArgumentEntry.getZoneStates().entrySet()) {
|
||||
if (updateZone(zoneEntry)) {
|
||||
@ -97,6 +101,10 @@ public class GeofencingArgumentEntry implements ArgumentEntry {
|
||||
zoneStates.put(zoneId, newZoneState);
|
||||
return true;
|
||||
}
|
||||
if (newZoneState.getPerimeterDefinition() == null) {
|
||||
zoneStates.remove(zoneId);
|
||||
return true;
|
||||
}
|
||||
return existingZoneState.update(newZoneState);
|
||||
}
|
||||
|
||||
|
||||
@ -15,16 +15,19 @@
|
||||
*/
|
||||
package org.thingsboard.server.service.cf.ctx.state.geofencing;
|
||||
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.node.ObjectNode;
|
||||
import com.google.common.util.concurrent.Futures;
|
||||
import com.google.common.util.concurrent.ListenableFuture;
|
||||
import com.google.common.util.concurrent.MoreExecutors;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.thingsboard.common.util.JacksonUtil;
|
||||
import org.thingsboard.common.util.geo.Coordinates;
|
||||
import org.thingsboard.server.common.data.cf.CalculatedFieldType;
|
||||
import org.thingsboard.server.common.data.cf.configuration.OutputType;
|
||||
import org.thingsboard.server.common.data.cf.configuration.geofencing.GeofencingCalculatedFieldConfiguration;
|
||||
import org.thingsboard.server.common.data.cf.configuration.geofencing.GeofencingReportStrategy;
|
||||
import org.thingsboard.server.common.data.cf.configuration.geofencing.GeofencingTransitionEvent;
|
||||
@ -39,7 +42,6 @@ import org.thingsboard.server.service.cf.ctx.state.CalculatedFieldCtx;
|
||||
import org.thingsboard.server.service.cf.ctx.state.SingleValueArgumentEntry;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Collectors;
|
||||
@ -51,18 +53,14 @@ import static org.thingsboard.server.common.data.cf.configuration.geofencing.Geo
|
||||
|
||||
@Data
|
||||
@Slf4j
|
||||
@NoArgsConstructor
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
public class GeofencingCalculatedFieldState extends BaseCalculatedFieldState {
|
||||
|
||||
private boolean dirty;
|
||||
private long lastDynamicArgumentsRefreshTs = -1;
|
||||
|
||||
public GeofencingCalculatedFieldState() {
|
||||
super(new ArrayList<>(), new HashMap<>(), false, -1);
|
||||
this.dirty = false;
|
||||
}
|
||||
|
||||
public GeofencingCalculatedFieldState(List<String> argNames) {
|
||||
super(argNames);
|
||||
public GeofencingCalculatedFieldState(List<String> requiredArguments) {
|
||||
super(requiredArguments);
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -71,49 +69,21 @@ public class GeofencingCalculatedFieldState extends BaseCalculatedFieldState {
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean updateState(CalculatedFieldCtx ctx, Map<String, ArgumentEntry> argumentValues) {
|
||||
if (arguments == null) {
|
||||
arguments = new HashMap<>();
|
||||
}
|
||||
|
||||
boolean stateUpdated = false;
|
||||
|
||||
for (var entry : argumentValues.entrySet()) {
|
||||
String key = entry.getKey();
|
||||
ArgumentEntry newEntry = entry.getValue();
|
||||
|
||||
checkArgumentSize(key, newEntry, ctx);
|
||||
|
||||
ArgumentEntry existingEntry = arguments.get(key);
|
||||
boolean entryUpdated;
|
||||
|
||||
if (existingEntry == null || newEntry.isForceResetPrevious()) {
|
||||
entryUpdated = switch (key) {
|
||||
case ENTITY_ID_LATITUDE_ARGUMENT_KEY, ENTITY_ID_LONGITUDE_ARGUMENT_KEY -> {
|
||||
if (!(newEntry instanceof SingleValueArgumentEntry singleValueArgumentEntry)) {
|
||||
throw new IllegalArgumentException("Unsupported argument entry type for " + key + " argument: " + newEntry.getType() + ". " +
|
||||
"Only SINGLE_VALUE type is allowed.");
|
||||
}
|
||||
arguments.put(key, singleValueArgumentEntry);
|
||||
yield true;
|
||||
}
|
||||
default -> {
|
||||
if (!(newEntry instanceof GeofencingArgumentEntry geofencingArgumentEntry)) {
|
||||
throw new IllegalArgumentException("Unsupported argument entry type for " + key + " argument: " + newEntry.getType() + ". " +
|
||||
"Only GEOFENCING type is allowed.");
|
||||
}
|
||||
arguments.put(key, geofencingArgumentEntry);
|
||||
yield true;
|
||||
}
|
||||
};
|
||||
} else {
|
||||
entryUpdated = existingEntry.updateEntry(newEntry);
|
||||
protected void validateNewEntry(String key, ArgumentEntry newEntry) {
|
||||
switch (key) {
|
||||
case ENTITY_ID_LATITUDE_ARGUMENT_KEY, ENTITY_ID_LONGITUDE_ARGUMENT_KEY -> {
|
||||
if (!(newEntry instanceof SingleValueArgumentEntry)) {
|
||||
throw new IllegalArgumentException("Unsupported argument entry type for " + key + " argument: " + newEntry.getType() + ". " +
|
||||
"Only SINGLE_VALUE type is allowed.");
|
||||
}
|
||||
}
|
||||
if (entryUpdated) {
|
||||
stateUpdated = true;
|
||||
default -> {
|
||||
if (!(newEntry instanceof GeofencingArgumentEntry)) {
|
||||
throw new IllegalArgumentException("Unsupported argument entry type for " + key + " argument: " + newEntry.getType() + ". " +
|
||||
"Only GEOFENCING type is allowed.");
|
||||
}
|
||||
}
|
||||
}
|
||||
return stateUpdated;
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -125,7 +95,7 @@ public class GeofencingCalculatedFieldState extends BaseCalculatedFieldState {
|
||||
var geofencingCfg = (GeofencingCalculatedFieldConfiguration) ctx.getCalculatedField().getConfiguration();
|
||||
Map<String, ZoneGroupConfiguration> zoneGroups = geofencingCfg.getZoneGroups();
|
||||
|
||||
ObjectNode resultNode = JacksonUtil.newObjectNode();
|
||||
ObjectNode valuesNode = JacksonUtil.newObjectNode();
|
||||
List<ListenableFuture<Boolean>> relationFutures = new ArrayList<>();
|
||||
|
||||
getGeofencingArguments().forEach((argumentKey, argumentEntry) -> {
|
||||
@ -154,10 +124,11 @@ public class GeofencingCalculatedFieldState extends BaseCalculatedFieldState {
|
||||
relationFutures.add(f);
|
||||
}
|
||||
});
|
||||
updateResultNode(argumentKey, zoneResults, zoneGroupCfg.getReportStrategy(), resultNode);
|
||||
updateValuesNode(argumentKey, zoneResults, zoneGroupCfg.getReportStrategy(), valuesNode);
|
||||
});
|
||||
|
||||
var result = new CalculatedFieldResult(ctx.getOutput().getType(), ctx.getOutput().getScope(), resultNode);
|
||||
OutputType outputType = ctx.getOutput().getType();
|
||||
var result = new CalculatedFieldResult(ctx.getOutput().getType(), ctx.getOutput().getScope(), toResultNode(outputType, valuesNode));
|
||||
if (relationFutures.isEmpty()) {
|
||||
return Futures.immediateFuture(result);
|
||||
}
|
||||
@ -171,7 +142,7 @@ public class GeofencingCalculatedFieldState extends BaseCalculatedFieldState {
|
||||
.collect(Collectors.toMap(Map.Entry::getKey, entry -> (GeofencingArgumentEntry) entry.getValue()));
|
||||
}
|
||||
|
||||
private void updateResultNode(String argumentKey, List<GeofencingEvalResult> zoneResults, GeofencingReportStrategy geofencingReportStrategy, ObjectNode resultNode) {
|
||||
private void updateValuesNode(String argumentKey, List<GeofencingEvalResult> zoneResults, GeofencingReportStrategy geofencingReportStrategy, ObjectNode resultNode) {
|
||||
GeofencingEvalResult aggregationResult = aggregateZoneGroup(zoneResults);
|
||||
final String eventKey = argumentKey + "Event";
|
||||
final String statusKey = argumentKey + "Status";
|
||||
@ -185,6 +156,16 @@ public class GeofencingCalculatedFieldState extends BaseCalculatedFieldState {
|
||||
}
|
||||
}
|
||||
|
||||
private JsonNode toResultNode(OutputType outputType, ObjectNode valuesNode) {
|
||||
if (OutputType.ATTRIBUTES.equals(outputType) || latestTimestamp == -1) {
|
||||
return valuesNode;
|
||||
}
|
||||
ObjectNode resultNode = JacksonUtil.newObjectNode();
|
||||
resultNode.put("ts", latestTimestamp);
|
||||
resultNode.set("values", valuesNode);
|
||||
return resultNode;
|
||||
}
|
||||
|
||||
private GeofencingEvalResult aggregateZoneGroup(List<GeofencingEvalResult> zoneResults) {
|
||||
boolean nowInside = zoneResults.stream().anyMatch(r -> INSIDE.equals(r.status()));
|
||||
boolean prevInside = zoneResults.stream()
|
||||
|
||||
@ -831,10 +831,11 @@ public class CalculatedFieldIntegrationTest extends CalculatedFieldControllerTes
|
||||
TenantProfile foundTenantProfile = doGet("/api/tenantProfile/" + tenantProfileEntityInfo.getId().getId().toString(), TenantProfile.class);
|
||||
assertThat(foundTenantProfile).isNotNull();
|
||||
assertThat(foundTenantProfile.getDefaultProfileConfiguration()).isNotNull();
|
||||
foundTenantProfile.getDefaultProfileConfiguration().setMinAllowedScheduledUpdateIntervalInSecForCF(TIMEOUT / 10);
|
||||
int minAllowedScheduledUpdateIntervalInSecForCF = TIMEOUT / 10;
|
||||
foundTenantProfile.getDefaultProfileConfiguration().setMinAllowedScheduledUpdateIntervalInSecForCF(minAllowedScheduledUpdateIntervalInSecForCF);
|
||||
TenantProfile savedTenantProfile = doPost("/api/tenantProfile", foundTenantProfile, TenantProfile.class);
|
||||
assertThat(savedTenantProfile).isNotNull();
|
||||
assertThat(savedTenantProfile.getDefaultProfileConfiguration().getMinAllowedScheduledUpdateIntervalInSecForCF()).isEqualTo(TIMEOUT / 10);
|
||||
assertThat(savedTenantProfile.getDefaultProfileConfiguration().getMinAllowedScheduledUpdateIntervalInSecForCF()).isEqualTo(minAllowedScheduledUpdateIntervalInSecForCF);
|
||||
loginTenantAdmin();
|
||||
|
||||
// --- Arrange entities ---
|
||||
@ -884,7 +885,8 @@ public class CalculatedFieldIntegrationTest extends CalculatedFieldControllerTes
|
||||
cfg.setOutput(out);
|
||||
|
||||
// Enable scheduled refresh with a 6-second interval
|
||||
cfg.setScheduledUpdateInterval(6);
|
||||
cfg.setScheduledUpdateInterval(minAllowedScheduledUpdateIntervalInSecForCF);
|
||||
cfg.setScheduledUpdateEnabled(true);
|
||||
|
||||
cf.setConfiguration(cfg);
|
||||
CalculatedField savedCalculatedField = doPost("/api/calculatedField", cf, CalculatedField.class);
|
||||
@ -935,7 +937,7 @@ public class CalculatedFieldIntegrationTest extends CalculatedFieldControllerTes
|
||||
relAllowedB.setType("AllowedZone");
|
||||
doPost("/api/relation", relAllowedB).andExpect(status().isOk());
|
||||
|
||||
awaitForCalculatedFieldEntityMessageProcessorToRegisterCfStateAsDirty(device.getId(), savedCalculatedField.getId());
|
||||
awaitForCalculatedFieldEntityMessageProcessorToRegisterCfStateAsReadyToRefreshDynamicArguments(device.getId(), savedCalculatedField.getId(), minAllowedScheduledUpdateIntervalInSecForCF);
|
||||
|
||||
// --- Same coordinates as before, but now we expect ENTERED since a new zone is registered ---
|
||||
doPost("/api/plugins/telemetry/DEVICE/" + device.getUuidId() + "/timeseries/unusedScope",
|
||||
|
||||
@ -155,6 +155,7 @@ import org.thingsboard.server.dao.timeseries.TimeseriesService;
|
||||
import org.thingsboard.server.queue.memory.InMemoryStorage;
|
||||
import org.thingsboard.server.service.cf.CfRocksDb;
|
||||
import org.thingsboard.server.service.cf.ctx.state.CalculatedFieldState;
|
||||
import org.thingsboard.server.service.cf.ctx.state.geofencing.GeofencingCalculatedFieldState;
|
||||
import org.thingsboard.server.service.entitiy.tenant.profile.TbTenantProfileService;
|
||||
import org.thingsboard.server.service.security.auth.jwt.RefreshTokenRequest;
|
||||
import org.thingsboard.server.service.security.auth.rest.LoginRequest;
|
||||
@ -1104,14 +1105,15 @@ public abstract class AbstractWebTest extends AbstractInMemoryStorageTest {
|
||||
});
|
||||
}
|
||||
|
||||
protected void awaitForCalculatedFieldEntityMessageProcessorToRegisterCfStateAsDirty(EntityId entityId, CalculatedFieldId cfId) {
|
||||
protected void awaitForCalculatedFieldEntityMessageProcessorToRegisterCfStateAsReadyToRefreshDynamicArguments(EntityId entityId, CalculatedFieldId cfId, int scheduledUpdateInterval) {
|
||||
CalculatedFieldEntityMessageProcessor processor = getCalculatedFieldEntityMessageProcessor(entityId);
|
||||
Map<CalculatedFieldId, CalculatedFieldState> statesMap = (Map<CalculatedFieldId, CalculatedFieldState>) ReflectionTestUtils.getField(processor, "states");
|
||||
Awaitility.await("CF state for entity actor marked as dirty").atMost(5, TimeUnit.SECONDS).until(() -> {
|
||||
Awaitility.await("CF state for entity actor ready to refresh dynamic arguments").atMost(TIMEOUT, TimeUnit.SECONDS).until(() -> {
|
||||
CalculatedFieldState calculatedFieldState = statesMap.get(cfId);
|
||||
boolean stateDirty = calculatedFieldState != null && calculatedFieldState.isDirty();
|
||||
log.warn("entityId {}, cfId {}, state dirty == {}", entityId, cfId, stateDirty);
|
||||
return stateDirty;
|
||||
boolean isReady = calculatedFieldState != null && ((GeofencingCalculatedFieldState) calculatedFieldState).getLastDynamicArgumentsRefreshTs()
|
||||
< System.currentTimeMillis() - TimeUnit.SECONDS.toMillis(scheduledUpdateInterval);
|
||||
log.warn("entityId {}, cfId {}, state ready to refresh == {}", entityId, cfId, isReady);
|
||||
return isReady;
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@ -257,7 +257,7 @@ public class GeofencingCalculatedFieldStateTest {
|
||||
assertThat(result2).isNotNull();
|
||||
assertThat(result2.getType()).isEqualTo(output.getType());
|
||||
assertThat(result2.getScope()).isEqualTo(output.getScope());
|
||||
assertThat(result2.getResult()).isEqualTo(
|
||||
assertThat(result2.getResult().get("values")).isEqualTo(
|
||||
JacksonUtil.newObjectNode()
|
||||
.put("allowedZonesEvent", "LEFT")
|
||||
.put("allowedZonesStatus", "OUTSIDE")
|
||||
@ -329,7 +329,7 @@ public class GeofencingCalculatedFieldStateTest {
|
||||
assertThat(result2).isNotNull();
|
||||
assertThat(result2.getType()).isEqualTo(output.getType());
|
||||
assertThat(result2.getScope()).isEqualTo(output.getScope());
|
||||
assertThat(result2.getResult()).isEqualTo(
|
||||
assertThat(result2.getResult().get("values")).isEqualTo(
|
||||
JacksonUtil.newObjectNode()
|
||||
.put("allowedZonesEvent", "LEFT")
|
||||
.put("restrictedZonesEvent", "ENTERED")
|
||||
@ -401,7 +401,7 @@ public class GeofencingCalculatedFieldStateTest {
|
||||
assertThat(result2).isNotNull();
|
||||
assertThat(result2.getType()).isEqualTo(output.getType());
|
||||
assertThat(result2.getScope()).isEqualTo(output.getScope());
|
||||
assertThat(result2.getResult()).isEqualTo(
|
||||
assertThat(result2.getResult().get("values")).isEqualTo(
|
||||
JacksonUtil.newObjectNode()
|
||||
.put("allowedZonesStatus", "OUTSIDE")
|
||||
.put("restrictedZonesStatus", "INSIDE")
|
||||
|
||||
@ -123,7 +123,8 @@ public class SimpleCalculatedFieldStateTest {
|
||||
Map<String, ArgumentEntry> newArgs = Map.of("key3", new TsRollingArgumentEntry(10, 30000L));
|
||||
assertThatThrownBy(() -> state.updateState(ctx, newArgs))
|
||||
.isInstanceOf(IllegalArgumentException.class)
|
||||
.hasMessage("Rolling argument entry is not supported for simple calculated fields.");
|
||||
.hasMessage("Unsupported argument type detected for argument: key3. " +
|
||||
"Rolling argument entry is not supported for simple calculated fields.");
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
@ -15,11 +15,8 @@
|
||||
*/
|
||||
package org.thingsboard.server.common.data.cf.configuration;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||
|
||||
public interface ScheduledUpdateSupportedCalculatedFieldConfiguration extends CalculatedFieldConfiguration {
|
||||
|
||||
@JsonIgnore
|
||||
boolean isScheduledUpdateEnabled();
|
||||
|
||||
int getScheduledUpdateInterval();
|
||||
|
||||
@ -34,6 +34,8 @@ public class GeofencingCalculatedFieldConfiguration implements ArgumentsBasedCal
|
||||
|
||||
private EntityCoordinates entityCoordinates;
|
||||
private Map<String, ZoneGroupConfiguration> zoneGroups;
|
||||
|
||||
private boolean scheduledUpdateEnabled;
|
||||
private int scheduledUpdateInterval;
|
||||
|
||||
private Output output;
|
||||
@ -61,11 +63,6 @@ public class GeofencingCalculatedFieldConfiguration implements ArgumentsBasedCal
|
||||
return output;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isScheduledUpdateEnabled() {
|
||||
return scheduledUpdateInterval > 0 && zoneGroups.values().stream().anyMatch(ZoneGroupConfiguration::hasDynamicSource);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void validate() {
|
||||
if (entityCoordinates == null) {
|
||||
|
||||
@ -173,7 +173,7 @@ public class DefaultTenantProfileConfiguration implements TenantProfileConfigura
|
||||
@Schema(example = "10")
|
||||
private long maxArgumentsPerCF = 10;
|
||||
@Schema(example = "3600")
|
||||
private int minAllowedScheduledUpdateIntervalInSecForCF = 3600;
|
||||
private int minAllowedScheduledUpdateIntervalInSecForCF = 60;
|
||||
@Schema(example = "10")
|
||||
private int maxRelationLevelPerCfArgument = 10;
|
||||
@Builder.Default
|
||||
|
||||
@ -101,33 +101,6 @@ public class GeofencingCalculatedFieldConfigurationTest {
|
||||
verify(zoneGroupConfigurationB).validate(zoneGroupBName);
|
||||
}
|
||||
|
||||
@Test
|
||||
void scheduledUpdateDisabledWhenIntervalIsZero() {
|
||||
var cfg = new GeofencingCalculatedFieldConfiguration();
|
||||
cfg.setScheduledUpdateInterval(0);
|
||||
assertThat(cfg.isScheduledUpdateEnabled()).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
void scheduledUpdateDisabledWhenIntervalIsGreaterThanZeroButNoZonesWithDynamicArguments() {
|
||||
var cfg = new GeofencingCalculatedFieldConfiguration();
|
||||
var zoneGroupConfigurationMock = mock(ZoneGroupConfiguration.class);
|
||||
when(zoneGroupConfigurationMock.hasDynamicSource()).thenReturn(false);
|
||||
cfg.setZoneGroups(Map.of("someGroupName", zoneGroupConfigurationMock));
|
||||
cfg.setScheduledUpdateInterval(60);
|
||||
assertThat(cfg.isScheduledUpdateEnabled()).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
void scheduledUpdateEnabledWhenIntervalIsGreaterThanZeroAndDynamicArgumentsPresent() {
|
||||
var cfg = new GeofencingCalculatedFieldConfiguration();
|
||||
var zoneGroupConfigurationMock = mock(ZoneGroupConfiguration.class);
|
||||
when(zoneGroupConfigurationMock.hasDynamicSource()).thenReturn(true);
|
||||
cfg.setZoneGroups(Map.of("someGroupName", zoneGroupConfigurationMock));
|
||||
cfg.setScheduledUpdateInterval(60);
|
||||
assertThat(cfg.isScheduledUpdateEnabled()).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
void testGetArgumentsOverride() {
|
||||
var cfg = new GeofencingCalculatedFieldConfiguration();
|
||||
|
||||
@ -147,10 +147,7 @@ public enum MsgType {
|
||||
/* CF Manager Actor -> CF Entity actor */
|
||||
CF_ENTITY_TELEMETRY_MSG,
|
||||
CF_ENTITY_INIT_CF_MSG,
|
||||
CF_ENTITY_DELETE_MSG,
|
||||
|
||||
CF_DYNAMIC_ARGUMENTS_REFRESH_MSG,
|
||||
CF_ENTITY_DYNAMIC_ARGUMENTS_REFRESH_MSG;
|
||||
CF_ENTITY_DELETE_MSG;
|
||||
|
||||
@Getter
|
||||
private final boolean ignoreOnStart;
|
||||
|
||||
@ -99,53 +99,7 @@ public class CalculatedFieldServiceTest extends AbstractServiceTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSaveGeofencingCalculatedField_shouldNotChangeScheduledInterval() {
|
||||
// Arrange a device
|
||||
Device device = createTestDevice();
|
||||
|
||||
// Build a valid Geofencing configuration
|
||||
GeofencingCalculatedFieldConfiguration cfg = new GeofencingCalculatedFieldConfiguration();
|
||||
|
||||
// Coordinates: TS_LATEST, no dynamic source
|
||||
EntityCoordinates entityCoordinates = new EntityCoordinates("latitude", "longitude");
|
||||
cfg.setEntityCoordinates(entityCoordinates);
|
||||
|
||||
// Zone-group argument (ATTRIBUTE) — no DYNAMIC configuration, so no scheduling even if the scheduled interval is set
|
||||
ZoneGroupConfiguration zoneGroupConfiguration = new ZoneGroupConfiguration("allowed", REPORT_TRANSITION_EVENTS_AND_PRESENCE_STATUS, false);
|
||||
zoneGroupConfiguration.setRefEntityId(device.getId());
|
||||
cfg.setZoneGroups(Map.of("allowed", zoneGroupConfiguration));
|
||||
|
||||
// Set a scheduled interval to some value
|
||||
cfg.setScheduledUpdateInterval(600);
|
||||
|
||||
// Create & save Calculated Field
|
||||
CalculatedField cf = new CalculatedField();
|
||||
cf.setTenantId(tenantId);
|
||||
cf.setEntityId(device.getId());
|
||||
cf.setType(CalculatedFieldType.GEOFENCING);
|
||||
cf.setName("GF clamp test");
|
||||
cf.setConfigurationVersion(0);
|
||||
cf.setConfiguration(cfg);
|
||||
|
||||
CalculatedField saved = calculatedFieldService.save(cf);
|
||||
|
||||
assertThat(saved).isNotNull();
|
||||
assertThat(saved.getConfiguration()).isInstanceOf(GeofencingCalculatedFieldConfiguration.class);
|
||||
|
||||
var geofencingCalculatedFieldConfiguration = (GeofencingCalculatedFieldConfiguration) saved.getConfiguration();
|
||||
|
||||
// Assert: the interval is saved, but scheduling is not enabled
|
||||
int savedInterval = geofencingCalculatedFieldConfiguration.getScheduledUpdateInterval();
|
||||
boolean scheduledUpdateEnabled = geofencingCalculatedFieldConfiguration.isScheduledUpdateEnabled();
|
||||
|
||||
assertThat(savedInterval).isEqualTo(600);
|
||||
assertThat(scheduledUpdateEnabled).isFalse();
|
||||
|
||||
calculatedFieldService.deleteCalculatedField(tenantId, saved.getId());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSaveGeofencingCalculatedField_shouldThrowWhenScheduledIntervalIsLessThanMinAllowedIntervalInTenantProfile() {
|
||||
public void testSaveGeofencingCalculatedField_shouldThrowWhenScheduledIntervalLessThanMinAllowedIntervalInTenantProfile() {
|
||||
// Arrange a device
|
||||
Device device = createTestDevice();
|
||||
|
||||
@ -165,15 +119,22 @@ public class CalculatedFieldServiceTest extends AbstractServiceTest {
|
||||
zoneGroupConfiguration.setRefDynamicSourceConfiguration(dynamicSourceConfiguration);
|
||||
cfg.setZoneGroups(Map.of("allowed", zoneGroupConfiguration));
|
||||
|
||||
// Get tenant profile min.
|
||||
int min = tbTenantProfileCache.get(tenantId)
|
||||
.getDefaultProfileConfiguration()
|
||||
.getMinAllowedScheduledUpdateIntervalInSecForCF();
|
||||
int valueFromConfig = min - 10;
|
||||
|
||||
// Enable scheduling with an interval below tenant min
|
||||
cfg.setScheduledUpdateInterval(600);
|
||||
cfg.setScheduledUpdateEnabled(true);
|
||||
cfg.setScheduledUpdateInterval(valueFromConfig);
|
||||
|
||||
// Create & save Calculated Field
|
||||
CalculatedField cf = new CalculatedField();
|
||||
cf.setTenantId(tenantId);
|
||||
cf.setEntityId(device.getId());
|
||||
cf.setType(CalculatedFieldType.GEOFENCING);
|
||||
cf.setName("GF clamp test");
|
||||
cf.setName("GF min allowed scheduled update interval test");
|
||||
cf.setConfigurationVersion(0);
|
||||
cf.setConfiguration(cfg);
|
||||
|
||||
@ -185,7 +146,7 @@ public class CalculatedFieldServiceTest extends AbstractServiceTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSaveGeofencingCalculatedField_shouldThrowWhenRelationLevelIsGreaterThanMaxAllowedRelationLevelInTenantProfile() {
|
||||
public void testSaveGeofencingCalculatedField_shouldThrowWhenRelationLevelGreaterThanMaxAllowedRelationLevelInTenantProfile() {
|
||||
// Arrange a device
|
||||
Device device = createTestDevice();
|
||||
|
||||
@ -210,7 +171,7 @@ public class CalculatedFieldServiceTest extends AbstractServiceTest {
|
||||
cf.setTenantId(tenantId);
|
||||
cf.setEntityId(device.getId());
|
||||
cf.setType(CalculatedFieldType.GEOFENCING);
|
||||
cf.setName("GF clamp test");
|
||||
cf.setName("GF max relation level test");
|
||||
cf.setConfigurationVersion(0);
|
||||
cf.setConfiguration(cfg);
|
||||
|
||||
@ -221,7 +182,7 @@ public class CalculatedFieldServiceTest extends AbstractServiceTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSaveGeofencingCalculatedField_shouldUseScheduledIntervalFromConfig() {
|
||||
public void testSaveGeofencingCalculatedField_shouldSaveWithoutDataValidationExceptionOnScheduledUpdateInterval() {
|
||||
// Arrange a device
|
||||
Device device = createTestDevice();
|
||||
|
||||
@ -245,10 +206,10 @@ public class CalculatedFieldServiceTest extends AbstractServiceTest {
|
||||
int min = tbTenantProfileCache.get(tenantId)
|
||||
.getDefaultProfileConfiguration()
|
||||
.getMinAllowedScheduledUpdateIntervalInSecForCF();
|
||||
|
||||
int valueFromConfig = min + 100;
|
||||
|
||||
// Enable scheduling with an interval greater than tenant min
|
||||
int valueFromConfig = min + 100;
|
||||
cfg.setScheduledUpdateEnabled(true);
|
||||
cfg.setScheduledUpdateInterval(valueFromConfig);
|
||||
|
||||
// Create & save Calculated Field
|
||||
@ -256,7 +217,7 @@ public class CalculatedFieldServiceTest extends AbstractServiceTest {
|
||||
cf.setTenantId(tenantId);
|
||||
cf.setEntityId(device.getId());
|
||||
cf.setType(CalculatedFieldType.GEOFENCING);
|
||||
cf.setName("GF no clamp test");
|
||||
cf.setName("GF no validation error test");
|
||||
cf.setConfigurationVersion(0);
|
||||
cf.setConfiguration(cfg);
|
||||
|
||||
@ -267,7 +228,6 @@ 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.getScheduledUpdateInterval();
|
||||
assertThat(savedInterval).isEqualTo(valueFromConfig);
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user