63 changed files with 5133 additions and 215 deletions
@ -0,0 +1,25 @@ |
|||
{ |
|||
"widgetsBundle": { |
|||
"alias": "date", |
|||
"title": "Date", |
|||
"image": null |
|||
}, |
|||
"widgetTypes": [ |
|||
{ |
|||
"alias": "date_range_navigator", |
|||
"name": "Date-range-navigator", |
|||
"descriptor": { |
|||
"type": "static", |
|||
"sizeX": 5, |
|||
"sizeY": 5.5, |
|||
"resources": [], |
|||
"templateHtml": "<date-range-navigator-widget class=\"date-range-navigator-widget\" ctx=\"ctx\"></date-range-navigator-widget>", |
|||
"templateCss": "", |
|||
"controllerScript": "self.onInit = function() {\n scope = self.ctx.$scope;\n scope.ctx = self.ctx;\n}", |
|||
"settingsSchema": "{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"Settings\",\n \"properties\": {\n \"hidePicker\": {\n \"title\": \"Hide date range picker\",\n \"type\": \"boolean\",\n \"default\": false\n },\n \"onePanel\": {\n \"title\": \"Date range picker one panel\",\n \"type\": \"boolean\",\n \"default\": false\n },\n \"autoConfirm\": {\n \"title\": \"Date range picker auto confirm\",\n \"type\": \"boolean\",\n \"default\": false\n },\n \"showTemplate\": {\n \"title\": \"Date range picker show template\",\n \"type\": \"boolean\",\n \"default\": false\n },\n \"firstDayOfWeek\": {\n \"title\": \"First day of the week\",\n \"type\": \"number\",\n \"default\": 1\n },\n \"hideInterval\": {\n \"title\": \"Hide interval\",\n \"type\": \"boolean\",\n \"default\": false\n },\n \"initialInterval\": {\n\t\t\t\t\"title\": \"Initial interval\",\n\t\t\t\t\"type\": \"string\",\n\t\t\t\t\"default\": \"week\"\n\t\t\t},\n \"hideStepSize\": {\n \"title\": \"Hide step size\",\n \"type\": \"boolean\",\n \"default\": false\n },\n \"stepSize\": {\n\t\t\t\t\"title\": \"Initial step size\",\n\t\t\t\t\"type\": \"string\",\n\t\t\t\t\"default\": \"day\"\n\t\t\t},\n \"hideLabels\": {\n \"title\": \"Hide labels\",\n \"type\": \"boolean\",\n \"default\": false\n },\n \"useSessionStorage\": {\n \"title\": \"Use session storage\",\n \"type\": \"boolean\",\n \"default\": true\n }\n }\n },\n \"form\": [\n \"hidePicker\",\n\t\t\"onePanel\",\n\t\t\"autoConfirm\",\n\t\t\"showTemplate\",\n\t\t\"firstDayOfWeek\",\n \"hideInterval\",\n {\n\t\t\t\"key\": \"initialInterval\",\n\t\t\t\"type\": \"rc-select\",\n\t\t\t\"multiple\": false,\n\t\t\t\"items\": [\n\t\t\t\t{\n\t\t\t\t\t\"value\": \"hour\",\n\t\t\t\t\t\"label\": \"Hour\"\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"value\": \"day\",\n\t\t\t\t\t\"label\": \"Day\"\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"value\": \"week\",\n\t\t\t\t\t\"label\": \"Week\"\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"value\": \"twoWeeks\",\n\t\t\t\t\t\"label\": \"2 weeks\"\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"value\": \"month\",\n\t\t\t\t\t\"label\": \"Month\"\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"value\": \"threeMonths\",\n\t\t\t\t\t\"label\": \"3 months\"\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"value\": \"sixMonths\",\n\t\t\t\t\t\"label\": \"6 months\"\n\t\t\t\t}\n\t\t\t]\n\t\t},\n \"hideStepSize\",\n {\n\t\t\t\"key\": \"stepSize\",\n\t\t\t\"type\": \"rc-select\",\n\t\t\t\"multiple\": false,\n\t\t\t\"items\": [\n\t\t\t\t{\n\t\t\t\t\t\"value\": \"hour\",\n\t\t\t\t\t\"label\": \"Hour\"\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"value\": \"day\",\n\t\t\t\t\t\"label\": \"Day\"\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"value\": \"week\",\n\t\t\t\t\t\"label\": \"Week\"\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"value\": \"twoWeeks\",\n\t\t\t\t\t\"label\": \"2 weeks\"\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"value\": \"month\",\n\t\t\t\t\t\"label\": \"Month\"\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"value\": \"threeMonths\",\n\t\t\t\t\t\"label\": \"3 months\"\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"value\": \"sixMonths\",\n\t\t\t\t\t\"label\": \"6 months\"\n\t\t\t\t}\n\t\t\t]\n\t\t},\n\t\t\"hideLabels\",\n\t\t\"useSessionStorage\"\n ]\n}", |
|||
"dataKeySettingsSchema": "{}\n", |
|||
"defaultConfig": "{\"datasources\":[{\"type\":\"static\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Random\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.15479322438769105,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < -1000) {\\n\\tvalue = -1000;\\n} else if (value > 1000) {\\n\\tvalue = 1000;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"rgb(255, 255, 255)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"defaultInterval\":\"week\",\"stepSize\":\"day\"},\"title\":\"Date-range-navigator\",\"dropShadow\":true,\"enableFullscreen\":true,\"widgetStyle\":{},\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"useDashboardTimewindow\":true,\"showLegend\":false,\"actions\":{}}" |
|||
} |
|||
} |
|||
] |
|||
} |
|||
File diff suppressed because one or more lines are too long
@ -0,0 +1,266 @@ |
|||
/** |
|||
* Copyright © 2016-2019 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 com.datastax.driver.core.BoundStatement; |
|||
import com.datastax.driver.core.CodecRegistry; |
|||
import com.datastax.driver.core.ConsistencyLevel; |
|||
import com.datastax.driver.core.PreparedStatement; |
|||
import com.datastax.driver.core.ResultSet; |
|||
import com.datastax.driver.core.ResultSetFuture; |
|||
import com.datastax.driver.core.Session; |
|||
import com.datastax.driver.core.Statement; |
|||
import com.datastax.driver.core.TypeCodec; |
|||
import com.datastax.driver.core.exceptions.CodecNotFoundException; |
|||
import com.google.common.base.Function; |
|||
import com.google.common.util.concurrent.Futures; |
|||
import com.google.common.util.concurrent.ListenableFuture; |
|||
import com.google.gson.JsonElement; |
|||
import com.google.gson.JsonObject; |
|||
import com.google.gson.JsonParser; |
|||
import com.google.gson.JsonPrimitive; |
|||
import lombok.extern.slf4j.Slf4j; |
|||
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.util.TbNodeUtils; |
|||
import org.thingsboard.server.common.data.plugin.ComponentType; |
|||
import org.thingsboard.server.common.msg.TbMsg; |
|||
import org.thingsboard.server.dao.cassandra.CassandraCluster; |
|||
import org.thingsboard.server.dao.model.ModelConstants; |
|||
import org.thingsboard.server.dao.model.type.AuthorityCodec; |
|||
import org.thingsboard.server.dao.model.type.ComponentLifecycleStateCodec; |
|||
import org.thingsboard.server.dao.model.type.ComponentScopeCodec; |
|||
import org.thingsboard.server.dao.model.type.ComponentTypeCodec; |
|||
import org.thingsboard.server.dao.model.type.DeviceCredentialsTypeCodec; |
|||
import org.thingsboard.server.dao.model.type.EntityTypeCodec; |
|||
import org.thingsboard.server.dao.model.type.JsonCodec; |
|||
import org.thingsboard.server.dao.nosql.CassandraStatementTask; |
|||
|
|||
import javax.annotation.Nullable; |
|||
import java.util.ArrayList; |
|||
import java.util.List; |
|||
import java.util.Map; |
|||
import java.util.concurrent.ExecutionException; |
|||
import java.util.concurrent.ExecutorService; |
|||
import java.util.concurrent.Executors; |
|||
import java.util.concurrent.atomic.AtomicInteger; |
|||
|
|||
import static org.thingsboard.rule.engine.api.TbRelationTypes.SUCCESS; |
|||
import static org.thingsboard.rule.engine.api.util.DonAsynchron.withCallback; |
|||
|
|||
@Slf4j |
|||
@RuleNode(type = ComponentType.ACTION, |
|||
name = "save to custom table", |
|||
configClazz = TbSaveToCustomCassandraTableNodeConfiguration.class, |
|||
nodeDescription = "Node stores data from incoming Message payload to the Cassandra database into the predefined custom table" + |
|||
" that should have <b>cs_tb_</b> prefix, to avoid the data insertion to the common TB tables.<br>" + |
|||
"<b>Note:</b> rule node can be used only for Cassandra DB.", |
|||
nodeDetails = "Administrator should set the custom table name without prefix: <b>cs_tb_</b>. <br>" + |
|||
"Administrator can configure the mapping between the Message field names and Table columns name.<br>" + |
|||
"<b>Note:</b>If the mapping key is <b>$entity_id</b>, that is identified by the Message Originator, then to the appropriate column name(mapping value) will be write the message originator id.<br><br>" + |
|||
"If specified message field does not exist or is not a JSON Primitive, the outbound message will be routed via <b>failure</b> chain," + |
|||
" otherwise, the message will be routed via <b>success</b> chain.", |
|||
uiResources = {"static/rulenode/rulenode-core-config.js"}, |
|||
configDirective = "tbActionNodeCustomTableConfig", |
|||
icon = "file_upload") |
|||
public class TbSaveToCustomCassandraTableNode implements TbNode { |
|||
|
|||
private static final String TABLE_PREFIX = "cs_tb_"; |
|||
private static final JsonParser parser = new JsonParser(); |
|||
private static final String ENTITY_ID = "$entityId"; |
|||
|
|||
private TbSaveToCustomCassandraTableNodeConfiguration config; |
|||
private Session session; |
|||
private CassandraCluster cassandraCluster; |
|||
private ConsistencyLevel defaultWriteLevel; |
|||
private PreparedStatement saveStmt; |
|||
private ExecutorService readResultsProcessingExecutor; |
|||
private Map<String, String> fieldsMap; |
|||
|
|||
@Override |
|||
public void init(TbContext ctx, TbNodeConfiguration configuration) throws TbNodeException { |
|||
config = TbNodeUtils.convert(configuration, TbSaveToCustomCassandraTableNodeConfiguration.class); |
|||
cassandraCluster = ctx.getCassandraCluster(); |
|||
if (cassandraCluster == null) { |
|||
throw new RuntimeException("Unable to connect to Cassandra database"); |
|||
} else { |
|||
startExecutor(); |
|||
saveStmt = getSaveStmt(); |
|||
} |
|||
} |
|||
|
|||
@Override |
|||
public void onMsg(TbContext ctx, TbMsg msg) throws ExecutionException, InterruptedException, TbNodeException { |
|||
withCallback(save(msg, ctx), aVoid -> { |
|||
ctx.tellNext(msg, SUCCESS); |
|||
}, e -> ctx.tellFailure(msg, e), ctx.getDbCallbackExecutor()); |
|||
} |
|||
|
|||
@Override |
|||
public void destroy() { |
|||
stopExecutor(); |
|||
saveStmt = null; |
|||
} |
|||
|
|||
private void startExecutor() { |
|||
readResultsProcessingExecutor = Executors.newCachedThreadPool(); |
|||
} |
|||
|
|||
private void stopExecutor() { |
|||
if (readResultsProcessingExecutor != null) { |
|||
readResultsProcessingExecutor.shutdownNow(); |
|||
} |
|||
} |
|||
|
|||
private PreparedStatement prepare(String query) { |
|||
return getSession().prepare(query); |
|||
} |
|||
|
|||
private Session getSession() { |
|||
if (session == null) { |
|||
session = cassandraCluster.getSession(); |
|||
defaultWriteLevel = cassandraCluster.getDefaultWriteConsistencyLevel(); |
|||
CodecRegistry registry = session.getCluster().getConfiguration().getCodecRegistry(); |
|||
registerCodecIfNotFound(registry, new JsonCodec()); |
|||
registerCodecIfNotFound(registry, new DeviceCredentialsTypeCodec()); |
|||
registerCodecIfNotFound(registry, new AuthorityCodec()); |
|||
registerCodecIfNotFound(registry, new ComponentLifecycleStateCodec()); |
|||
registerCodecIfNotFound(registry, new ComponentTypeCodec()); |
|||
registerCodecIfNotFound(registry, new ComponentScopeCodec()); |
|||
registerCodecIfNotFound(registry, new EntityTypeCodec()); |
|||
} |
|||
return session; |
|||
} |
|||
|
|||
private void registerCodecIfNotFound(CodecRegistry registry, TypeCodec<?> codec) { |
|||
try { |
|||
registry.codecFor(codec.getCqlType(), codec.getJavaType()); |
|||
} catch (CodecNotFoundException e) { |
|||
registry.register(codec); |
|||
} |
|||
} |
|||
|
|||
|
|||
private PreparedStatement getSaveStmt() { |
|||
fieldsMap = config.getFieldsMapping(); |
|||
if (fieldsMap.isEmpty()) { |
|||
throw new RuntimeException("Fields(key,value) map is empty!"); |
|||
} else { |
|||
return prepareStatement(new ArrayList<>(fieldsMap.values())); |
|||
} |
|||
} |
|||
|
|||
private PreparedStatement prepareStatement(List<String> fieldsList) { |
|||
return prepare(createQuery(fieldsList)); |
|||
} |
|||
|
|||
private String createQuery(List<String> fieldsList) { |
|||
int size = fieldsList.size(); |
|||
StringBuilder query = new StringBuilder(); |
|||
query.append("INSERT INTO ") |
|||
.append(TABLE_PREFIX) |
|||
.append(config.getTableName()) |
|||
.append("("); |
|||
for (String field : fieldsList) { |
|||
query.append(field); |
|||
if (fieldsList.get(size - 1).equals(field)) { |
|||
query.append(")"); |
|||
} else { |
|||
query.append(","); |
|||
} |
|||
} |
|||
query.append(" VALUES("); |
|||
for (int i = 0; i < size; i++) { |
|||
if (i == size - 1) { |
|||
query.append("?)"); |
|||
} else { |
|||
query.append("?, "); |
|||
} |
|||
} |
|||
return query.toString(); |
|||
} |
|||
|
|||
private ListenableFuture<Void> save(TbMsg msg, TbContext ctx) { |
|||
JsonElement data = parser.parse(msg.getData()); |
|||
if (!data.isJsonObject()) { |
|||
throw new IllegalStateException("Invalid message structure, it is not a JSON Object:" + data); |
|||
} else { |
|||
JsonObject dataAsObject = data.getAsJsonObject(); |
|||
BoundStatement stmt = saveStmt.bind(); |
|||
AtomicInteger i = new AtomicInteger(0); |
|||
fieldsMap.forEach((key, value) -> { |
|||
if (key.equals(ENTITY_ID)) { |
|||
stmt.setUUID(i.get(), msg.getOriginator().getId()); |
|||
} else if (dataAsObject.has(key)) { |
|||
if (dataAsObject.get(key).isJsonPrimitive()) { |
|||
JsonPrimitive primitive = dataAsObject.get(key).getAsJsonPrimitive(); |
|||
if (primitive.isNumber()) { |
|||
stmt.setLong(i.get(), dataAsObject.get(key).getAsLong()); |
|||
} else if (primitive.isBoolean()) { |
|||
stmt.setBool(i.get(), dataAsObject.get(key).getAsBoolean()); |
|||
} else if (primitive.isString()) { |
|||
stmt.setString(i.get(), dataAsObject.get(key).getAsString()); |
|||
} else { |
|||
stmt.setToNull(i.get()); |
|||
} |
|||
} else { |
|||
throw new IllegalStateException("Message data key: '" + key + "' with value: '" + value + "' is not a JSON Primitive!"); |
|||
} |
|||
} else { |
|||
throw new RuntimeException("Message data doesn't contain key: " + "'" + key + "'!"); |
|||
} |
|||
i.getAndIncrement(); |
|||
}); |
|||
return getFuture(executeAsyncWrite(ctx, stmt), rs -> null); |
|||
} |
|||
} |
|||
|
|||
private ResultSetFuture executeAsyncWrite(TbContext ctx, Statement statement) { |
|||
return executeAsync(ctx, statement, defaultWriteLevel); |
|||
} |
|||
|
|||
private ResultSetFuture executeAsync(TbContext ctx, Statement statement, ConsistencyLevel level) { |
|||
if (log.isDebugEnabled()) { |
|||
log.debug("Execute cassandra async statement {}", statementToString(statement)); |
|||
} |
|||
if (statement.getConsistencyLevel() == null) { |
|||
statement.setConsistencyLevel(level); |
|||
} |
|||
return ctx.getCassandraBufferedRateExecutor().submit(new CassandraStatementTask(ctx.getTenantId(), getSession(), statement)); |
|||
} |
|||
|
|||
private static String statementToString(Statement statement) { |
|||
if (statement instanceof BoundStatement) { |
|||
return ((BoundStatement) statement).preparedStatement().getQueryString(); |
|||
} else { |
|||
return statement.toString(); |
|||
} |
|||
} |
|||
|
|||
private <T> ListenableFuture<T> getFuture(ResultSetFuture future, java.util.function.Function<ResultSet, T> transformer) { |
|||
return Futures.transform(future, new Function<ResultSet, T>() { |
|||
@Nullable |
|||
@Override |
|||
public T apply(@Nullable ResultSet input) { |
|||
return transformer.apply(input); |
|||
} |
|||
}, readResultsProcessingExecutor); |
|||
} |
|||
|
|||
} |
|||
@ -0,0 +1,41 @@ |
|||
/** |
|||
* Copyright © 2016-2019 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.Data; |
|||
import org.thingsboard.rule.engine.api.NodeConfiguration; |
|||
|
|||
import java.util.HashMap; |
|||
import java.util.Map; |
|||
|
|||
@Data |
|||
public class TbSaveToCustomCassandraTableNodeConfiguration implements NodeConfiguration<TbSaveToCustomCassandraTableNodeConfiguration> { |
|||
|
|||
|
|||
private String tableName; |
|||
private Map<String, String> fieldsMapping; |
|||
|
|||
|
|||
@Override |
|||
public TbSaveToCustomCassandraTableNodeConfiguration defaultConfiguration() { |
|||
TbSaveToCustomCassandraTableNodeConfiguration configuration = new TbSaveToCustomCassandraTableNodeConfiguration(); |
|||
configuration.setTableName(""); |
|||
Map<String, String> map = new HashMap<>(); |
|||
map.put("", ""); |
|||
configuration.setFieldsMapping(map); |
|||
return configuration; |
|||
} |
|||
} |
|||
@ -0,0 +1,131 @@ |
|||
/** |
|||
* Copyright © 2016-2019 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.geo; |
|||
|
|||
import com.google.gson.JsonElement; |
|||
import com.google.gson.JsonObject; |
|||
import com.google.gson.JsonParser; |
|||
import org.locationtech.spatial4j.context.jts.JtsSpatialContext; |
|||
import org.locationtech.spatial4j.context.jts.JtsSpatialContextFactory; |
|||
import org.springframework.util.StringUtils; |
|||
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.util.TbNodeUtils; |
|||
import org.thingsboard.server.common.msg.TbMsg; |
|||
|
|||
import java.util.Collections; |
|||
import java.util.List; |
|||
|
|||
public abstract class AbstractGeofencingNode<T extends TbGpsGeofencingFilterNodeConfiguration> implements TbNode { |
|||
|
|||
protected T config; |
|||
protected JtsSpatialContext jtsCtx; |
|||
|
|||
@Override |
|||
public void init(TbContext ctx, TbNodeConfiguration configuration) throws TbNodeException { |
|||
this.config = TbNodeUtils.convert(configuration, getConfigClazz()); |
|||
JtsSpatialContextFactory factory = new JtsSpatialContextFactory(); |
|||
factory.normWrapLongitude = true; |
|||
jtsCtx = factory.newSpatialContext(); |
|||
} |
|||
|
|||
abstract protected Class<T> getConfigClazz(); |
|||
|
|||
protected boolean checkMatches(TbMsg msg) throws TbNodeException { |
|||
JsonElement msgDataElement = new JsonParser().parse(msg.getData()); |
|||
if (!msgDataElement.isJsonObject()) { |
|||
throw new TbNodeException("Incoming Message is not a valid JSON object"); |
|||
} |
|||
JsonObject msgDataObj = msgDataElement.getAsJsonObject(); |
|||
double latitude = getValueFromMessageByName(msg, msgDataObj, config.getLatitudeKeyName()); |
|||
double longitude = getValueFromMessageByName(msg, msgDataObj, config.getLongitudeKeyName()); |
|||
List<Perimeter> perimeters = getPerimeters(msg, msgDataObj); |
|||
boolean matches = false; |
|||
for (Perimeter perimeter : perimeters) { |
|||
if (checkMatches(perimeter, latitude, longitude)) { |
|||
matches = true; |
|||
break; |
|||
} |
|||
} |
|||
return matches; |
|||
} |
|||
|
|||
protected boolean checkMatches(Perimeter perimeter, double latitude, double longitude) throws TbNodeException { |
|||
if (perimeter.getPerimeterType() == PerimeterType.CIRCLE) { |
|||
Coordinates entityCoordinates = new Coordinates(latitude, longitude); |
|||
Coordinates perimeterCoordinates = new Coordinates(perimeter.getCenterLatitude(), perimeter.getCenterLongitude()); |
|||
return perimeter.getRange() > GeoUtil.distance(entityCoordinates, perimeterCoordinates, perimeter.getRangeUnit()); |
|||
} else if (perimeter.getPerimeterType() == PerimeterType.POLYGON) { |
|||
return GeoUtil.contains(perimeter.getPolygonsDefinition(), new Coordinates(latitude, longitude)); |
|||
} else { |
|||
throw new TbNodeException("Unsupported perimeter type: " + perimeter.getPerimeterType()); |
|||
} |
|||
} |
|||
|
|||
protected List<Perimeter> getPerimeters(TbMsg msg, JsonObject msgDataObj) throws TbNodeException { |
|||
if (config.isFetchPerimeterInfoFromMessageMetadata()) { |
|||
//TODO: add fetching perimeters from the message itself, if configuration is empty.
|
|||
if (!StringUtils.isEmpty(msg.getMetaData().getValue("perimeter"))) { |
|||
Perimeter perimeter = new Perimeter(); |
|||
perimeter.setPerimeterType(PerimeterType.POLYGON); |
|||
perimeter.setPolygonsDefinition(msg.getMetaData().getValue("perimeter")); |
|||
return Collections.singletonList(perimeter); |
|||
} else if (!StringUtils.isEmpty(msg.getMetaData().getValue("centerLatitude"))) { |
|||
Perimeter perimeter = new Perimeter(); |
|||
perimeter.setPerimeterType(PerimeterType.CIRCLE); |
|||
perimeter.setCenterLatitude(Double.parseDouble(msg.getMetaData().getValue("centerLatitude"))); |
|||
perimeter.setCenterLongitude(Double.parseDouble(msg.getMetaData().getValue("centerLongitude"))); |
|||
perimeter.setRange(Double.parseDouble(msg.getMetaData().getValue("range"))); |
|||
perimeter.setRangeUnit(RangeUnit.valueOf(msg.getMetaData().getValue("rangeUnit"))); |
|||
return Collections.singletonList(perimeter); |
|||
} else { |
|||
throw new TbNodeException("Missing perimeter definition!"); |
|||
} |
|||
} else { |
|||
Perimeter perimeter = new Perimeter(); |
|||
perimeter.setPerimeterType(config.getPerimeterType()); |
|||
perimeter.setCenterLatitude(config.getCenterLatitude()); |
|||
perimeter.setCenterLongitude(config.getCenterLongitude()); |
|||
perimeter.setRange(config.getRange()); |
|||
perimeter.setRangeUnit(config.getRangeUnit()); |
|||
perimeter.setPolygonsDefinition(config.getPolygonsDefinition()); |
|||
return Collections.singletonList(perimeter); |
|||
} |
|||
} |
|||
|
|||
protected Double getValueFromMessageByName(TbMsg msg, JsonObject msgDataObj, String keyName) throws TbNodeException { |
|||
double value; |
|||
if (msgDataObj.has(keyName) && msgDataObj.get(keyName).isJsonPrimitive()) { |
|||
value = msgDataObj.get(keyName).getAsDouble(); |
|||
} else { |
|||
String valueStr = msg.getMetaData().getValue(keyName); |
|||
if (!StringUtils.isEmpty(valueStr)) { |
|||
value = Double.parseDouble(valueStr); |
|||
} else { |
|||
throw new TbNodeException("Incoming Message has no " + keyName + " in data or metadata!"); |
|||
} |
|||
} |
|||
return value; |
|||
} |
|||
|
|||
@Override |
|||
public void destroy() { |
|||
|
|||
} |
|||
|
|||
} |
|||
@ -0,0 +1,24 @@ |
|||
/** |
|||
* Copyright © 2016-2019 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.geo; |
|||
|
|||
import lombok.Data; |
|||
|
|||
@Data |
|||
public class Coordinates { |
|||
private final double latitude; |
|||
private final double longitude; |
|||
} |
|||
@ -0,0 +1,29 @@ |
|||
/** |
|||
* Copyright © 2016-2019 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.geo; |
|||
|
|||
import lombok.AllArgsConstructor; |
|||
import lombok.Data; |
|||
|
|||
@Data |
|||
@AllArgsConstructor |
|||
public class EntityGeofencingState { |
|||
|
|||
private boolean inside; |
|||
private long stateSwitchTime; |
|||
private boolean stayed; |
|||
|
|||
} |
|||
@ -0,0 +1,68 @@ |
|||
/** |
|||
* Copyright © 2016-2019 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.geo; |
|||
|
|||
import com.google.gson.JsonArray; |
|||
import com.google.gson.JsonElement; |
|||
import com.google.gson.JsonParser; |
|||
import org.locationtech.spatial4j.context.SpatialContext; |
|||
import org.locationtech.spatial4j.context.jts.JtsSpatialContext; |
|||
import org.locationtech.spatial4j.context.jts.JtsSpatialContextFactory; |
|||
import org.locationtech.spatial4j.distance.DistanceUtils; |
|||
import org.locationtech.spatial4j.shape.Point; |
|||
import org.locationtech.spatial4j.shape.Shape; |
|||
import org.locationtech.spatial4j.shape.ShapeFactory; |
|||
import org.locationtech.spatial4j.shape.SpatialRelation; |
|||
|
|||
public class GeoUtil { |
|||
|
|||
private static final SpatialContext distCtx = SpatialContext.GEO; |
|||
private static final JtsSpatialContext jtsCtx; |
|||
|
|||
static { |
|||
JtsSpatialContextFactory factory = new JtsSpatialContextFactory(); |
|||
factory.normWrapLongitude = true; |
|||
jtsCtx = factory.newSpatialContext(); |
|||
} |
|||
|
|||
public static synchronized double distance(Coordinates x, Coordinates y, RangeUnit unit) { |
|||
Point xLL = distCtx.getShapeFactory().pointXY(x.getLongitude(), x.getLatitude()); |
|||
Point yLL = distCtx.getShapeFactory().pointXY(y.getLongitude(), y.getLatitude()); |
|||
return unit.fromKm(distCtx.getDistCalc().distance(xLL, yLL) * DistanceUtils.DEG_TO_KM); |
|||
} |
|||
|
|||
public static synchronized boolean contains(String polygon, Coordinates coordinates) { |
|||
ShapeFactory.PolygonBuilder polygonBuilder = jtsCtx.getShapeFactory().polygon(); |
|||
JsonArray polygonArray = new JsonParser().parse(polygon).getAsJsonArray(); |
|||
boolean first = true; |
|||
double firstLat = 0.0; |
|||
double firstLng = 0.0; |
|||
for (JsonElement jsonElement : polygonArray) { |
|||
double lat = jsonElement.getAsJsonArray().get(0).getAsDouble(); |
|||
double lng = jsonElement.getAsJsonArray().get(1).getAsDouble(); |
|||
if (first) { |
|||
firstLat = lat; |
|||
firstLng = lng; |
|||
first = false; |
|||
} |
|||
polygonBuilder.pointXY(jtsCtx.getShapeFactory().normX(lng), jtsCtx.getShapeFactory().normY(lat)); |
|||
} |
|||
polygonBuilder.pointXY(jtsCtx.getShapeFactory().normX(firstLng), jtsCtx.getShapeFactory().normY(firstLat)); |
|||
Shape shape = polygonBuilder.buildOrRect(); |
|||
Point point = jtsCtx.makePoint(coordinates.getLongitude(), coordinates.getLatitude()); |
|||
return shape.relate(point).equals(SpatialRelation.CONTAINS); |
|||
} |
|||
} |
|||
@ -0,0 +1,34 @@ |
|||
/** |
|||
* Copyright © 2016-2019 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.geo; |
|||
|
|||
import lombok.Data; |
|||
|
|||
@Data |
|||
public class Perimeter { |
|||
|
|||
private PerimeterType perimeterType; |
|||
|
|||
//For Polygons
|
|||
private String polygonsDefinition; |
|||
|
|||
//For Circles
|
|||
private Double centerLatitude; |
|||
private Double centerLongitude; |
|||
private Double range; |
|||
private RangeUnit rangeUnit; |
|||
|
|||
} |
|||
@ -0,0 +1,20 @@ |
|||
/** |
|||
* Copyright © 2016-2019 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.geo; |
|||
|
|||
public enum PerimeterType { |
|||
CIRCLE, POLYGON |
|||
} |
|||
@ -0,0 +1,30 @@ |
|||
/** |
|||
* Copyright © 2016-2019 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.geo; |
|||
|
|||
public enum RangeUnit { |
|||
METER(1000.0), KILOMETER(1.0), FOOT(3280.84), MILE(0.62137), NAUTICAL_MILE(0.539957); |
|||
|
|||
private final double fromKm; |
|||
|
|||
RangeUnit(double fromKm) { |
|||
this.fromKm = fromKm; |
|||
} |
|||
|
|||
public double fromKm(double v) { |
|||
return v * fromKm; |
|||
} |
|||
} |
|||
@ -0,0 +1,120 @@ |
|||
/** |
|||
* Copyright © 2016-2019 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.geo; |
|||
|
|||
import com.google.gson.Gson; |
|||
import com.google.gson.JsonObject; |
|||
import com.google.gson.JsonParser; |
|||
import lombok.extern.slf4j.Slf4j; |
|||
import org.thingsboard.rule.engine.api.RuleNode; |
|||
import org.thingsboard.rule.engine.api.TbContext; |
|||
import org.thingsboard.rule.engine.api.TbNodeException; |
|||
import org.thingsboard.server.common.data.DataConstants; |
|||
import org.thingsboard.server.common.data.id.EntityId; |
|||
import org.thingsboard.server.common.data.kv.AttributeKvEntry; |
|||
import org.thingsboard.server.common.data.kv.BaseAttributeKvEntry; |
|||
import org.thingsboard.server.common.data.kv.StringDataEntry; |
|||
import org.thingsboard.server.common.data.plugin.ComponentType; |
|||
import org.thingsboard.server.common.msg.TbMsg; |
|||
|
|||
import java.util.Collections; |
|||
import java.util.HashMap; |
|||
import java.util.List; |
|||
import java.util.Map; |
|||
import java.util.Optional; |
|||
import java.util.concurrent.ExecutionException; |
|||
import java.util.concurrent.TimeUnit; |
|||
import java.util.concurrent.TimeoutException; |
|||
|
|||
/** |
|||
* Created by ashvayka on 19.01.18. |
|||
*/ |
|||
@Slf4j |
|||
@RuleNode( |
|||
type = ComponentType.ACTION, |
|||
name = "gps geofencing events", |
|||
configClazz = TbGpsGeofencingActionNodeConfiguration.class, |
|||
relationTypes = {"Entered", "Left", "Inside", "Outside"}, |
|||
nodeDescription = "Produces incoming messages using GPS based geofencing", |
|||
nodeDetails = "Extracts latitude and longitude parameters from incoming message and returns different events based on configuration parameters", |
|||
uiResources = {"static/rulenode/rulenode-core-config.js"}, |
|||
configDirective = "tbActionNodeGpsGeofencingConfig") |
|||
public class TbGpsGeofencingActionNode extends AbstractGeofencingNode<TbGpsGeofencingActionNodeConfiguration> { |
|||
|
|||
private final Map<EntityId, EntityGeofencingState> entityStates = new HashMap<>(); |
|||
private final Gson gson = new Gson(); |
|||
private final JsonParser parser = new JsonParser(); |
|||
|
|||
@Override |
|||
public void onMsg(TbContext ctx, TbMsg msg) throws TbNodeException { |
|||
boolean matches = checkMatches(msg); |
|||
long ts = System.currentTimeMillis(); |
|||
|
|||
EntityGeofencingState entityState = entityStates.computeIfAbsent(msg.getOriginator(), key -> { |
|||
try { |
|||
Optional<AttributeKvEntry> entry = ctx.getAttributesService() |
|||
.find(ctx.getTenantId(), msg.getOriginator(), DataConstants.SERVER_SCOPE, ctx.getNodeId()) |
|||
.get(1, TimeUnit.MINUTES); |
|||
if (entry.isPresent()) { |
|||
JsonObject element = parser.parse(entry.get().getValueAsString()).getAsJsonObject(); |
|||
return new EntityGeofencingState(element.get("inside").getAsBoolean(), element.get("stateSwitchTime").getAsLong(), element.get("stayed").getAsBoolean()); |
|||
} else { |
|||
return new EntityGeofencingState(false, 0L, false); |
|||
} |
|||
} catch (InterruptedException | TimeoutException | ExecutionException e) { |
|||
throw new RuntimeException(e); |
|||
} |
|||
}); |
|||
if (entityState.getStateSwitchTime() == 0L || entityState.isInside() != matches) { |
|||
switchState(ctx, msg.getOriginator(), entityState, matches, ts); |
|||
ctx.tellNext(msg, matches ? "Entered" : "Left"); |
|||
} else if (!entityState.isStayed()) { |
|||
long stayTime = ts - entityState.getStateSwitchTime(); |
|||
if (stayTime > (entityState.isInside() ? |
|||
TimeUnit.valueOf(config.getMinInsideDurationTimeUnit()).toMillis(config.getMinInsideDuration()) : TimeUnit.valueOf(config.getMinOutsideDurationTimeUnit()).toMillis(config.getMinOutsideDuration()))) { |
|||
setStaid(ctx, msg.getOriginator(), entityState); |
|||
ctx.tellNext(msg, entityState.isInside() ? "Inside" : "Outside"); |
|||
} |
|||
} |
|||
} |
|||
|
|||
private void switchState(TbContext ctx, EntityId entityId, EntityGeofencingState entityState, boolean matches, long ts) { |
|||
entityState.setInside(matches); |
|||
entityState.setStateSwitchTime(ts); |
|||
entityState.setStayed(false); |
|||
persist(ctx, entityId, entityState); |
|||
} |
|||
|
|||
private void setStaid(TbContext ctx, EntityId entityId, EntityGeofencingState entityState) { |
|||
entityState.setStayed(true); |
|||
persist(ctx, entityId, entityState); |
|||
} |
|||
|
|||
private void persist(TbContext ctx, EntityId entityId, EntityGeofencingState entityState) { |
|||
JsonObject object = new JsonObject(); |
|||
object.addProperty("inside", entityState.isInside()); |
|||
object.addProperty("stateSwitchTime", entityState.getStateSwitchTime()); |
|||
object.addProperty("stayed", entityState.isStayed()); |
|||
AttributeKvEntry entry = new BaseAttributeKvEntry(new StringDataEntry(ctx.getNodeId(), gson.toJson(object)), System.currentTimeMillis()); |
|||
List<AttributeKvEntry> attributeKvEntryList = Collections.singletonList(entry); |
|||
ctx.getAttributesService().save(ctx.getTenantId(), entityId, DataConstants.SERVER_SCOPE, attributeKvEntryList); |
|||
} |
|||
|
|||
@Override |
|||
protected Class<TbGpsGeofencingActionNodeConfiguration> getConfigClazz() { |
|||
return TbGpsGeofencingActionNodeConfiguration.class; |
|||
} |
|||
} |
|||
@ -0,0 +1,49 @@ |
|||
/** |
|||
* Copyright © 2016-2019 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.geo; |
|||
|
|||
import lombok.Data; |
|||
import org.thingsboard.rule.engine.api.NodeConfiguration; |
|||
|
|||
import java.util.Collections; |
|||
import java.util.List; |
|||
import java.util.concurrent.TimeUnit; |
|||
|
|||
/** |
|||
* Created by ashvayka on 19.01.18. |
|||
*/ |
|||
@Data |
|||
public class TbGpsGeofencingActionNodeConfiguration extends TbGpsGeofencingFilterNodeConfiguration { |
|||
|
|||
private int minInsideDuration; |
|||
private int minOutsideDuration; |
|||
|
|||
private String minInsideDurationTimeUnit; |
|||
private String minOutsideDurationTimeUnit; |
|||
|
|||
@Override |
|||
public TbGpsGeofencingActionNodeConfiguration defaultConfiguration() { |
|||
TbGpsGeofencingActionNodeConfiguration configuration = new TbGpsGeofencingActionNodeConfiguration(); |
|||
configuration.setLatitudeKeyName("latitude"); |
|||
configuration.setLongitudeKeyName("longitude"); |
|||
configuration.setFetchPerimeterInfoFromMessageMetadata(true); |
|||
configuration.setMinInsideDurationTimeUnit(TimeUnit.MINUTES.name()); |
|||
configuration.setMinOutsideDurationTimeUnit(TimeUnit.MINUTES.name()); |
|||
configuration.setMinInsideDuration(1); |
|||
configuration.setMinOutsideDuration(1); |
|||
return configuration; |
|||
} |
|||
} |
|||
@ -0,0 +1,67 @@ |
|||
/** |
|||
* Copyright © 2016-2019 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.geo; |
|||
|
|||
import com.google.gson.JsonArray; |
|||
import com.google.gson.JsonElement; |
|||
import com.google.gson.JsonObject; |
|||
import com.google.gson.JsonParser; |
|||
import lombok.extern.slf4j.Slf4j; |
|||
import org.locationtech.spatial4j.context.jts.JtsSpatialContext; |
|||
import org.locationtech.spatial4j.context.jts.JtsSpatialContextFactory; |
|||
import org.locationtech.spatial4j.shape.Point; |
|||
import org.locationtech.spatial4j.shape.Shape; |
|||
import org.locationtech.spatial4j.shape.ShapeFactory; |
|||
import org.locationtech.spatial4j.shape.SpatialRelation; |
|||
import org.springframework.util.StringUtils; |
|||
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.util.TbNodeUtils; |
|||
import org.thingsboard.rule.engine.filter.TbMsgTypeFilterNodeConfiguration; |
|||
import org.thingsboard.server.common.data.plugin.ComponentType; |
|||
import org.thingsboard.server.common.msg.TbMsg; |
|||
|
|||
import java.util.Collections; |
|||
import java.util.List; |
|||
|
|||
/** |
|||
* Created by ashvayka on 19.01.18. |
|||
*/ |
|||
@Slf4j |
|||
@RuleNode( |
|||
type = ComponentType.FILTER, |
|||
name = "gps geofencing filter", |
|||
configClazz = TbGpsGeofencingFilterNodeConfiguration.class, |
|||
relationTypes = {"True", "False"}, |
|||
nodeDescription = "Filter incoming messages by GPS based geofencing", |
|||
nodeDetails = "Extracts latitude and longitude parameters from incoming message and returns 'True' if they are inside configured perimeters, 'False' otherwise.", |
|||
uiResources = {"static/rulenode/rulenode-core-config.js"}, |
|||
configDirective = "tbFilterNodeGpsGeofencingConfig") |
|||
public class TbGpsGeofencingFilterNode extends AbstractGeofencingNode<TbGpsGeofencingFilterNodeConfiguration> { |
|||
|
|||
@Override |
|||
public void onMsg(TbContext ctx, TbMsg msg) throws TbNodeException { |
|||
ctx.tellNext(msg, checkMatches(msg) ? "True" : "False"); |
|||
} |
|||
|
|||
@Override |
|||
protected Class<TbGpsGeofencingFilterNodeConfiguration> getConfigClazz() { |
|||
return TbGpsGeofencingFilterNodeConfiguration.class; |
|||
} |
|||
} |
|||
@ -0,0 +1,56 @@ |
|||
/** |
|||
* Copyright © 2016-2019 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.geo; |
|||
|
|||
import lombok.Data; |
|||
import org.thingsboard.rule.engine.api.NodeConfiguration; |
|||
import org.thingsboard.server.common.data.EntityType; |
|||
import org.thingsboard.server.common.msg.session.SessionMsgType; |
|||
|
|||
import java.util.Arrays; |
|||
import java.util.Collections; |
|||
import java.util.List; |
|||
|
|||
/** |
|||
* Created by ashvayka on 19.01.18. |
|||
*/ |
|||
@Data |
|||
public class TbGpsGeofencingFilterNodeConfiguration implements NodeConfiguration<TbGpsGeofencingFilterNodeConfiguration> { |
|||
|
|||
private String latitudeKeyName; |
|||
private String longitudeKeyName; |
|||
private boolean fetchPerimeterInfoFromMessageMetadata; |
|||
|
|||
private PerimeterType perimeterType; |
|||
|
|||
//For Polygons
|
|||
private String polygonsDefinition; |
|||
|
|||
//For Circles
|
|||
private Double centerLatitude; |
|||
private Double centerLongitude; |
|||
private Double range; |
|||
private RangeUnit rangeUnit; |
|||
|
|||
@Override |
|||
public TbGpsGeofencingFilterNodeConfiguration defaultConfiguration() { |
|||
TbGpsGeofencingFilterNodeConfiguration configuration = new TbGpsGeofencingFilterNodeConfiguration(); |
|||
configuration.setLatitudeKeyName("latitude"); |
|||
configuration.setLongitudeKeyName("longitude"); |
|||
configuration.setFetchPerimeterInfoFromMessageMetadata(true); |
|||
return configuration; |
|||
} |
|||
} |
|||
@ -0,0 +1,135 @@ |
|||
/** |
|||
* Copyright © 2016-2019 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.metadata; |
|||
|
|||
import com.google.gson.Gson; |
|||
import com.google.gson.JsonElement; |
|||
import com.google.gson.JsonObject; |
|||
import com.google.gson.JsonParser; |
|||
import com.google.gson.reflect.TypeToken; |
|||
import lombok.AllArgsConstructor; |
|||
import lombok.Data; |
|||
import lombok.extern.slf4j.Slf4j; |
|||
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.util.EntityDetails; |
|||
import org.thingsboard.server.common.data.ContactBased; |
|||
import org.thingsboard.server.common.msg.TbMsg; |
|||
import org.thingsboard.server.common.msg.TbMsgMetaData; |
|||
|
|||
import java.lang.reflect.Type; |
|||
import java.util.Map; |
|||
import java.util.concurrent.ExecutionException; |
|||
|
|||
import static org.thingsboard.rule.engine.api.TbRelationTypes.SUCCESS; |
|||
|
|||
@Slf4j |
|||
public abstract class TbAbstractGetEntityDetailsNode<C extends TbAbstractGetEntityDetailsNodeConfiguration> implements TbNode { |
|||
|
|||
private static final Gson gson = new Gson(); |
|||
private static final JsonParser jsonParser = new JsonParser(); |
|||
private static final Type TYPE = new TypeToken<Map<String, String>>() {}.getType(); |
|||
|
|||
protected C config; |
|||
|
|||
@Override |
|||
public void init(TbContext ctx, TbNodeConfiguration configuration) throws TbNodeException { |
|||
this.config = loadGetEntityDetailsNodeConfiguration(configuration); |
|||
} |
|||
|
|||
@Override |
|||
public void onMsg(TbContext ctx, TbMsg msg) { |
|||
try { |
|||
ctx.tellNext(getDetails(ctx, msg), SUCCESS); |
|||
} catch (Exception e) { |
|||
ctx.tellFailure(msg, e); |
|||
} |
|||
} |
|||
|
|||
@Override |
|||
public void destroy() {} |
|||
|
|||
protected abstract C loadGetEntityDetailsNodeConfiguration(TbNodeConfiguration configuration) throws TbNodeException; |
|||
|
|||
protected abstract TbMsg getDetails(TbContext ctx, TbMsg msg); |
|||
|
|||
protected MessageData getDataAsJson(TbMsg msg) { |
|||
if (this.config.isAddToMetadata()) { |
|||
return new MessageData(gson.toJsonTree(msg.getMetaData().getData(), TYPE), "metadata"); |
|||
} else { |
|||
return new MessageData(jsonParser.parse(msg.getData()), "data"); |
|||
} |
|||
} |
|||
|
|||
protected TbMsg transformMsg(TbContext ctx, TbMsg msg, JsonElement resultObject, MessageData messageData) { |
|||
if (messageData.getDataType().equals("metadata")) { |
|||
Map<String, String> metadataMap = gson.fromJson(resultObject.toString(), TYPE); |
|||
return ctx.transformMsg(msg, msg.getType(), msg.getOriginator(), new TbMsgMetaData(metadataMap), msg.getData()); |
|||
} else { |
|||
return ctx.transformMsg(msg, msg.getType(), msg.getOriginator(), msg.getMetaData(), gson.toJson(resultObject)); |
|||
} |
|||
} |
|||
|
|||
protected JsonElement addContactProperties(JsonElement data, ContactBased entity, EntityDetails entityDetails, String prefix) { |
|||
JsonObject dataAsObject = data.getAsJsonObject(); |
|||
switch (entityDetails) { |
|||
case ADDRESS: |
|||
if (entity.getAddress() != null) |
|||
dataAsObject.addProperty(prefix + "address", entity.getAddress()); |
|||
break; |
|||
case ADDRESS2: |
|||
if (entity.getAddress2() != null) |
|||
dataAsObject.addProperty(prefix + "address2", entity.getAddress2()); |
|||
break; |
|||
case CITY: |
|||
if (entity.getCity() != null) dataAsObject.addProperty(prefix + "city", entity.getCity()); |
|||
break; |
|||
case COUNTRY: |
|||
if (entity.getCountry() != null) |
|||
dataAsObject.addProperty(prefix + "country", entity.getCountry()); |
|||
break; |
|||
case STATE: |
|||
if (entity.getState() != null) dataAsObject.addProperty(prefix + "state", entity.getState()); |
|||
break; |
|||
case EMAIL: |
|||
if (entity.getEmail() != null) dataAsObject.addProperty(prefix + "email", entity.getEmail()); |
|||
break; |
|||
case PHONE: |
|||
if (entity.getPhone() != null) dataAsObject.addProperty(prefix + "phone", entity.getPhone()); |
|||
break; |
|||
case ZIP: |
|||
if (entity.getZip() != null) dataAsObject.addProperty(prefix + "zip", entity.getZip()); |
|||
break; |
|||
case ADDITIONAL_INFO: |
|||
if (entity.getAdditionalInfo().hasNonNull("description")) { |
|||
dataAsObject.addProperty(prefix + "additionalInfo", entity.getAdditionalInfo().get("description").asText()); |
|||
} |
|||
break; |
|||
} |
|||
return dataAsObject; |
|||
} |
|||
|
|||
@Data |
|||
@AllArgsConstructor |
|||
protected static class MessageData { |
|||
private JsonElement data; |
|||
private String dataType; |
|||
} |
|||
|
|||
|
|||
} |
|||
@ -0,0 +1,31 @@ |
|||
/** |
|||
* Copyright © 2016-2019 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.metadata; |
|||
|
|||
import lombok.Data; |
|||
import org.thingsboard.rule.engine.util.EntityDetails; |
|||
|
|||
import java.util.List; |
|||
|
|||
@Data |
|||
public abstract class TbAbstractGetEntityDetailsNodeConfiguration { |
|||
|
|||
|
|||
private List<EntityDetails> detailsList; |
|||
|
|||
private boolean addToMetadata; |
|||
|
|||
} |
|||
@ -0,0 +1,101 @@ |
|||
/** |
|||
* Copyright © 2016-2019 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.metadata; |
|||
|
|||
import com.google.gson.JsonElement; |
|||
import com.google.gson.JsonObject; |
|||
import lombok.extern.slf4j.Slf4j; |
|||
import org.thingsboard.rule.engine.api.RuleNode; |
|||
import org.thingsboard.rule.engine.api.TbContext; |
|||
import org.thingsboard.rule.engine.api.TbNodeConfiguration; |
|||
import org.thingsboard.rule.engine.api.TbNodeException; |
|||
import org.thingsboard.rule.engine.api.util.TbNodeUtils; |
|||
import org.thingsboard.rule.engine.util.EntityDetails; |
|||
import org.thingsboard.server.common.data.Customer; |
|||
import org.thingsboard.server.common.data.Device; |
|||
import org.thingsboard.server.common.data.EntityView; |
|||
import org.thingsboard.server.common.data.asset.Asset; |
|||
import org.thingsboard.server.common.data.id.AssetId; |
|||
import org.thingsboard.server.common.data.id.DeviceId; |
|||
import org.thingsboard.server.common.data.id.EntityViewId; |
|||
import org.thingsboard.server.common.data.plugin.ComponentType; |
|||
import org.thingsboard.server.common.msg.TbMsg; |
|||
|
|||
@Slf4j |
|||
@RuleNode(type = ComponentType.ENRICHMENT, |
|||
name = "customer details", |
|||
configClazz = TbGetCustomerDetailsNodeConfiguration.class, |
|||
nodeDescription = "Adds fields from Customer details to the message body or metadata", |
|||
nodeDetails = "If checkbox: <b>Add selected details to the message metadata</b> is selected, existing fields will be added to the message metadata instead of message data.<br><br>" + |
|||
"<b>Note:</b> only Device, Asset, and Entity View type are allowed.<br><br>" + |
|||
"If the originator of the message is not assigned to Customer, or originator type is not supported - Message will be forwarded to <b>Failure</b> chain, otherwise, <b>Success</b> chain will be used.", |
|||
uiResources = {"static/rulenode/rulenode-core-config.js"}, |
|||
configDirective = "tbEnrichmentNodeEntityDetailsConfig") |
|||
public class TbGetCustomerDetailsNode extends TbAbstractGetEntityDetailsNode<TbGetCustomerDetailsNodeConfiguration> { |
|||
|
|||
private static final String CUSTOMER_PREFIX = "customer_"; |
|||
|
|||
@Override |
|||
protected TbGetCustomerDetailsNodeConfiguration loadGetEntityDetailsNodeConfiguration(TbNodeConfiguration configuration) throws TbNodeException { |
|||
return TbNodeUtils.convert(configuration, TbGetCustomerDetailsNodeConfiguration.class); |
|||
} |
|||
|
|||
@Override |
|||
protected TbMsg getDetails(TbContext ctx, TbMsg msg) { |
|||
return getCustomerTbMsg(ctx, msg, getDataAsJson(msg)); |
|||
} |
|||
|
|||
private TbMsg getCustomerTbMsg(TbContext ctx, TbMsg msg, MessageData messageData) { |
|||
JsonElement resultObject = null; |
|||
if (!config.getDetailsList().isEmpty()) { |
|||
for (EntityDetails entityDetails : config.getDetailsList()) { |
|||
resultObject = addContactProperties(messageData.getData(), getCustomer(ctx, msg), entityDetails, CUSTOMER_PREFIX); |
|||
} |
|||
return transformMsg(ctx, msg, resultObject, messageData); |
|||
} else { |
|||
return msg; |
|||
} |
|||
} |
|||
|
|||
private Customer getCustomer(TbContext ctx, TbMsg msg) { |
|||
switch (msg.getOriginator().getEntityType()) { |
|||
case DEVICE: |
|||
Device device = ctx.getDeviceService().findDeviceById(ctx.getTenantId(), new DeviceId(msg.getOriginator().getId())); |
|||
if (!device.getCustomerId().isNullUid()) { |
|||
return ctx.getCustomerService().findCustomerById(ctx.getTenantId(), device.getCustomerId()); |
|||
} else { |
|||
throw new RuntimeException("Device with name '" + device.getName() + "' is not assigned to Customer."); |
|||
} |
|||
case ASSET: |
|||
Asset asset = ctx.getAssetService().findAssetById(ctx.getTenantId(), new AssetId(msg.getOriginator().getId())); |
|||
if (!asset.getCustomerId().isNullUid()) { |
|||
return ctx.getCustomerService().findCustomerById(ctx.getTenantId(), asset.getCustomerId()); |
|||
} else { |
|||
throw new RuntimeException("Asset with name '" + asset.getName() + "' is not assigned to Customer."); |
|||
} |
|||
case ENTITY_VIEW: |
|||
EntityView entityView = ctx.getEntityViewService().findEntityViewById(ctx.getTenantId(), new EntityViewId(msg.getOriginator().getId())); |
|||
if (!entityView.getCustomerId().isNullUid()) { |
|||
return ctx.getCustomerService().findCustomerById(ctx.getTenantId(), entityView.getCustomerId()); |
|||
} else { |
|||
throw new RuntimeException("EntityView with name '" + entityView.getName() + "' is not assigned to Customer."); |
|||
} |
|||
default: |
|||
throw new RuntimeException("Entity with entityType '" + msg.getOriginator().getEntityType() + "' is not supported."); |
|||
} |
|||
} |
|||
|
|||
} |
|||
@ -0,0 +1,33 @@ |
|||
/** |
|||
* Copyright © 2016-2019 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.metadata; |
|||
|
|||
import lombok.Data; |
|||
import org.thingsboard.rule.engine.api.NodeConfiguration; |
|||
|
|||
import java.util.Collections; |
|||
|
|||
@Data |
|||
public class TbGetCustomerDetailsNodeConfiguration extends TbAbstractGetEntityDetailsNodeConfiguration implements NodeConfiguration<TbGetCustomerDetailsNodeConfiguration> { |
|||
|
|||
|
|||
@Override |
|||
public TbGetCustomerDetailsNodeConfiguration defaultConfiguration() { |
|||
TbGetCustomerDetailsNodeConfiguration configuration = new TbGetCustomerDetailsNodeConfiguration(); |
|||
configuration.setDetailsList(Collections.emptyList()); |
|||
return configuration; |
|||
} |
|||
} |
|||
@ -0,0 +1,68 @@ |
|||
/** |
|||
* Copyright © 2016-2019 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.metadata; |
|||
|
|||
import com.google.gson.JsonElement; |
|||
import com.google.gson.JsonObject; |
|||
import lombok.extern.slf4j.Slf4j; |
|||
import org.thingsboard.rule.engine.api.RuleNode; |
|||
import org.thingsboard.rule.engine.api.TbContext; |
|||
import org.thingsboard.rule.engine.api.TbNodeConfiguration; |
|||
import org.thingsboard.rule.engine.api.TbNodeException; |
|||
import org.thingsboard.rule.engine.api.util.TbNodeUtils; |
|||
import org.thingsboard.rule.engine.util.EntityDetails; |
|||
import org.thingsboard.server.common.data.ContactBased; |
|||
import org.thingsboard.server.common.data.Tenant; |
|||
import org.thingsboard.server.common.data.plugin.ComponentType; |
|||
import org.thingsboard.server.common.msg.TbMsg; |
|||
|
|||
@Slf4j |
|||
@RuleNode(type = ComponentType.ENRICHMENT, |
|||
name = "tenant details", |
|||
configClazz = TbGetTenantDetailsNodeConfiguration.class, |
|||
nodeDescription = "Adds fields from Tenant details to the message body or metadata", |
|||
nodeDetails = "If checkbox: <b>Add selected details to the message metadata</b> is selected, existing fields will be added to the message metadata instead of message data.<br><br>" + |
|||
"<b>Note:</b> only Device, Asset, and Entity View type are allowed.<br><br>" + |
|||
"If the originator of the message is not assigned to Tenant, or originator type is not supported - Message will be forwarded to <b>Failure</b> chain, otherwise, <b>Success</b> chain will be used.", |
|||
uiResources = {"static/rulenode/rulenode-core-config.js"}, |
|||
configDirective = "tbEnrichmentNodeEntityDetailsConfig") |
|||
public class TbGetTenantDetailsNode extends TbAbstractGetEntityDetailsNode<TbGetTenantDetailsNodeConfiguration> { |
|||
|
|||
private static final String TENANT_PREFIX = "tenant_"; |
|||
|
|||
@Override |
|||
protected TbGetTenantDetailsNodeConfiguration loadGetEntityDetailsNodeConfiguration(TbNodeConfiguration configuration) throws TbNodeException { |
|||
return TbNodeUtils.convert(configuration, TbGetTenantDetailsNodeConfiguration.class); |
|||
} |
|||
|
|||
@Override |
|||
protected TbMsg getDetails(TbContext ctx, TbMsg msg) { |
|||
return getTenantTbMsg(ctx, msg, getDataAsJson(msg)); |
|||
} |
|||
|
|||
private TbMsg getTenantTbMsg(TbContext ctx, TbMsg msg, MessageData messageData) { |
|||
JsonElement resultObject = null; |
|||
Tenant tenant = ctx.getTenantService().findTenantById(ctx.getTenantId()); |
|||
if (!config.getDetailsList().isEmpty()) { |
|||
for (EntityDetails entityDetails : config.getDetailsList()) { |
|||
resultObject = addContactProperties(messageData.getData(), tenant, entityDetails, TENANT_PREFIX); |
|||
} |
|||
return transformMsg(ctx, msg, resultObject, messageData); |
|||
} else { |
|||
return msg; |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,33 @@ |
|||
/** |
|||
* Copyright © 2016-2019 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.metadata; |
|||
|
|||
import lombok.Data; |
|||
import org.thingsboard.rule.engine.api.NodeConfiguration; |
|||
|
|||
import java.util.Collections; |
|||
|
|||
@Data |
|||
public class TbGetTenantDetailsNodeConfiguration extends TbAbstractGetEntityDetailsNodeConfiguration implements NodeConfiguration<TbGetTenantDetailsNodeConfiguration> { |
|||
|
|||
|
|||
@Override |
|||
public TbGetTenantDetailsNodeConfiguration defaultConfiguration() { |
|||
TbGetTenantDetailsNodeConfiguration configuration = new TbGetTenantDetailsNodeConfiguration(); |
|||
configuration.setDetailsList(Collections.emptyList()); |
|||
return configuration; |
|||
} |
|||
} |
|||
@ -0,0 +1,22 @@ |
|||
/** |
|||
* Copyright © 2016-2019 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.util; |
|||
|
|||
public enum EntityDetails { |
|||
|
|||
COUNTRY, CITY, STATE, ZIP, ADDRESS, ADDRESS2, PHONE, EMAIL, ADDITIONAL_INFO |
|||
|
|||
} |
|||
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large
@ -0,0 +1,301 @@ |
|||
/* |
|||
* Copyright © 2016-2019 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. |
|||
*/ |
|||
/* eslint-disable import/no-unresolved, import/default */ |
|||
import widgetTpl from './date-range-navigator.tpl.html'; |
|||
import './date-range-navigator.scss'; |
|||
/* eslint-enable import/no-unresolved, import/default */ |
|||
|
|||
export default angular.module('thingsboard.widgets.dateRangeNavigator', []) |
|||
.directive('dateRangeNavigatorWidget', DateRangeNavigatorWidget) |
|||
.name; |
|||
|
|||
/*@ngInject*/ |
|||
function DateRangeNavigatorWidget() { |
|||
return { |
|||
restrict: "E", |
|||
scope: true, |
|||
bindToController: { |
|||
tableId: '=', |
|||
ctx: '=' |
|||
}, |
|||
controller: DateRangeNavigatorWidgetController, |
|||
controllerAs: 'vm', |
|||
templateUrl: widgetTpl |
|||
}; |
|||
} |
|||
|
|||
/*@ngInject*/ |
|||
function DateRangeNavigatorWidgetController($scope, $window, $filter) { |
|||
|
|||
let vm = this, |
|||
hour = 3600000, |
|||
day = 86400000, |
|||
week = 604800000, |
|||
month = 2629743000, |
|||
words = [ |
|||
"Mon", |
|||
"Tue", |
|||
"Wed", |
|||
"Thu", |
|||
"Fri", |
|||
"Sat", |
|||
"Sun", |
|||
"January", |
|||
"February", |
|||
"March", |
|||
"April", |
|||
"May", |
|||
"June", |
|||
"July", |
|||
"August", |
|||
"September", |
|||
"October", |
|||
"November", |
|||
"December", |
|||
"Ok", |
|||
'Custom Date Range', |
|||
'Date Range Template', |
|||
'Today', |
|||
'Yesterday', |
|||
'This Week', |
|||
'Last Week', |
|||
'Month', |
|||
'This Month', |
|||
'Last Month', |
|||
'Year', |
|||
'This Year', |
|||
'Last Year' |
|||
], |
|||
firstUpdate = true; |
|||
|
|||
$scope.datesMap = { |
|||
hour: { |
|||
ts: hour, |
|||
label: "Hour" |
|||
}, |
|||
day: { |
|||
ts: day, |
|||
label: "Day" |
|||
}, |
|||
week: { |
|||
ts: week, |
|||
label: "Week" |
|||
}, |
|||
twoWeeks: { |
|||
ts: week * 2, |
|||
label: "2 weeks" |
|||
}, |
|||
month: { |
|||
ts: month, |
|||
label: "Month" |
|||
}, |
|||
threeMonths: { |
|||
ts: month * 3, |
|||
label: "3 months" |
|||
}, |
|||
sixMonths: { |
|||
ts: month * 6, |
|||
label: "6 months" |
|||
} |
|||
}; |
|||
$scope.advancedModel = {}; |
|||
$scope.endRestrictionDate = Date.now(); |
|||
$scope.localizationMap = getLocalizationMap(); |
|||
|
|||
$scope.changeInterval = changeInterval; |
|||
$scope.goForth = goForth; |
|||
$scope.goBack = goBack; |
|||
$scope.triggerChange = triggerChange; |
|||
|
|||
$scope.$watch('vm.ctx', function () { |
|||
if (vm.ctx && vm.ctx.dashboard.dashboardTimewindow) { |
|||
$scope.settings = vm.ctx.widgetConfig.settings; |
|||
let selection; |
|||
if ($scope.settings.useSessionStorage) { |
|||
selection = readFromStorage('date-range'); |
|||
} |
|||
if (selection) { |
|||
$scope.advancedModel = { |
|||
selectedTemplateName: selection.name, |
|||
dateStart: selection.start, |
|||
dateEnd: selection.end |
|||
}; |
|||
} else { |
|||
let end = new Date(); |
|||
end.setHours(23, 59, 59, 999); |
|||
|
|||
let formattedDate = getFormattedDate( |
|||
(end.getTime() + 1) - $scope.datesMap[$scope.settings.initialInterval || "week"].ts, |
|||
end.getTime() |
|||
); |
|||
$scope.advancedModel = formattedDate; |
|||
} |
|||
$scope.selectedStepSize = $scope.datesMap[$scope.settings.stepSize || "day"].ts; |
|||
|
|||
widgetContextTimewindowSync(); |
|||
} |
|||
}); |
|||
|
|||
$scope.$on('dashboardTimewindowChanged', function () { |
|||
$scope.dashboardTimewindowChanged = true; |
|||
widgetContextTimewindowSync(); |
|||
}); |
|||
|
|||
function getLocalizationMap() { |
|||
let result = {}; |
|||
|
|||
words.forEach(function (key) { |
|||
result[key] = $filter('translate')('widgets.date-range-navigator.localizationMap.' + key); |
|||
}); |
|||
|
|||
return result; |
|||
} |
|||
|
|||
function triggerChange() { |
|||
updateTimewindow($scope.advancedModel.dateStart.getTime(), $scope.advancedModel.dateEnd.getTime() + day - 1); |
|||
} |
|||
|
|||
function widgetContextTimewindowSync() { |
|||
if (vm.ctx && vm.ctx.dashboardTimewindow && $scope.dashboardTimewindowChanged && |
|||
vm.ctx.dashboard.dashboardTimewindow.history && |
|||
vm.ctx.dashboard.dashboardTimewindow.history.fixedTimewindow) { |
|||
|
|||
|
|||
if (!firstUpdate) { |
|||
updateAdvancedModel(); |
|||
} |
|||
updateDateInterval(); |
|||
if ($scope.settings.useSessionStorage) { |
|||
updateStorageDate(); |
|||
} |
|||
if (firstUpdate) { |
|||
updateTimewindow($scope.advancedModel.dateStart.getTime(), $scope.advancedModel.dateEnd.getTime()); |
|||
firstUpdate = false; |
|||
} |
|||
} |
|||
} |
|||
|
|||
function getFormattedDate(startTime, endTime) { |
|||
var template; |
|||
|
|||
let startDate = new Date(startTime); |
|||
let endDate = new Date(endTime); |
|||
|
|||
if (getDateDiff(startDate, endDate) === 0) { |
|||
template = $filter('date')(startDate, 'dd MMM yyyy'); |
|||
} else { |
|||
template = $filter('date')( |
|||
startDate, |
|||
'dd' + (startDate.getMonth() !== endDate.getMonth() || startDate.getFullYear() !== endDate.getFullYear() ? ' MMM' : '') + (startDate.getFullYear() !== endDate.getFullYear() ? ' yyyy' : '') |
|||
) |
|||
+ ' - ' + |
|||
$filter('date')( |
|||
endDate, |
|||
'dd MMM yyyy' |
|||
); |
|||
} |
|||
|
|||
return { |
|||
selectedTemplateName: template, |
|||
dateStart: startDate, |
|||
dateEnd: endDate |
|||
}; |
|||
} |
|||
|
|||
function readFromStorage(itemKey) { |
|||
if ($window.sessionStorage.getItem(itemKey)) { |
|||
let selection = angular.fromJson($window.sessionStorage.getItem(itemKey)); |
|||
selection.start = new Date(parseInt(selection.start)); |
|||
selection.end = new Date(parseInt(selection.end)); |
|||
return selection; |
|||
} |
|||
|
|||
return undefined; |
|||
} |
|||
|
|||
function goForth() { |
|||
let startTime = vm.ctx.dashboard.dashboardTimewindow.history ? |
|||
vm.ctx.dashboard.dashboardTimewindow.history.fixedTimewindow.startTimeMs : |
|||
$scope.advancedModel.dateStart.getTime(); |
|||
let endTime = vm.ctx.dashboard.dashboardTimewindow.history ? |
|||
vm.ctx.dashboard.dashboardTimewindow.history.fixedTimewindow.endTimeMs : |
|||
$scope.advancedModel.dateEnd.getTime(); |
|||
updateTimewindow(startTime + $scope.selectedStepSize, endTime + $scope.selectedStepSize); |
|||
} |
|||
|
|||
function goBack() { |
|||
let startTime = vm.ctx.dashboard.dashboardTimewindow.history ? |
|||
vm.ctx.dashboard.dashboardTimewindow.history.fixedTimewindow.startTimeMs : |
|||
$scope.advancedModel.dateStart.getTime(); |
|||
let endTime = vm.ctx.dashboard.dashboardTimewindow.history ? |
|||
vm.ctx.dashboard.dashboardTimewindow.history.fixedTimewindow.endTimeMs : |
|||
$scope.advancedModel.dateEnd.getTime(); |
|||
updateTimewindow(startTime - $scope.selectedStepSize, endTime - $scope.selectedStepSize); |
|||
} |
|||
|
|||
function changeInterval() { |
|||
let endTime = vm.ctx.dashboard.dashboardTimewindow.history ? |
|||
vm.ctx.dashboard.dashboardTimewindow.history.fixedTimewindow.endTimeMs : |
|||
$scope.advancedModel.dateEnd.getTime(); |
|||
updateTimewindow(endTime - $scope.selectedDateInterval / 2, endTime + $scope.selectedDateInterval / 2); |
|||
} |
|||
|
|||
function getDateDiff(date1, date2) { |
|||
if (!date1 || !date2) return; |
|||
var _d1 = new Date(date1.getFullYear(), date1.getMonth(), date1.getDate()), |
|||
_d2 = new Date(date2.getFullYear(), date2.getMonth(), date2.getDate()); |
|||
return _d2 - _d1; |
|||
} |
|||
|
|||
function updateTimewindow(startTime, endTime) { |
|||
vm.ctx.dashboard.dashboardTimewindowApi.onUpdateTimewindow(startTime, endTime, 10); |
|||
} |
|||
|
|||
function updateDateInterval() { |
|||
let interval = $scope.advancedModel.dateEnd.getTime() - $scope.advancedModel.dateStart.getTime(); |
|||
|
|||
for (let i in $scope.datesMap) { |
|||
if ($scope.datesMap.hasOwnProperty(i)) { |
|||
if ($scope.datesMap[i].ts === interval || $scope.datesMap[i].ts === interval + 1 || $scope.datesMap[i].ts === interval - 1) { |
|||
$scope.selectedDateInterval = $scope.datesMap[i].ts; |
|||
$scope.customInterval = false; |
|||
return; |
|||
} |
|||
} |
|||
} |
|||
|
|||
$scope.selectedDateInterval = interval; |
|||
$scope.customInterval = {ts: interval, label: "Custom interval"}; |
|||
} |
|||
|
|||
function updateAdvancedModel() { |
|||
$scope.advancedModel = getFormattedDate(vm.ctx.dashboard.dashboardTimewindow.history.fixedTimewindow.startTimeMs, vm.ctx.dashboard.dashboardTimewindow.history.fixedTimewindow.endTimeMs); |
|||
} |
|||
|
|||
function updateStorageDate() { |
|||
saveIntoStorage('date-range', { |
|||
start: $scope.advancedModel.dateStart.getTime(), |
|||
end: $scope.advancedModel.dateEnd.getTime(), |
|||
name: $scope.advancedModel.selectedTemplateName |
|||
}); |
|||
} |
|||
|
|||
function saveIntoStorage(keyName, selection) { |
|||
if (selection) { |
|||
$window.sessionStorage.setItem(keyName, angular.toJson(selection)); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,108 @@ |
|||
/** |
|||
* Copyright © 2016-2019 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. |
|||
*/ |
|||
.date-range-navigator-widget { |
|||
display: flex; |
|||
height: 100%; |
|||
} |
|||
|
|||
.date-range-navigator { |
|||
display: flex; |
|||
flex-wrap: wrap; |
|||
align-items: center; |
|||
justify-content: space-evenly; |
|||
width: 100%; |
|||
margin: auto; |
|||
|
|||
.drn__element { |
|||
display: flex; |
|||
flex-direction: row; |
|||
align-items: center; |
|||
max-width: 100%; |
|||
height: 60px; |
|||
margin: 4px 0; |
|||
} |
|||
|
|||
.navigation { |
|||
md-input-container { |
|||
margin: 0; |
|||
} |
|||
} |
|||
|
|||
.picker { |
|||
.picker__wrapper { |
|||
position: relative; |
|||
max-width: 100%; |
|||
padding: 2px; |
|||
|
|||
> label { |
|||
position: absolute; |
|||
right: -3px; |
|||
bottom: 100%; |
|||
left: 0; |
|||
padding-left: 3px; |
|||
color: #787878; |
|||
transform: scale(.75); |
|||
transform-origin: left bottom; |
|||
} |
|||
} |
|||
|
|||
.md-select-value { |
|||
min-width: 225px; |
|||
border-color: #e1e1e1; |
|||
|
|||
.md-select-icon { |
|||
color: #757575; |
|||
} |
|||
} |
|||
} |
|||
|
|||
&.short-mode { |
|||
display: block; |
|||
width: 90%; |
|||
|
|||
.drn__element { |
|||
width: 100%; |
|||
|
|||
md-input-container { |
|||
flex: 1; |
|||
} |
|||
} |
|||
|
|||
.picker { |
|||
.picker__wrapper { |
|||
width: 100%; |
|||
} |
|||
|
|||
.md-select-value { |
|||
min-width: initial; |
|||
} |
|||
} |
|||
|
|||
&.labels-hidden { |
|||
.drn__element { |
|||
margin: 0; |
|||
} |
|||
} |
|||
} |
|||
|
|||
&.long-mode { |
|||
&.labels-hidden { |
|||
.drn__element { |
|||
height: 36px; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,72 @@ |
|||
<!-- |
|||
|
|||
Copyright © 2016-2019 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. |
|||
|
|||
--> |
|||
<div class="date-range-navigator" |
|||
ng-class="{'short-mode':vm.ctx.width < 400, 'labels-hidden': settings.hideLabels, 'long-mode': vm.ctx.width >= 400}" |
|||
> |
|||
<div class="drn__element picker" ng-hide="settings.hidePicker" ng-if="vm.ctx"> |
|||
<div class="picker__wrapper"> |
|||
<label ng-hide="settings.hideLabels" ng-bind="'widgets.date-range-navigator.localizationMap.Date picker' | translate"></label> |
|||
<md-date-range ng-model="advancedModel" |
|||
md-on-select="triggerChange()" |
|||
localization-map="localizationMap" |
|||
one-panel="settings.onePanel" |
|||
auto-confirm="settings.autoConfirm" |
|||
show-template="settings.showTemplate" |
|||
first-day-of-week="settings.firstDayOfWeek" |
|||
></md-date-range> |
|||
</div> |
|||
</div> |
|||
|
|||
<div class="drn__element navigation" ng-hide="settings.hideInterval"> |
|||
<md-input-container class="md-block" |
|||
flex-gt-sm |
|||
> |
|||
<label ng-hide="settings.hideLabels" ng-bind="'widgets.date-range-navigator.localizationMap.Interval' | translate"></label> |
|||
<md-select ng-model="selectedDateInterval" |
|||
ng-change="changeInterval()" |
|||
aria-label="Pick date interval" |
|||
> |
|||
<md-option ng-if="customInterval" ng-value="customInterval.ts">{{'widgets.date-range-navigator.localizationMap.' + customInterval.label | translate}}</md-option> |
|||
<md-option ng-repeat="(dateKey, dateValue) in datesMap" ng-value="dateValue.ts"> |
|||
{{'widgets.date-range-navigator.localizationMap.' + dateValue.label | translate}} |
|||
</md-option> |
|||
</md-select> |
|||
</md-input-container> |
|||
</div> |
|||
|
|||
<div class="drn__element step" ng-hide="settings.hideStepSize"> |
|||
<md-button ng-click="goBack()" class="md-icon-button"> |
|||
<md-icon>keyboard_arrow_left</md-icon> |
|||
</md-button> |
|||
<md-input-container class="md-block" |
|||
flex-gt-sm |
|||
> |
|||
<label ng-hide="settings.hideLabels" ng-bind="'widgets.date-range-navigator.localizationMap.Step size' | translate"></label> |
|||
<md-select ng-model="selectedStepSize" |
|||
aria-label="Pick date interval" |
|||
> |
|||
<md-option ng-repeat="(dateKey, dateValue) in datesMap" ng-value="dateValue.ts"> |
|||
{{'widgets.date-range-navigator.localizationMap.'+dateValue.label | translate}} |
|||
</md-option> |
|||
</md-select> |
|||
</md-input-container> |
|||
<md-button ng-click="goForth()" class="md-icon-button"> |
|||
<md-icon>keyboard_arrow_right</md-icon> |
|||
</md-button> |
|||
</div> |
|||
</div> |
|||
Loading…
Reference in new issue