From 4a3b28d331afef9a74f070e9dd6b1d9f343f3ec7 Mon Sep 17 00:00:00 2001 From: Andrii Shvaika Date: Tue, 15 Sep 2020 18:16:38 +0300 Subject: [PATCH] Method to create default rule chain for device profile --- .../device_profile/rule_chain_template.json | 135 ++++++++++++++++++ .../controller/RuleChainController.java | 26 ++++ .../service/install/InstallScripts.java | 37 +++-- .../rule/DefaultRuleChainCreateRequest.java | 31 ++++ .../profile/DeviceProfileAlarmState.java | 17 ++- .../rule/engine/profile/DeviceState.java | 22 +-- .../profile/TbDeviceProfileNodeTest.java | 2 + 7 files changed, 252 insertions(+), 18 deletions(-) create mode 100644 application/src/main/data/json/tenant/device_profile/rule_chain_template.json create mode 100644 common/data/src/main/java/org/thingsboard/server/common/data/rule/DefaultRuleChainCreateRequest.java diff --git a/application/src/main/data/json/tenant/device_profile/rule_chain_template.json b/application/src/main/data/json/tenant/device_profile/rule_chain_template.json new file mode 100644 index 0000000000..a17e1cc6f0 --- /dev/null +++ b/application/src/main/data/json/tenant/device_profile/rule_chain_template.json @@ -0,0 +1,135 @@ +{ + "ruleChain": { + "additionalInfo": { + "description": "" + }, + "name": "Device Profile Rule Chain Template", + "firstRuleNodeId": null, + "root": false, + "debugMode": false, + "configuration": null + }, + "metadata": { + "firstNodeIndex": 6, + "nodes": [ + { + "additionalInfo": { + "layoutX": 822, + "layoutY": 294 + }, + "type": "org.thingsboard.rule.engine.telemetry.TbMsgTimeseriesNode", + "name": "Save Timeseries", + "debugMode": false, + "configuration": { + "defaultTTL": 0 + } + }, + { + "additionalInfo": { + "layoutX": 824, + "layoutY": 221 + }, + "type": "org.thingsboard.rule.engine.telemetry.TbMsgAttributesNode", + "name": "Save Client Attributes", + "debugMode": false, + "configuration": { + "scope": "CLIENT_SCOPE" + } + }, + { + "additionalInfo": { + "layoutX": 494, + "layoutY": 309 + }, + "type": "org.thingsboard.rule.engine.filter.TbMsgTypeSwitchNode", + "name": "Message Type Switch", + "debugMode": false, + "configuration": { + "version": 0 + } + }, + { + "additionalInfo": { + "layoutX": 824, + "layoutY": 383 + }, + "type": "org.thingsboard.rule.engine.action.TbLogNode", + "name": "Log RPC from Device", + "debugMode": false, + "configuration": { + "jsScript": "return '\\nIncoming message:\\n' + JSON.stringify(msg) + '\\nIncoming metadata:\\n' + JSON.stringify(metadata);" + } + }, + { + "additionalInfo": { + "layoutX": 823, + "layoutY": 444 + }, + "type": "org.thingsboard.rule.engine.action.TbLogNode", + "name": "Log Other", + "debugMode": false, + "configuration": { + "jsScript": "return '\\nIncoming message:\\n' + JSON.stringify(msg) + '\\nIncoming metadata:\\n' + JSON.stringify(metadata);" + } + }, + { + "additionalInfo": { + "layoutX": 822, + "layoutY": 507 + }, + "type": "org.thingsboard.rule.engine.rpc.TbSendRPCRequestNode", + "name": "RPC Call Request", + "debugMode": false, + "configuration": { + "timeoutInSeconds": 60 + } + }, + { + "additionalInfo": { + "description": "", + "layoutX": 209, + "layoutY": 307 + }, + "type": "org.thingsboard.rule.engine.profile.TbDeviceProfileNode", + "name": "Device Profile Node", + "debugMode": false, + "configuration": { + "version": 0 + } + } + ], + "connections": [ + { + "fromIndex": 2, + "toIndex": 4, + "type": "Other" + }, + { + "fromIndex": 2, + "toIndex": 1, + "type": "Post attributes" + }, + { + "fromIndex": 2, + "toIndex": 0, + "type": "Post telemetry" + }, + { + "fromIndex": 2, + "toIndex": 3, + "type": "RPC Request from Device" + }, + { + "fromIndex": 2, + "toIndex": 5, + "type": "RPC Request to Device" + }, + { + "fromIndex": 6, + "toIndex": 2, + "type": "Success" + } + ], + "ruleChainConnections": null + } +} \ No newline at end of file diff --git a/application/src/main/java/org/thingsboard/server/controller/RuleChainController.java b/application/src/main/java/org/thingsboard/server/controller/RuleChainController.java index 7e8291356d..c1c06544ed 100644 --- a/application/src/main/java/org/thingsboard/server/controller/RuleChainController.java +++ b/application/src/main/java/org/thingsboard/server/controller/RuleChainController.java @@ -47,6 +47,7 @@ import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.common.data.plugin.ComponentLifecycleEvent; +import org.thingsboard.server.common.data.rule.DefaultRuleChainCreateRequest; import org.thingsboard.server.common.data.rule.RuleChain; import org.thingsboard.server.common.data.rule.RuleChainMetaData; import org.thingsboard.server.common.data.rule.RuleNode; @@ -55,6 +56,7 @@ import org.thingsboard.server.common.msg.TbMsgDataType; import org.thingsboard.server.common.msg.TbMsgMetaData; import org.thingsboard.server.dao.event.EventService; import org.thingsboard.server.queue.util.TbCoreComponent; +import org.thingsboard.server.service.install.InstallScripts; import org.thingsboard.server.service.script.JsInvokeService; import org.thingsboard.server.service.script.RuleNodeJsScriptEngine; import org.thingsboard.server.service.security.permission.Operation; @@ -77,6 +79,9 @@ public class RuleChainController extends BaseController { private static final ObjectMapper objectMapper = new ObjectMapper(); + @Autowired + private InstallScripts installScripts; + @Autowired private EventService eventService; @@ -146,6 +151,27 @@ public class RuleChainController extends BaseController { } } + @PreAuthorize("hasAnyAuthority('TENANT_ADMIN')") + @RequestMapping(value = "/ruleChain/device/default", method = RequestMethod.POST) + @ResponseBody + public RuleChain saveRuleChain(@RequestBody DefaultRuleChainCreateRequest request) throws ThingsboardException { + try { + checkNotNull(request); + checkNotNull(request.getName()); + + RuleChain savedRuleChain = installScripts.createDefaultRuleChain(getCurrentUser().getTenantId(), request.getName()); + + logEntityAction(savedRuleChain.getId(), savedRuleChain, null, ActionType.ADDED, null); + + return savedRuleChain; + } catch (Exception e) { + RuleChain ruleChain = new RuleChain(); + ruleChain.setName(request.getName()); + logEntityAction(emptyId(EntityType.RULE_CHAIN), ruleChain, null, ActionType.ADDED, e); + throw handleException(e); + } + } + @PreAuthorize("hasAnyAuthority('TENANT_ADMIN')") @RequestMapping(value = "/ruleChain/{ruleChainId}/root", method = RequestMethod.POST) @ResponseBody diff --git a/application/src/main/java/org/thingsboard/server/service/install/InstallScripts.java b/application/src/main/java/org/thingsboard/server/service/install/InstallScripts.java index 5181834b80..eabbb495a4 100644 --- a/application/src/main/java/org/thingsboard/server/service/install/InstallScripts.java +++ b/application/src/main/java/org/thingsboard/server/service/install/InstallScripts.java @@ -57,6 +57,7 @@ public class InstallScripts { public static final String JSON_DIR = "json"; public static final String SYSTEM_DIR = "system"; public static final String TENANT_DIR = "tenant"; + public static final String DEVICE_PROFILE_DIR = "device_profile"; public static final String DEMO_DIR = "demo"; public static final String RULE_CHAINS_DIR = "rule_chains"; public static final String WIDGET_BUNDLES_DIR = "widget_bundles"; @@ -83,6 +84,10 @@ public class InstallScripts { return Paths.get(getDataDir(), JSON_DIR, TENANT_DIR, RULE_CHAINS_DIR); } + public Path getDeviceProfileDefaultRuleChainTemplateFilePath() { + return Paths.get(getDataDir(), JSON_DIR, DEVICE_PROFILE_DIR, "rule_chain_template.json"); + } + public String getDataDir() { if (!StringUtils.isEmpty(dataDir)) { if (!Paths.get(this.dataDir).toFile().isDirectory()) { @@ -110,15 +115,7 @@ public class InstallScripts { dirStream.forEach( path -> { try { - JsonNode ruleChainJson = objectMapper.readTree(path.toFile()); - RuleChain ruleChain = objectMapper.treeToValue(ruleChainJson.get("ruleChain"), RuleChain.class); - RuleChainMetaData ruleChainMetaData = objectMapper.treeToValue(ruleChainJson.get("metadata"), RuleChainMetaData.class); - - ruleChain.setTenantId(tenantId); - ruleChain = ruleChainService.saveRuleChain(ruleChain); - - ruleChainMetaData.setRuleChainId(ruleChain.getId()); - ruleChainService.saveRuleChainMetaData(new TenantId(EntityId.NULL_UUID), ruleChainMetaData); + createRuleChainFromFile(tenantId, path, null); } catch (Exception e) { log.error("Unable to load rule chain from json: [{}]", path.toString()); throw new RuntimeException("Unable to load rule chain from json", e); @@ -128,6 +125,28 @@ public class InstallScripts { } } + public RuleChain createDefaultRuleChain(TenantId tenantId, String ruleChainName) throws IOException { + return createRuleChainFromFile(tenantId, getDeviceProfileDefaultRuleChainTemplateFilePath(), ruleChainName); + } + + public RuleChain createRuleChainFromFile(TenantId tenantId, Path templateFilePath, String newRuleChainName) throws IOException { + JsonNode ruleChainJson = objectMapper.readTree(templateFilePath.toFile()); + RuleChain ruleChain = objectMapper.treeToValue(ruleChainJson.get("ruleChain"), RuleChain.class); + RuleChainMetaData ruleChainMetaData = objectMapper.treeToValue(ruleChainJson.get("metadata"), RuleChainMetaData.class); + + ruleChain.setTenantId(tenantId); + if (!StringUtils.isEmpty(newRuleChainName)) { + ruleChain.setName(newRuleChainName); + } + ruleChain = ruleChainService.saveRuleChain(ruleChain); + + ruleChainMetaData.setRuleChainId(ruleChain.getId()); + ruleChainService.saveRuleChainMetaData(new TenantId(EntityId.NULL_UUID), ruleChainMetaData); + + return ruleChain; + } + + public void loadSystemWidgets() throws Exception { Path widgetBundlesDir = Paths.get(getDataDir(), JSON_DIR, SYSTEM_DIR, WIDGET_BUNDLES_DIR); try (DirectoryStream dirStream = Files.newDirectoryStream(widgetBundlesDir, path -> path.toString().endsWith(JSON_EXT))) { diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/rule/DefaultRuleChainCreateRequest.java b/common/data/src/main/java/org/thingsboard/server/common/data/rule/DefaultRuleChainCreateRequest.java new file mode 100644 index 0000000000..0a921c6526 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/rule/DefaultRuleChainCreateRequest.java @@ -0,0 +1,31 @@ +/** + * 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.server.common.data.rule; + +import lombok.Data; +import lombok.extern.slf4j.Slf4j; + +import java.io.Serializable; + +@Data +@Slf4j +public class DefaultRuleChainCreateRequest implements Serializable { + + private static final long serialVersionUID = 5600333716030561537L; + + private String name; + +} diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/DeviceProfileAlarmState.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/DeviceProfileAlarmState.java index 8b086f942f..cdc44eaf3f 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/DeviceProfileAlarmState.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/DeviceProfileAlarmState.java @@ -22,6 +22,7 @@ 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.alarm.AlarmStatus; 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; @@ -34,11 +35,13 @@ 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.alarm.AlarmService; import org.thingsboard.server.dao.util.mapping.JacksonUtil; import java.util.Comparator; import java.util.Map; import java.util.TreeMap; +import java.util.concurrent.ExecutionException; @Data class DeviceProfileAlarmState { @@ -47,6 +50,7 @@ class DeviceProfileAlarmState { private final DeviceProfileAlarm alarmDefinition; private volatile Map createRulesSortedBySeverityDesc; private volatile Alarm currentAlarm; + private volatile boolean initialFetchDone; public DeviceProfileAlarmState(EntityId originator, DeviceProfileAlarm alarmDefinition) { this.originator = originator; @@ -55,7 +59,15 @@ class DeviceProfileAlarmState { this.createRulesSortedBySeverityDesc.putAll(alarmDefinition.getCreateRules()); } - public void process(TbContext ctx, TbMsg msg, DeviceDataSnapshot data) { + public void process(TbContext ctx, TbMsg msg, DeviceDataSnapshot data) throws ExecutionException, InterruptedException { + if (!initialFetchDone) { + Alarm alarm = ctx.getAlarmService().findLatestByOriginatorAndType(ctx.getTenantId(), originator, alarmDefinition.getAlarmType()).get(); + if (alarm != null && !alarm.getStatus().isCleared()) { + currentAlarm = alarm; + } + initialFetchDone = true; + } + AlarmSeverity resultSeverity = null; for (Map.Entry kv : createRulesSortedBySeverityDesc.entrySet()) { AlarmRule alarmRule = kv.getValue(); @@ -69,6 +81,7 @@ class DeviceProfileAlarmState { } else if (currentAlarm != null) { AlarmRule clearRule = alarmDefinition.getClearRule(); if (eval(clearRule.getCondition(), data)) { + ctx.getAlarmService().clearAlarm(ctx.getTenantId(), currentAlarm.getId(), JacksonUtil.OBJECT_MAPPER.createObjectNode(), System.currentTimeMillis()); pushMsg(ctx, new TbAlarmResult(false, false, true, currentAlarm), msg); currentAlarm = null; } @@ -112,6 +125,8 @@ class DeviceProfileAlarmState { } } else { currentAlarm = new Alarm(); + currentAlarm.setType(alarmDefinition.getAlarmType()); + currentAlarm.setStatus(AlarmStatus.ACTIVE_UNACK); currentAlarm.setSeverity(severity); currentAlarm.setStartTs(System.currentTimeMillis()); currentAlarm.setEndTs(currentAlarm.getStartTs()); diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/DeviceState.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/DeviceState.java index 2442778cf0..3ffe140928 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/DeviceState.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/DeviceState.java @@ -62,15 +62,17 @@ class DeviceState { } } - private void processTelemetry(TbContext ctx, TbMsg msg) { + private void processTelemetry(TbContext ctx, TbMsg msg) throws ExecutionException, InterruptedException { Map> tsKvMap = JsonConverter.convertToSortedTelemetry(new JsonParser().parse(msg.getData()), TbMsgTimeseriesNode.getTs(msg)); - tsKvMap.forEach((ts, data) -> { + for (Map.Entry> entry : tsKvMap.entrySet()) { + Long ts = entry.getKey(); + List data = entry.getValue(); 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); } @@ -140,7 +142,9 @@ class DeviceState { if (!latestTsKeys.isEmpty()) { List 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 (entry.getValue() != null) { + result.putValue(new EntityKey(EntityKeyType.TIME_SERIES, entry.getKey()), toEntityValue(entry)); + } } } if (!clientAttributeKeys.isEmpty()) { @@ -161,10 +165,12 @@ class DeviceState { private void addToSnapshot(DeviceDataSnapshot snapshot, Set commonAttributeKeys, List 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); + if (entry.getValue() != null) { + 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); + } } } } diff --git a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/profile/TbDeviceProfileNodeTest.java b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/profile/TbDeviceProfileNodeTest.java index b0612e5c2a..ba2c4a9f3f 100644 --- a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/profile/TbDeviceProfileNodeTest.java +++ b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/profile/TbDeviceProfileNodeTest.java @@ -137,6 +137,7 @@ public class TbDeviceProfileNodeTest { alarmRule.setCondition(alarmCondition); DeviceProfileAlarm dpa = new DeviceProfileAlarm(); dpa.setId("highTemperatureAlarmID"); + dpa.setAlarmType("highTemperatureAlarm"); dpa.setCreateRules(Collections.singletonMap(AlarmSeverity.CRITICAL, alarmRule)); deviceProfileData.setAlarms(Collections.singletonList(dpa)); deviceProfile.setProfileData(deviceProfileData); @@ -144,6 +145,7 @@ public class TbDeviceProfileNodeTest { 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.findLatestByOriginatorAndType(tenantId, deviceId, "highTemperatureAlarm")).thenReturn(Futures.immediateFuture(null)); Mockito.when(alarmService.createOrUpdateAlarm(Mockito.any())).thenAnswer(AdditionalAnswers.returnsFirstArg()); TbMsg theMsg = TbMsg.newMsg("ALARM", deviceId, new TbMsgMetaData(), "");