Commit 4a3b28d331afef9a74f070e9dd6b1d9f343f3ec7
1 parent
e353ab3c
Method to create default rule chain for device profile
Showing
7 changed files
with
252 additions
and
18 deletions
1 | +{ | |
2 | + "ruleChain": { | |
3 | + "additionalInfo": { | |
4 | + "description": "" | |
5 | + }, | |
6 | + "name": "Device Profile Rule Chain Template", | |
7 | + "firstRuleNodeId": null, | |
8 | + "root": false, | |
9 | + "debugMode": false, | |
10 | + "configuration": null | |
11 | + }, | |
12 | + "metadata": { | |
13 | + "firstNodeIndex": 6, | |
14 | + "nodes": [ | |
15 | + { | |
16 | + "additionalInfo": { | |
17 | + "layoutX": 822, | |
18 | + "layoutY": 294 | |
19 | + }, | |
20 | + "type": "org.thingsboard.rule.engine.telemetry.TbMsgTimeseriesNode", | |
21 | + "name": "Save Timeseries", | |
22 | + "debugMode": false, | |
23 | + "configuration": { | |
24 | + "defaultTTL": 0 | |
25 | + } | |
26 | + }, | |
27 | + { | |
28 | + "additionalInfo": { | |
29 | + "layoutX": 824, | |
30 | + "layoutY": 221 | |
31 | + }, | |
32 | + "type": "org.thingsboard.rule.engine.telemetry.TbMsgAttributesNode", | |
33 | + "name": "Save Client Attributes", | |
34 | + "debugMode": false, | |
35 | + "configuration": { | |
36 | + "scope": "CLIENT_SCOPE" | |
37 | + } | |
38 | + }, | |
39 | + { | |
40 | + "additionalInfo": { | |
41 | + "layoutX": 494, | |
42 | + "layoutY": 309 | |
43 | + }, | |
44 | + "type": "org.thingsboard.rule.engine.filter.TbMsgTypeSwitchNode", | |
45 | + "name": "Message Type Switch", | |
46 | + "debugMode": false, | |
47 | + "configuration": { | |
48 | + "version": 0 | |
49 | + } | |
50 | + }, | |
51 | + { | |
52 | + "additionalInfo": { | |
53 | + "layoutX": 824, | |
54 | + "layoutY": 383 | |
55 | + }, | |
56 | + "type": "org.thingsboard.rule.engine.action.TbLogNode", | |
57 | + "name": "Log RPC from Device", | |
58 | + "debugMode": false, | |
59 | + "configuration": { | |
60 | + "jsScript": "return '\\nIncoming message:\\n' + JSON.stringify(msg) + '\\nIncoming metadata:\\n' + JSON.stringify(metadata);" | |
61 | + } | |
62 | + }, | |
63 | + { | |
64 | + "additionalInfo": { | |
65 | + "layoutX": 823, | |
66 | + "layoutY": 444 | |
67 | + }, | |
68 | + "type": "org.thingsboard.rule.engine.action.TbLogNode", | |
69 | + "name": "Log Other", | |
70 | + "debugMode": false, | |
71 | + "configuration": { | |
72 | + "jsScript": "return '\\nIncoming message:\\n' + JSON.stringify(msg) + '\\nIncoming metadata:\\n' + JSON.stringify(metadata);" | |
73 | + } | |
74 | + }, | |
75 | + { | |
76 | + "additionalInfo": { | |
77 | + "layoutX": 822, | |
78 | + "layoutY": 507 | |
79 | + }, | |
80 | + "type": "org.thingsboard.rule.engine.rpc.TbSendRPCRequestNode", | |
81 | + "name": "RPC Call Request", | |
82 | + "debugMode": false, | |
83 | + "configuration": { | |
84 | + "timeoutInSeconds": 60 | |
85 | + } | |
86 | + }, | |
87 | + { | |
88 | + "additionalInfo": { | |
89 | + "description": "", | |
90 | + "layoutX": 209, | |
91 | + "layoutY": 307 | |
92 | + }, | |
93 | + "type": "org.thingsboard.rule.engine.profile.TbDeviceProfileNode", | |
94 | + "name": "Device Profile Node", | |
95 | + "debugMode": false, | |
96 | + "configuration": { | |
97 | + "version": 0 | |
98 | + } | |
99 | + } | |
100 | + ], | |
101 | + "connections": [ | |
102 | + { | |
103 | + "fromIndex": 2, | |
104 | + "toIndex": 4, | |
105 | + "type": "Other" | |
106 | + }, | |
107 | + { | |
108 | + "fromIndex": 2, | |
109 | + "toIndex": 1, | |
110 | + "type": "Post attributes" | |
111 | + }, | |
112 | + { | |
113 | + "fromIndex": 2, | |
114 | + "toIndex": 0, | |
115 | + "type": "Post telemetry" | |
116 | + }, | |
117 | + { | |
118 | + "fromIndex": 2, | |
119 | + "toIndex": 3, | |
120 | + "type": "RPC Request from Device" | |
121 | + }, | |
122 | + { | |
123 | + "fromIndex": 2, | |
124 | + "toIndex": 5, | |
125 | + "type": "RPC Request to Device" | |
126 | + }, | |
127 | + { | |
128 | + "fromIndex": 6, | |
129 | + "toIndex": 2, | |
130 | + "type": "Success" | |
131 | + } | |
132 | + ], | |
133 | + "ruleChainConnections": null | |
134 | + } | |
135 | +} | |
\ No newline at end of file | ... | ... |
... | ... | @@ -47,6 +47,7 @@ import org.thingsboard.server.common.data.id.TenantId; |
47 | 47 | import org.thingsboard.server.common.data.page.PageData; |
48 | 48 | import org.thingsboard.server.common.data.page.PageLink; |
49 | 49 | import org.thingsboard.server.common.data.plugin.ComponentLifecycleEvent; |
50 | +import org.thingsboard.server.common.data.rule.DefaultRuleChainCreateRequest; | |
50 | 51 | import org.thingsboard.server.common.data.rule.RuleChain; |
51 | 52 | import org.thingsboard.server.common.data.rule.RuleChainMetaData; |
52 | 53 | import org.thingsboard.server.common.data.rule.RuleNode; |
... | ... | @@ -55,6 +56,7 @@ import org.thingsboard.server.common.msg.TbMsgDataType; |
55 | 56 | import org.thingsboard.server.common.msg.TbMsgMetaData; |
56 | 57 | import org.thingsboard.server.dao.event.EventService; |
57 | 58 | import org.thingsboard.server.queue.util.TbCoreComponent; |
59 | +import org.thingsboard.server.service.install.InstallScripts; | |
58 | 60 | import org.thingsboard.server.service.script.JsInvokeService; |
59 | 61 | import org.thingsboard.server.service.script.RuleNodeJsScriptEngine; |
60 | 62 | import org.thingsboard.server.service.security.permission.Operation; |
... | ... | @@ -78,6 +80,9 @@ public class RuleChainController extends BaseController { |
78 | 80 | private static final ObjectMapper objectMapper = new ObjectMapper(); |
79 | 81 | |
80 | 82 | @Autowired |
83 | + private InstallScripts installScripts; | |
84 | + | |
85 | + @Autowired | |
81 | 86 | private EventService eventService; |
82 | 87 | |
83 | 88 | @Autowired |
... | ... | @@ -147,6 +152,27 @@ public class RuleChainController extends BaseController { |
147 | 152 | } |
148 | 153 | |
149 | 154 | @PreAuthorize("hasAnyAuthority('TENANT_ADMIN')") |
155 | + @RequestMapping(value = "/ruleChain/device/default", method = RequestMethod.POST) | |
156 | + @ResponseBody | |
157 | + public RuleChain saveRuleChain(@RequestBody DefaultRuleChainCreateRequest request) throws ThingsboardException { | |
158 | + try { | |
159 | + checkNotNull(request); | |
160 | + checkNotNull(request.getName()); | |
161 | + | |
162 | + RuleChain savedRuleChain = installScripts.createDefaultRuleChain(getCurrentUser().getTenantId(), request.getName()); | |
163 | + | |
164 | + logEntityAction(savedRuleChain.getId(), savedRuleChain, null, ActionType.ADDED, null); | |
165 | + | |
166 | + return savedRuleChain; | |
167 | + } catch (Exception e) { | |
168 | + RuleChain ruleChain = new RuleChain(); | |
169 | + ruleChain.setName(request.getName()); | |
170 | + logEntityAction(emptyId(EntityType.RULE_CHAIN), ruleChain, null, ActionType.ADDED, e); | |
171 | + throw handleException(e); | |
172 | + } | |
173 | + } | |
174 | + | |
175 | + @PreAuthorize("hasAnyAuthority('TENANT_ADMIN')") | |
150 | 176 | @RequestMapping(value = "/ruleChain/{ruleChainId}/root", method = RequestMethod.POST) |
151 | 177 | @ResponseBody |
152 | 178 | public RuleChain setRootRuleChain(@PathVariable(RULE_CHAIN_ID) String strRuleChainId) throws ThingsboardException { | ... | ... |
... | ... | @@ -57,6 +57,7 @@ public class InstallScripts { |
57 | 57 | public static final String JSON_DIR = "json"; |
58 | 58 | public static final String SYSTEM_DIR = "system"; |
59 | 59 | public static final String TENANT_DIR = "tenant"; |
60 | + public static final String DEVICE_PROFILE_DIR = "device_profile"; | |
60 | 61 | public static final String DEMO_DIR = "demo"; |
61 | 62 | public static final String RULE_CHAINS_DIR = "rule_chains"; |
62 | 63 | public static final String WIDGET_BUNDLES_DIR = "widget_bundles"; |
... | ... | @@ -83,6 +84,10 @@ public class InstallScripts { |
83 | 84 | return Paths.get(getDataDir(), JSON_DIR, TENANT_DIR, RULE_CHAINS_DIR); |
84 | 85 | } |
85 | 86 | |
87 | + public Path getDeviceProfileDefaultRuleChainTemplateFilePath() { | |
88 | + return Paths.get(getDataDir(), JSON_DIR, DEVICE_PROFILE_DIR, "rule_chain_template.json"); | |
89 | + } | |
90 | + | |
86 | 91 | public String getDataDir() { |
87 | 92 | if (!StringUtils.isEmpty(dataDir)) { |
88 | 93 | if (!Paths.get(this.dataDir).toFile().isDirectory()) { |
... | ... | @@ -110,15 +115,7 @@ public class InstallScripts { |
110 | 115 | dirStream.forEach( |
111 | 116 | path -> { |
112 | 117 | try { |
113 | - JsonNode ruleChainJson = objectMapper.readTree(path.toFile()); | |
114 | - RuleChain ruleChain = objectMapper.treeToValue(ruleChainJson.get("ruleChain"), RuleChain.class); | |
115 | - RuleChainMetaData ruleChainMetaData = objectMapper.treeToValue(ruleChainJson.get("metadata"), RuleChainMetaData.class); | |
116 | - | |
117 | - ruleChain.setTenantId(tenantId); | |
118 | - ruleChain = ruleChainService.saveRuleChain(ruleChain); | |
119 | - | |
120 | - ruleChainMetaData.setRuleChainId(ruleChain.getId()); | |
121 | - ruleChainService.saveRuleChainMetaData(new TenantId(EntityId.NULL_UUID), ruleChainMetaData); | |
118 | + createRuleChainFromFile(tenantId, path, null); | |
122 | 119 | } catch (Exception e) { |
123 | 120 | log.error("Unable to load rule chain from json: [{}]", path.toString()); |
124 | 121 | throw new RuntimeException("Unable to load rule chain from json", e); |
... | ... | @@ -128,6 +125,28 @@ public class InstallScripts { |
128 | 125 | } |
129 | 126 | } |
130 | 127 | |
128 | + public RuleChain createDefaultRuleChain(TenantId tenantId, String ruleChainName) throws IOException { | |
129 | + return createRuleChainFromFile(tenantId, getDeviceProfileDefaultRuleChainTemplateFilePath(), ruleChainName); | |
130 | + } | |
131 | + | |
132 | + public RuleChain createRuleChainFromFile(TenantId tenantId, Path templateFilePath, String newRuleChainName) throws IOException { | |
133 | + JsonNode ruleChainJson = objectMapper.readTree(templateFilePath.toFile()); | |
134 | + RuleChain ruleChain = objectMapper.treeToValue(ruleChainJson.get("ruleChain"), RuleChain.class); | |
135 | + RuleChainMetaData ruleChainMetaData = objectMapper.treeToValue(ruleChainJson.get("metadata"), RuleChainMetaData.class); | |
136 | + | |
137 | + ruleChain.setTenantId(tenantId); | |
138 | + if (!StringUtils.isEmpty(newRuleChainName)) { | |
139 | + ruleChain.setName(newRuleChainName); | |
140 | + } | |
141 | + ruleChain = ruleChainService.saveRuleChain(ruleChain); | |
142 | + | |
143 | + ruleChainMetaData.setRuleChainId(ruleChain.getId()); | |
144 | + ruleChainService.saveRuleChainMetaData(new TenantId(EntityId.NULL_UUID), ruleChainMetaData); | |
145 | + | |
146 | + return ruleChain; | |
147 | + } | |
148 | + | |
149 | + | |
131 | 150 | public void loadSystemWidgets() throws Exception { |
132 | 151 | Path widgetBundlesDir = Paths.get(getDataDir(), JSON_DIR, SYSTEM_DIR, WIDGET_BUNDLES_DIR); |
133 | 152 | try (DirectoryStream<Path> dirStream = Files.newDirectoryStream(widgetBundlesDir, path -> path.toString().endsWith(JSON_EXT))) { | ... | ... |
common/data/src/main/java/org/thingsboard/server/common/data/rule/DefaultRuleChainCreateRequest.java
0 → 100644
1 | +/** | |
2 | + * Copyright © 2016-2020 The Thingsboard Authors | |
3 | + * | |
4 | + * Licensed under the Apache License, Version 2.0 (the "License"); | |
5 | + * you may not use this file except in compliance with the License. | |
6 | + * You may obtain a copy of the License at | |
7 | + * | |
8 | + * http://www.apache.org/licenses/LICENSE-2.0 | |
9 | + * | |
10 | + * Unless required by applicable law or agreed to in writing, software | |
11 | + * distributed under the License is distributed on an "AS IS" BASIS, | |
12 | + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
13 | + * See the License for the specific language governing permissions and | |
14 | + * limitations under the License. | |
15 | + */ | |
16 | +package org.thingsboard.server.common.data.rule; | |
17 | + | |
18 | +import lombok.Data; | |
19 | +import lombok.extern.slf4j.Slf4j; | |
20 | + | |
21 | +import java.io.Serializable; | |
22 | + | |
23 | +@Data | |
24 | +@Slf4j | |
25 | +public class DefaultRuleChainCreateRequest implements Serializable { | |
26 | + | |
27 | + private static final long serialVersionUID = 5600333716030561537L; | |
28 | + | |
29 | + private String name; | |
30 | + | |
31 | +} | ... | ... |
... | ... | @@ -22,6 +22,7 @@ import org.thingsboard.rule.engine.api.TbContext; |
22 | 22 | import org.thingsboard.server.common.data.DataConstants; |
23 | 23 | import org.thingsboard.server.common.data.alarm.Alarm; |
24 | 24 | import org.thingsboard.server.common.data.alarm.AlarmSeverity; |
25 | +import org.thingsboard.server.common.data.alarm.AlarmStatus; | |
25 | 26 | import org.thingsboard.server.common.data.device.profile.AlarmCondition; |
26 | 27 | import org.thingsboard.server.common.data.device.profile.AlarmRule; |
27 | 28 | import org.thingsboard.server.common.data.device.profile.DeviceProfileAlarm; |
... | ... | @@ -34,11 +35,13 @@ import org.thingsboard.server.common.data.query.NumericFilterPredicate; |
34 | 35 | import org.thingsboard.server.common.data.query.StringFilterPredicate; |
35 | 36 | import org.thingsboard.server.common.msg.TbMsg; |
36 | 37 | import org.thingsboard.server.common.msg.TbMsgMetaData; |
38 | +import org.thingsboard.server.dao.alarm.AlarmService; | |
37 | 39 | import org.thingsboard.server.dao.util.mapping.JacksonUtil; |
38 | 40 | |
39 | 41 | import java.util.Comparator; |
40 | 42 | import java.util.Map; |
41 | 43 | import java.util.TreeMap; |
44 | +import java.util.concurrent.ExecutionException; | |
42 | 45 | |
43 | 46 | @Data |
44 | 47 | class DeviceProfileAlarmState { |
... | ... | @@ -47,6 +50,7 @@ class DeviceProfileAlarmState { |
47 | 50 | private final DeviceProfileAlarm alarmDefinition; |
48 | 51 | private volatile Map<AlarmSeverity, AlarmRule> createRulesSortedBySeverityDesc; |
49 | 52 | private volatile Alarm currentAlarm; |
53 | + private volatile boolean initialFetchDone; | |
50 | 54 | |
51 | 55 | public DeviceProfileAlarmState(EntityId originator, DeviceProfileAlarm alarmDefinition) { |
52 | 56 | this.originator = originator; |
... | ... | @@ -55,7 +59,15 @@ class DeviceProfileAlarmState { |
55 | 59 | this.createRulesSortedBySeverityDesc.putAll(alarmDefinition.getCreateRules()); |
56 | 60 | } |
57 | 61 | |
58 | - public void process(TbContext ctx, TbMsg msg, DeviceDataSnapshot data) { | |
62 | + public void process(TbContext ctx, TbMsg msg, DeviceDataSnapshot data) throws ExecutionException, InterruptedException { | |
63 | + if (!initialFetchDone) { | |
64 | + Alarm alarm = ctx.getAlarmService().findLatestByOriginatorAndType(ctx.getTenantId(), originator, alarmDefinition.getAlarmType()).get(); | |
65 | + if (alarm != null && !alarm.getStatus().isCleared()) { | |
66 | + currentAlarm = alarm; | |
67 | + } | |
68 | + initialFetchDone = true; | |
69 | + } | |
70 | + | |
59 | 71 | AlarmSeverity resultSeverity = null; |
60 | 72 | for (Map.Entry<AlarmSeverity, AlarmRule> kv : createRulesSortedBySeverityDesc.entrySet()) { |
61 | 73 | AlarmRule alarmRule = kv.getValue(); |
... | ... | @@ -69,6 +81,7 @@ class DeviceProfileAlarmState { |
69 | 81 | } else if (currentAlarm != null) { |
70 | 82 | AlarmRule clearRule = alarmDefinition.getClearRule(); |
71 | 83 | if (eval(clearRule.getCondition(), data)) { |
84 | + ctx.getAlarmService().clearAlarm(ctx.getTenantId(), currentAlarm.getId(), JacksonUtil.OBJECT_MAPPER.createObjectNode(), System.currentTimeMillis()); | |
72 | 85 | pushMsg(ctx, new TbAlarmResult(false, false, true, currentAlarm), msg); |
73 | 86 | currentAlarm = null; |
74 | 87 | } |
... | ... | @@ -112,6 +125,8 @@ class DeviceProfileAlarmState { |
112 | 125 | } |
113 | 126 | } else { |
114 | 127 | currentAlarm = new Alarm(); |
128 | + currentAlarm.setType(alarmDefinition.getAlarmType()); | |
129 | + currentAlarm.setStatus(AlarmStatus.ACTIVE_UNACK); | |
115 | 130 | currentAlarm.setSeverity(severity); |
116 | 131 | currentAlarm.setStartTs(System.currentTimeMillis()); |
117 | 132 | currentAlarm.setEndTs(currentAlarm.getStartTs()); | ... | ... |
... | ... | @@ -62,15 +62,17 @@ class DeviceState { |
62 | 62 | } |
63 | 63 | } |
64 | 64 | |
65 | - private void processTelemetry(TbContext ctx, TbMsg msg) { | |
65 | + private void processTelemetry(TbContext ctx, TbMsg msg) throws ExecutionException, InterruptedException { | |
66 | 66 | Map<Long, List<KvEntry>> tsKvMap = JsonConverter.convertToSortedTelemetry(new JsonParser().parse(msg.getData()), TbMsgTimeseriesNode.getTs(msg)); |
67 | - tsKvMap.forEach((ts, data) -> { | |
67 | + for (Map.Entry<Long, List<KvEntry>> entry : tsKvMap.entrySet()) { | |
68 | + Long ts = entry.getKey(); | |
69 | + List<KvEntry> data = entry.getValue(); | |
68 | 70 | latestValues = merge(latestValues, ts, data); |
69 | 71 | for (DeviceProfileAlarm alarm : deviceProfile.getAlarmSettings()) { |
70 | 72 | DeviceProfileAlarmState alarmState = alarmStates.computeIfAbsent(alarm.getId(), a -> new DeviceProfileAlarmState(msg.getOriginator(), alarm)); |
71 | 73 | alarmState.process(ctx, msg, latestValues); |
72 | 74 | } |
73 | - }); | |
75 | + } | |
74 | 76 | ctx.tellSuccess(msg); |
75 | 77 | } |
76 | 78 | |
... | ... | @@ -140,7 +142,9 @@ class DeviceState { |
140 | 142 | if (!latestTsKeys.isEmpty()) { |
141 | 143 | List<TsKvEntry> data = ctx.getTimeseriesService().findLatest(ctx.getTenantId(), originator, latestTsKeys).get(); |
142 | 144 | for (TsKvEntry entry : data) { |
143 | - result.putValue(new EntityKey(EntityKeyType.TIME_SERIES, entry.getKey()), toEntityValue(entry)); | |
145 | + if (entry.getValue() != null) { | |
146 | + result.putValue(new EntityKey(EntityKeyType.TIME_SERIES, entry.getKey()), toEntityValue(entry)); | |
147 | + } | |
144 | 148 | } |
145 | 149 | } |
146 | 150 | if (!clientAttributeKeys.isEmpty()) { |
... | ... | @@ -161,10 +165,12 @@ class DeviceState { |
161 | 165 | |
162 | 166 | private void addToSnapshot(DeviceDataSnapshot snapshot, Set<String> commonAttributeKeys, List<AttributeKvEntry> data) { |
163 | 167 | for (AttributeKvEntry entry : data) { |
164 | - EntityKeyValue value = toEntityValue(entry); | |
165 | - snapshot.putValue(new EntityKey(EntityKeyType.CLIENT_ATTRIBUTE, entry.getKey()), value); | |
166 | - if (commonAttributeKeys.contains(entry.getKey())) { | |
167 | - snapshot.putValue(new EntityKey(EntityKeyType.ATTRIBUTE, entry.getKey()), value); | |
168 | + if (entry.getValue() != null) { | |
169 | + EntityKeyValue value = toEntityValue(entry); | |
170 | + snapshot.putValue(new EntityKey(EntityKeyType.CLIENT_ATTRIBUTE, entry.getKey()), value); | |
171 | + if (commonAttributeKeys.contains(entry.getKey())) { | |
172 | + snapshot.putValue(new EntityKey(EntityKeyType.ATTRIBUTE, entry.getKey()), value); | |
173 | + } | |
168 | 174 | } |
169 | 175 | } |
170 | 176 | } | ... | ... |
... | ... | @@ -137,6 +137,7 @@ public class TbDeviceProfileNodeTest { |
137 | 137 | alarmRule.setCondition(alarmCondition); |
138 | 138 | DeviceProfileAlarm dpa = new DeviceProfileAlarm(); |
139 | 139 | dpa.setId("highTemperatureAlarmID"); |
140 | + dpa.setAlarmType("highTemperatureAlarm"); | |
140 | 141 | dpa.setCreateRules(Collections.singletonMap(AlarmSeverity.CRITICAL, alarmRule)); |
141 | 142 | deviceProfileData.setAlarms(Collections.singletonList(dpa)); |
142 | 143 | deviceProfile.setProfileData(deviceProfileData); |
... | ... | @@ -144,6 +145,7 @@ public class TbDeviceProfileNodeTest { |
144 | 145 | Mockito.when(cache.get(tenantId, deviceId)).thenReturn(deviceProfile); |
145 | 146 | Mockito.when(timeseriesService.findLatest(tenantId, deviceId, Collections.singleton("temperature"))) |
146 | 147 | .thenReturn(Futures.immediateFuture(Collections.emptyList())); |
148 | + Mockito.when(alarmService.findLatestByOriginatorAndType(tenantId, deviceId, "highTemperatureAlarm")).thenReturn(Futures.immediateFuture(null)); | |
147 | 149 | Mockito.when(alarmService.createOrUpdateAlarm(Mockito.any())).thenAnswer(AdditionalAnswers.returnsFirstArg()); |
148 | 150 | |
149 | 151 | TbMsg theMsg = TbMsg.newMsg("ALARM", deviceId, new TbMsgMetaData(), ""); | ... | ... |