227 changed files with 7473 additions and 1577 deletions
@ -0,0 +1,52 @@ |
|||||
|
/** |
||||
|
* 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.audit.ActionType; |
||||
|
import org.thingsboard.server.common.data.id.EntityId; |
||||
|
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; |
||||
|
import org.thingsboard.server.service.cf.ctx.state.CalculatedFieldCtx; |
||||
|
|
||||
|
@Data |
||||
|
public class CalculatedFieldRelationActionMsg implements ToCalculatedFieldSystemMsg { |
||||
|
|
||||
|
private final TenantId tenantId; |
||||
|
private final EntityId relatedEntityId; |
||||
|
private final ActionType action; |
||||
|
private final CalculatedFieldCtx calculatedField; |
||||
|
private final TbCallback callback; |
||||
|
|
||||
|
public CalculatedFieldRelationActionMsg(TenantId tenantId, |
||||
|
EntityId relatedEntityId, ActionType action, |
||||
|
CalculatedFieldCtx calculatedField, |
||||
|
TbCallback callback) { |
||||
|
this.tenantId = tenantId; |
||||
|
this.relatedEntityId = relatedEntityId; |
||||
|
this.action = action; |
||||
|
this.calculatedField = calculatedField; |
||||
|
this.callback = callback; |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public MsgType getMsgType() { |
||||
|
return MsgType.CF_RELATION_ACTION_MSG; |
||||
|
} |
||||
|
|
||||
|
} |
||||
@ -0,0 +1,188 @@ |
|||||
|
/** |
||||
|
* Copyright © 2016-2025 The Thingsboard Authors |
||||
|
* |
||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
|
* you may not use this file except in compliance with the License. |
||||
|
* You may obtain a copy of the License at |
||||
|
* |
||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
* |
||||
|
* Unless required by applicable law or agreed to in writing, software |
||||
|
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
|
* See the License for the specific language governing permissions and |
||||
|
* limitations under the License. |
||||
|
*/ |
||||
|
package org.thingsboard.server.service.cf.ctx.state.aggregation; |
||||
|
|
||||
|
import com.fasterxml.jackson.databind.node.ObjectNode; |
||||
|
import com.google.common.util.concurrent.Futures; |
||||
|
import com.google.common.util.concurrent.ListenableFuture; |
||||
|
import lombok.Getter; |
||||
|
import lombok.Setter; |
||||
|
import lombok.extern.slf4j.Slf4j; |
||||
|
import org.thingsboard.common.util.JacksonUtil; |
||||
|
import org.thingsboard.server.actors.TbActorRef; |
||||
|
import org.thingsboard.server.common.data.cf.CalculatedFieldType; |
||||
|
import org.thingsboard.server.common.data.cf.configuration.Output; |
||||
|
import org.thingsboard.server.common.data.cf.configuration.aggregation.AggFunctionInput; |
||||
|
import org.thingsboard.server.common.data.cf.configuration.aggregation.AggInput; |
||||
|
import org.thingsboard.server.common.data.cf.configuration.aggregation.AggKeyInput; |
||||
|
import org.thingsboard.server.common.data.cf.configuration.aggregation.AggMetric; |
||||
|
import org.thingsboard.server.common.data.cf.configuration.aggregation.RelatedEntitiesAggregationCalculatedFieldConfiguration; |
||||
|
import org.thingsboard.server.common.data.id.EntityId; |
||||
|
import org.thingsboard.server.service.cf.CalculatedFieldResult; |
||||
|
import org.thingsboard.server.service.cf.TelemetryCalculatedFieldResult; |
||||
|
import org.thingsboard.server.service.cf.ctx.state.ArgumentEntry; |
||||
|
import org.thingsboard.server.service.cf.ctx.state.BaseCalculatedFieldState; |
||||
|
import org.thingsboard.server.service.cf.ctx.state.CalculatedFieldCtx; |
||||
|
import org.thingsboard.server.service.cf.ctx.state.aggregation.function.AggEntry; |
||||
|
|
||||
|
import java.util.HashMap; |
||||
|
import java.util.Map; |
||||
|
import java.util.Map.Entry; |
||||
|
|
||||
|
import static java.util.concurrent.TimeUnit.SECONDS; |
||||
|
|
||||
|
@Slf4j |
||||
|
@Getter |
||||
|
public class RelatedEntitiesAggregationCalculatedFieldState extends BaseCalculatedFieldState { |
||||
|
|
||||
|
@Setter |
||||
|
private long lastArgsRefreshTs = -1; |
||||
|
@Setter |
||||
|
private long lastMetricsEvalTs = -1; |
||||
|
private long deduplicationIntervalMs = -1; |
||||
|
private Map<String, AggMetric> metrics; |
||||
|
|
||||
|
public RelatedEntitiesAggregationCalculatedFieldState(EntityId entityId) { |
||||
|
super(entityId); |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public void setCtx(CalculatedFieldCtx ctx, TbActorRef actorCtx) { |
||||
|
super.setCtx(ctx, actorCtx); |
||||
|
var configuration = (RelatedEntitiesAggregationCalculatedFieldConfiguration) ctx.getCalculatedField().getConfiguration(); |
||||
|
metrics = configuration.getMetrics(); |
||||
|
deduplicationIntervalMs = SECONDS.toMillis(configuration.getDeduplicationIntervalInSec()); |
||||
|
} |
||||
|
|
||||
|
public void scheduleReevaluation() { |
||||
|
ctx.scheduleReevaluation(deduplicationIntervalMs, actorCtx); |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public void reset() { // must reset everything dependent on arguments
|
||||
|
super.reset(); |
||||
|
lastArgsRefreshTs = -1; |
||||
|
lastMetricsEvalTs = -1; |
||||
|
metrics = null; |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public CalculatedFieldType getType() { |
||||
|
return CalculatedFieldType.RELATED_ENTITIES_AGGREGATION; |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public Map<String, ArgumentEntry> update(Map<String, ArgumentEntry> argumentValues, CalculatedFieldCtx ctx) { |
||||
|
lastArgsRefreshTs = System.currentTimeMillis(); |
||||
|
return super.update(argumentValues, ctx); |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public ListenableFuture<CalculatedFieldResult> performCalculation(Map<String, ArgumentEntry> updatedArgs, CalculatedFieldCtx ctx) throws Exception { |
||||
|
boolean cfUpdated = updatedArgs != null && updatedArgs.isEmpty(); |
||||
|
if (shouldRecalculate() || cfUpdated) { |
||||
|
Output output = ctx.getOutput(); |
||||
|
ObjectNode aggResult = aggregateMetrics(output); |
||||
|
lastMetricsEvalTs = System.currentTimeMillis(); |
||||
|
ctx.scheduleReevaluation(deduplicationIntervalMs, actorCtx); |
||||
|
return Futures.immediateFuture(TelemetryCalculatedFieldResult.builder() |
||||
|
.type(output.getType()) |
||||
|
.scope(output.getScope()) |
||||
|
.result(toSimpleResult(ctx.isUseLatestTs(), aggResult)) |
||||
|
.build()); |
||||
|
} else { |
||||
|
return Futures.immediateFuture(TelemetryCalculatedFieldResult.EMPTY); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public Map<String, ArgumentEntry> updateEntityData(Map<String, ArgumentEntry> fetchedArgs) { |
||||
|
lastMetricsEvalTs = -1; |
||||
|
return update(fetchedArgs, ctx); |
||||
|
} |
||||
|
|
||||
|
public void cleanupEntityData(EntityId relatedEntityId) { |
||||
|
arguments.values().forEach(argEntry -> { |
||||
|
RelatedEntitiesArgumentEntry aggEntry = (RelatedEntitiesArgumentEntry) argEntry; |
||||
|
aggEntry.getEntityInputs().remove(relatedEntityId); |
||||
|
}); |
||||
|
lastMetricsEvalTs = -1; |
||||
|
lastArgsRefreshTs = System.currentTimeMillis(); |
||||
|
} |
||||
|
|
||||
|
private boolean shouldRecalculate() { |
||||
|
boolean intervalPassed = lastMetricsEvalTs <= System.currentTimeMillis() - deduplicationIntervalMs; |
||||
|
boolean argsUpdatedDuringInterval = lastArgsRefreshTs > lastMetricsEvalTs; |
||||
|
return intervalPassed && argsUpdatedDuringInterval; |
||||
|
} |
||||
|
|
||||
|
private Map<EntityId, Map<String, ArgumentEntry>> prepareInputs() { |
||||
|
Map<EntityId, Map<String, ArgumentEntry>> inputs = new HashMap<>(); |
||||
|
for (Map.Entry<String, ArgumentEntry> argEntry : arguments.entrySet()) { |
||||
|
String key = argEntry.getKey(); |
||||
|
RelatedEntitiesArgumentEntry relatedEntitiesArgumentEntry = (RelatedEntitiesArgumentEntry) argEntry.getValue(); |
||||
|
relatedEntitiesArgumentEntry.getEntityInputs().forEach((entityId, argumentEntry) -> { |
||||
|
inputs.computeIfAbsent(entityId, k -> new HashMap<>()).put(key, argumentEntry); |
||||
|
}); |
||||
|
} |
||||
|
return inputs; |
||||
|
} |
||||
|
|
||||
|
private ObjectNode aggregateMetrics(Output output) throws Exception { |
||||
|
ObjectNode aggResult = JacksonUtil.newObjectNode(); |
||||
|
Map<EntityId, Map<String, ArgumentEntry>> inputs = prepareInputs(); |
||||
|
for (Entry<String, AggMetric> entry : metrics.entrySet()) { |
||||
|
String metricKey = entry.getKey(); |
||||
|
AggMetric metric = entry.getValue(); |
||||
|
|
||||
|
AggEntry aggMetricEntry = AggEntry.createAggFunction(metric.getFunction()); |
||||
|
aggregateMetric(metric, aggMetricEntry, inputs); |
||||
|
aggMetricEntry.result(output.getDecimalsByDefault()).ifPresent(result -> { |
||||
|
aggResult.set(metricKey, JacksonUtil.valueToTree(result)); |
||||
|
}); |
||||
|
} |
||||
|
return aggResult; |
||||
|
} |
||||
|
|
||||
|
private void aggregateMetric(AggMetric metric, AggEntry aggEntry, Map<EntityId, Map<String, ArgumentEntry>> inputs) throws Exception { |
||||
|
for (Map<String, ArgumentEntry> entityInputs : inputs.values()) { |
||||
|
if (applyAggregation(metric.getFilter(), entityInputs)) { |
||||
|
Object arg = resolveAggregationInput(metric.getInput(), entityInputs); |
||||
|
if (arg != null) { |
||||
|
aggEntry.update(arg); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private boolean applyAggregation(String filter, Map<String, ArgumentEntry> entityInputs) throws Exception { |
||||
|
if (filter == null || filter.isEmpty()) { |
||||
|
return true; |
||||
|
} else { |
||||
|
Object filterResult = ctx.evaluateTbelExpression(filter, entityInputs, getLatestTimestamp()).get(); |
||||
|
return filterResult instanceof Boolean booleanResult && booleanResult; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private Object resolveAggregationInput(AggInput aggInput, Map<String, ArgumentEntry> entityInputs) throws Exception { |
||||
|
if (aggInput instanceof AggFunctionInput functionInput) { |
||||
|
return ctx.evaluateTbelExpression(functionInput.getFunction(), entityInputs, getLatestTimestamp()).get(); |
||||
|
} else { |
||||
|
String inputKey = ((AggKeyInput) aggInput).getKey(); |
||||
|
return entityInputs.get(inputKey).getValue(); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
} |
||||
@ -0,0 +1,86 @@ |
|||||
|
/** |
||||
|
* Copyright © 2016-2025 The Thingsboard Authors |
||||
|
* |
||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
|
* you may not use this file except in compliance with the License. |
||||
|
* You may obtain a copy of the License at |
||||
|
* |
||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
* |
||||
|
* Unless required by applicable law or agreed to in writing, software |
||||
|
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
|
* See the License for the specific language governing permissions and |
||||
|
* limitations under the License. |
||||
|
*/ |
||||
|
package org.thingsboard.server.service.cf.ctx.state.aggregation; |
||||
|
|
||||
|
import lombok.AllArgsConstructor; |
||||
|
import lombok.Data; |
||||
|
import org.thingsboard.script.api.tbel.TbelCfArg; |
||||
|
import org.thingsboard.script.api.tbel.TbelCfRelatedEntitiesArgumentValue; |
||||
|
import org.thingsboard.script.api.tbel.TbelCfSingleValueArg; |
||||
|
import org.thingsboard.server.common.data.id.EntityId; |
||||
|
import org.thingsboard.server.service.cf.ctx.state.ArgumentEntry; |
||||
|
import org.thingsboard.server.service.cf.ctx.state.ArgumentEntryType; |
||||
|
import org.thingsboard.server.service.cf.ctx.state.SingleValueArgumentEntry; |
||||
|
|
||||
|
import java.util.Map; |
||||
|
import java.util.stream.Collectors; |
||||
|
|
||||
|
@Data |
||||
|
@AllArgsConstructor |
||||
|
public class RelatedEntitiesArgumentEntry implements ArgumentEntry { |
||||
|
|
||||
|
private final Map<EntityId, ArgumentEntry> entityInputs; |
||||
|
|
||||
|
private boolean forceResetPrevious; |
||||
|
|
||||
|
@Override |
||||
|
public ArgumentEntryType getType() { |
||||
|
return ArgumentEntryType.RELATED_ENTITIES; |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public Object getValue() { |
||||
|
return entityInputs; |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public boolean updateEntry(ArgumentEntry entry) { |
||||
|
if (entry instanceof RelatedEntitiesArgumentEntry relatedEntitiesArgumentEntry) { |
||||
|
entityInputs.putAll(relatedEntitiesArgumentEntry.entityInputs); |
||||
|
return true; |
||||
|
} else if (entry instanceof SingleValueArgumentEntry singleValueArgumentEntry) { |
||||
|
if (entry.isForceResetPrevious()) { |
||||
|
entityInputs.put(singleValueArgumentEntry.getEntityId(), singleValueArgumentEntry); |
||||
|
return true; |
||||
|
} |
||||
|
ArgumentEntry argumentEntry = entityInputs.get(singleValueArgumentEntry.getEntityId()); |
||||
|
if (argumentEntry != null) { |
||||
|
argumentEntry.updateEntry(singleValueArgumentEntry); |
||||
|
} else { |
||||
|
entityInputs.put(singleValueArgumentEntry.getEntityId(), singleValueArgumentEntry); |
||||
|
} |
||||
|
return true; |
||||
|
} else { |
||||
|
throw new IllegalArgumentException("Unsupported argument entry type for aggregation argument entry: " + entry.getType()); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public boolean isEmpty() { |
||||
|
return entityInputs.isEmpty(); |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public TbelCfArg toTbelCfArg() { |
||||
|
var inputs = entityInputs.entrySet().stream() |
||||
|
.collect(Collectors.toMap( |
||||
|
e -> e.getKey().getId(), |
||||
|
e -> (TbelCfSingleValueArg) e.getValue().toTbelCfArg() |
||||
|
)); |
||||
|
return new TbelCfRelatedEntitiesArgumentValue(inputs); |
||||
|
} |
||||
|
|
||||
|
} |
||||
@ -0,0 +1,58 @@ |
|||||
|
/** |
||||
|
* Copyright © 2016-2025 The Thingsboard Authors |
||||
|
* |
||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
|
* you may not use this file except in compliance with the License. |
||||
|
* You may obtain a copy of the License at |
||||
|
* |
||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
* |
||||
|
* Unless required by applicable law or agreed to in writing, software |
||||
|
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
|
* See the License for the specific language governing permissions and |
||||
|
* limitations under the License. |
||||
|
*/ |
||||
|
package org.thingsboard.server.service.cf.ctx.state.aggregation.function; |
||||
|
|
||||
|
import com.fasterxml.jackson.annotation.JsonIgnore; |
||||
|
import com.fasterxml.jackson.annotation.JsonSubTypes; |
||||
|
import com.fasterxml.jackson.annotation.JsonTypeInfo; |
||||
|
import org.thingsboard.server.common.data.cf.configuration.aggregation.AggFunction; |
||||
|
|
||||
|
import java.util.Optional; |
||||
|
|
||||
|
@JsonTypeInfo( |
||||
|
use = JsonTypeInfo.Id.NAME, |
||||
|
include = JsonTypeInfo.As.PROPERTY, |
||||
|
property = "type" |
||||
|
) |
||||
|
@JsonSubTypes({ |
||||
|
@JsonSubTypes.Type(value = AvgAggEntry.class, name = "AVG"), |
||||
|
@JsonSubTypes.Type(value = CountAggEntry.class, name = "COUNT"), |
||||
|
@JsonSubTypes.Type(value = CountUniqueAggEntry.class, name = "COUNT_UNIQUE"), |
||||
|
@JsonSubTypes.Type(value = MaxAggEntry.class, name = "MAX"), |
||||
|
@JsonSubTypes.Type(value = MinAggEntry.class, name = "MIN"), |
||||
|
@JsonSubTypes.Type(value = SumAggEntry.class, name = "SUM") |
||||
|
}) |
||||
|
public interface AggEntry { |
||||
|
|
||||
|
@JsonIgnore |
||||
|
AggFunction getType(); |
||||
|
|
||||
|
void update(Object value); |
||||
|
|
||||
|
Optional<Object> result(Integer precision); |
||||
|
|
||||
|
static AggEntry createAggFunction(AggFunction function) { |
||||
|
return switch (function) { |
||||
|
case MIN -> new MinAggEntry(); |
||||
|
case MAX -> new MaxAggEntry(); |
||||
|
case SUM -> new SumAggEntry(); |
||||
|
case AVG -> new AvgAggEntry(); |
||||
|
case COUNT -> new CountAggEntry(); |
||||
|
case COUNT_UNIQUE -> new CountUniqueAggEntry(); |
||||
|
}; |
||||
|
} |
||||
|
|
||||
|
} |
||||
@ -0,0 +1,47 @@ |
|||||
|
/** |
||||
|
* Copyright © 2016-2025 The Thingsboard Authors |
||||
|
* |
||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
|
* you may not use this file except in compliance with the License. |
||||
|
* You may obtain a copy of the License at |
||||
|
* |
||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
* |
||||
|
* Unless required by applicable law or agreed to in writing, software |
||||
|
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
|
* See the License for the specific language governing permissions and |
||||
|
* limitations under the License. |
||||
|
*/ |
||||
|
package org.thingsboard.server.service.cf.ctx.state.aggregation.function; |
||||
|
|
||||
|
import org.thingsboard.script.api.tbel.TbUtils; |
||||
|
import org.thingsboard.server.common.data.cf.configuration.aggregation.AggFunction; |
||||
|
|
||||
|
import java.math.BigDecimal; |
||||
|
import java.math.RoundingMode; |
||||
|
|
||||
|
public class AvgAggEntry extends BaseAggEntry { |
||||
|
|
||||
|
private BigDecimal sum = BigDecimal.ZERO; |
||||
|
private long count = 0L; |
||||
|
|
||||
|
@Override |
||||
|
protected void doUpdate(double value) { |
||||
|
if (value != 0.0) { |
||||
|
sum = sum.add(BigDecimal.valueOf(value)); |
||||
|
} |
||||
|
this.count++; |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
protected Object prepareResult(Integer precision) { |
||||
|
double result = sum.divide(BigDecimal.valueOf(count), RoundingMode.HALF_UP).doubleValue(); |
||||
|
return TbUtils.roundResult(result, precision); |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public AggFunction getType() { |
||||
|
return AggFunction.AVG; |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,55 @@ |
|||||
|
/** |
||||
|
* Copyright © 2016-2025 The Thingsboard Authors |
||||
|
* |
||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
|
* you may not use this file except in compliance with the License. |
||||
|
* You may obtain a copy of the License at |
||||
|
* |
||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
* |
||||
|
* Unless required by applicable law or agreed to in writing, software |
||||
|
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
|
* See the License for the specific language governing permissions and |
||||
|
* limitations under the License. |
||||
|
*/ |
||||
|
package org.thingsboard.server.service.cf.ctx.state.aggregation.function; |
||||
|
|
||||
|
import java.util.Optional; |
||||
|
|
||||
|
public abstract class BaseAggEntry implements AggEntry { |
||||
|
|
||||
|
private boolean hasResult = false; |
||||
|
|
||||
|
@Override |
||||
|
public void update(Object value) { |
||||
|
doUpdate(extractDoubleValue(value)); |
||||
|
hasResult = true; |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public Optional<Object> result(Integer precision) { |
||||
|
if (hasResult) { |
||||
|
hasResult = false; |
||||
|
return Optional.of(prepareResult(precision)); |
||||
|
} else { |
||||
|
return Optional.empty(); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
protected abstract void doUpdate(double value); |
||||
|
|
||||
|
protected abstract Object prepareResult(Integer precision); |
||||
|
|
||||
|
protected double extractDoubleValue(Object value) { |
||||
|
try { |
||||
|
if (value instanceof Number number) { |
||||
|
return number.doubleValue(); |
||||
|
} |
||||
|
return Double.parseDouble(value.toString()); |
||||
|
} catch (Exception e) { |
||||
|
throw new NumberFormatException("Cannot parse value " + value.toString()); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
} |
||||
@ -0,0 +1,41 @@ |
|||||
|
/** |
||||
|
* Copyright © 2016-2025 The Thingsboard Authors |
||||
|
* |
||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
|
* you may not use this file except in compliance with the License. |
||||
|
* You may obtain a copy of the License at |
||||
|
* |
||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
* |
||||
|
* Unless required by applicable law or agreed to in writing, software |
||||
|
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
|
* See the License for the specific language governing permissions and |
||||
|
* limitations under the License. |
||||
|
*/ |
||||
|
package org.thingsboard.server.service.cf.ctx.state.aggregation.function; |
||||
|
|
||||
|
import org.thingsboard.server.common.data.cf.configuration.aggregation.AggFunction; |
||||
|
|
||||
|
import java.util.Optional; |
||||
|
|
||||
|
public class CountAggEntry implements AggEntry { |
||||
|
|
||||
|
private long count = 0L; |
||||
|
|
||||
|
@Override |
||||
|
public void update(Object value) { |
||||
|
count++; |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public Optional<Object> result(Integer precision) { |
||||
|
return Optional.of(count); |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public AggFunction getType() { |
||||
|
return AggFunction.COUNT; |
||||
|
} |
||||
|
|
||||
|
} |
||||
@ -0,0 +1,43 @@ |
|||||
|
/** |
||||
|
* Copyright © 2016-2025 The Thingsboard Authors |
||||
|
* |
||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
|
* you may not use this file except in compliance with the License. |
||||
|
* You may obtain a copy of the License at |
||||
|
* |
||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
* |
||||
|
* Unless required by applicable law or agreed to in writing, software |
||||
|
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
|
* See the License for the specific language governing permissions and |
||||
|
* limitations under the License. |
||||
|
*/ |
||||
|
package org.thingsboard.server.service.cf.ctx.state.aggregation.function; |
||||
|
|
||||
|
import org.thingsboard.server.common.data.cf.configuration.aggregation.AggFunction; |
||||
|
|
||||
|
import java.util.Optional; |
||||
|
import java.util.Set; |
||||
|
|
||||
|
public class CountUniqueAggEntry implements AggEntry { |
||||
|
|
||||
|
private Set<String> items; |
||||
|
|
||||
|
@Override |
||||
|
public void update(Object value) { |
||||
|
if (value != null) { |
||||
|
items.add(String.valueOf(value)); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public Optional<Object> result(Integer precision) { |
||||
|
return Optional.of(items.size()); |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public AggFunction getType() { |
||||
|
return AggFunction.COUNT_UNIQUE; |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,41 @@ |
|||||
|
/** |
||||
|
* Copyright © 2016-2025 The Thingsboard Authors |
||||
|
* |
||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
|
* you may not use this file except in compliance with the License. |
||||
|
* You may obtain a copy of the License at |
||||
|
* |
||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
* |
||||
|
* Unless required by applicable law or agreed to in writing, software |
||||
|
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
|
* See the License for the specific language governing permissions and |
||||
|
* limitations under the License. |
||||
|
*/ |
||||
|
package org.thingsboard.server.service.cf.ctx.state.aggregation.function; |
||||
|
|
||||
|
import org.thingsboard.script.api.tbel.TbUtils; |
||||
|
import org.thingsboard.server.common.data.cf.configuration.aggregation.AggFunction; |
||||
|
|
||||
|
public class MaxAggEntry extends BaseAggEntry { |
||||
|
|
||||
|
private double max = Double.MIN_VALUE; |
||||
|
|
||||
|
@Override |
||||
|
protected void doUpdate(double value) { |
||||
|
if (value > max) { |
||||
|
max = value; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
protected Object prepareResult(Integer precision) { |
||||
|
return TbUtils.roundResult(max, precision); |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public AggFunction getType() { |
||||
|
return AggFunction.MAX; |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,41 @@ |
|||||
|
/** |
||||
|
* Copyright © 2016-2025 The Thingsboard Authors |
||||
|
* |
||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
|
* you may not use this file except in compliance with the License. |
||||
|
* You may obtain a copy of the License at |
||||
|
* |
||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
* |
||||
|
* Unless required by applicable law or agreed to in writing, software |
||||
|
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
|
* See the License for the specific language governing permissions and |
||||
|
* limitations under the License. |
||||
|
*/ |
||||
|
package org.thingsboard.server.service.cf.ctx.state.aggregation.function; |
||||
|
|
||||
|
import org.thingsboard.script.api.tbel.TbUtils; |
||||
|
import org.thingsboard.server.common.data.cf.configuration.aggregation.AggFunction; |
||||
|
|
||||
|
public class MinAggEntry extends BaseAggEntry { |
||||
|
|
||||
|
private double min = Double.MAX_VALUE; |
||||
|
|
||||
|
@Override |
||||
|
protected void doUpdate(double value) { |
||||
|
if (value < min) { |
||||
|
min = value; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
protected Object prepareResult(Integer precision) { |
||||
|
return TbUtils.roundResult(min, precision); |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public AggFunction getType() { |
||||
|
return AggFunction.MIN; |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,43 @@ |
|||||
|
/** |
||||
|
* Copyright © 2016-2025 The Thingsboard Authors |
||||
|
* |
||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
|
* you may not use this file except in compliance with the License. |
||||
|
* You may obtain a copy of the License at |
||||
|
* |
||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
* |
||||
|
* Unless required by applicable law or agreed to in writing, software |
||||
|
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
|
* See the License for the specific language governing permissions and |
||||
|
* limitations under the License. |
||||
|
*/ |
||||
|
package org.thingsboard.server.service.cf.ctx.state.aggregation.function; |
||||
|
|
||||
|
import org.thingsboard.script.api.tbel.TbUtils; |
||||
|
import org.thingsboard.server.common.data.cf.configuration.aggregation.AggFunction; |
||||
|
|
||||
|
import java.math.BigDecimal; |
||||
|
|
||||
|
public class SumAggEntry extends BaseAggEntry { |
||||
|
|
||||
|
private BigDecimal sum = BigDecimal.ZERO; |
||||
|
|
||||
|
@Override |
||||
|
protected void doUpdate(double value) { |
||||
|
if (value != 0.0) { |
||||
|
sum = sum.add(BigDecimal.valueOf(value)); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
protected Object prepareResult(Integer precision) { |
||||
|
return TbUtils.roundResult(sum.doubleValue(), precision); |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public AggFunction getType() { |
||||
|
return AggFunction.SUM; |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,24 @@ |
|||||
|
/** |
||||
|
* Copyright © 2016-2025 The Thingsboard Authors |
||||
|
* |
||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
|
* you may not use this file except in compliance with the License. |
||||
|
* You may obtain a copy of the License at |
||||
|
* |
||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
* |
||||
|
* Unless required by applicable law or agreed to in writing, software |
||||
|
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
|
* See the License for the specific language governing permissions and |
||||
|
* limitations under the License. |
||||
|
*/ |
||||
|
package org.thingsboard.server.service.security.auth; |
||||
|
|
||||
|
import org.thingsboard.server.service.security.model.SecurityUser; |
||||
|
|
||||
|
public class MfaConfigurationToken extends AbstractJwtAuthenticationToken { |
||||
|
public MfaConfigurationToken(SecurityUser securityUser) { |
||||
|
super(securityUser); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,28 @@ |
|||||
|
/** |
||||
|
* Copyright © 2016-2025 The Thingsboard Authors |
||||
|
* |
||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
|
* you may not use this file except in compliance with the License. |
||||
|
* You may obtain a copy of the License at |
||||
|
* |
||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
* |
||||
|
* Unless required by applicable law or agreed to in writing, software |
||||
|
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
|
* See the License for the specific language governing permissions and |
||||
|
* limitations under the License. |
||||
|
*/ |
||||
|
package org.thingsboard.server.service.security.permission; |
||||
|
|
||||
|
import org.springframework.stereotype.Component; |
||||
|
|
||||
|
@Component |
||||
|
public class MfaConfigurationPermissions extends AbstractPermissions { |
||||
|
|
||||
|
public MfaConfigurationPermissions() { |
||||
|
super(); |
||||
|
// for compatibility with PE
|
||||
|
} |
||||
|
|
||||
|
} |
||||
@ -0,0 +1,786 @@ |
|||||
|
/** |
||||
|
* 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.cf; |
||||
|
|
||||
|
import com.fasterxml.jackson.databind.node.ArrayNode; |
||||
|
import com.fasterxml.jackson.databind.node.ObjectNode; |
||||
|
import org.junit.After; |
||||
|
import org.junit.Before; |
||||
|
import org.junit.Test; |
||||
|
import org.springframework.test.annotation.DirtiesContext; |
||||
|
import org.thingsboard.server.common.data.AttributeScope; |
||||
|
import org.thingsboard.server.common.data.Device; |
||||
|
import org.thingsboard.server.common.data.DeviceProfile; |
||||
|
import org.thingsboard.server.common.data.Tenant; |
||||
|
import org.thingsboard.server.common.data.User; |
||||
|
import org.thingsboard.server.common.data.asset.Asset; |
||||
|
import org.thingsboard.server.common.data.asset.AssetProfile; |
||||
|
import org.thingsboard.server.common.data.cf.CalculatedField; |
||||
|
import org.thingsboard.server.common.data.cf.CalculatedFieldType; |
||||
|
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.Output; |
||||
|
import org.thingsboard.server.common.data.cf.configuration.OutputType; |
||||
|
import org.thingsboard.server.common.data.cf.configuration.ReferencedEntityKey; |
||||
|
import org.thingsboard.server.common.data.cf.configuration.aggregation.AggFunction; |
||||
|
import org.thingsboard.server.common.data.cf.configuration.aggregation.AggFunctionInput; |
||||
|
import org.thingsboard.server.common.data.cf.configuration.aggregation.AggKeyInput; |
||||
|
import org.thingsboard.server.common.data.cf.configuration.aggregation.AggMetric; |
||||
|
import org.thingsboard.server.common.data.cf.configuration.aggregation.RelatedEntitiesAggregationCalculatedFieldConfiguration; |
||||
|
import org.thingsboard.server.common.data.debug.DebugSettings; |
||||
|
import org.thingsboard.server.common.data.device.data.DefaultDeviceConfiguration; |
||||
|
import org.thingsboard.server.common.data.device.data.DefaultDeviceTransportConfiguration; |
||||
|
import org.thingsboard.server.common.data.device.data.DeviceData; |
||||
|
import org.thingsboard.server.common.data.id.AssetProfileId; |
||||
|
import org.thingsboard.server.common.data.id.DeviceProfileId; |
||||
|
import org.thingsboard.server.common.data.id.EntityId; |
||||
|
import org.thingsboard.server.common.data.relation.EntityRelation; |
||||
|
import org.thingsboard.server.common.data.relation.EntitySearchDirection; |
||||
|
import org.thingsboard.server.common.data.relation.RelationPathLevel; |
||||
|
import org.thingsboard.server.common.data.relation.RelationTypeGroup; |
||||
|
import org.thingsboard.server.common.data.security.Authority; |
||||
|
import org.thingsboard.server.controller.AbstractControllerTest; |
||||
|
import org.thingsboard.server.dao.service.DaoSqlTest; |
||||
|
|
||||
|
import java.util.HashMap; |
||||
|
import java.util.Map; |
||||
|
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; |
||||
|
import static org.thingsboard.server.cf.CalculatedFieldIntegrationTest.POLL_INTERVAL; |
||||
|
|
||||
|
@DaoSqlTest |
||||
|
@DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_EACH_TEST_METHOD) |
||||
|
public class RelatedEntitiesAggregationCalculatedFieldTest extends AbstractControllerTest { |
||||
|
|
||||
|
private Tenant savedTenant; |
||||
|
|
||||
|
private DeviceProfile deviceProfile; |
||||
|
private Device device1; |
||||
|
private String accessToken1 = "1234567890111"; |
||||
|
private Device device2; |
||||
|
private String accessToken2 = "1234567890222"; |
||||
|
|
||||
|
private AssetProfile assetProfile; |
||||
|
private Asset asset; |
||||
|
|
||||
|
private final long deduplicationInterval = 5; |
||||
|
|
||||
|
@Before |
||||
|
public void beforeEach() throws Exception { |
||||
|
loginSysAdmin(); |
||||
|
|
||||
|
updateDefaultTenantProfileConfig(tenantProfileConfig -> { |
||||
|
tenantProfileConfig.setMinAllowedDeduplicationIntervalInSecForCF(1); |
||||
|
}); |
||||
|
|
||||
|
Tenant tenant = new Tenant(); |
||||
|
tenant.setTitle("My tenant"); |
||||
|
savedTenant = saveTenant(tenant); |
||||
|
assertThat(savedTenant).isNotNull(); |
||||
|
|
||||
|
User tenantAdmin = new User(); |
||||
|
tenantAdmin.setAuthority(Authority.TENANT_ADMIN); |
||||
|
tenantAdmin.setTenantId(savedTenant.getId()); |
||||
|
tenantAdmin.setEmail("tenant@thingsboard.org"); |
||||
|
tenantAdmin.setFirstName("John"); |
||||
|
tenantAdmin.setLastName("Doe"); |
||||
|
|
||||
|
createUserAndLogin(tenantAdmin, "testPassword"); |
||||
|
|
||||
|
deviceProfile = doPost("/api/deviceProfile", createDeviceProfile("Device Profile"), DeviceProfile.class); |
||||
|
device1 = createDevice("Device 1", deviceProfile.getId(), accessToken1); |
||||
|
device2 = createDevice("Device 2", deviceProfile.getId(), accessToken2); |
||||
|
|
||||
|
postTelemetry(device1.getId(), "{\"occupied\":true}"); |
||||
|
postTelemetry(device2.getId(), "{\"occupied\":false}"); |
||||
|
|
||||
|
assetProfile = doPost("/api/assetProfile", createAssetProfile("Asset Profile"), AssetProfile.class); |
||||
|
asset = createAsset("Asset", assetProfile.getId()); |
||||
|
|
||||
|
createEntityRelation(asset.getId(), device1.getId(), "Contains"); |
||||
|
createEntityRelation(asset.getId(), device2.getId(), "Contains"); |
||||
|
} |
||||
|
|
||||
|
@After |
||||
|
public void afterTest() throws Exception { |
||||
|
loginSysAdmin(); |
||||
|
|
||||
|
deleteTenant(savedTenant.getId()); |
||||
|
} |
||||
|
|
||||
|
@Test |
||||
|
public void testCreateCfOnProfile_checkInitialAggregation() throws Exception { |
||||
|
Asset asset2 = createAsset("Asset 2", assetProfile.getId()); |
||||
|
Device device3 = createDevice("Device 3", "1234567890333"); |
||||
|
Device device4 = createDevice("Device 4", "1234567890444"); |
||||
|
|
||||
|
createEntityRelation(asset2.getId(), device3.getId(), "Contains"); |
||||
|
createEntityRelation(asset2.getId(), device4.getId(), "Contains"); |
||||
|
|
||||
|
createOccupancyCF(assetProfile.getId()); |
||||
|
|
||||
|
await().alias("create CF and perform initial aggregation").atMost(TIMEOUT, TimeUnit.SECONDS) |
||||
|
.pollInterval(POLL_INTERVAL, TimeUnit.SECONDS) |
||||
|
.untilAsserted(() -> { |
||||
|
verifyTelemetry(asset.getId(), Map.of( |
||||
|
"freeSpaces", "1", |
||||
|
"occupiedSpaces", "1", |
||||
|
"totalSpaces", "2" |
||||
|
)); |
||||
|
|
||||
|
verifyTelemetry(asset2.getId(), Map.of( |
||||
|
"freeSpaces", "2", |
||||
|
"occupiedSpaces", "0", |
||||
|
"totalSpaces", "2" |
||||
|
)); |
||||
|
}); |
||||
|
|
||||
|
postTelemetry(device3.getId(), "{\"occupied\":true}"); |
||||
|
|
||||
|
await().alias("update telemetry and perform aggregation") |
||||
|
.atLeast(deduplicationInterval / 2, TimeUnit.SECONDS) |
||||
|
.atMost(TIMEOUT, TimeUnit.SECONDS) |
||||
|
.pollInterval(POLL_INTERVAL, TimeUnit.SECONDS) |
||||
|
.untilAsserted(() -> { |
||||
|
verifyTelemetry(asset2.getId(), Map.of( |
||||
|
"freeSpaces", "1", |
||||
|
"occupiedSpaces", "1", |
||||
|
"totalSpaces", "2" |
||||
|
)); |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
@Test |
||||
|
public void testAddEntityToProfile_checkAggregation() throws Exception { |
||||
|
createOccupancyCF(assetProfile.getId()); |
||||
|
|
||||
|
Device device3 = createDevice("Device 3", "1234567890333"); |
||||
|
Device device4 = createDevice("Device 4", "1234567890444"); |
||||
|
postTelemetry(device3.getId(), "{\"occupied\":true}"); |
||||
|
postTelemetry(device4.getId(), "{\"occupied\":true}"); |
||||
|
|
||||
|
Asset asset2 = createAsset("Asset 2", assetProfile.getId()); |
||||
|
|
||||
|
await().alias("add entity to profile with no related entities and perform aggregation").atMost(deduplicationInterval, TimeUnit.SECONDS) |
||||
|
.pollInterval(POLL_INTERVAL, TimeUnit.SECONDS) |
||||
|
.untilAsserted(() -> { |
||||
|
ObjectNode occupancy = getLatestTelemetry(asset2.getId(), "freeSpaces", "occupiedSpaces", "totalSpaces"); |
||||
|
assertThat(occupancy).isNotNull(); |
||||
|
assertThat(occupancy.get("freeSpaces").get(0).get("value").isNull()).isTrue(); |
||||
|
assertThat(occupancy.get("occupiedSpaces").get(0).get("value").isNull()).isTrue(); |
||||
|
assertThat(occupancy.get("totalSpaces").get(0).get("value").isNull()).isTrue(); |
||||
|
}); |
||||
|
|
||||
|
createEntityRelation(asset2.getId(), device3.getId(), "Contains"); |
||||
|
createEntityRelation(asset2.getId(), device4.getId(), "Contains"); |
||||
|
|
||||
|
await().alias("create relations and perform aggregation").atMost(deduplicationInterval, TimeUnit.SECONDS) |
||||
|
.pollInterval(POLL_INTERVAL, TimeUnit.SECONDS) |
||||
|
.untilAsserted(() -> { |
||||
|
verifyTelemetry(asset2.getId(), Map.of( |
||||
|
"freeSpaces", "0", |
||||
|
"occupiedSpaces", "2", |
||||
|
"totalSpaces", "2" |
||||
|
)); |
||||
|
}); |
||||
|
|
||||
|
postTelemetry(device3.getId(), "{\"occupied\":false}"); |
||||
|
|
||||
|
await().alias("update telemetry and perform aggregation").atMost(deduplicationInterval, TimeUnit.SECONDS) |
||||
|
.pollInterval(POLL_INTERVAL, TimeUnit.SECONDS) |
||||
|
.untilAsserted(() -> { |
||||
|
verifyTelemetry(asset2.getId(), Map.of( |
||||
|
"freeSpaces", "1", |
||||
|
"occupiedSpaces", "1", |
||||
|
"totalSpaces", "2" |
||||
|
)); |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
@Test |
||||
|
public void testChangeEntityProfile_checkAggregation() throws Exception { |
||||
|
Asset asset2 = createAsset("Asset 2", assetProfile.getId()); |
||||
|
Device device3 = createDevice("Device 3", "1234567890333"); |
||||
|
Device device4 = createDevice("Device 4", "1234567890444"); |
||||
|
|
||||
|
createEntityRelation(asset2.getId(), device3.getId(), "Contains"); |
||||
|
createEntityRelation(asset2.getId(), device4.getId(), "Contains"); |
||||
|
|
||||
|
createOccupancyCF(assetProfile.getId()); |
||||
|
|
||||
|
await().alias("create CF and perform initial aggregation").atMost(deduplicationInterval, TimeUnit.SECONDS) |
||||
|
.pollInterval(POLL_INTERVAL, TimeUnit.SECONDS) |
||||
|
.untilAsserted(() -> { |
||||
|
verifyTelemetry(asset.getId(), Map.of( |
||||
|
"freeSpaces", "1", |
||||
|
"occupiedSpaces", "1", |
||||
|
"totalSpaces", "2" |
||||
|
)); |
||||
|
|
||||
|
verifyTelemetry(asset2.getId(), Map.of( |
||||
|
"freeSpaces", "2", |
||||
|
"occupiedSpaces", "0", |
||||
|
"totalSpaces", "2" |
||||
|
)); |
||||
|
}); |
||||
|
|
||||
|
AssetProfile newAssetProfile = createAssetProfile("New Asset Profile"); |
||||
|
asset2.setAssetProfileId(newAssetProfile.getId()); |
||||
|
doPost("/api/asset", asset2, Asset.class); |
||||
|
|
||||
|
postTelemetry(device3.getId(), "{\"occupied\":true}"); |
||||
|
|
||||
|
await().alias("change profile and no aggregation").atMost(deduplicationInterval, TimeUnit.SECONDS) |
||||
|
.pollInterval(POLL_INTERVAL, TimeUnit.SECONDS) |
||||
|
.untilAsserted(() -> { |
||||
|
verifyTelemetry(asset2.getId(), Map.of( |
||||
|
"freeSpaces", "2", |
||||
|
"occupiedSpaces", "0", |
||||
|
"totalSpaces", "2" |
||||
|
)); |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
@Test |
||||
|
public void testCreateCfOnAssetAndNoTelemetryOnDevices_checkDefaultValueUsed() throws Exception { |
||||
|
Asset asset2 = createAsset("Asset 2", assetProfile.getId()); |
||||
|
Device device3 = createDevice("Device 3", "1234567890333"); |
||||
|
Device device4 = createDevice("Device 4", "1234567890444"); |
||||
|
|
||||
|
createEntityRelation(asset2.getId(), device3.getId(), "Contains"); |
||||
|
createEntityRelation(asset2.getId(), device4.getId(), "Contains"); |
||||
|
|
||||
|
createOccupancyCF(asset2.getId()); |
||||
|
|
||||
|
await().alias("create CF and perform aggregation with default values").atMost(deduplicationInterval, TimeUnit.SECONDS) |
||||
|
.pollInterval(POLL_INTERVAL, TimeUnit.SECONDS) |
||||
|
.untilAsserted(() -> { |
||||
|
verifyTelemetry(asset2.getId(), Map.of( |
||||
|
"freeSpaces", "2", |
||||
|
"occupiedSpaces", "0", |
||||
|
"totalSpaces", "2" |
||||
|
)); |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
@Test |
||||
|
public void testCreateCfAndUpdateTelemetry_checkAggregation() throws Exception { |
||||
|
createOccupancyCF(asset.getId()); |
||||
|
checkInitialCalculation(); |
||||
|
|
||||
|
postTelemetry(device1.getId(), "{\"occupied\":false}"); |
||||
|
|
||||
|
await().alias("update telemetry and perform aggregation") |
||||
|
.atLeast(deduplicationInterval / 2, TimeUnit.SECONDS) |
||||
|
.atMost(TIMEOUT, TimeUnit.SECONDS) |
||||
|
.pollInterval(POLL_INTERVAL, TimeUnit.SECONDS) |
||||
|
.untilAsserted(() -> { |
||||
|
verifyTelemetry(asset.getId(), Map.of( |
||||
|
"freeSpaces", "2", |
||||
|
"occupiedSpaces", "0", |
||||
|
"totalSpaces", "2" |
||||
|
)); |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
@Test |
||||
|
public void testDeleteCf_checkNoAggregation() throws Exception { |
||||
|
CalculatedField cf = createOccupancyCF(asset.getId()); |
||||
|
checkInitialCalculation(); |
||||
|
|
||||
|
doDelete("/api/calculatedField/" + cf.getId().getId().toString()) |
||||
|
.andExpect(status().isOk()); |
||||
|
|
||||
|
postTelemetry(device1.getId(), "{\"occupied\":false}"); |
||||
|
|
||||
|
await().alias("delete cf and update telemetry and no aggregation").atMost(deduplicationInterval, TimeUnit.SECONDS) |
||||
|
.pollInterval(POLL_INTERVAL, TimeUnit.SECONDS) |
||||
|
.untilAsserted(() -> { |
||||
|
verifyTelemetry(asset.getId(), Map.of( |
||||
|
"freeSpaces", "1", |
||||
|
"occupiedSpaces", "1", |
||||
|
"totalSpaces", "2" |
||||
|
)); |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
@Test |
||||
|
public void testUpdateTelemetry_checkAggregationNotExecutedUntilDeduplicationInterval() throws Exception { |
||||
|
createOccupancyCF(asset.getId()); |
||||
|
checkInitialCalculation(); |
||||
|
|
||||
|
postTelemetry(device1.getId(), "{\"occupied\":false}"); |
||||
|
|
||||
|
await().alias("update telemetry -> no changes").atMost(TIMEOUT, TimeUnit.SECONDS) |
||||
|
.pollInterval(POLL_INTERVAL, TimeUnit.SECONDS) |
||||
|
.untilAsserted(this::checkInitialCalculationValues); |
||||
|
|
||||
|
postTelemetry(device2.getId(), "{\"occupied\":false}"); |
||||
|
|
||||
|
await().alias("create CF and perform initial calculation") |
||||
|
.atLeast(deduplicationInterval / 2, TimeUnit.SECONDS) |
||||
|
.atMost(TIMEOUT, TimeUnit.SECONDS) |
||||
|
.untilAsserted(() -> { |
||||
|
verifyTelemetry(asset.getId(), Map.of( |
||||
|
"freeSpaces", "2", |
||||
|
"occupiedSpaces", "0", |
||||
|
"totalSpaces", "2" |
||||
|
)); |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
@Test |
||||
|
public void testDeleteTelemetry_checkAggregationWithPreviousValuesOrDefault() throws Exception { |
||||
|
Asset asset2 = createAsset("Asset 2", assetProfile.getId()); |
||||
|
Device device3 = createDevice("Device 3", "1234567890333"); |
||||
|
Device device4 = createDevice("Device 4", "1234567890444"); |
||||
|
|
||||
|
createEntityRelation(asset2.getId(), device3.getId(), "Contains"); |
||||
|
createEntityRelation(asset2.getId(), device4.getId(), "Contains"); |
||||
|
|
||||
|
long currentTime = System.currentTimeMillis(); |
||||
|
long firstTs = currentTime - 10; |
||||
|
long secondTs = currentTime - 10; |
||||
|
long thirdTs = currentTime - 5; |
||||
|
postTelemetry(device3.getId(), "{\"ts\": " + firstTs + ", \"values\": {\"occupied\":true}}"); |
||||
|
postTelemetry(device4.getId(), "{\"ts\": " + secondTs + ", \"values\": {\"occupied\":true}}"); |
||||
|
postTelemetry(device3.getId(), "{\"ts\": " + thirdTs + ", \"values\": {\"occupied\":true}}"); |
||||
|
|
||||
|
createOccupancyCF(asset2.getId()); |
||||
|
|
||||
|
await().alias("create CF and perform aggregation").atMost(deduplicationInterval, TimeUnit.SECONDS) |
||||
|
.pollInterval(POLL_INTERVAL, TimeUnit.SECONDS) |
||||
|
.untilAsserted(() -> { |
||||
|
verifyTelemetry(asset2.getId(), Map.of( |
||||
|
"freeSpaces", "0", |
||||
|
"occupiedSpaces", "2", |
||||
|
"totalSpaces", "2" |
||||
|
)); |
||||
|
}); |
||||
|
|
||||
|
doDelete("/api/plugins/telemetry/DEVICE/" + device3.getId() + "/timeseries/delete?keys=occupied&deleteAllDataForKeys=false&rewriteLatestIfDeleted=true&deleteLatest=true&startTs=" + thirdTs + "&endTs=" + thirdTs + 1, String.class); |
||||
|
doDelete("/api/plugins/telemetry/DEVICE/" + device4.getId() + "/timeseries/delete?keys=occupied&deleteAllDataForKeys=false&rewriteLatestIfDeleted=true&deleteLatest=true&startTs=" + secondTs + "&endTs=" + secondTs + 1, String.class); |
||||
|
|
||||
|
await().alias("delete latest telemetry and perform aggregation with previous or default values").atMost(deduplicationInterval * 2, TimeUnit.SECONDS) |
||||
|
.pollInterval(POLL_INTERVAL, TimeUnit.SECONDS) |
||||
|
.untilAsserted(() -> { |
||||
|
verifyTelemetry(asset2.getId(), Map.of( |
||||
|
"freeSpaces", "1", |
||||
|
"occupiedSpaces", "1", |
||||
|
"totalSpaces", "2" |
||||
|
)); |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
@Test |
||||
|
public void testDeleteAttr_checkAggregationWithDefault() throws Exception { |
||||
|
Asset asset2 = createAsset("Asset 2", assetProfile.getId()); |
||||
|
Device device3 = createDevice("Device 3", "1234567890333"); |
||||
|
Device device4 = createDevice("Device 4", "1234567890444"); |
||||
|
|
||||
|
createEntityRelation(asset2.getId(), device3.getId(), "Contains"); |
||||
|
createEntityRelation(asset2.getId(), device4.getId(), "Contains"); |
||||
|
|
||||
|
postAttributes(device3.getId(), AttributeScope.SERVER_SCOPE, "{\"occupied\":true}"); |
||||
|
postAttributes(device4.getId(), AttributeScope.SERVER_SCOPE, "{\"occupied\":true}"); |
||||
|
|
||||
|
createOccupancyCFWithAttr(asset2.getId()); |
||||
|
|
||||
|
await().alias("create CF and perform aggregation").atMost(deduplicationInterval, TimeUnit.SECONDS) |
||||
|
.pollInterval(POLL_INTERVAL, TimeUnit.SECONDS) |
||||
|
.untilAsserted(() -> { |
||||
|
verifyTelemetry(asset2.getId(), Map.of( |
||||
|
"freeSpaces", "0", |
||||
|
"occupiedSpaces", "2", |
||||
|
"totalSpaces", "2" |
||||
|
)); |
||||
|
}); |
||||
|
|
||||
|
doDelete("/api/plugins/telemetry/DEVICE/" + device3.getUuidId() + "/SERVER_SCOPE?keys=occupied", String.class); |
||||
|
doDelete("/api/plugins/telemetry/DEVICE/" + device4.getUuidId() + "/SERVER_SCOPE?keys=occupied", String.class); |
||||
|
|
||||
|
await().alias("delete attribute and perform aggregation with default values").atMost(deduplicationInterval * 2, TimeUnit.SECONDS) |
||||
|
.pollInterval(POLL_INTERVAL, TimeUnit.SECONDS) |
||||
|
.untilAsserted(() -> { |
||||
|
verifyTelemetry(asset2.getId(), Map.of( |
||||
|
"freeSpaces", "2", |
||||
|
"occupiedSpaces", "0", |
||||
|
"totalSpaces", "2" |
||||
|
)); |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
@Test |
||||
|
public void testCreateRelation_checkAggregation() throws Exception { |
||||
|
createOccupancyCF(asset.getId()); |
||||
|
checkInitialCalculation(); |
||||
|
|
||||
|
Device device3 = createDevice("Device 3", deviceProfile.getId(), "1234567890333"); |
||||
|
|
||||
|
postTelemetry(device3.getId(), "{\"occupied\":true}"); |
||||
|
|
||||
|
createEntityRelation(asset.getId(), device3.getId(), "Contains"); |
||||
|
|
||||
|
await().alias("create relation and perform aggregation").atMost(deduplicationInterval, TimeUnit.SECONDS) |
||||
|
.pollInterval(POLL_INTERVAL, TimeUnit.SECONDS) |
||||
|
.untilAsserted(() -> { |
||||
|
verifyTelemetry(asset.getId(), Map.of( |
||||
|
"freeSpaces", "1", |
||||
|
"occupiedSpaces", "2", |
||||
|
"totalSpaces", "3" |
||||
|
)); |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
@Test |
||||
|
public void testDeleteRelation_checkAggregation() throws Exception { |
||||
|
createOccupancyCF(asset.getId()); |
||||
|
checkInitialCalculation(); |
||||
|
|
||||
|
deleteEntityRelation(new EntityRelation(asset.getId(), device1.getId(), "Contains", RelationTypeGroup.COMMON)); |
||||
|
|
||||
|
await().alias("create relation and perform aggregation").atMost(deduplicationInterval, TimeUnit.SECONDS) |
||||
|
.pollInterval(POLL_INTERVAL, TimeUnit.SECONDS) |
||||
|
.untilAsserted(() -> { |
||||
|
verifyTelemetry(asset.getId(), Map.of( |
||||
|
"freeSpaces", "1", |
||||
|
"occupiedSpaces", "0", |
||||
|
"totalSpaces", "1" |
||||
|
)); |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
@Test |
||||
|
public void testUpdateRelationPath_checkAggregation() throws Exception { |
||||
|
CalculatedField cf = createOccupancyCF(asset.getId()); |
||||
|
checkInitialCalculation(); |
||||
|
|
||||
|
Device device3 = createDevice("Device 3", "1234567890333"); |
||||
|
createEntityRelation(asset.getId(), device3.getId(), "Has"); |
||||
|
postTelemetry(device3.getId(), "{\"occupied\":true}"); |
||||
|
|
||||
|
var configuration = (RelatedEntitiesAggregationCalculatedFieldConfiguration) cf.getConfiguration(); |
||||
|
configuration.setRelation(new RelationPathLevel(EntitySearchDirection.FROM, "Has")); |
||||
|
saveCalculatedField(cf); |
||||
|
|
||||
|
await().alias("update relation path and perform aggregation").atMost(deduplicationInterval, TimeUnit.SECONDS) |
||||
|
.pollInterval(POLL_INTERVAL, TimeUnit.SECONDS) |
||||
|
.untilAsserted(() -> { |
||||
|
verifyTelemetry(asset.getId(), Map.of( |
||||
|
"freeSpaces", "0", |
||||
|
"occupiedSpaces", "1", |
||||
|
"totalSpaces", "1" |
||||
|
)); |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
@Test |
||||
|
public void testUpdateArguments_checkAggregation() throws Exception { |
||||
|
CalculatedField cf = createOccupancyCF(asset.getId()); |
||||
|
checkInitialCalculation(); |
||||
|
|
||||
|
postTelemetry(device1.getId(), "{\"occupiedStatus\":false}"); |
||||
|
postTelemetry(device2.getId(), "{\"occupiedStatus\":false}"); |
||||
|
|
||||
|
var configuration = (RelatedEntitiesAggregationCalculatedFieldConfiguration) cf.getConfiguration(); |
||||
|
Argument argument = new Argument(); |
||||
|
argument.setRefEntityKey(new ReferencedEntityKey("oc", ArgumentType.TS_LATEST, null)); |
||||
|
argument.setDefaultValue("false"); |
||||
|
configuration.setArguments(Map.of("oc", argument)); |
||||
|
saveCalculatedField(cf); |
||||
|
|
||||
|
await().alias("update arguments and perform aggregation").atMost(deduplicationInterval, TimeUnit.SECONDS) |
||||
|
.pollInterval(POLL_INTERVAL, TimeUnit.SECONDS) |
||||
|
.untilAsserted(() -> { |
||||
|
verifyTelemetry(asset.getId(), Map.of( |
||||
|
"freeSpaces", "2", |
||||
|
"occupiedSpaces", "0", |
||||
|
"totalSpaces", "2" |
||||
|
)); |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
@Test |
||||
|
public void testUpdateMetrics_checkAggregation() throws Exception { |
||||
|
postTelemetry(device1.getId(), "{\"temperature\":24.2}"); |
||||
|
postTelemetry(device2.getId(), "{\"temperature\":19.6}"); |
||||
|
CalculatedField cf = createAvgTemperatureCF(asset.getId()); |
||||
|
|
||||
|
await().alias("create avg temp cf and perform initial aggregation").atMost(TIMEOUT, TimeUnit.SECONDS) |
||||
|
.pollInterval(POLL_INTERVAL, TimeUnit.SECONDS) |
||||
|
.untilAsserted(() -> { |
||||
|
verifyTelemetry(asset.getId(), Map.of("avgTemperature", "24")); |
||||
|
}); |
||||
|
|
||||
|
var configuration = (RelatedEntitiesAggregationCalculatedFieldConfiguration) cf.getConfiguration(); |
||||
|
AggMetric aggMetric = new AggMetric(); |
||||
|
aggMetric.setInput(new AggKeyInput("temp")); |
||||
|
aggMetric.setFilter("return temp < 100;"); |
||||
|
aggMetric.setFunction(AggFunction.MAX); |
||||
|
configuration.setMetrics(Map.of("maxTemperature", aggMetric)); |
||||
|
saveCalculatedField(cf); |
||||
|
|
||||
|
await().alias("update metrics and perform aggregation").atMost(TIMEOUT, TimeUnit.SECONDS) |
||||
|
.pollInterval(POLL_INTERVAL, TimeUnit.SECONDS) |
||||
|
.untilAsserted(() -> { |
||||
|
verifyTelemetry(asset.getId(), Map.of("maxTemperature", "24")); |
||||
|
}); |
||||
|
|
||||
|
postTelemetry(device1.getId(), "{\"temperature\":101.3}"); |
||||
|
postTelemetry(device2.getId(), "{\"temperature\":25.8}"); |
||||
|
|
||||
|
await().alias("update telemetry and perform aggregation") |
||||
|
.atLeast(deduplicationInterval / 2, TimeUnit.SECONDS) |
||||
|
.atMost(TIMEOUT, TimeUnit.SECONDS) |
||||
|
.pollInterval(POLL_INTERVAL, TimeUnit.SECONDS) |
||||
|
.untilAsserted(() -> { |
||||
|
verifyTelemetry(asset.getId(), Map.of("maxTemperature", "26")); |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
@Test |
||||
|
public void testUpdateOutput_checkAggregation() throws Exception { |
||||
|
postTelemetry(device1.getId(), "{\"temperature\":24.2}"); |
||||
|
postTelemetry(device2.getId(), "{\"temperature\":19.6}"); |
||||
|
CalculatedField cf = createAvgTemperatureCF(asset.getId()); |
||||
|
|
||||
|
await().alias("create avg temp cf and perform initial aggregation").atMost(deduplicationInterval, TimeUnit.SECONDS) |
||||
|
.pollInterval(POLL_INTERVAL, TimeUnit.SECONDS) |
||||
|
.untilAsserted(() -> { |
||||
|
verifyTelemetry(asset.getId(), Map.of("avgTemperature", "24")); |
||||
|
}); |
||||
|
|
||||
|
var configuration = (RelatedEntitiesAggregationCalculatedFieldConfiguration) cf.getConfiguration(); |
||||
|
Output output = new Output(); |
||||
|
output.setType(OutputType.ATTRIBUTES); |
||||
|
output.setScope(AttributeScope.SERVER_SCOPE); |
||||
|
configuration.setOutput(output); |
||||
|
saveCalculatedField(cf); |
||||
|
|
||||
|
await().alias("update output and perform aggregation").atMost(deduplicationInterval, TimeUnit.SECONDS) |
||||
|
.pollInterval(POLL_INTERVAL, TimeUnit.SECONDS) |
||||
|
.untilAsserted(() -> { |
||||
|
ArrayNode avgTemperature = getServerAttributes(asset.getId(), "avgTemperature"); |
||||
|
assertThat(avgTemperature).isNotNull(); |
||||
|
assertThat(avgTemperature.get(0)).isNotNull(); |
||||
|
assertThat(avgTemperature.get(0).get("value").asText()).isEqualTo("24.2"); |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
@Test |
||||
|
public void testUpdateDeduplicationInterval_checkAggregationNotExecutedUntilDeduplicationInterval() throws Exception { |
||||
|
postTelemetry(device1.getId(), "{\"temperature\":24.2}"); |
||||
|
postTelemetry(device2.getId(), "{\"temperature\":19.6}"); |
||||
|
CalculatedField cf = createAvgTemperatureCF(asset.getId()); |
||||
|
|
||||
|
await().alias("create avg temp cf and perform initial aggregation").atMost(deduplicationInterval, TimeUnit.SECONDS) |
||||
|
.pollInterval(POLL_INTERVAL, TimeUnit.SECONDS) |
||||
|
.untilAsserted(() -> { |
||||
|
verifyTelemetry(asset.getId(), Map.of("avgTemperature", "24")); |
||||
|
}); |
||||
|
|
||||
|
var configuration = (RelatedEntitiesAggregationCalculatedFieldConfiguration) cf.getConfiguration(); |
||||
|
configuration.setDeduplicationIntervalInSec(2 * deduplicationInterval); |
||||
|
saveCalculatedField(cf); |
||||
|
|
||||
|
await().alias("update deduplication interval and perform aggregation").atMost(deduplicationInterval / 2, TimeUnit.SECONDS) |
||||
|
.pollInterval(POLL_INTERVAL, TimeUnit.SECONDS) |
||||
|
.untilAsserted(() -> { |
||||
|
verifyTelemetry(asset.getId(), Map.of("avgTemperature", "24")); |
||||
|
}); |
||||
|
|
||||
|
postTelemetry(device2.getId(), "{\"temperature\":32.1}"); |
||||
|
|
||||
|
await().alias("update telemetry and perform aggregation").atMost(2 * deduplicationInterval, TimeUnit.SECONDS) |
||||
|
.pollInterval(POLL_INTERVAL, TimeUnit.SECONDS) |
||||
|
.untilAsserted(() -> { |
||||
|
verifyTelemetry(asset.getId(), Map.of("avgTemperature", "28")); |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
private void checkInitialCalculation() { |
||||
|
await().alias("create CF and perform initial aggregation").atMost(deduplicationInterval, TimeUnit.SECONDS) |
||||
|
.pollInterval(POLL_INTERVAL, TimeUnit.SECONDS) |
||||
|
.untilAsserted(this::checkInitialCalculationValues); |
||||
|
} |
||||
|
|
||||
|
private void checkInitialCalculationValues() throws Exception { |
||||
|
ObjectNode occupancy = getLatestTelemetry(asset.getId(), "freeSpaces", "occupiedSpaces", "totalSpaces"); |
||||
|
assertThat(occupancy).isNotNull(); |
||||
|
assertThat(occupancy.get("freeSpaces").get(0).get("value").asText()).isEqualTo("1"); |
||||
|
assertThat(occupancy.get("occupiedSpaces").get(0).get("value").asText()).isEqualTo("1"); |
||||
|
assertThat(occupancy.get("totalSpaces").get(0).get("value").asText()).isEqualTo("2"); |
||||
|
} |
||||
|
|
||||
|
private CalculatedField createAvgTemperatureCF(EntityId entityId) { |
||||
|
Map<String, Argument> arguments = new HashMap<>(); |
||||
|
Argument argument = new Argument(); |
||||
|
argument.setRefEntityKey(new ReferencedEntityKey("temperature", ArgumentType.TS_LATEST, null)); |
||||
|
argument.setDefaultValue("20"); |
||||
|
arguments.put("temp", argument); |
||||
|
|
||||
|
Map<String, AggMetric> aggMetrics = new HashMap<>(); |
||||
|
|
||||
|
AggMetric avgMetric = new AggMetric(); |
||||
|
avgMetric.setFunction(AggFunction.AVG); |
||||
|
avgMetric.setFilter("return temp >= 20;"); |
||||
|
avgMetric.setInput(new AggKeyInput("temp")); |
||||
|
aggMetrics.put("avgTemperature", avgMetric); |
||||
|
|
||||
|
Output output = new Output(); |
||||
|
output.setType(OutputType.TIME_SERIES); |
||||
|
output.setDecimalsByDefault(0); |
||||
|
|
||||
|
return createAggCf("Average temperature", entityId, |
||||
|
new RelationPathLevel(EntitySearchDirection.FROM, "Contains"), |
||||
|
arguments, |
||||
|
aggMetrics, |
||||
|
output); |
||||
|
} |
||||
|
|
||||
|
private CalculatedField createOccupancyCF(EntityId entityId) { |
||||
|
Map<String, Argument> arguments = new HashMap<>(); |
||||
|
Argument argument = new Argument(); |
||||
|
argument.setRefEntityKey(new ReferencedEntityKey("occupied", ArgumentType.TS_LATEST, null)); |
||||
|
argument.setDefaultValue("false"); |
||||
|
arguments.put("oc", argument); |
||||
|
|
||||
|
Map<String, AggMetric> aggMetrics = new HashMap<>(); |
||||
|
|
||||
|
AggMetric freeSpaces = new AggMetric(); |
||||
|
freeSpaces.setFunction(AggFunction.COUNT); |
||||
|
freeSpaces.setFilter("return oc == false;"); |
||||
|
freeSpaces.setInput(new AggKeyInput("oc")); |
||||
|
aggMetrics.put("freeSpaces", freeSpaces); |
||||
|
|
||||
|
AggMetric occupiedSpaces = new AggMetric(); |
||||
|
occupiedSpaces.setFunction(AggFunction.COUNT); |
||||
|
occupiedSpaces.setFilter("return oc == true;"); |
||||
|
occupiedSpaces.setInput(new AggKeyInput("oc")); |
||||
|
aggMetrics.put("occupiedSpaces", occupiedSpaces); |
||||
|
|
||||
|
AggMetric totalSpaces = new AggMetric(); |
||||
|
totalSpaces.setFunction(AggFunction.COUNT); |
||||
|
totalSpaces.setInput(new AggFunctionInput("return 1;")); |
||||
|
aggMetrics.put("totalSpaces", totalSpaces); |
||||
|
|
||||
|
Output output = new Output(); |
||||
|
output.setType(OutputType.TIME_SERIES); |
||||
|
output.setDecimalsByDefault(0); |
||||
|
|
||||
|
return createAggCf("Occupied spaces", entityId, |
||||
|
new RelationPathLevel(EntitySearchDirection.FROM, "Contains"), |
||||
|
arguments, |
||||
|
aggMetrics, |
||||
|
output); |
||||
|
} |
||||
|
|
||||
|
private CalculatedField createOccupancyCFWithAttr(EntityId entityId) { |
||||
|
Map<String, Argument> arguments = new HashMap<>(); |
||||
|
Argument argument = new Argument(); |
||||
|
argument.setRefEntityKey(new ReferencedEntityKey("occupied", ArgumentType.ATTRIBUTE, AttributeScope.SERVER_SCOPE)); |
||||
|
argument.setDefaultValue("false"); |
||||
|
arguments.put("oc", argument); |
||||
|
|
||||
|
Map<String, AggMetric> aggMetrics = new HashMap<>(); |
||||
|
|
||||
|
AggMetric freeSpaces = new AggMetric(); |
||||
|
freeSpaces.setFunction(AggFunction.COUNT); |
||||
|
freeSpaces.setFilter("return oc == false;"); |
||||
|
freeSpaces.setInput(new AggKeyInput("oc")); |
||||
|
aggMetrics.put("freeSpaces", freeSpaces); |
||||
|
|
||||
|
AggMetric occupiedSpaces = new AggMetric(); |
||||
|
occupiedSpaces.setFunction(AggFunction.COUNT); |
||||
|
occupiedSpaces.setFilter("return oc == true;"); |
||||
|
occupiedSpaces.setInput(new AggKeyInput("oc")); |
||||
|
aggMetrics.put("occupiedSpaces", occupiedSpaces); |
||||
|
|
||||
|
AggMetric totalSpaces = new AggMetric(); |
||||
|
totalSpaces.setFunction(AggFunction.COUNT); |
||||
|
totalSpaces.setInput(new AggFunctionInput("return 1;")); |
||||
|
aggMetrics.put("totalSpaces", totalSpaces); |
||||
|
|
||||
|
Output output = new Output(); |
||||
|
output.setType(OutputType.TIME_SERIES); |
||||
|
output.setDecimalsByDefault(0); |
||||
|
|
||||
|
return createAggCf("Occupied spaces", entityId, |
||||
|
new RelationPathLevel(EntitySearchDirection.FROM, "Contains"), |
||||
|
arguments, |
||||
|
aggMetrics, |
||||
|
output); |
||||
|
} |
||||
|
|
||||
|
private CalculatedField createAggCf(String name, |
||||
|
EntityId entityId, |
||||
|
RelationPathLevel relation, |
||||
|
Map<String, Argument> inputs, |
||||
|
Map<String, AggMetric> metrics, |
||||
|
Output output) { |
||||
|
CalculatedField calculatedField = new CalculatedField(); |
||||
|
calculatedField.setName(name); |
||||
|
calculatedField.setEntityId(entityId); |
||||
|
calculatedField.setType(CalculatedFieldType.RELATED_ENTITIES_AGGREGATION); |
||||
|
|
||||
|
RelatedEntitiesAggregationCalculatedFieldConfiguration configuration = new RelatedEntitiesAggregationCalculatedFieldConfiguration(); |
||||
|
configuration.setRelation(relation); |
||||
|
configuration.setArguments(inputs); |
||||
|
configuration.setDeduplicationIntervalInSec(deduplicationInterval); |
||||
|
configuration.setMetrics(metrics); |
||||
|
configuration.setOutput(output); |
||||
|
|
||||
|
calculatedField.setConfiguration(configuration); |
||||
|
calculatedField.setDebugSettings(DebugSettings.all()); |
||||
|
return saveCalculatedField(calculatedField); |
||||
|
} |
||||
|
|
||||
|
private Device createDevice(String name, DeviceProfileId deviceProfileId, String accessToken) { |
||||
|
Device device = new Device(); |
||||
|
device.setName(name); |
||||
|
device.setDeviceProfileId(deviceProfileId); |
||||
|
DeviceData deviceData = new DeviceData(); |
||||
|
deviceData.setTransportConfiguration(new DefaultDeviceTransportConfiguration()); |
||||
|
deviceData.setConfiguration(new DefaultDeviceConfiguration()); |
||||
|
device.setDeviceData(deviceData); |
||||
|
return doPost("/api/device?accessToken=" + accessToken, device, Device.class); |
||||
|
} |
||||
|
|
||||
|
private Asset createAsset(String name, AssetProfileId assetProfileId) { |
||||
|
Asset asset = new Asset(); |
||||
|
asset.setName(name); |
||||
|
asset.setAssetProfileId(assetProfileId); |
||||
|
return doPost("/api/asset", asset, Asset.class); |
||||
|
} |
||||
|
|
||||
|
private void verifyTelemetry(EntityId entityId, Map<String, String> expectedResults) throws Exception { |
||||
|
ObjectNode result = getLatestTelemetry(entityId, expectedResults.keySet().toArray(new String[0])); |
||||
|
assertThat(result).isNotNull(); |
||||
|
expectedResults.forEach((key, value) -> assertThat(result.get(key).get(0).get("value").asText()).isEqualTo(value)); |
||||
|
} |
||||
|
|
||||
|
private ObjectNode getLatestTelemetry(EntityId entityId, String... keys) throws Exception { |
||||
|
return doGetAsync("/api/plugins/telemetry/" + entityId.getEntityType() + "/" + entityId.getId() + "/values/timeseries?keys=" + String.join(",", keys), ObjectNode.class); |
||||
|
} |
||||
|
|
||||
|
private ArrayNode getServerAttributes(EntityId entityId, String... keys) throws Exception { |
||||
|
return doGetAsync("/api/plugins/telemetry/" + entityId.getEntityType() + "/" + entityId.getId() + "/values/attributes/SERVER_SCOPE?keys=" + String.join(",", keys), ArrayNode.class); |
||||
|
} |
||||
|
|
||||
|
} |
||||
@ -0,0 +1,100 @@ |
|||||
|
/** |
||||
|
* Copyright © 2016-2025 The Thingsboard Authors |
||||
|
* |
||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
|
* you may not use this file except in compliance with the License. |
||||
|
* You may obtain a copy of the License at |
||||
|
* |
||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
* |
||||
|
* Unless required by applicable law or agreed to in writing, software |
||||
|
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
|
* See the License for the specific language governing permissions and |
||||
|
* limitations under the License. |
||||
|
*/ |
||||
|
package org.thingsboard.server.service.cf.ctx.state; |
||||
|
|
||||
|
import org.junit.jupiter.api.BeforeEach; |
||||
|
import org.junit.jupiter.api.Test; |
||||
|
import org.thingsboard.server.common.data.id.DeviceId; |
||||
|
import org.thingsboard.server.common.data.id.EntityId; |
||||
|
import org.thingsboard.server.common.data.kv.BasicTsKvEntry; |
||||
|
import org.thingsboard.server.common.data.kv.LongDataEntry; |
||||
|
import org.thingsboard.server.service.cf.ctx.state.aggregation.RelatedEntitiesArgumentEntry; |
||||
|
|
||||
|
import java.util.HashMap; |
||||
|
import java.util.Map; |
||||
|
import java.util.UUID; |
||||
|
|
||||
|
import static org.assertj.core.api.Assertions.assertThat; |
||||
|
import static org.assertj.core.api.Assertions.assertThatThrownBy; |
||||
|
|
||||
|
public class RelatedEntitiesArgumentEntryTest { |
||||
|
|
||||
|
private RelatedEntitiesArgumentEntry entry; |
||||
|
|
||||
|
private final DeviceId device1 = new DeviceId(UUID.fromString("1984e5f4-9ff0-4187-84ae-e4438bba4c8a")); |
||||
|
private final DeviceId device2 = new DeviceId(UUID.fromString("937fc062-1a9d-438f-aa22-55a93fc908b7")); |
||||
|
|
||||
|
private final long ts = System.currentTimeMillis(); |
||||
|
|
||||
|
@BeforeEach |
||||
|
void setUp() { |
||||
|
Map<EntityId, ArgumentEntry> aggInputs = new HashMap<>(); |
||||
|
aggInputs.put(device1, new SingleValueArgumentEntry(device1, new BasicTsKvEntry(ts - 100, new LongDataEntry("key", 12L), 1L))); |
||||
|
aggInputs.put(device2, new SingleValueArgumentEntry(device2, new BasicTsKvEntry(ts - 150, new LongDataEntry("key", 16L), 6L))); |
||||
|
|
||||
|
entry = new RelatedEntitiesArgumentEntry(aggInputs, false); |
||||
|
} |
||||
|
|
||||
|
@Test |
||||
|
void testUpdateEntryWhenNotAggEntryPassed() { |
||||
|
assertThatThrownBy(() -> entry.updateEntry(new TsRollingArgumentEntry(5, 30000L))) |
||||
|
.isInstanceOf(IllegalArgumentException.class) |
||||
|
.hasMessage("Unsupported argument entry type for aggregation argument entry: " + ArgumentEntryType.TS_ROLLING); |
||||
|
} |
||||
|
|
||||
|
@Test |
||||
|
void testUpdateEntryWhenAggArgumentEntryPasser() { |
||||
|
DeviceId device3 = new DeviceId(UUID.randomUUID()); |
||||
|
DeviceId device4 = new DeviceId(UUID.randomUUID()); |
||||
|
|
||||
|
RelatedEntitiesArgumentEntry relatedEntitiesArgumentEntry = new RelatedEntitiesArgumentEntry(Map.of( |
||||
|
device3, new SingleValueArgumentEntry(device3, new BasicTsKvEntry(ts - 50, new LongDataEntry("key", 16L), 13L)), |
||||
|
device4, new SingleValueArgumentEntry(device4, new BasicTsKvEntry(ts - 60, new LongDataEntry("key", 23L), 7L)) |
||||
|
), false); |
||||
|
|
||||
|
assertThat(entry.updateEntry(relatedEntitiesArgumentEntry)).isTrue(); |
||||
|
|
||||
|
Map<EntityId, ArgumentEntry> aggInputs = entry.getEntityInputs(); |
||||
|
assertThat(aggInputs.size()).isEqualTo(4); |
||||
|
assertThat(aggInputs.get(device3)).isEqualTo(relatedEntitiesArgumentEntry.getEntityInputs().get(device3)); |
||||
|
assertThat(aggInputs.get(device4)).isEqualTo(relatedEntitiesArgumentEntry.getEntityInputs().get(device4)); |
||||
|
} |
||||
|
|
||||
|
@Test |
||||
|
void testUpdateEntryWhenSingleValueArgumentEntryPassedAndNoEntriesById() { |
||||
|
DeviceId device3 = new DeviceId(UUID.randomUUID()); |
||||
|
|
||||
|
SingleValueArgumentEntry singleEntityArgumentEntry = new SingleValueArgumentEntry(device3, new BasicTsKvEntry(ts - 50, new LongDataEntry("key", 18L), 10L)); |
||||
|
|
||||
|
assertThat(entry.updateEntry(singleEntityArgumentEntry)).isTrue(); |
||||
|
|
||||
|
Map<EntityId, ArgumentEntry> aggInputs = entry.getEntityInputs(); |
||||
|
assertThat(aggInputs.size()).isEqualTo(3); |
||||
|
assertThat(aggInputs.get(device3)).isEqualTo(singleEntityArgumentEntry); |
||||
|
} |
||||
|
|
||||
|
@Test |
||||
|
void testUpdateEntryWhenSingleValueArgumentEntryPassedAndEntryByIdExist() { |
||||
|
SingleValueArgumentEntry singleEntityArgumentEntry = new SingleValueArgumentEntry(device2, new BasicTsKvEntry(ts - 50, new LongDataEntry("key", 18L), 10L)); |
||||
|
|
||||
|
assertThat(entry.updateEntry(singleEntityArgumentEntry)).isTrue(); |
||||
|
|
||||
|
Map<EntityId, ArgumentEntry> aggInputs = entry.getEntityInputs(); |
||||
|
assertThat(aggInputs.size()).isEqualTo(2); |
||||
|
assertThat(aggInputs.get(device2)).isEqualTo(singleEntityArgumentEntry); |
||||
|
} |
||||
|
|
||||
|
} |
||||
@ -0,0 +1,92 @@ |
|||||
|
/** |
||||
|
* 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.transport.lwm2m.rpc.sql; |
||||
|
|
||||
|
import com.fasterxml.jackson.core.type.TypeReference; |
||||
|
import com.fasterxml.jackson.databind.node.ObjectNode; |
||||
|
import lombok.extern.slf4j.Slf4j; |
||||
|
import org.junit.Test; |
||||
|
import org.thingsboard.server.transport.lwm2m.rpc.AbstractRpcLwM2MIntegrationTest; |
||||
|
|
||||
|
import java.util.List; |
||||
|
import java.util.concurrent.atomic.AtomicReference; |
||||
|
|
||||
|
import static java.util.concurrent.TimeUnit.SECONDS; |
||||
|
import static org.awaitility.Awaitility.await; |
||||
|
import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.OBJECT_INSTANCE_ID_0; |
||||
|
import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.RESOURCE_ID_2; |
||||
|
import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.RESOURCE_ID_6; |
||||
|
import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.RESOURCE_ID_7; |
||||
|
import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.RESOURCE_ID_9; |
||||
|
|
||||
|
@Slf4j |
||||
|
public class RpcLwm2mIntegrationInitReadCompositeAllTest extends AbstractRpcLwM2MIntegrationTest { |
||||
|
|
||||
|
/** |
||||
|
" \"/3_1.2/0/9\": \"batteryLevel\", - Telemetry |
||||
|
" \"/3_1.2/0/20\": \"batteryStatus\" - Observe, Telemetry |
||||
|
" \"/5_1.2/0/6\": \"pkgname\" - Attributes |
||||
|
" \"/5_1.2/0/7\": \"pkgversion\" - Attributes |
||||
|
" \"/5_1.2/0/9\": \"firmwareUpdateDeliveryMethod\"\ - Telemetry |
||||
|
" \"/19_1.1/0/2\": \"dataCreationTime\" - Telemetry |
||||
|
* "observeStrategy": 1 |
||||
|
*/ |
||||
|
@Test |
||||
|
public void testInitReadCompositeAsObserveStrategyCompositeAll() throws Exception { |
||||
|
|
||||
|
|
||||
|
// init test
|
||||
|
String RESOURCE_3_9 = "batteryLevel"; |
||||
|
String RESOURCE_3_20 = "batteryStatus"; |
||||
|
String RESOURCE_5_6 = "pkgname"; |
||||
|
String RESOURCE_5_7 = "pkgversion"; |
||||
|
String RESOURCE_5_9 = "firmwareUpdateDeliveryMethod"; |
||||
|
String RESOURCE_19_2 = "dataCreationTime"; |
||||
|
|
||||
|
String idVwr_3_0_20 = idVer_3_0_9 = objectIdVer_3 + "/" + OBJECT_INSTANCE_ID_0 + "/" + 20; |
||||
|
String IdVer5_0_6 = objectInstanceIdVer_5 + "/" + RESOURCE_ID_6; |
||||
|
String IdVer5_0_7 = objectInstanceIdVer_5 + "/" + RESOURCE_ID_7; |
||||
|
String IdVer5_0_9 = objectInstanceIdVer_5 + "/" + RESOURCE_ID_9; |
||||
|
String idVer_19_0_2 = objectIdVer_19 + "/" + OBJECT_INSTANCE_ID_0 + "/" + RESOURCE_ID_2; |
||||
|
countUpdateAttrTelemetryResource(idVer_3_0_9); |
||||
|
countUpdateAttrTelemetryResource(idVwr_3_0_20); |
||||
|
countUpdateAttrTelemetryResource(IdVer5_0_6); |
||||
|
countUpdateAttrTelemetryResource(IdVer5_0_7); |
||||
|
countUpdateAttrTelemetryResource(IdVer5_0_9); |
||||
|
countUpdateAttrTelemetryResource(idVer_19_0_2); |
||||
|
|
||||
|
|
||||
|
AtomicReference<ObjectNode> actualValues = new AtomicReference<>(); |
||||
|
await().atMost(40, SECONDS).until(() -> { |
||||
|
actualValues.set(doGetAsync( |
||||
|
"/api/plugins/telemetry/DEVICE/" + lwM2MTestClient.getDeviceIdStr() + "/values/timeseries?keys=" |
||||
|
+ RESOURCE_3_9 + "," + RESOURCE_3_20 + "," + RESOURCE_5_9 + "," + RESOURCE_19_2, ObjectNode.class)); |
||||
|
return actualValues.get() != null && !actualValues.get().isEmpty() |
||||
|
&& !actualValues.get().get(RESOURCE_3_9).isEmpty() |
||||
|
&& !actualValues.get().get(RESOURCE_3_20).isEmpty() |
||||
|
&& !actualValues.get().get(RESOURCE_5_9).isEmpty() |
||||
|
&& !actualValues.get().get(RESOURCE_19_2).isEmpty(); |
||||
|
}); |
||||
|
|
||||
|
AtomicReference<List<String>> actualKeys =new AtomicReference<>(); |
||||
|
await().atMost(40, SECONDS).until(() -> { |
||||
|
actualKeys.set(doGetAsyncTyped("/api/plugins/telemetry/DEVICE/" + lwM2MTestClient.getDeviceIdStr() + "/keys/attributes/CLIENT_SCOPE", new TypeReference<>() { |
||||
|
})); |
||||
|
return actualKeys.get() != null && !actualKeys.get().isEmpty() && !actualKeys.get().isEmpty() |
||||
|
&& actualKeys.get().contains(RESOURCE_5_6)&& actualKeys.get().contains(RESOURCE_5_7); |
||||
|
}); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,92 @@ |
|||||
|
/** |
||||
|
* 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.transport.lwm2m.rpc.sql; |
||||
|
|
||||
|
import com.fasterxml.jackson.core.type.TypeReference; |
||||
|
import com.fasterxml.jackson.databind.node.ObjectNode; |
||||
|
import lombok.extern.slf4j.Slf4j; |
||||
|
import org.junit.Test; |
||||
|
import org.thingsboard.server.transport.lwm2m.rpc.AbstractRpcLwM2MIntegrationTest; |
||||
|
|
||||
|
import java.util.List; |
||||
|
import java.util.concurrent.atomic.AtomicReference; |
||||
|
|
||||
|
import static java.util.concurrent.TimeUnit.SECONDS; |
||||
|
import static org.awaitility.Awaitility.await; |
||||
|
import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.OBJECT_INSTANCE_ID_0; |
||||
|
import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.RESOURCE_ID_2; |
||||
|
import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.RESOURCE_ID_6; |
||||
|
import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.RESOURCE_ID_7; |
||||
|
import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.RESOURCE_ID_9; |
||||
|
|
||||
|
@Slf4j |
||||
|
public class RpcLwm2mIntegrationInitReadCompositeByObjectTest extends AbstractRpcLwM2MIntegrationTest { |
||||
|
|
||||
|
/** |
||||
|
" \"/3_1.2/0/9\": \"batteryLevel\", - Telemetry |
||||
|
" \"/3_1.2/0/20\": \"batteryStatus\" - Observe, Telemetry |
||||
|
" \"/5_1.2/0/6\": \"pkgname\" - Attributes |
||||
|
" \"/5_1.2/0/7\": \"pkgversion\" - Attributes |
||||
|
" \"/5_1.2/0/9\": \"firmwareUpdateDeliveryMethod\"\ - Telemetry |
||||
|
" \"/19_1.1/0/2\": \"dataCreationTime\" - Telemetry |
||||
|
* "observeStrategy": 2 |
||||
|
*/ |
||||
|
@Test |
||||
|
public void testInitReadCompositeAsObserveStrategyCompositeByObject() throws Exception { |
||||
|
|
||||
|
|
||||
|
// init test
|
||||
|
String RESOURCE_3_9 = "batteryLevel"; |
||||
|
String RESOURCE_3_20 = "batteryStatus"; |
||||
|
String RESOURCE_5_6 = "pkgname"; |
||||
|
String RESOURCE_5_7 = "pkgversion"; |
||||
|
String RESOURCE_5_9 = "firmwareUpdateDeliveryMethod"; |
||||
|
String RESOURCE_19_2 = "dataCreationTime"; |
||||
|
|
||||
|
String idVwr_3_0_20 = idVer_3_0_9 = objectIdVer_3 + "/" + OBJECT_INSTANCE_ID_0 + "/" + 20; |
||||
|
String IdVer5_0_6 = objectInstanceIdVer_5 + "/" + RESOURCE_ID_6; |
||||
|
String IdVer5_0_7 = objectInstanceIdVer_5 + "/" + RESOURCE_ID_7; |
||||
|
String IdVer5_0_9 = objectInstanceIdVer_5 + "/" + RESOURCE_ID_9; |
||||
|
String idVer_19_0_2 = objectIdVer_19 + "/" + OBJECT_INSTANCE_ID_0 + "/" + RESOURCE_ID_2; |
||||
|
countUpdateAttrTelemetryResource(idVer_3_0_9); |
||||
|
countUpdateAttrTelemetryResource(idVwr_3_0_20); |
||||
|
countUpdateAttrTelemetryResource(IdVer5_0_6); |
||||
|
countUpdateAttrTelemetryResource(IdVer5_0_7); |
||||
|
countUpdateAttrTelemetryResource(IdVer5_0_9); |
||||
|
countUpdateAttrTelemetryResource(idVer_19_0_2); |
||||
|
|
||||
|
|
||||
|
AtomicReference<ObjectNode> actualValues = new AtomicReference<>(); |
||||
|
await().atMost(40, SECONDS).until(() -> { |
||||
|
actualValues.set(doGetAsync( |
||||
|
"/api/plugins/telemetry/DEVICE/" + lwM2MTestClient.getDeviceIdStr() + "/values/timeseries?keys=" |
||||
|
+ RESOURCE_3_9 + "," + RESOURCE_3_20 + "," + RESOURCE_5_9 + "," + RESOURCE_19_2, ObjectNode.class)); |
||||
|
return actualValues.get() != null && !actualValues.get().isEmpty() |
||||
|
&& !actualValues.get().get(RESOURCE_3_9).isEmpty() |
||||
|
&& !actualValues.get().get(RESOURCE_3_20).isEmpty() |
||||
|
&& !actualValues.get().get(RESOURCE_5_9).isEmpty() |
||||
|
&& !actualValues.get().get(RESOURCE_19_2).isEmpty(); |
||||
|
}); |
||||
|
|
||||
|
AtomicReference<List<String>> actualKeys =new AtomicReference<>(); |
||||
|
await().atMost(40, SECONDS).until(() -> { |
||||
|
actualKeys.set(doGetAsyncTyped("/api/plugins/telemetry/DEVICE/" + lwM2MTestClient.getDeviceIdStr() + "/keys/attributes/CLIENT_SCOPE", new TypeReference<>() { |
||||
|
})); |
||||
|
return actualKeys.get() != null && !actualKeys.get().isEmpty() && !actualKeys.get().isEmpty() |
||||
|
&& actualKeys.get().contains(RESOURCE_5_6)&& actualKeys.get().contains(RESOURCE_5_7); |
||||
|
}); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,29 @@ |
|||||
|
/** |
||||
|
* 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.transport.lwm2m.security.sql; |
||||
|
|
||||
|
import org.junit.Test; |
||||
|
import org.thingsboard.server.transport.lwm2m.security.AbstractSecurityLwM2MIntegrationTest; |
||||
|
import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.LwM2MProfileBootstrapConfigType.LWM2M_ONLY; |
||||
|
public class NoSecLwM2MIntegrationBS3SectionTriggerTest extends AbstractSecurityLwM2MIntegrationTest { |
||||
|
|
||||
|
@Test |
||||
|
public void testWithNoSecConnectLwm2mSuccessBootstrapRequestTrigger_3_ConnectBsSuccess_UpdateLwm2mSection_3_AndLm2m_1_ConnectLwm2mSuccess() throws Exception { |
||||
|
String clientEndpoint = CLIENT_ENDPOINT_NO_SEC_BS + "Trigger_3" + LWM2M_ONLY.name(); |
||||
|
String awaitAlias = "await on client state (NoSecBS Trigger Lwm2m section)"; |
||||
|
basicTestConnectionBootstrapRequestTriggerBefore(clientEndpoint, awaitAlias, LWM2M_ONLY, 3); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,37 @@ |
|||||
|
/** |
||||
|
* 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.transport.lwm2m.security.sql; |
||||
|
|
||||
|
import org.junit.Test; |
||||
|
import org.thingsboard.server.transport.lwm2m.security.AbstractSecurityLwM2MIntegrationTest; |
||||
|
import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.LwM2MProfileBootstrapConfigType.LWM2M_ONLY; |
||||
|
import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.LwM2MProfileBootstrapConfigType.NONE; |
||||
|
public class NoSecLwM2MIntegrationBSLwm2mOnlyNoneTriggerOneSectionTest extends AbstractSecurityLwM2MIntegrationTest { |
||||
|
|
||||
|
@Test |
||||
|
public void testWithNoSecConnectLwm2mSuccessBootstrapRequestTriggerConnectBsSuccess_UpdateLwm2mSectionAndLm2m_ConnectLwm2mSuccess() throws Exception { |
||||
|
String clientEndpoint = CLIENT_ENDPOINT_NO_SEC_BS + "Trigger" + LWM2M_ONLY.name(); |
||||
|
String awaitAlias = "await on client state (NoSecBS Trigger Lwm2m section)"; |
||||
|
basicTestConnectionBootstrapRequestTriggerBefore(clientEndpoint, awaitAlias, LWM2M_ONLY, 1); |
||||
|
} |
||||
|
|
||||
|
@Test |
||||
|
public void testWithNoSecConnectLwm2mSuccessBootstrapRequestTriggerConnectBsSuccess_UpdateNoneSectionAndLm2m_ConnectLwm2mSuccess() throws Exception { |
||||
|
String clientEndpoint = CLIENT_ENDPOINT_NO_SEC_BS + "Trigger" + NONE.name(); |
||||
|
String awaitAlias = "await on client state (NoSecBS Trigger None section)"; |
||||
|
basicTestConnectionBootstrapRequestTriggerBefore(clientEndpoint, awaitAlias, NONE, 1); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,39 @@ |
|||||
|
/** |
||||
|
* 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.transport.lwm2m.security.sql; |
||||
|
|
||||
|
import org.junit.Test; |
||||
|
import org.thingsboard.server.transport.lwm2m.security.AbstractSecurityLwM2MIntegrationTest; |
||||
|
import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.LwM2MClientState.ON_REGISTRATION_SUCCESS; |
||||
|
import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.LwM2MProfileBootstrapConfigType.BOTH; |
||||
|
import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.LwM2MProfileBootstrapConfigType.LWM2M_ONLY; |
||||
|
|
||||
|
public class NoSecLwM2MIntegrationBSNoTriggerTest extends AbstractSecurityLwM2MIntegrationTest { |
||||
|
|
||||
|
@Test |
||||
|
public void testWithNoSecConnectBsSuccess_UpdateTwoSectionsBootstrapAndLm2m_ConnectLwm2mSuccess() throws Exception { |
||||
|
String clientEndpoint = CLIENT_ENDPOINT_NO_SEC_BS + "NoTrigger" + BOTH.name(); |
||||
|
String awaitAlias = "await on client state (NoSecBS two section)"; |
||||
|
basicTestConnectionStartBS(clientEndpoint, awaitAlias, BOTH, expectedStatusesRegistrationBsSuccess, ON_REGISTRATION_SUCCESS); |
||||
|
} |
||||
|
|
||||
|
@Test |
||||
|
public void testWithNoSecConnectBsSuccess_UpdateLwm2mSectionAndLm2m_ConnectLwm2mSuccess() throws Exception { |
||||
|
String clientEndpoint = CLIENT_ENDPOINT_NO_SEC_BS + "NoTrigger" + LWM2M_ONLY.name(); |
||||
|
String awaitAlias = "await on client state (NoSecBS Lwm2m section)"; |
||||
|
basicTestConnectionStartBS(clientEndpoint, awaitAlias, LWM2M_ONLY, expectedStatusesRegistrationBsSuccess, ON_REGISTRATION_SUCCESS); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,29 @@ |
|||||
|
/** |
||||
|
* 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.transport.lwm2m.security.sql; |
||||
|
|
||||
|
import org.junit.Test; |
||||
|
import org.thingsboard.server.transport.lwm2m.security.AbstractSecurityLwM2MIntegrationTest; |
||||
|
import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.LwM2MProfileBootstrapConfigType.BOOTSTRAP_ONLY; |
||||
|
public class NoSecLwM2MIntegrationBSOnlyTriggerOneSectionTest extends AbstractSecurityLwM2MIntegrationTest { |
||||
|
|
||||
|
@Test |
||||
|
public void testWithNoSecConnectLwm2mSuccessBootstrapRequestTriggerConnectBsSuccess_UpdateBootstrapSectionAndLm2m_ConnectLwm2mSuccess() throws Exception { |
||||
|
String clientEndpoint = CLIENT_ENDPOINT_NO_SEC_BS + "Trigger" + BOOTSTRAP_ONLY.name(); |
||||
|
String awaitAlias = "await on client state (NoSecBS Trigger Bootstrap section)"; |
||||
|
basicTestConnectionBootstrapRequestTriggerBefore(clientEndpoint, awaitAlias, BOOTSTRAP_ONLY, 1); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,31 @@ |
|||||
|
/** |
||||
|
* 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.transport.lwm2m.security.sql; |
||||
|
|
||||
|
import org.junit.Test; |
||||
|
import org.thingsboard.server.transport.lwm2m.security.AbstractSecurityLwM2MIntegrationTest; |
||||
|
import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.LwM2MProfileBootstrapConfigType.BOTH; |
||||
|
|
||||
|
public class NoSecLwM2MIntegrationBSTriggerTest extends AbstractSecurityLwM2MIntegrationTest { |
||||
|
|
||||
|
@Test |
||||
|
public void testWithNoSecConnectLwm2mSuccessBootstrapRequestTriggerConnectBsSuccess_UpdateTwoSectionAndLm2m_ConnectLwm2mSuccess() throws Exception { |
||||
|
String clientEndpoint = CLIENT_ENDPOINT_NO_SEC_BS + "Trigger" + BOTH.name(); |
||||
|
String awaitAlias = "await on client state (NoSecBS Trigger Two section)"; |
||||
|
basicTestConnectionBootstrapRequestTriggerBefore(clientEndpoint, awaitAlias, BOTH, 1); |
||||
|
} |
||||
|
} |
||||
|
|
||||
@ -0,0 +1,20 @@ |
|||||
|
/** |
||||
|
* 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.aggregation; |
||||
|
|
||||
|
public enum AggFunction { |
||||
|
MIN, MAX, SUM, AVG, COUNT, COUNT_UNIQUE |
||||
|
} |
||||
@ -0,0 +1,34 @@ |
|||||
|
/** |
||||
|
* 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.aggregation; |
||||
|
|
||||
|
import lombok.AllArgsConstructor; |
||||
|
import lombok.Data; |
||||
|
import lombok.NoArgsConstructor; |
||||
|
|
||||
|
@Data |
||||
|
@AllArgsConstructor |
||||
|
@NoArgsConstructor |
||||
|
public class AggFunctionInput implements AggInput { |
||||
|
|
||||
|
private String function; |
||||
|
|
||||
|
@Override |
||||
|
public String getType() { |
||||
|
return "function"; |
||||
|
} |
||||
|
|
||||
|
} |
||||
@ -0,0 +1,38 @@ |
|||||
|
/** |
||||
|
* 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.aggregation; |
||||
|
|
||||
|
import com.fasterxml.jackson.annotation.JsonIgnore; |
||||
|
import com.fasterxml.jackson.annotation.JsonIgnoreProperties; |
||||
|
import com.fasterxml.jackson.annotation.JsonSubTypes; |
||||
|
import com.fasterxml.jackson.annotation.JsonTypeInfo; |
||||
|
|
||||
|
@JsonTypeInfo( |
||||
|
use = JsonTypeInfo.Id.NAME, |
||||
|
include = JsonTypeInfo.As.PROPERTY, |
||||
|
property = "type" |
||||
|
) |
||||
|
@JsonSubTypes({ |
||||
|
@JsonSubTypes.Type(value = AggKeyInput.class, name = "key"), |
||||
|
@JsonSubTypes.Type(value = AggFunctionInput.class, name = "function") |
||||
|
}) |
||||
|
@JsonIgnoreProperties(ignoreUnknown = true) |
||||
|
public interface AggInput { |
||||
|
|
||||
|
@JsonIgnore |
||||
|
String getType(); |
||||
|
|
||||
|
} |
||||
@ -0,0 +1,34 @@ |
|||||
|
/** |
||||
|
* 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.aggregation; |
||||
|
|
||||
|
import lombok.AllArgsConstructor; |
||||
|
import lombok.Data; |
||||
|
import lombok.NoArgsConstructor; |
||||
|
|
||||
|
@Data |
||||
|
@AllArgsConstructor |
||||
|
@NoArgsConstructor |
||||
|
public class AggKeyInput implements AggInput { |
||||
|
|
||||
|
private String key; |
||||
|
|
||||
|
@Override |
||||
|
public String getType() { |
||||
|
return "key"; |
||||
|
} |
||||
|
|
||||
|
} |
||||
@ -0,0 +1,31 @@ |
|||||
|
/** |
||||
|
* 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.aggregation; |
||||
|
|
||||
|
import com.fasterxml.jackson.annotation.JsonIgnoreProperties; |
||||
|
import com.fasterxml.jackson.annotation.JsonInclude; |
||||
|
import lombok.Data; |
||||
|
|
||||
|
@Data |
||||
|
@JsonInclude(JsonInclude.Include.NON_NULL) |
||||
|
@JsonIgnoreProperties(ignoreUnknown = true) |
||||
|
public class AggMetric { |
||||
|
|
||||
|
private AggFunction function; |
||||
|
private String filter; |
||||
|
private AggInput input; |
||||
|
|
||||
|
} |
||||
Some files were not shown because too many files changed in this diff
Loading…
Reference in new issue