26 changed files with 1215 additions and 79 deletions
@ -0,0 +1,32 @@ |
|||
/** |
|||
* Copyright © 2016-2020 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.rule.engine.api; |
|||
|
|||
import org.thingsboard.server.common.data.DeviceProfile; |
|||
import org.thingsboard.server.common.data.id.DeviceId; |
|||
import org.thingsboard.server.common.data.id.DeviceProfileId; |
|||
import org.thingsboard.server.common.data.id.TenantId; |
|||
|
|||
/** |
|||
* Created by ashvayka on 02.04.18. |
|||
*/ |
|||
public interface RuleEngineDeviceProfileCache { |
|||
|
|||
DeviceProfile get(TenantId tenantId, DeviceProfileId deviceProfileId); |
|||
|
|||
DeviceProfile get(TenantId tenantId, DeviceId deviceId); |
|||
|
|||
} |
|||
@ -0,0 +1,37 @@ |
|||
/** |
|||
* Copyright © 2016-2020 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.rule.engine.action; |
|||
|
|||
import lombok.AllArgsConstructor; |
|||
import lombok.Data; |
|||
import org.thingsboard.server.common.data.alarm.Alarm; |
|||
|
|||
@Data |
|||
@AllArgsConstructor |
|||
public class TbAlarmResult { |
|||
boolean isCreated; |
|||
boolean isUpdated; |
|||
boolean isSeverityUpdated; |
|||
boolean isCleared; |
|||
Alarm alarm; |
|||
|
|||
public TbAlarmResult(boolean isCreated, boolean isUpdated, boolean isCleared, Alarm alarm) { |
|||
this.isCreated = isCreated; |
|||
this.isUpdated = isUpdated; |
|||
this.isCleared = isCleared; |
|||
this.alarm = alarm; |
|||
} |
|||
} |
|||
@ -0,0 +1,22 @@ |
|||
/** |
|||
* Copyright © 2016-2020 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.rule.engine.profile; |
|||
|
|||
public enum AlarmStateUpdateResult { |
|||
|
|||
NONE, CREATED, UPDATED, SEVERITY_UPDATED, CLEARED; |
|||
|
|||
} |
|||
@ -0,0 +1,49 @@ |
|||
/** |
|||
* Copyright © 2016-2020 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.rule.engine.profile; |
|||
|
|||
import lombok.Getter; |
|||
import lombok.Setter; |
|||
import org.thingsboard.server.common.data.query.EntityKey; |
|||
|
|||
import java.util.Map; |
|||
import java.util.Set; |
|||
import java.util.concurrent.ConcurrentHashMap; |
|||
|
|||
public class DeviceDataSnapshot { |
|||
|
|||
private volatile boolean ready; |
|||
@Getter @Setter |
|||
private long ts; |
|||
private final Map<EntityKey, EntityKeyValue> values = new ConcurrentHashMap<>(); |
|||
|
|||
public DeviceDataSnapshot(Set<EntityKey> entityKeySet) { |
|||
entityKeySet.forEach(key -> values.put(key, new EntityKeyValue())); |
|||
this.ready = false; |
|||
} |
|||
|
|||
void putValue(EntityKey key, EntityKeyValue value) { |
|||
values.put(key, value); |
|||
} |
|||
|
|||
EntityKeyValue getValue(EntityKey key) { |
|||
return values.get(key); |
|||
} |
|||
|
|||
boolean isReady() { |
|||
return ready; |
|||
} |
|||
} |
|||
@ -0,0 +1,302 @@ |
|||
/** |
|||
* Copyright © 2016-2020 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.rule.engine.profile; |
|||
|
|||
import com.fasterxml.jackson.databind.JsonNode; |
|||
import lombok.Data; |
|||
import org.thingsboard.rule.engine.action.TbAlarmResult; |
|||
import org.thingsboard.rule.engine.api.TbContext; |
|||
import org.thingsboard.server.common.data.DataConstants; |
|||
import org.thingsboard.server.common.data.alarm.Alarm; |
|||
import org.thingsboard.server.common.data.alarm.AlarmSeverity; |
|||
import org.thingsboard.server.common.data.device.profile.AlarmCondition; |
|||
import org.thingsboard.server.common.data.device.profile.AlarmRule; |
|||
import org.thingsboard.server.common.data.device.profile.DeviceProfileAlarm; |
|||
import org.thingsboard.server.common.data.id.EntityId; |
|||
import org.thingsboard.server.common.data.query.BooleanFilterPredicate; |
|||
import org.thingsboard.server.common.data.query.ComplexFilterPredicate; |
|||
import org.thingsboard.server.common.data.query.KeyFilter; |
|||
import org.thingsboard.server.common.data.query.KeyFilterPredicate; |
|||
import org.thingsboard.server.common.data.query.NumericFilterPredicate; |
|||
import org.thingsboard.server.common.data.query.StringFilterPredicate; |
|||
import org.thingsboard.server.common.msg.TbMsg; |
|||
import org.thingsboard.server.common.msg.TbMsgMetaData; |
|||
import org.thingsboard.server.dao.util.mapping.JacksonUtil; |
|||
|
|||
import java.util.Comparator; |
|||
import java.util.Map; |
|||
import java.util.TreeMap; |
|||
|
|||
@Data |
|||
class DeviceProfileAlarmState { |
|||
|
|||
private final EntityId originator; |
|||
private final DeviceProfileAlarm alarmDefinition; |
|||
private volatile Map<AlarmSeverity, AlarmRule> createRulesSortedBySeverityDesc; |
|||
private volatile Alarm currentAlarm; |
|||
|
|||
public DeviceProfileAlarmState(EntityId originator, DeviceProfileAlarm alarmDefinition) { |
|||
this.originator = originator; |
|||
this.alarmDefinition = alarmDefinition; |
|||
this.createRulesSortedBySeverityDesc = new TreeMap<>(Comparator.comparingInt(AlarmSeverity::ordinal)); |
|||
this.createRulesSortedBySeverityDesc.putAll(alarmDefinition.getCreateRules()); |
|||
} |
|||
|
|||
public void process(TbContext ctx, TbMsg msg, DeviceDataSnapshot data) { |
|||
AlarmSeverity resultSeverity = null; |
|||
for (Map.Entry<AlarmSeverity, AlarmRule> kv : createRulesSortedBySeverityDesc.entrySet()) { |
|||
AlarmRule alarmRule = kv.getValue(); |
|||
if (eval(alarmRule.getCondition(), data)) { |
|||
resultSeverity = kv.getKey(); |
|||
break; |
|||
} |
|||
} |
|||
if (resultSeverity != null) { |
|||
pushMsg(ctx, calculateAlarmResult(ctx, resultSeverity), msg); |
|||
} else if (currentAlarm != null) { |
|||
AlarmRule clearRule = alarmDefinition.getClearRule(); |
|||
if (eval(clearRule.getCondition(), data)) { |
|||
pushMsg(ctx, new TbAlarmResult(false, false, true, currentAlarm), msg); |
|||
currentAlarm = null; |
|||
} |
|||
} |
|||
} |
|||
|
|||
public void pushMsg(TbContext ctx, TbAlarmResult alarmResult, TbMsg originalMsg) { |
|||
JsonNode jsonNodes = JacksonUtil.valueToTree(alarmResult.getAlarm()); |
|||
String data = jsonNodes.toString(); |
|||
TbMsgMetaData metaData = originalMsg.getMetaData().copy(); |
|||
String relationType; |
|||
if (alarmResult.isCreated()) { |
|||
relationType = "Alarm Created"; |
|||
metaData.putValue(DataConstants.IS_NEW_ALARM, Boolean.TRUE.toString()); |
|||
} else if (alarmResult.isUpdated()) { |
|||
relationType = "Alarm Updated"; |
|||
metaData.putValue(DataConstants.IS_EXISTING_ALARM, Boolean.TRUE.toString()); |
|||
} else if (alarmResult.isSeverityUpdated()) { |
|||
relationType = "Alarm Severity Updated"; |
|||
metaData.putValue(DataConstants.IS_EXISTING_ALARM, Boolean.TRUE.toString()); |
|||
metaData.putValue(DataConstants.IS_SEVERITY_UPDATED_ALARM, Boolean.TRUE.toString()); |
|||
} else { |
|||
relationType = "Alarm Cleared"; |
|||
metaData.putValue(DataConstants.IS_CLEARED_ALARM, Boolean.TRUE.toString()); |
|||
} |
|||
TbMsg newMsg = ctx.newMsg(originalMsg.getQueueName(), "ALARM", originalMsg.getOriginator(), metaData, data); |
|||
ctx.tellNext(newMsg, relationType); |
|||
} |
|||
|
|||
private TbAlarmResult calculateAlarmResult(TbContext ctx, AlarmSeverity severity) { |
|||
if (currentAlarm != null) { |
|||
currentAlarm.setEndTs(System.currentTimeMillis()); |
|||
AlarmSeverity oldSeverity = currentAlarm.getSeverity(); |
|||
if (!oldSeverity.equals(severity)) { |
|||
currentAlarm.setSeverity(severity); |
|||
currentAlarm = ctx.getAlarmService().createOrUpdateAlarm(currentAlarm); |
|||
return new TbAlarmResult(false, false, true, false, currentAlarm); |
|||
} else { |
|||
currentAlarm = ctx.getAlarmService().createOrUpdateAlarm(currentAlarm); |
|||
return new TbAlarmResult(false, true, false, false, currentAlarm); |
|||
} |
|||
} else { |
|||
currentAlarm = new Alarm(); |
|||
currentAlarm.setSeverity(severity); |
|||
currentAlarm.setStartTs(System.currentTimeMillis()); |
|||
currentAlarm.setEndTs(currentAlarm.getStartTs()); |
|||
currentAlarm.setDetails(JacksonUtil.OBJECT_MAPPER.createObjectNode()); |
|||
currentAlarm.setOriginator(originator); |
|||
currentAlarm.setTenantId(ctx.getTenantId()); |
|||
currentAlarm.setPropagate(alarmDefinition.isPropagate()); |
|||
if (alarmDefinition.getPropagateRelationTypes() != null) { |
|||
currentAlarm.setPropagateRelationTypes(alarmDefinition.getPropagateRelationTypes()); |
|||
} |
|||
currentAlarm = ctx.getAlarmService().createOrUpdateAlarm(currentAlarm); |
|||
boolean updated = currentAlarm.getStartTs() != currentAlarm.getEndTs(); |
|||
return new TbAlarmResult(!updated, updated, false, false, currentAlarm); |
|||
} |
|||
} |
|||
|
|||
private boolean eval(AlarmCondition condition, DeviceDataSnapshot data) { |
|||
boolean eval = true; |
|||
for (KeyFilter keyFilter : condition.getCondition()) { |
|||
EntityKeyValue value = data.getValue(keyFilter.getKey()); |
|||
if (value == null) { |
|||
return false; |
|||
} |
|||
eval = eval && eval(value, keyFilter.getPredicate()); |
|||
} |
|||
//TODO: use condition duration;
|
|||
return eval; |
|||
} |
|||
|
|||
private boolean eval(EntityKeyValue value, KeyFilterPredicate predicate) { |
|||
switch (predicate.getType()) { |
|||
case STRING: |
|||
return evalStrPredicate(value, (StringFilterPredicate) predicate); |
|||
case NUMERIC: |
|||
return evalNumPredicate(value, (NumericFilterPredicate) predicate); |
|||
case COMPLEX: |
|||
return evalComplexPredicate(value, (ComplexFilterPredicate) predicate); |
|||
case BOOLEAN: |
|||
return evalBoolPredicate(value, (BooleanFilterPredicate) predicate); |
|||
default: |
|||
return false; |
|||
} |
|||
} |
|||
|
|||
private boolean evalComplexPredicate(EntityKeyValue ekv, ComplexFilterPredicate predicate) { |
|||
switch (predicate.getOperation()) { |
|||
case OR: |
|||
for (KeyFilterPredicate kfp : predicate.getPredicates()) { |
|||
if (eval(ekv, kfp)) { |
|||
return true; |
|||
} |
|||
} |
|||
return false; |
|||
case AND: |
|||
for (KeyFilterPredicate kfp : predicate.getPredicates()) { |
|||
if (!eval(ekv, kfp)) { |
|||
return false; |
|||
} |
|||
} |
|||
return true; |
|||
default: |
|||
throw new RuntimeException("Operation not supported: " + predicate.getOperation()); |
|||
} |
|||
} |
|||
|
|||
|
|||
private boolean evalBoolPredicate(EntityKeyValue ekv, BooleanFilterPredicate predicate) { |
|||
Boolean value; |
|||
switch (ekv.getDataType()) { |
|||
case LONG: |
|||
value = ekv.getLngValue() > 0; |
|||
break; |
|||
case DOUBLE: |
|||
value = ekv.getDblValue() > 0; |
|||
break; |
|||
case BOOLEAN: |
|||
value = ekv.getBoolValue(); |
|||
break; |
|||
case STRING: |
|||
try { |
|||
value = Boolean.parseBoolean(ekv.getStrValue()); |
|||
break; |
|||
} catch (RuntimeException e) { |
|||
return false; |
|||
} |
|||
case JSON: |
|||
try { |
|||
value = Boolean.parseBoolean(ekv.getJsonValue()); |
|||
break; |
|||
} catch (RuntimeException e) { |
|||
return false; |
|||
} |
|||
default: |
|||
return false; |
|||
} |
|||
if (value == null) { |
|||
return false; |
|||
} |
|||
switch (predicate.getOperation()) { |
|||
case EQUAL: |
|||
return value.equals(predicate.getValue().getDefaultValue()); |
|||
case NOT_EQUAL: |
|||
return !value.equals(predicate.getValue().getDefaultValue()); |
|||
default: |
|||
throw new RuntimeException("Operation not supported: " + predicate.getOperation()); |
|||
} |
|||
} |
|||
|
|||
private boolean evalNumPredicate(EntityKeyValue ekv, NumericFilterPredicate predicate) { |
|||
Double value; |
|||
switch (ekv.getDataType()) { |
|||
case LONG: |
|||
value = ekv.getLngValue().doubleValue(); |
|||
break; |
|||
case DOUBLE: |
|||
value = ekv.getDblValue(); |
|||
break; |
|||
case BOOLEAN: |
|||
value = ekv.getBoolValue() ? 1.0 : 0.0; |
|||
break; |
|||
case STRING: |
|||
try { |
|||
value = Double.parseDouble(ekv.getStrValue()); |
|||
break; |
|||
} catch (RuntimeException e) { |
|||
return false; |
|||
} |
|||
case JSON: |
|||
try { |
|||
value = Double.parseDouble(ekv.getJsonValue()); |
|||
break; |
|||
} catch (RuntimeException e) { |
|||
return false; |
|||
} |
|||
default: |
|||
return false; |
|||
} |
|||
if (value == null) { |
|||
return false; |
|||
} |
|||
|
|||
Double predicateValue = predicate.getValue().getDefaultValue(); |
|||
switch (predicate.getOperation()) { |
|||
case NOT_EQUAL: |
|||
return !value.equals(predicateValue); |
|||
case EQUAL: |
|||
return value.equals(predicateValue); |
|||
case GREATER: |
|||
return value > predicateValue; |
|||
case GREATER_OR_EQUAL: |
|||
return value >= predicateValue; |
|||
case LESS: |
|||
return value < predicateValue; |
|||
case LESS_OR_EQUAL: |
|||
return value <= predicateValue; |
|||
default: |
|||
throw new RuntimeException("Operation not supported: " + predicate.getOperation()); |
|||
} |
|||
} |
|||
|
|||
private boolean evalStrPredicate(EntityKeyValue ekv, StringFilterPredicate predicate) { |
|||
String val; |
|||
String predicateValue; |
|||
if (predicate.isIgnoreCase()) { |
|||
val = ekv.getStrValue().toLowerCase(); |
|||
predicateValue = predicate.getValue().getDefaultValue().toLowerCase(); |
|||
} else { |
|||
val = ekv.getStrValue(); |
|||
predicateValue = predicate.getValue().getDefaultValue(); |
|||
} |
|||
switch (predicate.getOperation()) { |
|||
case CONTAINS: |
|||
return val.contains(predicateValue); |
|||
case EQUAL: |
|||
return val.equals(predicateValue); |
|||
case STARTS_WITH: |
|||
return val.startsWith(predicateValue); |
|||
case ENDS_WITH: |
|||
return val.endsWith(predicateValue); |
|||
case NOT_EQUAL: |
|||
return !val.equals(predicateValue); |
|||
case NOT_CONTAINS: |
|||
return !val.contains(predicateValue); |
|||
default: |
|||
throw new RuntimeException("Operation not supported: " + predicate.getOperation()); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,58 @@ |
|||
/** |
|||
* Copyright © 2016-2020 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.rule.engine.profile; |
|||
|
|||
import lombok.AccessLevel; |
|||
import lombok.Getter; |
|||
import org.thingsboard.server.common.data.DeviceProfile; |
|||
import org.thingsboard.server.common.data.device.profile.AlarmRule; |
|||
import org.thingsboard.server.common.data.device.profile.DeviceProfileAlarm; |
|||
import org.thingsboard.server.common.data.query.EntityKey; |
|||
import org.thingsboard.server.common.data.query.KeyFilter; |
|||
|
|||
import java.util.List; |
|||
import java.util.Set; |
|||
import java.util.concurrent.ConcurrentHashMap; |
|||
import java.util.concurrent.CopyOnWriteArrayList; |
|||
|
|||
|
|||
class DeviceProfileState { |
|||
|
|||
private DeviceProfile deviceProfile; |
|||
@Getter(AccessLevel.PACKAGE) |
|||
private final List<DeviceProfileAlarm> alarmSettings = new CopyOnWriteArrayList<>(); |
|||
@Getter(AccessLevel.PACKAGE) |
|||
private final Set<EntityKey> entityKeys = ConcurrentHashMap.newKeySet(); |
|||
|
|||
DeviceProfileState(DeviceProfile deviceProfile) { |
|||
updateDeviceProfile(deviceProfile); |
|||
} |
|||
|
|||
void updateDeviceProfile(DeviceProfile deviceProfile) { |
|||
this.deviceProfile = deviceProfile; |
|||
alarmSettings.clear(); |
|||
if (deviceProfile.getProfileData().getAlarms() != null) { |
|||
alarmSettings.addAll(deviceProfile.getProfileData().getAlarms()); |
|||
for (DeviceProfileAlarm alarm : deviceProfile.getProfileData().getAlarms()) { |
|||
for (AlarmRule alarmRule : alarm.getCreateRules().values()) { |
|||
for (KeyFilter keyFilter : alarmRule.getCondition().getCondition()) { |
|||
entityKeys.add(keyFilter.getKey()); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,189 @@ |
|||
/** |
|||
* Copyright © 2016-2020 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.rule.engine.profile; |
|||
|
|||
import com.google.gson.JsonParser; |
|||
import org.thingsboard.rule.engine.api.TbContext; |
|||
import org.thingsboard.rule.engine.telemetry.TbMsgTimeseriesNode; |
|||
import org.thingsboard.server.common.data.DataConstants; |
|||
import org.thingsboard.server.common.data.Device; |
|||
import org.thingsboard.server.common.data.device.profile.DeviceProfileAlarm; |
|||
import org.thingsboard.server.common.data.id.DeviceId; |
|||
import org.thingsboard.server.common.data.id.EntityId; |
|||
import org.thingsboard.server.common.data.kv.AttributeKvEntry; |
|||
import org.thingsboard.server.common.data.kv.KvEntry; |
|||
import org.thingsboard.server.common.data.kv.TsKvEntry; |
|||
import org.thingsboard.server.common.data.query.EntityKey; |
|||
import org.thingsboard.server.common.data.query.EntityKeyType; |
|||
import org.thingsboard.server.common.msg.TbMsg; |
|||
import org.thingsboard.server.common.msg.session.SessionMsgType; |
|||
import org.thingsboard.server.common.transport.adaptor.JsonConverter; |
|||
import org.thingsboard.server.dao.sql.query.EntityKeyMapping; |
|||
|
|||
import java.util.HashSet; |
|||
import java.util.List; |
|||
import java.util.Map; |
|||
import java.util.Set; |
|||
import java.util.concurrent.ConcurrentHashMap; |
|||
import java.util.concurrent.ConcurrentMap; |
|||
import java.util.concurrent.ExecutionException; |
|||
|
|||
class DeviceState { |
|||
|
|||
private DeviceProfileState deviceProfile; |
|||
private DeviceDataSnapshot latestValues; |
|||
private final ConcurrentMap<String, DeviceProfileAlarmState> alarmStates = new ConcurrentHashMap<>(); |
|||
|
|||
public DeviceState(DeviceProfileState deviceProfile) { |
|||
this.deviceProfile = deviceProfile; |
|||
} |
|||
|
|||
public void process(TbContext ctx, TbMsg msg) throws ExecutionException, InterruptedException { |
|||
if (latestValues == null) { |
|||
latestValues = fetchLatestValues(ctx, msg.getOriginator()); |
|||
} |
|||
if (msg.getType().equals(SessionMsgType.POST_TELEMETRY_REQUEST.name())) { |
|||
processTelemetry(ctx, msg); |
|||
} else { |
|||
ctx.tellSuccess(msg); |
|||
} |
|||
} |
|||
|
|||
private void processTelemetry(TbContext ctx, TbMsg msg) { |
|||
Map<Long, List<KvEntry>> tsKvMap = JsonConverter.convertToSortedTelemetry(new JsonParser().parse(msg.getData()), TbMsgTimeseriesNode.getTs(msg)); |
|||
tsKvMap.forEach((ts, data) -> { |
|||
latestValues = merge(latestValues, ts, data); |
|||
for (DeviceProfileAlarm alarm : deviceProfile.getAlarmSettings()) { |
|||
DeviceProfileAlarmState alarmState = alarmStates.computeIfAbsent(alarm.getId(), a -> new DeviceProfileAlarmState(msg.getOriginator(), alarm)); |
|||
alarmState.process(ctx, msg, latestValues); |
|||
} |
|||
}); |
|||
ctx.tellSuccess(msg); |
|||
} |
|||
|
|||
private DeviceDataSnapshot merge(DeviceDataSnapshot latestValues, Long ts, List<KvEntry> data) { |
|||
latestValues.setTs(ts); |
|||
for (KvEntry entry : data) { |
|||
latestValues.putValue(new EntityKey(EntityKeyType.TIME_SERIES, entry.getKey()), toEntityValue(entry)); |
|||
} |
|||
return latestValues; |
|||
} |
|||
|
|||
private DeviceDataSnapshot fetchLatestValues(TbContext ctx, EntityId originator) throws ExecutionException, InterruptedException { |
|||
DeviceDataSnapshot result = new DeviceDataSnapshot(deviceProfile.getEntityKeys()); |
|||
|
|||
Set<String> serverAttributeKeys = new HashSet<>(); |
|||
Set<String> clientAttributeKeys = new HashSet<>(); |
|||
Set<String> sharedAttributeKeys = new HashSet<>(); |
|||
Set<String> commonAttributeKeys = new HashSet<>(); |
|||
Set<String> latestTsKeys = new HashSet<>(); |
|||
|
|||
Device device = null; |
|||
for (EntityKey entityKey : deviceProfile.getEntityKeys()) { |
|||
String key = entityKey.getKey(); |
|||
switch (entityKey.getType()) { |
|||
case SERVER_ATTRIBUTE: |
|||
serverAttributeKeys.add(key); |
|||
break; |
|||
case CLIENT_ATTRIBUTE: |
|||
clientAttributeKeys.add(key); |
|||
break; |
|||
case SHARED_ATTRIBUTE: |
|||
sharedAttributeKeys.add(key); |
|||
break; |
|||
case ATTRIBUTE: |
|||
serverAttributeKeys.add(key); |
|||
clientAttributeKeys.add(key); |
|||
sharedAttributeKeys.add(key); |
|||
commonAttributeKeys.add(key); |
|||
break; |
|||
case TIME_SERIES: |
|||
latestTsKeys.add(key); |
|||
break; |
|||
case ENTITY_FIELD: |
|||
if (device == null) { |
|||
device = ctx.getDeviceService().findDeviceById(ctx.getTenantId(), new DeviceId(originator.getId())); |
|||
} |
|||
if (device != null) { |
|||
switch (key) { |
|||
case EntityKeyMapping.NAME: |
|||
result.putValue(entityKey, EntityKeyValue.fromString(device.getName())); |
|||
break; |
|||
case EntityKeyMapping.TYPE: |
|||
result.putValue(entityKey, EntityKeyValue.fromString(device.getType())); |
|||
break; |
|||
case EntityKeyMapping.CREATED_TIME: |
|||
result.putValue(entityKey, EntityKeyValue.fromLong(device.getCreatedTime())); |
|||
break; |
|||
case EntityKeyMapping.LABEL: |
|||
result.putValue(entityKey, EntityKeyValue.fromString(device.getLabel())); |
|||
break; |
|||
} |
|||
} |
|||
break; |
|||
} |
|||
} |
|||
|
|||
if (!latestTsKeys.isEmpty()) { |
|||
List<TsKvEntry> data = ctx.getTimeseriesService().findLatest(ctx.getTenantId(), originator, latestTsKeys).get(); |
|||
for (TsKvEntry entry : data) { |
|||
result.putValue(new EntityKey(EntityKeyType.TIME_SERIES, entry.getKey()), toEntityValue(entry)); |
|||
} |
|||
} |
|||
if (!clientAttributeKeys.isEmpty()) { |
|||
addToSnapshot(result, commonAttributeKeys, |
|||
ctx.getAttributesService().find(ctx.getTenantId(), originator, DataConstants.CLIENT_SCOPE, clientAttributeKeys).get()); |
|||
} |
|||
if (!sharedAttributeKeys.isEmpty()) { |
|||
addToSnapshot(result, commonAttributeKeys, |
|||
ctx.getAttributesService().find(ctx.getTenantId(), originator, DataConstants.SHARED_SCOPE, sharedAttributeKeys).get()); |
|||
} |
|||
if (!serverAttributeKeys.isEmpty()) { |
|||
addToSnapshot(result, commonAttributeKeys, |
|||
ctx.getAttributesService().find(ctx.getTenantId(), originator, DataConstants.SERVER_SCOPE, serverAttributeKeys).get()); |
|||
} |
|||
|
|||
return result; |
|||
} |
|||
|
|||
private void addToSnapshot(DeviceDataSnapshot snapshot, Set<String> commonAttributeKeys, List<AttributeKvEntry> data) { |
|||
for (AttributeKvEntry entry : data) { |
|||
EntityKeyValue value = toEntityValue(entry); |
|||
snapshot.putValue(new EntityKey(EntityKeyType.CLIENT_ATTRIBUTE, entry.getKey()), value); |
|||
if (commonAttributeKeys.contains(entry.getKey())) { |
|||
snapshot.putValue(new EntityKey(EntityKeyType.ATTRIBUTE, entry.getKey()), value); |
|||
} |
|||
} |
|||
} |
|||
|
|||
private EntityKeyValue toEntityValue(KvEntry entry) { |
|||
switch (entry.getDataType()) { |
|||
case STRING: |
|||
return EntityKeyValue.fromString(entry.getStrValue().get()); |
|||
case LONG: |
|||
return EntityKeyValue.fromLong(entry.getLongValue().get()); |
|||
case DOUBLE: |
|||
return EntityKeyValue.fromDouble(entry.getDoubleValue().get()); |
|||
case BOOLEAN: |
|||
return EntityKeyValue.fromBool(entry.getBooleanValue().get()); |
|||
case JSON: |
|||
return EntityKeyValue.fromJson(entry.getJsonValue().get()); |
|||
default: |
|||
throw new RuntimeException("Can't parse entry: " + entry.getDataType()); |
|||
} |
|||
} |
|||
|
|||
} |
|||
@ -0,0 +1,22 @@ |
|||
/** |
|||
* Copyright © 2016-2020 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.rule.engine.profile; |
|||
|
|||
public class EntityKeyState { |
|||
|
|||
|
|||
|
|||
} |
|||
@ -0,0 +1,109 @@ |
|||
/** |
|||
* Copyright © 2016-2020 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.rule.engine.profile; |
|||
|
|||
import lombok.Getter; |
|||
import org.thingsboard.server.common.data.kv.DataType; |
|||
|
|||
class EntityKeyValue { |
|||
|
|||
@Getter |
|||
private DataType dataType; |
|||
private Long lngValue; |
|||
private Double dblValue; |
|||
private Boolean boolValue; |
|||
private String strValue; |
|||
|
|||
public Long getLngValue() { |
|||
return dataType == DataType.LONG ? lngValue : null; |
|||
} |
|||
|
|||
public void setLngValue(Long lngValue) { |
|||
this.dataType = DataType.LONG; |
|||
this.lngValue = lngValue; |
|||
} |
|||
|
|||
public Double getDblValue() { |
|||
return dataType == DataType.DOUBLE ? dblValue : null; |
|||
} |
|||
|
|||
public void setDblValue(Double dblValue) { |
|||
this.dataType = DataType.DOUBLE; |
|||
this.dblValue = dblValue; |
|||
} |
|||
|
|||
public Boolean getBoolValue() { |
|||
return dataType == DataType.BOOLEAN ? boolValue : null; |
|||
} |
|||
|
|||
public void setBoolValue(Boolean boolValue) { |
|||
this.dataType = DataType.BOOLEAN; |
|||
this.boolValue = boolValue; |
|||
} |
|||
|
|||
public String getStrValue() { |
|||
return dataType == DataType.STRING ? strValue : null; |
|||
} |
|||
|
|||
public void setStrValue(String strValue) { |
|||
this.dataType = DataType.STRING; |
|||
this.strValue = strValue; |
|||
} |
|||
|
|||
public void setJsonValue(String jsonValue) { |
|||
this.dataType = DataType.JSON; |
|||
this.strValue = jsonValue; |
|||
} |
|||
|
|||
public String getJsonValue() { |
|||
return dataType == DataType.JSON ? strValue : null; |
|||
} |
|||
|
|||
boolean isSet() { |
|||
return dataType != null; |
|||
} |
|||
|
|||
static EntityKeyValue fromString(String s) { |
|||
EntityKeyValue result = new EntityKeyValue(); |
|||
result.setStrValue(s); |
|||
return result; |
|||
} |
|||
|
|||
static EntityKeyValue fromBool(boolean b) { |
|||
EntityKeyValue result = new EntityKeyValue(); |
|||
result.setBoolValue(b); |
|||
return result; |
|||
} |
|||
|
|||
static EntityKeyValue fromLong(long l) { |
|||
EntityKeyValue result = new EntityKeyValue(); |
|||
result.setLngValue(l); |
|||
return result; |
|||
} |
|||
|
|||
static EntityKeyValue fromDouble(double d) { |
|||
EntityKeyValue result = new EntityKeyValue(); |
|||
result.setDblValue(d); |
|||
return result; |
|||
} |
|||
|
|||
static EntityKeyValue fromJson(String s) { |
|||
EntityKeyValue result = new EntityKeyValue(); |
|||
result.setJsonValue(s); |
|||
return result; |
|||
} |
|||
|
|||
} |
|||
@ -0,0 +1,120 @@ |
|||
/** |
|||
* Copyright © 2016-2020 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.rule.engine.profile; |
|||
|
|||
import lombok.extern.slf4j.Slf4j; |
|||
import org.apache.commons.lang3.BooleanUtils; |
|||
import org.apache.kafka.clients.producer.KafkaProducer; |
|||
import org.apache.kafka.clients.producer.Producer; |
|||
import org.apache.kafka.clients.producer.ProducerConfig; |
|||
import org.apache.kafka.clients.producer.ProducerRecord; |
|||
import org.apache.kafka.clients.producer.RecordMetadata; |
|||
import org.apache.kafka.common.header.Headers; |
|||
import org.apache.kafka.common.header.internals.RecordHeader; |
|||
import org.apache.kafka.common.header.internals.RecordHeaders; |
|||
import org.thingsboard.rule.engine.api.EmptyNodeConfiguration; |
|||
import org.thingsboard.rule.engine.api.RuleEngineDeviceProfileCache; |
|||
import org.thingsboard.rule.engine.api.RuleNode; |
|||
import org.thingsboard.rule.engine.api.TbContext; |
|||
import org.thingsboard.rule.engine.api.TbNode; |
|||
import org.thingsboard.rule.engine.api.TbNodeConfiguration; |
|||
import org.thingsboard.rule.engine.api.TbNodeException; |
|||
import org.thingsboard.rule.engine.api.TbRelationTypes; |
|||
import org.thingsboard.rule.engine.api.util.TbNodeUtils; |
|||
import org.thingsboard.rule.engine.kafka.TbKafkaNodeConfiguration; |
|||
import org.thingsboard.server.common.data.DeviceProfile; |
|||
import org.thingsboard.server.common.data.EntityType; |
|||
import org.thingsboard.server.common.data.id.DeviceId; |
|||
import org.thingsboard.server.common.data.id.EntityId; |
|||
import org.thingsboard.server.common.data.plugin.ComponentType; |
|||
import org.thingsboard.server.common.msg.TbMsg; |
|||
import org.thingsboard.server.common.msg.TbMsgMetaData; |
|||
|
|||
import java.nio.charset.Charset; |
|||
import java.nio.charset.StandardCharsets; |
|||
import java.util.HashMap; |
|||
import java.util.Map; |
|||
import java.util.Properties; |
|||
import java.util.concurrent.ConcurrentHashMap; |
|||
import java.util.concurrent.ExecutionException; |
|||
|
|||
@Slf4j |
|||
@RuleNode( |
|||
type = ComponentType.ACTION, |
|||
name = "device profile", |
|||
customRelations = true, |
|||
relationTypes = {"Alarm Created", "Alarm Updated", "Alarm Severity Updated", "Alarm Cleared", "Success", "Failure"}, |
|||
configClazz = EmptyNodeConfiguration.class, |
|||
nodeDescription = "Process device messages based on device profile settings", |
|||
nodeDetails = "Create and clear alarms based on alarm rules defined in device profile. Generates ", |
|||
uiResources = {"static/rulenode/rulenode-core-config.js"}, |
|||
configDirective = "tbNodeEmptyConfig" |
|||
) |
|||
public class TbDeviceProfileNode implements TbNode { |
|||
|
|||
private RuleEngineDeviceProfileCache cache; |
|||
private Map<DeviceId, DeviceState> deviceStates = new ConcurrentHashMap<>(); |
|||
|
|||
@Override |
|||
public void init(TbContext ctx, TbNodeConfiguration configuration) throws TbNodeException { |
|||
cache = ctx.getDeviceProfileCache(); |
|||
} |
|||
|
|||
/** |
|||
* TODO: |
|||
* 1. Duration in the alarm conditions; |
|||
* 2. Update of the Profile (rules); |
|||
* 3. Update of the Device attributes (client, server and shared); |
|||
* 4. Dynamic values evaluation; |
|||
*/ |
|||
|
|||
@Override |
|||
public void onMsg(TbContext ctx, TbMsg msg) throws ExecutionException, InterruptedException { |
|||
EntityType originatorType = msg.getOriginator().getEntityType(); |
|||
if (EntityType.DEVICE.equals(originatorType)) { |
|||
DeviceId deviceId = new DeviceId(msg.getOriginator().getId()); |
|||
DeviceState deviceState = getOrCreateDeviceState(ctx, msg, deviceId); |
|||
if (deviceState != null) { |
|||
deviceState.process(ctx, msg); |
|||
} else { |
|||
ctx.tellFailure(msg, new IllegalStateException("Device profile for device [" + deviceId + "] not found!")); |
|||
} |
|||
} else if (EntityType.DEVICE_PROFILE.equals(originatorType)) { |
|||
//TODO: check that the profile rule set was changed. If yes - invalidate the rules.
|
|||
ctx.tellSuccess(msg); |
|||
} else { |
|||
ctx.tellSuccess(msg); |
|||
} |
|||
} |
|||
|
|||
private DeviceState getOrCreateDeviceState(TbContext ctx, TbMsg msg, DeviceId deviceId) { |
|||
DeviceState deviceState = deviceStates.get(deviceId); |
|||
if (deviceState == null) { |
|||
DeviceProfile deviceProfile = cache.get(ctx.getTenantId(), deviceId); |
|||
if (deviceProfile != null) { |
|||
deviceState = new DeviceState(new DeviceProfileState(deviceProfile)); |
|||
deviceStates.put(deviceId, deviceState); |
|||
} |
|||
} |
|||
return deviceState; |
|||
} |
|||
|
|||
@Override |
|||
public void destroy() { |
|||
|
|||
} |
|||
|
|||
} |
|||
@ -0,0 +1,183 @@ |
|||
/** |
|||
* Copyright © 2016-2020 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.rule.engine.profile; |
|||
|
|||
import com.fasterxml.jackson.databind.ObjectMapper; |
|||
import com.fasterxml.jackson.databind.node.ObjectNode; |
|||
import com.google.common.util.concurrent.Futures; |
|||
import org.junit.Test; |
|||
import org.junit.runner.RunWith; |
|||
import org.mockito.AdditionalAnswers; |
|||
import org.mockito.Mock; |
|||
import org.mockito.Mockito; |
|||
import org.mockito.invocation.InvocationOnMock; |
|||
import org.mockito.runners.MockitoJUnitRunner; |
|||
import org.mockito.stubbing.Answer; |
|||
import org.springframework.util.StringUtils; |
|||
import org.thingsboard.rule.engine.api.RuleEngineAlarmService; |
|||
import org.thingsboard.rule.engine.api.RuleEngineDeviceProfileCache; |
|||
import org.thingsboard.rule.engine.api.TbContext; |
|||
import org.thingsboard.rule.engine.api.TbNodeConfiguration; |
|||
import org.thingsboard.rule.engine.api.TbNodeException; |
|||
import org.thingsboard.server.common.data.DeviceProfile; |
|||
import org.thingsboard.server.common.data.alarm.Alarm; |
|||
import org.thingsboard.server.common.data.alarm.AlarmSeverity; |
|||
import org.thingsboard.server.common.data.device.profile.AlarmCondition; |
|||
import org.thingsboard.server.common.data.device.profile.AlarmRule; |
|||
import org.thingsboard.server.common.data.device.profile.DeviceProfileAlarm; |
|||
import org.thingsboard.server.common.data.device.profile.DeviceProfileData; |
|||
import org.thingsboard.server.common.data.id.DeviceId; |
|||
import org.thingsboard.server.common.data.id.DeviceProfileId; |
|||
import org.thingsboard.server.common.data.id.TenantId; |
|||
import org.thingsboard.server.common.data.query.EntityKey; |
|||
import org.thingsboard.server.common.data.query.EntityKeyType; |
|||
import org.thingsboard.server.common.data.query.EntityKeyValueType; |
|||
import org.thingsboard.server.common.data.query.FilterPredicateValue; |
|||
import org.thingsboard.server.common.data.query.KeyFilter; |
|||
import org.thingsboard.server.common.data.query.NumericFilterPredicate; |
|||
import org.thingsboard.server.common.msg.TbMsg; |
|||
import org.thingsboard.server.common.msg.TbMsgDataType; |
|||
import org.thingsboard.server.common.msg.TbMsgMetaData; |
|||
import org.thingsboard.server.common.msg.session.SessionMsgType; |
|||
import org.thingsboard.server.dao.alarm.AlarmService; |
|||
import org.thingsboard.server.dao.timeseries.TimeseriesService; |
|||
|
|||
import java.util.Collections; |
|||
import java.util.UUID; |
|||
|
|||
import static org.mockito.Mockito.verify; |
|||
|
|||
@RunWith(MockitoJUnitRunner.class) |
|||
public class TbDeviceProfileNodeTest { |
|||
|
|||
private static final ObjectMapper mapper = new ObjectMapper(); |
|||
|
|||
private TbDeviceProfileNode node; |
|||
|
|||
@Mock |
|||
private TbContext ctx; |
|||
@Mock |
|||
private RuleEngineDeviceProfileCache cache; |
|||
@Mock |
|||
private TimeseriesService timeseriesService; |
|||
@Mock |
|||
private RuleEngineAlarmService alarmService; |
|||
|
|||
private TenantId tenantId = new TenantId(UUID.randomUUID()); |
|||
private DeviceId deviceId = new DeviceId(UUID.randomUUID()); |
|||
private DeviceProfileId deviceProfileId = new DeviceProfileId(UUID.randomUUID()); |
|||
|
|||
@Test |
|||
public void testRandomMessageType() throws Exception { |
|||
init(); |
|||
|
|||
DeviceProfile deviceProfile = new DeviceProfile(); |
|||
DeviceProfileData deviceProfileData = new DeviceProfileData(); |
|||
deviceProfileData.setAlarms(Collections.emptyList()); |
|||
deviceProfile.setProfileData(deviceProfileData); |
|||
|
|||
Mockito.when(cache.get(tenantId, deviceId)).thenReturn(deviceProfile); |
|||
ObjectNode data = mapper.createObjectNode(); |
|||
data.put("temperature", 42); |
|||
TbMsg msg = TbMsg.newMsg("123456789", deviceId, new TbMsgMetaData(), |
|||
TbMsgDataType.JSON, mapper.writeValueAsString(data), null, null); |
|||
node.onMsg(ctx, msg); |
|||
verify(ctx).tellSuccess(msg); |
|||
verify(ctx, Mockito.never()).tellFailure(Mockito.any(), Mockito.any()); |
|||
} |
|||
|
|||
@Test |
|||
public void testEmptyProfile() throws Exception { |
|||
init(); |
|||
|
|||
DeviceProfile deviceProfile = new DeviceProfile(); |
|||
DeviceProfileData deviceProfileData = new DeviceProfileData(); |
|||
deviceProfileData.setAlarms(Collections.emptyList()); |
|||
deviceProfile.setProfileData(deviceProfileData); |
|||
|
|||
Mockito.when(cache.get(tenantId, deviceId)).thenReturn(deviceProfile); |
|||
ObjectNode data = mapper.createObjectNode(); |
|||
data.put("temperature", 42); |
|||
TbMsg msg = TbMsg.newMsg(SessionMsgType.POST_TELEMETRY_REQUEST.name(), deviceId, new TbMsgMetaData(), |
|||
TbMsgDataType.JSON, mapper.writeValueAsString(data), null, null); |
|||
node.onMsg(ctx, msg); |
|||
verify(ctx).tellSuccess(msg); |
|||
verify(ctx, Mockito.never()).tellFailure(Mockito.any(), Mockito.any()); |
|||
} |
|||
|
|||
@Test |
|||
public void testAlarmCreate() throws Exception { |
|||
init(); |
|||
|
|||
DeviceProfile deviceProfile = new DeviceProfile(); |
|||
DeviceProfileData deviceProfileData = new DeviceProfileData(); |
|||
KeyFilter highTempFilter = new KeyFilter(); |
|||
highTempFilter.setKey(new EntityKey(EntityKeyType.TIME_SERIES, "temperature")); |
|||
highTempFilter.setValueType(EntityKeyValueType.NUMERIC); |
|||
NumericFilterPredicate highTemperaturePredicate = new NumericFilterPredicate(); |
|||
highTemperaturePredicate.setOperation(NumericFilterPredicate.NumericOperation.GREATER); |
|||
highTemperaturePredicate.setValue(new FilterPredicateValue<>(30.0)); |
|||
highTempFilter.setPredicate(highTemperaturePredicate); |
|||
AlarmCondition alarmCondition = new AlarmCondition(); |
|||
alarmCondition.setCondition(Collections.singletonList(highTempFilter)); |
|||
AlarmRule alarmRule = new AlarmRule(); |
|||
alarmRule.setCondition(alarmCondition); |
|||
DeviceProfileAlarm dpa = new DeviceProfileAlarm(); |
|||
dpa.setId("highTemperatureAlarmID"); |
|||
dpa.setCreateRules(Collections.singletonMap(AlarmSeverity.CRITICAL, alarmRule)); |
|||
deviceProfileData.setAlarms(Collections.singletonList(dpa)); |
|||
deviceProfile.setProfileData(deviceProfileData); |
|||
|
|||
Mockito.when(cache.get(tenantId, deviceId)).thenReturn(deviceProfile); |
|||
Mockito.when(timeseriesService.findLatest(tenantId, deviceId, Collections.singleton("temperature"))) |
|||
.thenReturn(Futures.immediateFuture(Collections.emptyList())); |
|||
Mockito.when(alarmService.createOrUpdateAlarm(Mockito.any())).thenAnswer(AdditionalAnswers.returnsFirstArg()); |
|||
|
|||
TbMsg theMsg = TbMsg.newMsg("ALARM", deviceId, new TbMsgMetaData(), ""); |
|||
Mockito.when(ctx.newMsg(Mockito.anyString(), Mockito.anyString(), Mockito.any(), Mockito.any(), Mockito.anyString())).thenReturn(theMsg); |
|||
|
|||
ObjectNode data = mapper.createObjectNode(); |
|||
data.put("temperature", 42); |
|||
TbMsg msg = TbMsg.newMsg(SessionMsgType.POST_TELEMETRY_REQUEST.name(), deviceId, new TbMsgMetaData(), |
|||
TbMsgDataType.JSON, mapper.writeValueAsString(data), null, null); |
|||
node.onMsg(ctx, msg); |
|||
verify(ctx).tellSuccess(msg); |
|||
verify(ctx).tellNext(theMsg, "Alarm Created"); |
|||
verify(ctx, Mockito.never()).tellFailure(Mockito.any(), Mockito.any()); |
|||
|
|||
TbMsg theMsg2 = TbMsg.newMsg("ALARM", deviceId, new TbMsgMetaData(), "2"); |
|||
Mockito.when(ctx.newMsg(Mockito.anyString(), Mockito.anyString(), Mockito.any(), Mockito.any(), Mockito.anyString())).thenReturn(theMsg2); |
|||
|
|||
|
|||
TbMsg msg2 = TbMsg.newMsg(SessionMsgType.POST_TELEMETRY_REQUEST.name(), deviceId, new TbMsgMetaData(), |
|||
TbMsgDataType.JSON, mapper.writeValueAsString(data), null, null); |
|||
node.onMsg(ctx, msg2); |
|||
verify(ctx).tellSuccess(msg2); |
|||
verify(ctx).tellNext(theMsg2, "Alarm Updated"); |
|||
|
|||
} |
|||
|
|||
private void init() throws TbNodeException { |
|||
Mockito.when(ctx.getTenantId()).thenReturn(tenantId); |
|||
Mockito.when(ctx.getDeviceProfileCache()).thenReturn(cache); |
|||
Mockito.when(ctx.getTimeseriesService()).thenReturn(timeseriesService); |
|||
Mockito.when(ctx.getAlarmService()).thenReturn(alarmService); |
|||
TbNodeConfiguration nodeConfiguration = new TbNodeConfiguration(mapper.createObjectNode()); |
|||
node = new TbDeviceProfileNode(); |
|||
node.init(ctx, nodeConfiguration); |
|||
} |
|||
|
|||
} |
|||
Loading…
Reference in new issue