Browse Source

添加支持DLT6451997协议支持
添加设备列表新增时手动绑定父设备功能

tangfudong 2 years ago
parent
commit
5e99867ae9
40 changed files with 2835 additions and 31 deletions
  1. 72 0
      data/components/2c089bb8-0412-449e-94f1-212d35a50219/component.js
  2. BIN
      data/components/2c089bb8-0412-449e-94f1-212d35a50219/iot-websocket-component-0.4.2-SNAPSHOT.jar
  3. 74 0
      data/components/305a8b86-4566-4f2a-a57f-f84ca47471a1/component.js
  4. BIN
      data/components/305a8b86-4566-4f2a-a57f-f84ca47471a1/iot-DLT645-component-0.4.2-SNAPSHOT.jar
  5. 55 0
      data/converters/d7e84930-5460-4638-aa3f-e0c2015628f4/converter.js
  6. 5 0
      data/init/category.json
  7. 33 0
      data/init/deviceInfo.json
  8. 24 0
      data/init/product.json
  9. 29 0
      data/init/protocolComponent.json
  10. 7 0
      data/init/protocolConverter.json
  11. 163 0
      data/init/thingModel.json
  12. 20 3
      iot-common/src/main/java/cc/iotkit/common/ComponentClassLoader.java
  13. 87 0
      iot-components/iot-DLT645-component/src/main/java/cc/iotkit/comp/DLT645/DLT645Component.java
  14. 19 0
      iot-components/iot-DLT645-component/src/main/java/cc/iotkit/comp/DLT645/DLT645Config.java
  15. 184 0
      iot-components/iot-DLT645-component/src/main/java/cc/iotkit/comp/DLT645/DLT645Verticle.java
  16. 309 0
      iot-components/iot-DLT645-component/src/main/java/cc/iotkit/comp/DLT645/analysis/DLT645Analysis.java
  17. 104 0
      iot-components/iot-DLT645-component/src/main/java/cc/iotkit/comp/DLT645/analysis/DLT645Converter.java
  18. 86 0
      iot-components/iot-DLT645-component/src/main/java/cc/iotkit/comp/DLT645/analysis/DLT645Data.java
  19. 359 0
      iot-components/iot-DLT645-component/src/main/java/cc/iotkit/comp/DLT645/analysis/DLT645DataFormat.java
  20. 183 0
      iot-components/iot-DLT645-component/src/main/java/cc/iotkit/comp/DLT645/analysis/DLT645FunCode.java
  21. 57 0
      iot-components/iot-DLT645-component/src/main/java/cc/iotkit/comp/DLT645/analysis/DLT645V1997Data.java
  22. 87 0
      iot-components/iot-DLT645-component/src/main/java/cc/iotkit/comp/DLT645/analysis/DLT645v1997CsvLoader.java
  23. 11 0
      iot-components/iot-DLT645-component/src/main/java/cc/iotkit/comp/DLT645/utils/ByteRef.java
  24. 76 0
      iot-components/iot-DLT645-component/src/main/java/cc/iotkit/comp/DLT645/utils/ByteUtils.java
  25. 11 0
      iot-components/iot-DLT645-component/src/main/java/cc/iotkit/comp/DLT645/utils/BytesRef.java
  26. 451 0
      iot-components/iot-DLT645-component/src/main/java/cc/iotkit/comp/DLT645/utils/ContainerUtils.java
  27. 143 0
      iot-components/iot-DLT645-component/src/main/resources/DLT645-1997.csv
  28. 74 0
      iot-components/iot-DLT645-component/src/main/resources/component.js
  29. 1 0
      iot-components/iot-DLT645-component/src/main/resources/component.spi
  30. 1 0
      iot-components/iot-DLT645-component/src/main/resources/convert.spi
  31. 15 3
      iot-components/iot-component-server/src/main/java/cc/iotkit/comps/DeviceComponentManager.java
  32. 36 16
      iot-components/iot-websocket-component/src/main/java/cc/iotkit/comp/websocket/server/WebSocketServerVerticle.java
  33. 5 0
      iot-data/iot-data-cache/src/main/java/cc/iotkit/data/service/DeviceInfoDataCache.java
  34. 5 0
      iot-data/iot-data-cache/src/main/java/cc/iotkit/data/service/DeviceInfoPropertyDataCache.java
  35. 6 0
      iot-data/iot-data-service/src/main/java/cc/iotkit/data/IDeviceInfoData.java
  36. 10 0
      iot-data/iot-model/src/main/java/cc/iotkit/model/protocol/ProtocolComponent.java
  37. 2 0
      iot-data/iot-rdb-data-service/src/main/java/cc/iotkit/data/model/TbProtocolComponent.java
  38. 12 0
      iot-data/iot-rdb-data-service/src/main/java/cc/iotkit/data/service/DeviceInfoDataImpl.java
  39. 18 7
      iot-standalone/src/main/java/cc/iotkit/manager/controller/DeviceController.java
  40. 1 2
      iot-standalone/src/main/java/cc/iotkit/manager/controller/ProtocolController.java

+ 72 - 0
data/components/2c089bb8-0412-449e-94f1-212d35a50219/component.js

@@ -0,0 +1,72 @@
+var mid=1;
+
+var access_token="";
+
+function getMid(){
+	mid++;
+	if(mid>10000){
+		mid=1;
+	}
+	return mid;
+};
+function getPingData(data){
+	var ping={
+		productKey:"",
+		deviceName:"",
+		content:{
+			id:getMid(),
+			type:data
+		}
+	};
+	return {
+		type:"action",
+		data:{
+			productKey:"",
+			deviceName:"",
+			state:""
+		},
+		action:{
+			type:"ack",
+			content:JSON.stringify(ping)
+		}
+	}
+};
+//必须提供onReceive方法
+this.onReceive=function(head,type,payload){
+	var data=JSON.parse(payload)
+	if(data.type=="auth_required"){
+		var auth={
+			productKey:"",
+			deviceName:"",
+			content:{
+				type:"auth",
+				access_token:access_token
+			}
+		};
+		return {
+			type:"action",
+			data:{
+				productKey:"",
+				deviceName:"",
+				state:""
+			},
+			action:{
+				type:"ack",
+				content:JSON.stringify(auth)
+			}
+		}
+	}else if(data.type=="auth_ok"){
+		return getPingData(data.heartBeatData);
+	}else if(data.type=="pong"){
+		apiTool.log("receive pong!");
+	}else if("ping"==type){
+		return getPingData(data.heartBeatData);
+	}
+	return {
+		productKey:"",
+		deviceName:"",
+		mid:0,
+		content:{
+		}
+	}
+};

BIN
data/components/2c089bb8-0412-449e-94f1-212d35a50219/iot-websocket-component-0.4.2-SNAPSHOT.jar


+ 74 - 0
data/components/305a8b86-4566-4f2a-a57f-f84ca47471a1/component.js

@@ -0,0 +1,74 @@
+var mid=1;
+
+var gatewayPk="BRD3x4fkKxkaxXFt"
+var smartMeterPk="PjmkANSTDt85bZPj"
+
+function getMid(){
+	mid++;
+	if(mid>10000){
+		mid=1;
+	}
+	return mid;
+};
+function register(head){
+	var mac= head.mac;
+	return {
+		type:"register",
+		data:{
+			productKey:gatewayPk,
+			deviceName:mac,
+			model:""
+		}
+	};
+}
+
+function deviceStateChange(head,type){
+	var mac=head.mac;
+	return {
+		type:"state",
+		data:{
+			productKey:gatewayPk,
+			deviceName:mac,
+			state:type
+		}
+	}
+}
+
+function dltHandle(payload){
+	var dltData= JSON.parse(payload);
+	var identify= dltData.identify;
+	var content={};
+	content[identify]=dltData.data;
+	return {
+		type:"report",
+		data:{
+			productKey:smartMeterPk,
+			deviceName:dltData.deviceAddress,
+			mid:getMid(),
+			content:{
+				type:"property",
+				identifier: "report", //属性上报
+				occur: new Date().getTime(), //时间戳,设备上的事件或数据产生的本地时间
+				time: new Date().getTime(), //时间戳,消息上报时间
+				data: content
+			}
+		},
+	}
+}
+
+//必须提供onReceive方法
+this.onReceive=function(head,type,payload){
+	if("register"==type){
+		return register(head);
+	}else if("online"==type){
+		return deviceStateChange(head,type);
+	}else if("offline"==type){
+		return deviceStateChange(head,type);
+	}else if("dlt"==type){
+		return dltHandle(payload);
+	}
+};
+
+this.onRegistered=function (data,status) {
+	apiTool.log("onRegistered调用");
+}

BIN
data/components/305a8b86-4566-4f2a-a57f-f84ca47471a1/iot-DLT645-component-0.4.2-SNAPSHOT.jar


+ 55 - 0
data/converters/d7e84930-5460-4638-aa3f-e0c2015628f4/converter.js

@@ -0,0 +1,55 @@
+
+var mid=1;
+
+function getMid(){
+	mid++;
+	if(mid>10000){
+		mid=1;
+	}
+	return mid+"";
+}
+
+this.decode = function (msg) {
+	//对msg进行解析,并返回物模型数据
+	var content=msg.content;
+	var type = content.type;
+
+	if (type=="report") {
+		//属性上报
+		return {
+			mid: msg.mid,
+			productKey: msg.productKey,
+			deviceName: msg.deviceName,
+			type:"property",
+			identifier: "report", //属性上报
+			occur: new Date().getTime(), //时间戳,设备上的事件或数据产生的本地时间
+			time: new Date().getTime(), //时间戳,消息上报时间
+			data: content.params,
+		};
+	}
+	return null;
+};
+
+this.encode = function (service,device) {
+	var type=service.type;
+	var identifier=service.identifier;
+	var entityId=service.deviceName;
+	var deviceMid=getMid();
+	var params={};
+	var target={};
+	if("property"==type&&"set"==identifier){
+		var domain=entityId.split(".")[0];
+		var powerstate=service.params.powerstate==1?"turn_on":"turn_off";
+		params.type="call_service";
+		params.domain=domain;
+		params.service=powerstate;
+		target.entity_id=entityId;
+		params.target=target;
+	}
+	return {
+		productKey:service.productKey,
+		deviceName:service.deviceName,
+		mid:deviceMid,
+		content:params
+	}
+};

+ 5 - 0
data/init/category.json

@@ -38,5 +38,10 @@
     "id": "SmartPlug",
     "name": "智能插座",
     "createAt": 1645409421118
+  },
+  {
+    "id": "FreshAir",
+    "name": "新风",
+    "createAt": 1681444312184
   }
 ]

+ 33 - 0
data/init/deviceInfo.json

@@ -1633,5 +1633,38 @@
     "property": {},
     "tag": {},
     "createAt": 1646522674443
+  },
+  {
+    "id": "168187356997901234567891230000120",
+    "deviceId": "168187356997901234567891230000120",
+    "productKey": "BRD3x4fkKxkaxXFt",
+    "deviceName": "123456789123",
+    "uid": "fa1c5eaa-de6e-48b6-805e-8f091c7bb831",
+    "subUid": [],
+    "state": {
+      "online": false,
+      "onlineTime": 1653380299997,
+      "offlineTime": 1653380471302
+    },
+    "property": {},
+    "tag": {},
+    "createAt": 1646522674443
+  },
+  {
+    "id": "168191541600402017121609130000126",
+    "deviceId": "168191541600402017121609130000126",
+    "productKey": "PwMfpXmp4ZWkGahn",
+    "deviceName": "201712160913",
+    "uid": "fa1c5eaa-de6e-48b6-805e-8f091c7bb831",
+    "parentId": "168187356997901234567891230000120",
+    "subUid": [],
+    "state": {
+      "online": false,
+      "onlineTime": 1653380299997,
+      "offlineTime": 1653380471302
+    },
+    "property": {},
+    "tag": {},
+    "createAt": 1646522674443
   }
 ]

+ 24 - 0
data/init/product.json

@@ -119,5 +119,29 @@
     "nodeType": 1,
     "uid": "fa1c5eaa-de6e-48b6-805e-8f091c7bb831",
     "createAt": 1649653149339
+  },
+  {
+    "id": "bGdZt8ffBETtsirm",
+    "name": "新风",
+    "category": "FreshAir",
+    "nodeType": 1,
+    "uid": "fa1c5eaa-de6e-48b6-805e-8f091c7bb831",
+    "createAt": 1649653149339
+  },
+  {
+    "id": "BRD3x4fkKxkaxXFt",
+    "name": "智能电表采集器",
+    "category": "gateway",
+    "nodeType": 0,
+    "uid": "fa1c5eaa-de6e-48b6-805e-8f091c7bb831",
+    "createAt": 1649653149339
+  },
+  {
+    "id": "PjmkANSTDt85bZPj",
+    "name": "智能电表",
+    "category": "SmartMeter",
+    "nodeType": 1,
+    "uid": "fa1c5eaa-de6e-48b6-805e-8f091c7bb831",
+    "createAt": 1649653149339
   }
 ]

+ 29 - 0
data/init/protocolComponent.json

@@ -8,6 +8,7 @@
     "jarFile": "iot-mqtt-component-0.4.2-SNAPSHOT.jar",
     "config": "{\"port\":1883,\"ssl\":false,\"type\":\"server\"}",
     "converter": "6260396d67aced2696184053",
+    "converType": "custom",
     "state": "running",
     "createAt": 1650473458084
   },
@@ -20,6 +21,7 @@
     "jarFile": "iot-emqx-component-0.4.2-SNAPSHOT.jar",
     "config": "{\"port\":\"1884\",\"ssl\":false,\"type\":\"client\",\"subscribeTopics\":[\"/sys/+/+/s/#\",\"/sys/client/connected\",\"/sys/client/disconnected\",\"/sys/session/subscribed\",\"/sys/session/unsubscribed\"],\"authPort\":\"8088\",\"broker\":\"127.0.0.1\",\"clientId\":\"test\",\"username\":\"test\",\"password\":\"123\"}",
     "converter": "6260396d67aced2696184053",
+    "converType": "custom",
     "state": "stopped",
     "createAt": 1653180468724
   },
@@ -32,6 +34,33 @@
     "jarFile": "iot-http-biz-component-0.4.2-SNAPSHOT.jar",
     "config": "{\"port\":\"8084\"}",
     "converter": "",
+    "converType": "",
+    "state": "stopped",
+    "createAt": 1650685502665
+  },
+  {
+    "id": "2c089bb8-0412-449e-94f1-212d35a50219",
+    "uid": "fa1c5eaa-de6e-48b6-805e-8f091c7bb831",
+    "name": "WEBSOCKET服务端",
+    "type": "device",
+    "protocol": "websocket",
+    "jarFile": "iot-websocket-component-0.4.2-SNAPSHOT.jar",
+    "config": "{\"port\":\"2454\",\"ssl\":false,\"type\":\"server\",\"ip\":\"\",\"url\":\"\",\"heartBeatTime\":10000,\"heartBeatData\":\"\",\"accessTokens\":[{\"id\":\"b4f02276-13d8-499d-89b3-8acd6330b310\",\"tokenName\":\"tokrn\",\"tokenStr\":\"NxW4nPt2SPnc87pdFXAQmCZY4Kb0nRsPDC6z4Qzpp1AtRWDJxg8iZMqFpcwZ2igi\"}]}",
+    "converter": "d7e84930-5460-4638-aa3f-e0c2015628f4",
+    "converType": "custom",
+    "state": "stopped",
+    "createAt": 1650685502665
+  }
+,
+  {
+    "id": "305a8b86-4566-4f2a-a57f-f84ca47471a1",
+    "uid": "fa1c5eaa-de6e-48b6-805e-8f091c7bb831",
+    "name": "DLT645电表通讯组件",
+    "type": "device",
+    "protocol": "tcp",
+    "jarFile": "iot-DLT645-component-0.4.2-SNAPSHOT.jar",
+    "config": "{\"port\":2424,\"ssl\":false,\"type\":\"server\",\"parserType\":\"不处理\",\"parserConfiguration\":{\"delimited\":\"\",\"fix\":1,\"script\":\"\"},\"host\":\"127.0.0.1\"}",
+    "converType": "static",
     "state": "stopped",
     "createAt": 1650685502665
   }

+ 7 - 0
data/init/protocolConverter.json

@@ -19,5 +19,12 @@
     "name": "奇特MQTT标准协议",
     "desc": "奇特MQTT标准协议转换器",
     "createAt": 1650473325173
+  },
+  {
+    "id": "d7e84930-5460-4638-aa3f-e0c2015628f4",
+    "uid": "fa1c5eaa-de6e-48b6-805e-8f091c7bb831",
+    "name": "WS标准协议",
+    "desc": "WS标准协议转换器",
+    "createAt": 1650473325173
   }
 ]

+ 163 - 0
data/init/thingModel.json

@@ -982,5 +982,168 @@
         }
       ]
     }
+  },
+  {
+    "id": "a5fnnx3ksu7n2n0f",
+    "productKey": "a5fnnx3ksu7n2n0f",
+    "model": {
+      "properties": [
+        {
+          "identifier": "temp",
+          "dataType": {
+            "type": "int32",
+            "specs": {
+              "min": "17",
+              "max": "32"
+            }
+          },
+          "name": "温度",
+          "accessMode": "rw"
+        },
+        {
+          "identifier": "swing_modes",
+          "dataType": {
+            "type": "enum",
+            "specs": {
+              "off": "关",
+              "on": "开"
+            }
+          },
+          "name": "扫风开关",
+          "accessMode": "rw"
+        },
+        {
+          "identifier": "modes",
+          "dataType": {
+            "type": "enum",
+            "specs": {
+              "heat": "制热",
+              "off": "关闭",
+              "cool": "制冷",
+              "fan_only": "送风"
+            }
+          },
+          "name": "模式",
+          "accessMode": "rw"
+        },
+        {
+          "identifier": "fan_modes",
+          "dataType": {
+            "type": "enum",
+            "specs": {
+              "high": "高",
+              "medium": "中",
+              "low": "低"
+            }
+          },
+          "name": "风模式",
+          "accessMode": "rw"
+        }
+      ],
+      "services": [
+      ],
+      "events": [
+      ]
+    }
+  },
+  {
+    "id": "BRD3x4fkKxkaxXFt",
+    "productKey": "BRD3x4fkKxkaxXFt",
+    "model": {
+      "properties": [],
+      "services": [
+        {
+          "identifier": "readData",
+          "inputData": [
+            {
+              "identifier": "deviceAddr",
+              "dataType": {
+                "type": "text",
+                "specs": {
+                  "length": "12"
+                }
+              },
+              "name": "设备地址",
+              "required": false
+            },
+            {
+              "identifier": "dataIdentifier",
+              "dataType": {
+                "type": "text",
+                "specs": {
+                  "length": "4"
+                }
+              },
+              "name": "数据标识",
+              "required": false
+            }
+          ],
+          "outputData": [],
+          "name": "读数据"
+        },
+        {
+          "identifier": "writeData",
+          "inputData": [
+            {
+              "identifier": "deviceAddr",
+              "dataType": {
+                "type": "text",
+                "specs": {
+                  "length": "12"
+                }
+              },
+              "name": "设备地址",
+              "required": false
+            },
+            {
+              "identifier": "dataIdentifier",
+              "dataType": {
+                "type": "text",
+                "specs": {
+                  "length": "4"
+                }
+              },
+              "name": "数据标识",
+              "required": false
+            }
+          ],
+          "outputData": [],
+          "name": "写数据"
+        }
+      ],
+      "events": []
+    }
+  },
+  {
+    "id": "PjmkANSTDt85bZPj",
+    "productKey": "PjmkANSTDt85bZPj",
+    "model": {
+      "properties": [
+        {
+          "identifier": "9010",
+          "dataType": {
+            "type": "text",
+            "specs": {
+              "length": "20"
+            }
+          },
+          "name": "(当前)正向有功总电能",
+          "accessMode": "r"
+        },
+        {
+          "identifier": "9410",
+          "dataType": {
+            "type": "text",
+            "specs": {
+              "length": "20"
+            }
+          },
+          "name": "(上月)正向有功总电能",
+          "accessMode": "r"
+        }
+      ],
+      "services": [],
+      "events": []
+    }
   }
 ]

+ 20 - 3
iot-common/src/main/java/cc/iotkit/common/ComponentClassLoader.java

@@ -9,6 +9,9 @@
  */
 package cc.iotkit.common;
 
+import org.apache.commons.io.IOUtils;
+import org.apache.commons.lang3.StringUtils;
+
 import java.io.File;
 import java.io.IOException;
 import java.io.InputStream;
@@ -20,9 +23,6 @@ import java.nio.charset.StandardCharsets;
 import java.util.HashMap;
 import java.util.Map;
 
-import org.apache.commons.io.IOUtils;
-import org.apache.commons.lang3.StringUtils;
-
 public class ComponentClassLoader {
     private static final Map<String, URLClassLoader> classLoaders = new HashMap<>();
 
@@ -71,6 +71,23 @@ public class ComponentClassLoader {
         return componentClass.getDeclaredConstructor().newInstance();
     }
 
+    public static <T> T getConverter(String name, File jarFile) throws Exception {
+        URLClassLoader classLoader = classLoaders.get(name);
+        InputStream is = classLoader.getResourceAsStream("convert.spi");
+        if (is == null) {
+            return null;
+        }
+
+        //多行只取第1行,并处理空格
+        String[] lines = IOUtils.toString(is, StandardCharsets.UTF_8).split("\\s");
+        if (lines.length == 0) {
+            throw new RuntimeException("convert class does not exist");
+        }
+        String className = lines[0].trim();
+        Class<T> converterClass = findClass(name, className);
+        return converterClass.getDeclaredConstructor().newInstance();
+    }
+
     public static void closeClassLoader(String name)  {
         try {
             URLClassLoader classLoader = classLoaders.get(name);

+ 87 - 0
iot-components/iot-DLT645-component/src/main/java/cc/iotkit/comp/DLT645/DLT645Component.java

@@ -0,0 +1,87 @@
+/*
+ * +----------------------------------------------------------------------
+ * | Copyright (c) 奇特物联 2021-2022 All rights reserved.
+ * +----------------------------------------------------------------------
+ * | Licensed 未经许可不能去掉「奇特物联」相关版权
+ * +----------------------------------------------------------------------
+ * | Author: xw2sy@163.com
+ * +----------------------------------------------------------------------
+ */
+package cc.iotkit.comp.DLT645;
+
+import cc.iotkit.common.exception.BizException;
+import cc.iotkit.common.utils.JsonUtil;
+import cc.iotkit.comp.AbstractDeviceComponent;
+import cc.iotkit.comp.CompConfig;
+import cc.iotkit.converter.DeviceMessage;
+import io.vertx.core.Future;
+import io.vertx.core.Vertx;
+import lombok.Data;
+import lombok.SneakyThrows;
+import lombok.extern.slf4j.Slf4j;
+
+import java.util.UUID;
+import java.util.concurrent.CountDownLatch;
+
+@Data
+@Slf4j
+public class DLT645Component extends AbstractDeviceComponent {
+
+    private Vertx vertx;
+
+    private CountDownLatch countDownLatch;
+
+    private DLT645Verticle DLT645Verticle;
+
+    private String deployedId;
+
+    private String id;
+
+    @Override
+    public void create(CompConfig config) {
+        super.create(config);
+        vertx = Vertx.vertx();
+        this.id = UUID.randomUUID().toString();
+        DLT645Config DLT645Config = JsonUtil.parse(config.getOther(), DLT645Config.class);
+        DLT645Verticle = new DLT645Verticle(DLT645Config);
+    }
+
+    @Override
+    public void start() {
+        try {
+            DLT645Verticle.setExecutor(getHandler());
+            countDownLatch = new CountDownLatch(1);
+            Future<String> future = vertx.deployVerticle(DLT645Verticle);
+            future.onSuccess((s -> {
+                deployedId = s;
+                countDownLatch.countDown();
+            }));
+            future.onFailure((e) -> {
+                countDownLatch.countDown();
+                log.error("start GLT645 component failed", e);
+            });
+            countDownLatch.await();
+            future.succeeded();
+        } catch (Throwable e) {
+            throw new BizException("start GLT645 component error", e);
+        }
+    }
+
+    @SneakyThrows
+    public void stop() {
+        DLT645Verticle.stop();
+        Future<Void> future = vertx.undeploy(deployedId);
+        future.onSuccess(unused -> log.info("stop GLT645 component success"));
+    }
+
+    @Override
+    public void destroy() {
+    }
+
+
+    @Override
+    public DeviceMessage send(DeviceMessage message) {
+        DLT645Verticle.sendMsg(message);
+        return message;
+    }
+}

+ 19 - 0
iot-components/iot-DLT645-component/src/main/java/cc/iotkit/comp/DLT645/DLT645Config.java

@@ -0,0 +1,19 @@
+/*
+ * +----------------------------------------------------------------------
+ * | Copyright (c) 奇特物联 2021-2022 All rights reserved.
+ * +----------------------------------------------------------------------
+ * | Licensed 未经许可不能去掉「奇特物联」相关版权
+ * +----------------------------------------------------------------------
+ * | Author: xw2sy@163.com
+ * +----------------------------------------------------------------------
+ */
+package cc.iotkit.comp.DLT645;
+
+import lombok.Data;
+
+@Data
+public class DLT645Config {
+
+    private int port;
+
+}

+ 184 - 0
iot-components/iot-DLT645-component/src/main/java/cc/iotkit/comp/DLT645/DLT645Verticle.java

@@ -0,0 +1,184 @@
+package cc.iotkit.comp.DLT645;
+
+
+import cc.iotkit.common.utils.JsonUtil;
+import cc.iotkit.comp.DLT645.analysis.*;
+import cc.iotkit.comp.DLT645.utils.ByteUtils;
+import cc.iotkit.comp.DLT645.utils.ContainerUtils;
+import cc.iotkit.comp.IMessageHandler;
+import cc.iotkit.comp.model.ReceiveResult;
+import cc.iotkit.converter.DeviceMessage;
+import io.vertx.core.AbstractVerticle;
+import io.vertx.core.Future;
+import io.vertx.core.net.NetServer;
+import io.vertx.core.net.NetServerOptions;
+import io.vertx.core.net.NetSocket;
+import lombok.extern.slf4j.Slf4j;
+
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.atomic.AtomicReference;
+
+/**
+ * @author tfd
+ * @date 2023-04-07
+ */
+@Slf4j
+public class DLT645Verticle extends AbstractVerticle {
+
+    private IMessageHandler executor;
+
+    private final DLT645Config config;
+
+    private Map<String, NetSocket> clientMap = new ConcurrentHashMap();
+
+    private NetServer netServer ;
+
+    public DLT645Verticle(DLT645Config config) {
+        this.config = config;
+    }
+
+    public void setExecutor(IMessageHandler executor) {
+        this.executor = executor;
+    }
+
+    private List<DLT645Data> entityList;
+
+    private Map<String, DLT645Data> dinMap;
+
+    /**
+     * DL/T 645-1997 自定义协议-67开头,DLT645-68开头
+     * 注册上行:67+动作标识2B(注册:7267、心跳:6862)+设备唯一值6B
+     * 注册下行:67+动作标识2B(注册:7267、心跳:6862)+状态码1B(00:成功、99:失败)
+     * 心跳上行:67+动作标识2B(注册:7267、心跳:6862)
+     * 心跳下行:67+动作标识2B(注册:7267、心跳:6862)+00
+     */
+    @Override
+    public void start() {
+        NetServerOptions options=new NetServerOptions().setPort(config.getPort());
+        netServer=vertx.createNetServer(options);
+        netServer.connectHandler(socket -> {
+            log.info("TCP client connect address:{}", socket.remoteAddress());
+            AtomicReference<String> clientKey = new AtomicReference<>();
+            Map<String, Object> dataMap= new HashMap<>();
+            // 设置连接超时时间
+            long timeoutId = vertx.setTimer(10000, id -> {
+                 socket.close();
+            });
+            // 处理连接
+            socket.handler(data -> {
+                String hexStr=ByteUtils.byteArrayToHexString(data.getBytes(),false);
+                log.info("Received message:{}", hexStr);
+                if(hexStr.startsWith("67")){//67打头为网关自定义协议
+                    String funCode=hexStr.substring(2,6);
+                    log.info("收到自定义消息,行为码为:" + funCode + ",数据为:" + hexStr);
+                    if("7267".equals(funCode)){
+                        String mac=hexStr.substring(6,18);
+                        dataMap.put("mac",mac);
+                        executor.onReceive(dataMap, "register", "",r ->{
+                            if(r!=null){
+                                //注册成功
+                                clientKey.set(getClientKey(r));
+                                if(!clientMap.containsKey(clientKey.get())){
+                                    clientMap.put(clientKey.get(),socket);
+                                }
+                                vertx.cancelTimer(timeoutId);
+                                executor.onReceive(dataMap, "online", "");
+                                socket.write(hexStr+"00");
+                                return;
+                            }
+                            socket.write(hexStr+"99");
+                            return;
+                        });
+                    }else if("6862".equals(funCode)){//心跳
+                        socket.write(hexStr+"00");
+                        return;
+                    }
+                }else{//其他为电表协议
+                    Map<String, Object> result = DLT645Analysis.unPackCmd2Map(ByteUtils.hexStringToByteArray(hexStr));
+                    //获取功能码
+                    Object func = result.get(DLT645Analysis.FUN);
+                    DLT645FunCode funCode = DLT645FunCode.decodeEntity((byte) func);
+                    if(funCode.isError()){
+                        log.error("message erroe:{}", hexStr);
+                        return;
+                    }
+                    //获取设备地址
+                    byte[] adrrTmp = (byte[]) result.get(DLT645Analysis.ADR);
+                    byte[] addr = new byte[6];
+                    ByteUtils.byteInvertedOrder(adrrTmp,addr);
+                    //获取数据
+                    byte[] dat = (byte[]) result.get(DLT645Analysis.DAT);
+                    DLT645V1997Data dataEntity = new DLT645V1997Data();
+                    dataEntity.decodeValue(dat, dinMap);
+                    Map<String, Object> unPack = new HashMap<>();
+                    unPack.put("deviceAddress",ByteUtils.byteArrayToHexString(addr,false));
+                    unPack.put("funCode",funCode.getCode());
+                    unPack.put("identify",dataEntity.getKey());//数据标识
+                    unPack.put("data",dataEntity.getValue()+dataEntity.getUnit());//数据+单位
+                    executor.onReceive(new HashMap<>(), "dlt", JsonUtil.toJsonString(unPack));
+                }
+            });
+            socket.closeHandler(res->{
+                log.warn("TCP connection closed!");
+                if(clientMap.containsKey(clientKey.get())){
+                    executor.onReceive(dataMap, "offline", "");
+                    clientMap.remove(clientKey.get());
+                }
+            });
+            socket.exceptionHandler(res->{
+                log.warn("TCP connection exception!");
+                if(clientMap.containsKey(clientKey)){
+                    executor.onReceive(dataMap, "offline", "");
+                    clientMap.remove(clientKey.get());
+                }
+            });
+        });
+        netServer.listen(res -> {
+            if (res.succeeded()) {
+                log.info("TCP server start success!");
+                DLT645v1997CsvLoader template = new DLT645v1997CsvLoader();
+                entityList = template.loadCsvFile();
+                dinMap = ContainerUtils.buildMapByKey(entityList, DLT645V1997Data::getKey);
+            } else {
+                log.error("TCP server start fail: " + res.cause());
+            }
+        });
+    }
+
+    @Override
+    public void stop() {
+        for (String clientKey : clientMap.keySet()) {
+            Map<String,Object> dataMap=new HashMap<>();
+            dataMap.put("mac",clientKey.split("_")[1]);
+            executor.onReceive(dataMap, "offline", "");
+        }
+        if(entityList.size()>0){
+            entityList.clear();
+        }
+        if(!dinMap.isEmpty()){
+            dinMap.clear();
+        }
+        clientMap.clear();
+        netServer.close(voidAsyncResult -> log.info("close tcp server..."));
+    }
+
+    private String getClientKey(ReceiveResult result) {
+        return getClientKey(result.getProductKey(), result.getDeviceName());
+    }
+
+    private String getClientKey(String productKey, String deviceName) {
+        return String.format("%s_%s", productKey, deviceName);
+    }
+
+    public DeviceMessage sendMsg(DeviceMessage msg) {
+        NetSocket client = clientMap.get(getClientKey(msg.getProductKey(),msg.getDeviceName()));
+        log.info("send msg payload:{}", msg.getContent().toString());
+        Future<Void> result = client.write(msg.getContent().toString());
+        result.onFailure(e -> log.error("DLT645 server send msg failed", e));
+        return msg;
+    }
+
+}

+ 309 - 0
iot-components/iot-DLT645-component/src/main/java/cc/iotkit/comp/DLT645/analysis/DLT645Analysis.java

@@ -0,0 +1,309 @@
+package cc.iotkit.comp.DLT645.analysis;
+
+import cc.iotkit.comp.DLT645.utils.ByteRef;
+import cc.iotkit.comp.DLT645.utils.BytesRef;
+import cc.iotkit.comp.DLT645.utils.ContainerUtils;
+
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+
+/**
+ * DL/T 645-1997 通讯规约通信规约
+ * 帧起始符 地址域 帧起始符 控制码 数据长度域 数据域 校验码 结束符
+ *    1      6      1       1        1      N      1      1
+ */
+public class DLT645Analysis {
+    /**
+     * 静态实例
+     */
+    private static final DLT645Analysis DLT645Analysis = new DLT645Analysis();
+
+    public static DLT645Analysis inst() {
+        return DLT645Analysis;
+    }
+    /**
+     * 设备地址:4字节的byte[]
+     */
+    public static final String ADR = "ADR";
+    /**
+     * 功能码:1字节的byte
+     */
+    public static final String FUN = "FUN";
+    /**
+     * 数据报:不定长的byte[]
+     */
+    public static final String DAT = "DAT";
+
+    /**
+     * 索引表
+     */
+    private Map<String, DLT645Data> name2entity;
+    private Map<String, DLT645Data> din2entity;
+
+
+    /**
+     * 验证码
+     *
+     * @param arrCmd
+     * @param iOffSet
+     * @return
+     */
+    private static int GetVfy(byte[] arrCmd, int iOffSet) {
+        int iSize = arrCmd.length - 2 - iOffSet;
+        if (iSize < 0) {
+            return 0;
+        }
+
+        int bySum = 0x00;
+
+        int index = iOffSet;
+        for (int i = 0; i < iSize; i++) {
+            bySum += arrCmd[index++];
+            bySum &= 0xff;
+        }
+
+        return bySum & 0xff;
+    }
+
+    /**
+     * 打包
+     *
+     * @param arrAddr 6字节的地址码
+     * @param byCmd   命令字
+     * @param arrData 数据段
+     * @return 是否成功
+     */
+    public static byte[] packCmd(byte[] arrAddr, byte byCmd, byte[] arrData) {
+        // 检查:数据块的大小
+        int iDataSize = arrData.length;
+        if (iDataSize > 255) {
+            return null;
+        }
+        if (arrAddr.length != 6) {
+            return null;
+        }
+
+        // 初始化数组大小
+        byte[] arrCmd = new byte[iDataSize + 13];
+
+
+        int index = 0;
+
+
+        // 前导字符(在发送帧信息之前,先发送1个或多个字节FEH,以唤醒接收方)
+        arrCmd[index++] = (byte) 0xFE;
+
+        // 帧起始符
+        arrCmd[index++] = (byte) 0x68;
+
+        // 地址码
+        System.arraycopy(arrAddr, 0, arrCmd, index, arrAddr.length);
+        index += arrAddr.length;
+
+        // 帧起始符
+        arrCmd[index++] = (byte) 0x68;
+
+        // 控制码
+        arrCmd[index++] = byCmd;
+
+        // 帧长度
+        arrCmd[index++] = (byte) iDataSize;
+
+        // 数据域
+        System.arraycopy(arrData, 0, arrCmd, index, iDataSize);
+        // 每个字节加上0x33
+        for (int i = 0; i < arrData.length; i++) {
+            arrCmd[index + i] = (byte) ((arrCmd[index + i] & 0xff) + 0x33);
+        }
+        index += iDataSize;
+
+
+        // 校验码
+        arrCmd[index++] = (byte) GetVfy(arrCmd, 1);
+
+        // 结束符
+        arrCmd[index++] = 0x16;
+
+        return arrCmd;
+    }
+
+    /**
+     * 默认打包
+     *
+     * @param byCmd
+     * @param arrData
+     * @return
+     */
+    public static byte[] packCmd(byte byCmd, byte[] arrData) {
+        byte[] arrAddr = new byte[6];
+
+        arrAddr[0] = 0x01;
+        arrAddr[1] = 0x00;
+        arrAddr[2] = 0x00;
+        arrAddr[3] = 0x00;
+        arrAddr[4] = 0x00;
+        arrAddr[5] = 0x00;
+
+        return packCmd(arrAddr, byCmd, arrData);
+    }
+
+    /**
+     * 解包
+     *
+     * @param arrCmd     报文,前面有不确定的唤醒字符
+     * @param arrAddrRef 地址码
+     * @param byCmd      命令字
+     * @param arrDataRef 数据
+     * @return 是否成功
+     */
+    private static boolean unPackCmd2Map(byte[] arrCmd, BytesRef arrAddrRef, ByteRef byCmd, BytesRef arrDataRef) {
+        int iSize = arrCmd.length;
+
+        // 查找偏移量:DLT645电表前面会被塞入不定长的乱码数据,被用来激活电表,直到0x68字符出现
+        int iOffSet = 0;
+        int index = 0;
+        for (iOffSet = 0; iOffSet < iSize; iOffSet++) {
+            if ((arrCmd[index++] & 0xff) == 0x68) {
+                break;
+            }
+        }
+        if (iOffSet == iSize) {
+            return false;
+        }
+
+        // 检查:数据包大小
+        if (iSize < 12 + iOffSet) {
+            return false;
+        }
+
+//==============================================================================
+// 中国电力总局的DL/T 645-1997 多功能电能表通信规约
+// 引导码 起始符 地址码 起始符 功能码 帧长度 数据域 校验和 结束符
+//   N      1      6      1      1      1      N       1      1
+//==============================================================================
+
+        // 检查:起始符1
+        if (arrCmd[iOffSet + 0] != 0x68) {
+            return false;
+        }
+        // 检查:起始符2
+        if (arrCmd[iOffSet + 7] != 0x68) {
+            return false;
+        }
+        // 检查:结束符
+        if (arrCmd[iSize - 1] != 0x16) {
+            return false;
+        }
+
+        // 地址码
+        byte[] arrAddr = new byte[6];
+        System.arraycopy(arrCmd, iOffSet + 1, arrAddr, 0, 6);
+        arrAddrRef.setValue(arrAddr);
+
+
+        // 功能码
+        byCmd.setValue(arrCmd[iOffSet + 8]);
+
+
+        // 检查:帧长度
+        int iDataSize = arrCmd[iOffSet + 9];
+        if ((iDataSize + 12 + iOffSet) != iSize) {
+            return false;
+        }
+
+        // 数据域
+        byte[] arrData = new byte[iDataSize];
+        System.arraycopy(arrCmd, iOffSet + 10, arrData, 0, iDataSize);
+        // 每个字节先减去0x33
+        for (int i = 0; i < arrData.length; i++) {
+            arrData[i] = (byte) ((arrData[i] & 0xff) - 0x33);
+        }
+        arrDataRef.setValue(arrData);
+
+
+        // 检查:校验码
+        byte byVfyOK = (byte) (GetVfy(arrCmd, iOffSet) & 0xff);
+        return byVfyOK == arrCmd[iSize - 2];
+    }
+
+    public static boolean unPackCmd2Map(byte[] arrCmd, ByteRef byCmd, BytesRef arrData) {
+        BytesRef arrAddr = new BytesRef();
+        return unPackCmd2Map(arrCmd, arrAddr, byCmd, arrData);
+    }
+
+    /**
+     * 只有数据标识的DI0和DI1的请求命令
+     *
+     * @param DI0 数据标识
+     * @param DI1 数据标识
+     * @return
+     */
+    public static byte[] packCmdGetData(int DI0, int DI1) {
+        byte[] arrData = new byte[2];
+        arrData[0] = (byte) DI0;
+        arrData[1] = (byte) DI1;
+
+        return packCmd((byte) 0x01, arrData);
+    }
+
+    public static boolean unPackCmdGetData(byte[] arrCmd, BytesRef arrData) {
+        ByteRef byCmd = new ByteRef();
+        if (!unPackCmd2Map(arrCmd, byCmd, arrData)) {
+            return false;
+        }
+
+        return byCmd.getValue() == 0x81;
+    }
+
+    /**
+     * 包装成另一种格式
+     *
+     * @param arrCmd
+     * @return
+     */
+    public static Map<String, Object> unPackCmd2Map(byte[] arrCmd) {
+        ByteRef byFun = new ByteRef();
+        BytesRef byAddr = new BytesRef();
+        BytesRef arrData = new BytesRef();
+        if (!unPackCmd2Map(arrCmd, byAddr, byFun, arrData)) {
+            return null;
+        }
+
+        Map<String, Object> value = new HashMap<>();
+        value.put(ADR, byAddr.getValue());
+        value.put(FUN, byFun.getValue());
+        value.put(DAT, arrData.getValue());
+        return value;
+    }
+
+    /**
+     * 获得对象信息
+     *
+     * @return 对象副本
+     */
+    public synchronized Map<String, DLT645Data> getTemplateByName() {
+        if (this.name2entity == null) {
+            DLT645v1997CsvLoader loader = new DLT645v1997CsvLoader();
+            List<DLT645Data> entityList = loader.loadCsvFile();
+            Map<String, DLT645Data> nameMap = ContainerUtils.buildMapByKey(entityList, DLT645V1997Data::getName);
+            this.name2entity = new ConcurrentHashMap<>();
+            this.name2entity.putAll(nameMap);
+        }
+
+        return this.name2entity;
+    }
+
+    public synchronized Map<String, DLT645Data> getTemplateByDIn() {
+        if (this.din2entity == null) {
+            DLT645v1997CsvLoader loader = new DLT645v1997CsvLoader();
+            List<DLT645Data> entityList = loader.loadCsvFile();
+            Map<String, DLT645Data> keyMap = ContainerUtils.buildMapByKey(entityList, DLT645V1997Data::getKey);
+            this.din2entity = new ConcurrentHashMap<>();
+            this.din2entity.putAll(keyMap);
+        }
+
+        return this.din2entity;
+    }
+}

+ 104 - 0
iot-components/iot-DLT645-component/src/main/java/cc/iotkit/comp/DLT645/analysis/DLT645Converter.java

@@ -0,0 +1,104 @@
+package cc.iotkit.comp.DLT645.analysis;
+
+import cc.iotkit.common.thing.ThingService;
+import cc.iotkit.common.utils.JsonUtil;
+import cc.iotkit.common.utils.UniqueIdUtil;
+import cc.iotkit.comp.DLT645.utils.ByteUtils;
+import cc.iotkit.converter.Device;
+import cc.iotkit.converter.DeviceMessage;
+import cc.iotkit.converter.IConverter;
+import cc.iotkit.model.device.message.ThingModelMessage;
+import lombok.Data;
+import lombok.extern.slf4j.Slf4j;
+
+import java.util.Map;
+
+@Slf4j
+@Data
+public class DLT645Converter implements IConverter {
+    @Override
+    public void setScript(String script) {
+
+    }
+
+    /**
+     * 编码
+     * @param msg
+     * @return
+     */
+    @Override
+    public ThingModelMessage decode(DeviceMessage msg) {
+        ThingModelMessage tmm = null;
+        ReportData rd=JsonUtil.parse(JsonUtil.toJsonString(msg.getContent()),ReportData.class);
+        if(ThingModelMessage.TYPE_PROPERTY.equals(rd.type)&&"report".equals(rd.getIdentifier())){
+            tmm=ThingModelMessage.builder()
+                    .mid(msg.getMid())
+                    .productKey(msg.getProductKey())
+                    .deviceName(msg.getDeviceName())
+                    .identifier(rd.getIdentifier())
+                    .occurred(rd.getOccur())
+                    .time(rd.getTime())
+                    .type(rd.getType())
+                    .data(rd.getData())
+                    .build();
+        }
+        return tmm;
+    }
+    /**
+     * 解码
+     * @param service,device
+     * @return
+     */
+    @Override
+    public DeviceMessage encode(ThingService<?> service, Device device) {
+        DeviceMessage deviceMsg=new DeviceMessage();
+        deviceMsg.setProductKey(service.getProductKey());
+        deviceMsg.setDeviceName(service.getDeviceName());
+        deviceMsg.setMid(UniqueIdUtil.newRequestId());
+        Map<String,String> sd = (Map<String, String>) service.getParams();
+        String funCode="";
+        if(ThingService.TYPE_SERVICE.equals(service.getType())){//服务相关
+            if("readData".equals(service.getIdentifier())){//读数据
+                funCode=DLT645FunCode.func_v97_00001;
+            }else if("writeData".equals(service.getIdentifier())){//写数据
+                funCode=DLT645FunCode.func_v97_00100;
+            }
+            //...其他功能码
+        }
+        deviceMsg.setContent(packData(sd.get("deviceAddr"),funCode,sd.get("dataIdentifier")));
+        return deviceMsg;
+    }
+
+    @Override
+    public void putScriptEnv(String key, Object value) {
+
+    }
+
+    private String packData(String deviceAddress,String funCode,String dataIdentifier){
+        // 对设备地址进行编码
+        byte[] tmp = ByteUtils.hexStringToByteArray(deviceAddress);
+        byte[] adrr = new byte[6];
+        ByteUtils.byteInvertedOrder(tmp,adrr);
+
+        // 根据对象名获取对象格式信息,这个格式信息,记录在CSV文件中
+        DLT645Data dataEntity = DLT645Analysis.inst().getTemplateByDIn().get(dataIdentifier);
+        if (dataEntity == null) {
+            throw new RuntimeException("CSV模板文件中未定义对象:" + dataIdentifier + " ,你需要在模板中添加该对象信息");
+        }
+        byte byFun = Byte.decode(String.valueOf(DLT645FunCode.getCodev1997(funCode)));
+
+        // 使用DLT645协议框架编码
+        byte[] pack = DLT645Analysis.packCmd(adrr,byFun,dataEntity.getDIn());
+
+        // 将报文按要求的16进制格式的String对象返回
+        return ByteUtils.byteArrayToHexString(pack,false);
+    }
+    @Data
+    public static class ReportData{
+        private String type;
+        private String identifier;
+        private Long occur;
+        private Long time;
+        private Object data;
+    }
+}

+ 86 - 0
iot-components/iot-DLT645-component/src/main/java/cc/iotkit/comp/DLT645/analysis/DLT645Data.java

@@ -0,0 +1,86 @@
+package cc.iotkit.comp.DLT645.analysis;
+
+import lombok.Data;
+
+import java.util.Map;
+
+@Data
+public abstract class DLT645Data {
+    /**
+     * 名称
+     */
+    private String name;
+    /**
+     * 格式
+     */
+    private DLT645DataFormat format = new DLT645DataFormat();
+    /**
+     * 长度
+     */
+    private int length;
+    /**
+     * 单位
+     */
+    private String unit;
+    /**
+     * 可读
+     */
+    private boolean read;
+    /**
+     * 可写
+     */
+    private boolean write;
+    /**
+     * 数值
+     */
+    private Object value = 0.0;
+
+    /**
+     * 数值
+     */
+    private Object value2nd;
+
+    public abstract String getKey();
+
+    public abstract byte[] getDIn();
+
+    public abstract void setDIn(byte[] value);
+
+    public abstract int getDInLen();
+
+    public String toString() {
+        if (this.value2nd == null) {
+            return this.name + ":" + this.value + this.unit;
+        }
+
+        return this.name + ":" + this.value + this.unit + " " + this.value2nd;
+    }
+
+    public void decodeValue(byte[] data, Map<String, DLT645Data> dinMap) {
+
+        // DI值
+        this.setDIn(data);
+
+        // 获取字典信息
+        DLT645Data dict = dinMap.get(this.getKey());
+        if (dict == null) {
+            throw new RuntimeException("DIn info err,please configure:" + this.getKey());
+        }
+
+        this.format = dict.format;
+        this.name = dict.name;
+        this.read = dict.read;
+        this.write = dict.write;
+        this.length = dict.length;
+        this.unit = dict.unit;
+
+
+        // 基本值
+        this.value = this.format.decodeValue(data, this.format.getFormat(), this.getDInLen(), this.format.getLength());
+
+        // 组合值
+        if (this.format.getFormat2nd() != null && !this.format.getFormat2nd().isEmpty()) {
+            this.value2nd = this.format.decodeValue(data, this.format.getFormat2nd(), this.getDInLen() + this.format.getLength(), this.format.getLength2nd());
+        }
+    }
+}

+ 359 - 0
iot-components/iot-DLT645-component/src/main/java/cc/iotkit/comp/DLT645/analysis/DLT645DataFormat.java

@@ -0,0 +1,359 @@
+package cc.iotkit.comp.DLT645.analysis;
+
+import cc.iotkit.comp.DLT645.utils.ByteUtils;
+import lombok.Data;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+@Data
+public class DLT645DataFormat {
+    // 数值格式
+    public static final String FORMAT_X = "X";
+    public static final String FORMAT_N = "N";
+
+    // 时间格式
+    public static final String FORMAT_YYMMDDWW = "YYMMDDWW";
+    public static final String FORMAT_hhmmss = "hhmmss";
+    public static final String FORMAT_YYMMDDhhmm = "YYMMDDhhmm";
+    public static final String FORMAT_MMDDhhmm = "MMDDhhmm";
+    public static final String FORMAT_DDhh = "DDhh";
+    public static final String FORMAT_hhmm = "hhmm";
+    public static final String FORMAT_mmmm = "mmmm";
+
+    // 编码格式
+    public static final String FORMAT_NN___NN = "NN...NN";
+    public static final String FORMAT_XX___XX = "XX...XX";
+
+    // 状态格式
+    public static final String FORMAT_STATUS_WEEK = "周休日状态字";
+    public static final String FORMAT_STATUS_METER = "电表运行状态字";
+    public static final String FORMAT_STATUS_NETWORK = "电网状态字";
+
+
+    /**
+     * 格式类型
+     */
+    private String format = "";
+    /**
+     * 组合格式:第二个格式
+     */
+    private String format2nd = "";
+    /**
+     * 长度
+     */
+    private int length = 0;
+    /**
+     * 组合格式:第二个长度
+     */
+    private int length2nd = 0;
+    /**
+     * 缩小比例
+     */
+    private double ratio = 1.0;
+
+    public Object decodeValue(byte[] data, String format, int start, int length) throws RuntimeException {
+        // 前面4个字节是DI0~DI3
+        if (data.length < length + start) {
+            throw new RuntimeException("数据长度不正确!");
+        }
+
+        // 各种XX.XX格式
+        if (format.equals(FORMAT_X)) {
+            return this.getValue(data, start, length, this.ratio);
+        }
+        // 各种NN.NN格式
+        if (format.equals(FORMAT_N)) {
+            return this.getValue(data, start, length, this.ratio);
+        }
+        if (format.equals(FORMAT_NN___NN)) {
+            return this.getString(data, start, length);
+        }
+
+        // 时间格式
+        if (format.equals(FORMAT_hhmm) || format.equals(FORMAT_DDhh) || format.equals(FORMAT_YYMMDDWW) || format.equals(FORMAT_hhmmss) || format.equals(FORMAT_YYMMDDhhmm) || format.equals(FORMAT_MMDDhhmm)) {
+            return this.getDataTime(data, format, start, length);
+        }
+
+
+        if (format.equals(FORMAT_XX___XX)) {
+            this.format = FORMAT_XX___XX;
+            this.ratio = 1.0;
+            return true;
+        }
+
+
+        return false;
+    }
+
+    /**
+     * 解码格式:固定长度格式和可变长度格式
+     * 固定长度格式:根据XX.XX它格式本身长度进行判定
+     *
+     * @param format 格式名称
+     * @param length 可变格式的长度
+     * @return 是否成功
+     */
+    public boolean decodeFormat(String format, int length) {
+        // 统计字符种类的数量
+        Map<Character, Integer> charCount = charCount(format);
+
+        // 组合格式:XX.XXXX|YYMMDDhhmm
+        if (charCount.containsKey('|') && charCount.get('|').equals(1)) {
+            String format1 = format.substring(0, format.indexOf("|"));
+            String format2 = format.substring(format.indexOf("|") + 1);
+            this.decodeFormat(format2, -1);
+            this.format2nd = this.format;
+            this.length2nd = this.length;
+            this.decodeFormat(format1, -1);
+            return true;
+        }
+
+        // 各种XX.XX格式
+        if (charCount.containsKey('X') && charCount.containsKey('.') && charCount.get('.').equals(1)) {
+            this.format = FORMAT_X;
+            int point = format.length() - format.indexOf(".") - 1;
+            for (int i = 0; i < point; i++) {
+                this.ratio *= 10.0;
+            }
+            this.length = (format.length() - 1) / 2;
+            return true;
+        }
+        // XXXX格式
+        if (charCount.containsKey('X') && charCount.size() == 1) {
+            this.format = FORMAT_X;
+            this.ratio = 1.0;
+            this.length = length;
+            return true;
+        }
+        // 各种NN.NN格式
+        if (charCount.containsKey('N') && charCount.containsKey('.') && charCount.get('.').equals(1)) {
+            this.format = FORMAT_N;
+            int point = format.length() - format.indexOf(".") - 1;
+            for (int i = 0; i < point; i++) {
+                this.ratio *= 10.0;
+            }
+            this.length = (format.length() - 1) / 2;
+            return true;
+        }
+        // NNN格式
+        if (charCount.containsKey('N') && charCount.size() == 1) {
+            this.format = FORMAT_N;
+            this.ratio = 1.0;
+            this.length = length;
+            return true;
+        }
+
+
+        // 固定长度
+        if (this.isFixedLength(format)) {
+            this.format = format;
+            this.ratio = 1.0;
+            this.length = format.length() / 2;
+            return true;
+        }
+
+        // 可变长度
+        if (this.isVariableLength(format)) {
+            this.format = format;
+            this.ratio = 1.0;
+            this.length = length;
+            return true;
+        }
+
+        return false;
+    }
+
+    /**
+     * 是否为固定长度:它的长度是直接通过格式就能确定
+     *
+     * @param format
+     * @return
+     */
+    private boolean isFixedLength(String format) {
+        if (format.equalsIgnoreCase(FORMAT_hhmm)) {
+            return true;
+        }
+        if (format.equalsIgnoreCase(FORMAT_DDhh)) {
+            return true;
+        }
+        if (format.equalsIgnoreCase(FORMAT_MMDDhhmm)) {
+            return true;
+        }
+        if (format.equalsIgnoreCase(FORMAT_YYMMDDhhmm)) {
+            return true;
+        }
+        if (format.equalsIgnoreCase(FORMAT_hhmmss)) {
+            return true;
+        }
+        if (format.equalsIgnoreCase(FORMAT_mmmm)) {
+            return true;
+        }
+
+        return format.equalsIgnoreCase(FORMAT_YYMMDDWW);
+    }
+
+    /**
+     * 是否为可变长度:它的长度是通过用户在CSV文件中告知
+     *
+     * @param format
+     * @return
+     */
+    private boolean isVariableLength(String format) {
+        if (format.equalsIgnoreCase(FORMAT_NN___NN)) {
+            return true;
+        }
+        if (format.equalsIgnoreCase(FORMAT_XX___XX)) {
+            return true;
+        }
+        if (format.equalsIgnoreCase(FORMAT_STATUS_METER)) {
+            return true;
+        }
+        if (format.equalsIgnoreCase(FORMAT_STATUS_NETWORK)) {
+            return true;
+        }
+
+        return format.equalsIgnoreCase(FORMAT_STATUS_WEEK);
+    }
+
+    /**
+     * 统计字符数量,方面后面判定格式
+     *
+     * @param format DLT645规约中定义的XXX.XXX之类的各种数据格式文本
+     * @return 各字符数量
+     */
+    private Map<Character, Integer> charCount(String format) {
+
+        Map<Character, Integer> charSet = new HashMap<>();
+        for (int i = 0; i < format.length(); i++) {
+            Character ch = format.charAt(i);
+            Integer count = charSet.get(ch);
+            if (count == null) {
+                count = 0;
+            }
+
+            count++;
+            charSet.put(ch, count);
+        }
+
+        return charSet;
+    }
+
+    /**
+     * 4字节长度的double型数值
+     *
+     * @param data  data数组
+     * @param start 数据在数组中的起始位置
+     * @param ratio 倍率,比如缩小100倍数,那么填0.01
+     * @return 返回值
+     */
+    private Object getValue(byte[] data, int start, int length, double ratio) {
+        long sum = 0;
+        double rd = 1.0;
+        for (int i = 0; i < length; i++) {
+            long l = data[start + i] & 0x0f;
+            long h = (data[start + i] & 0xf0) >> 4;
+
+            l = (long) (l * rd);
+            sum += l;
+            rd = rd * 10.0;
+
+
+            h = (long) (h * rd);
+            sum += h;
+            rd = rd * 10.0;
+
+        }
+
+        if (ratio < 1.1 && ratio > 0.0) {
+            // 如果ratio==1
+            return sum;
+        } else {
+            return sum / ratio;
+        }
+    }
+
+    /**
+     * 日期格式的解码
+     *
+     * @param data   data数组
+     * @param format 日期格式
+     * @param start  数据在数组中的起始位置
+     * @param length 格式长度
+     * @return 返回值
+     */
+    private String getDataTime(byte[] data, String format, int start, int length) {
+        // 拆解成个位数列表
+        List<String> list = new ArrayList<>();
+        for (int i = 0; i < length; i++) {
+            Integer l = data[start + i] & 0x0f;
+            Integer h = (data[start + i] & 0xf0) >> 4;
+
+            list.add(l.toString());
+            list.add(h.toString());
+        }
+
+        // 格式1
+        if (format.equals(FORMAT_YYMMDDhhmm)) {
+            String result = "20" + list.get(9) + list.get(8) + "年" + list.get(7) + list.get(6) + "月" + list.get(5) + list.get(4) + "日";
+            result += " " + list.get(3) + list.get(2) + "点" + list.get(1) + list.get(0) + "分";
+            return result;
+        }
+
+        if (format.equals(FORMAT_YYMMDDWW)) {
+            String result = "20" + list.get(7) + list.get(6) + "年" + list.get(5) + list.get(4) + "月" + list.get(3) + list.get(2) + "日";
+            result += " 星期:" + list.get(1) + list.get(0);
+            return result;
+        }
+        if (format.equals(FORMAT_hhmmss)) {
+            String result = list.get(5) + list.get(4) + "点" + list.get(3) + list.get(2) + "分" + list.get(1) + list.get(0) + "秒";
+            return result;
+        }
+        if (format.equals(FORMAT_mmmm)) {
+            String result = list.get(3) + list.get(2) + list.get(1) + list.get(0) + "分";
+            return result;
+        }
+
+
+        if (format.equals(FORMAT_MMDDhhmm)) {
+            String result = list.get(7) + list.get(6) + "月" + list.get(5) + list.get(4) + "日 ";
+            result += list.get(3) + list.get(2) + "点" + list.get(1) + list.get(0) + "分";
+            return result;
+        }
+        if (format.equals(FORMAT_DDhh)) {
+            String result = list.get(3) + list.get(2) + "号 " + list.get(1) + list.get(0) + "点";
+            return result;
+        }
+        if (format.equals(FORMAT_hhmm)) {
+            String result = list.get(3) + list.get(2) + "点 " + list.get(1) + list.get(0) + "分";
+            return result;
+        }
+
+
+        return "";
+    }
+
+    private byte encodeBCD(byte a) {
+        return (byte) ((a / 10) * 16 + (a % 10));
+    }
+
+    private byte decodeBCD(byte a) {
+        return (byte) ((a / 16) * 10 + (a % 16));
+    }
+
+    private Object getString(byte[] data, int start, int length) {
+        byte[] tmp = new byte[length];
+
+        for (int i = 0; i < length; i++) {
+            tmp[i] = data[start + i];
+        }
+        for (int i = 0; i < length / 2; i++) {
+            byte by = tmp[i];
+            tmp[i] = tmp[length - i - 1];
+            tmp[length - i - 1] = by;
+        }
+        return ByteUtils.byteArrayToHexString(tmp, true).replace(" ", "");
+    }
+}

+ 183 - 0
iot-components/iot-DLT645-component/src/main/java/cc/iotkit/comp/DLT645/analysis/DLT645FunCode.java

@@ -0,0 +1,183 @@
+package cc.iotkit.comp.DLT645.analysis;
+
+import lombok.Data;
+
+/**
+ * 功能码
+ */
+@Data
+public class DLT645FunCode {
+
+    public static final String func_v97_00000 = "保留";
+    public static final String func_v97_00001 = "读数据";
+    public static final String func_v97_00010 = "读后续数据";
+    public static final String func_v97_00011 = "重读数据";
+    public static final String func_v97_00100 = "写数据";
+    public static final String func_v97_01000 = "广播校时";
+    public static final String func_v97_01010 = "写设备地址";
+    public static final String func_v97_01100 = "更改通信速率";
+    public static final String func_v97_01111 = "修改密码";
+    public static final String func_v97_10000 = "最大需量清零";
+    /**
+     * 方向:主站发出=false,从站应答=true
+     */
+    private boolean direct = false;
+    /**
+     * 从站是否异常应答
+     */
+    private boolean error = false;
+    /**
+     * 功能代码
+     */
+    private byte code = 0;
+    /**
+     * 是否最后的尾部
+     */
+    private boolean next = false;
+
+    public static DLT645FunCode decodeEntity(byte func) {
+        DLT645FunCode dlt645FunCode = new DLT645FunCode();
+        dlt645FunCode.decode(func);
+        return dlt645FunCode;
+    }
+
+    public static int getCodev1997(String text) {
+        if (func_v97_00000.equals(text)) {//
+            return 0b00000;
+        }
+        if (func_v97_00001.equals(text)) {
+            return 0b00001;
+        }
+        if (func_v97_00010.equals(text)) {
+            return 0b00010;
+        }
+        if (func_v97_00011.equals(text)) {
+            return 0b00011;
+        }
+        if (func_v97_00100.equals(text)) {
+            return 0b00100;
+        }
+        if (func_v97_01000.equals(text)) {
+            return 0b01000;
+        }
+        if (func_v97_01010.equals(text)) {
+            return 0b01010;
+        }
+        if (func_v97_01100.equals(text)) {
+            return 0b01100;
+        }
+        if (func_v97_01111.equals(text)) {
+            return 0b01111;
+        }
+        if (func_v97_10000.equals(text)) {
+            return 0b10000;
+        }
+        return 0b00000;
+    }
+
+    /**
+     * 编码
+     *
+     * @return 功能码
+     */
+    public byte encode() {
+        int func = 0;
+        if (this.direct) {
+            func |= 0x80;
+        }
+        if (this.error) {
+            func |= 0x40;
+        }
+        if (this.next) {
+            func |= 0x20;
+        }
+        func |= this.code & 0x1F;
+
+        return (byte) func;
+    }
+
+    /**
+     * 生成功能码
+     *
+     * @param dlt645FunCode
+     * @return
+     */
+    public byte encodeFunCode(DLT645FunCode dlt645FunCode) {
+        return dlt645FunCode.encode();
+    }
+
+    /**
+     * 解码
+     *
+     * @param func
+     */
+    public void decode(byte func) {
+        this.direct = (func & 0x80) > 0;
+        this.error = (func & 0x40) > 0;
+        this.next = (func & 0x20) > 0;
+        this.code = (byte) (func & 0x1F);
+    }
+
+    public String getCodeTextV1997() {
+        if (this.code == 0b00000) {
+            return func_v97_00000;
+        }
+        if (this.code == 0b01000) {
+            return func_v97_01000;
+        }
+        if (this.code == 0b00001) {
+            return func_v97_00001;
+        }
+        if (this.code == 0b00010) {
+            return func_v97_00010;
+        }
+        if (this.code == 0b00100) {
+            return func_v97_00100;
+        }
+        if (this.code == 0b01010) {
+            return func_v97_01010;
+        }
+        if (this.code == 0b01100) {
+            return func_v97_01100;
+        }
+        if (this.code == 0b01111) {
+            return func_v97_01111;
+        }
+        if (this.code == 0b10000) {
+            return func_v97_10000;
+        }
+
+        return "";
+    }
+
+    /**
+     * 获取文本描述
+     *
+     * @return 文本描述
+     */
+    public String getMessage() {
+        String message = "";
+        if (this.direct) {
+            message += "从站发出:";
+        } else {
+            message += "主站发出:";
+        }
+
+        message += this.getCodeTextV1997();
+
+        if (this.error) {
+            message += ":异常";
+        } else {
+            message += ":正常";
+        }
+
+
+        if (this.next) {
+            message += ":还有后续帧";
+        } else {
+            message += ":这是末尾帧";
+        }
+
+        return message;
+    }
+}

+ 57 - 0
iot-components/iot-DLT645-component/src/main/java/cc/iotkit/comp/DLT645/analysis/DLT645V1997Data.java

@@ -0,0 +1,57 @@
+package cc.iotkit.comp.DLT645.analysis;
+
+import lombok.Getter;
+import lombok.Setter;
+
+@Setter
+@Getter
+public class DLT645V1997Data extends DLT645Data {
+    /**
+     * DI1/DI0
+     */
+    private byte di0l = 0;
+    private byte di0h = 0;
+    private byte di1l = 0;
+    private byte di1h = 0;
+
+    @Override
+    public String getKey() {
+        String key = "";
+        key += Integer.toString(this.di1h, 16);
+        key += Integer.toString(this.di1l, 16);
+        key += Integer.toString(this.di0h, 16);
+        key += Integer.toString(this.di0l, 16);
+        return key.toUpperCase();
+    }
+
+    @Override
+    public byte[] getDIn() {
+        byte[] value = new byte[2];
+        value[0] = (byte) (this.di0l + (this.di0h << 4));
+        value[1] = (byte) (this.di1l + (this.di1h << 4));
+        return value;
+    }
+
+    @Override
+    public void setDIn(byte[] value) {
+        if (value.length < 2) {
+            throw new RuntimeException("数据长度小于2字节!");
+        }
+
+        // DI值
+        this.di1h = (byte) ((value[1] & 0xf0) >> 4);
+        this.di1l = (byte) (value[1] & 0x0f);
+        this.di0h = (byte) ((value[0] & 0xf0) >> 4);
+        this.di0l = (byte) (value[0] & 0x0f);
+    }
+
+    /**
+     * 1997版的DIn2字节
+     *
+     * @return
+     */
+    @Override
+    public int getDInLen() {
+        return 2;
+    }
+}

+ 87 - 0
iot-components/iot-DLT645-component/src/main/java/cc/iotkit/comp/DLT645/analysis/DLT645v1997CsvLoader.java

@@ -0,0 +1,87 @@
+package cc.iotkit.comp.DLT645.analysis;
+
+import cn.hutool.core.text.csv.CsvReader;
+import cn.hutool.core.text.csv.CsvUtil;
+import cn.hutool.core.util.CharsetUtil;
+import lombok.Data;
+
+import java.io.InputStreamReader;
+import java.io.Serializable;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * 数据实体的数据模板
+ */
+public class DLT645v1997CsvLoader {
+    /**
+     * 从CSV文件中装载映射表
+     *
+     */
+    public List<DLT645Data> loadCsvFile() {
+        CsvReader csvReader = CsvUtil.getReader();
+        InputStreamReader dataReader=new InputStreamReader(this.getClass().getClassLoader().getResourceAsStream("DLT645-1997.csv"),CharsetUtil.CHARSET_GBK);
+        List<JDecoderValueParam> rows = csvReader.read(dataReader, JDecoderValueParam.class);
+        List<DLT645Data> list = new ArrayList<>();
+        for (JDecoderValueParam jDecoderValueParam : rows) {
+            try {
+                DLT645V1997Data entity = new DLT645V1997Data();
+                entity.setName(jDecoderValueParam.getName());
+                entity.setDi1h((byte) Integer.parseInt(jDecoderValueParam.di1h, 16));
+                entity.setDi1l((byte) Integer.parseInt(jDecoderValueParam.di1l, 16));
+                entity.setDi0h((byte) Integer.parseInt(jDecoderValueParam.di0h, 16));
+                entity.setDi0l((byte) Integer.parseInt(jDecoderValueParam.di0l, 16));
+                entity.setLength(jDecoderValueParam.length);
+                entity.setUnit(jDecoderValueParam.unit);
+                entity.setRead(Boolean.parseBoolean(jDecoderValueParam.read));
+                entity.setWrite(Boolean.parseBoolean(jDecoderValueParam.write));
+
+                DLT645DataFormat format = new DLT645DataFormat();
+                if (format.decodeFormat(jDecoderValueParam.format, jDecoderValueParam.length)) {
+                    entity.setFormat(format);
+                } else {
+                    System.out.println("DLT645 CSV记录的格式错误:" + jDecoderValueParam.getName() + ":" + jDecoderValueParam.getFormat());
+                    continue;
+                }
+                list.add(entity);
+            } catch (Exception e) {
+                e.printStackTrace();
+            }
+        }
+        return list;
+    }
+
+
+    @Data
+    static public class JDecoderValueParam implements Serializable {
+        private String di1h;
+        private String di1l;
+        private String di0h;
+        private String di0l;
+        /**
+         * 编码格式
+         */
+        private String format;
+        /**
+         * 长度
+         */
+        private Integer length;
+        /**
+         * 单位
+         */
+        private String unit;
+
+        /**
+         * 是否可读
+         */
+        private String read;
+        /**
+         * 是否可写
+         */
+        private String write;
+        /**
+         * 名称
+         */
+        private String name;
+    }
+}

+ 11 - 0
iot-components/iot-DLT645-component/src/main/java/cc/iotkit/comp/DLT645/utils/ByteRef.java

@@ -0,0 +1,11 @@
+package cc.iotkit.comp.DLT645.utils;
+
+import lombok.AccessLevel;
+import lombok.Getter;
+import lombok.Setter;
+
+@Getter(value = AccessLevel.PUBLIC)
+@Setter(value = AccessLevel.PUBLIC)
+public class ByteRef {
+    private byte value = 0;
+}

+ 76 - 0
iot-components/iot-DLT645-component/src/main/java/cc/iotkit/comp/DLT645/utils/ByteUtils.java

@@ -0,0 +1,76 @@
+package cc.iotkit.comp.DLT645.utils;
+
+public class ByteUtils {
+
+    /**
+     * 根据十六进制生成byte
+     *
+     * @param hex 16进制的字符串 比如"FF"
+     * @return byte数字比如-1
+     */
+    public static byte hex2byte(String hex) {
+        return Integer.valueOf(hex, 16).byteValue();
+    }
+
+    /**
+     * 16进制的字符串表示转成字节数组
+     *
+     * @param hexString 16进制格式的字符串
+     * @return 转换后的字节数组
+     **/
+    public static byte[] hexStringToByteArray(String hexString) {
+        String string = hexString.replaceAll(" ", "");
+        final byte[] byteArray = new byte[string.length() / 2];
+        int pos = 0;
+        for (int i = 0; i < byteArray.length; i++) {
+            // 因为是16进制,最多只会占用4位,转换成字节需要两个16进制的字符,高位在先
+            byte high = (byte) (Character.digit(string.charAt(pos), 16) & 0xff);
+            byte low = (byte) (Character.digit(string.charAt(pos + 1), 16) & 0xff);
+            byteArray[i] = (byte) (high << 4 | low);
+            pos += 2;
+        }
+
+        return byteArray;
+    }
+
+    /**
+     * 字节数组转成16进制表示格式的字符串
+     *
+     * @param byteArray 要转换的字节数组
+     * @return 16进制表示格式的字符串
+     **/
+    public static String byteArrayToHexString(byte[] byteArray) {
+        return byteArrayToHexString(byteArray, true);
+    }
+
+    public static String byteArrayToHexString(byte[] byteArray, boolean blankz) {
+        final StringBuilder hexString = new StringBuilder();
+        for (int i = 0; i < byteArray.length; i++) {
+            if ((byteArray[i] & 0xff) < 0x10) {
+                // 0~F前面不零
+                hexString.append("0");
+            }
+
+            hexString.append(Integer.toHexString(0xFF & byteArray[i]));
+
+            if (blankz) {
+                hexString.append(" ");
+            }
+        }
+        return hexString.toString();
+    }
+
+    /**
+     * 字节逆序
+     *
+     **/
+    public static void byteInvertedOrder(byte[] tmp,byte[] retData) {
+        System.arraycopy(tmp, 0, retData, 0, Math.min(tmp.length, retData.length));
+        for (int i = 0; i < retData.length / 2; i++) {
+            byte by = retData[i];
+            retData[i] = retData[5 - i];
+            retData[5 - i] = by;
+        }
+    }
+
+}

+ 11 - 0
iot-components/iot-DLT645-component/src/main/java/cc/iotkit/comp/DLT645/utils/BytesRef.java

@@ -0,0 +1,11 @@
+package cc.iotkit.comp.DLT645.utils;
+
+import lombok.AccessLevel;
+import lombok.Getter;
+import lombok.Setter;
+
+@Getter(value = AccessLevel.PUBLIC)
+@Setter(value = AccessLevel.PUBLIC)
+public class BytesRef {
+    private byte[] value = new byte[] {};
+}

+ 451 - 0
iot-components/iot-DLT645-component/src/main/java/cc/iotkit/comp/DLT645/utils/ContainerUtils.java

@@ -0,0 +1,451 @@
+package cc.iotkit.comp.DLT645.utils;
+
+import java.io.Serializable;
+import java.lang.invoke.SerializedLambda;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.util.*;
+import java.util.function.Function;
+
+/**
+ * 集合操作的工具类
+ */
+public class ContainerUtils {
+    public ContainerUtils() {
+    }
+
+    /**
+     * 获取类函数的名称
+     * 例如:getMethodName(Integer.toHexString),返回的就是toHexString
+     *
+     * @param function
+     * @param <E>
+     * @param <R>
+     * @return
+     * @throws NoSuchMethodException
+     * @throws SecurityException
+     * @throws IllegalAccessException
+     * @throws IllegalArgumentException
+     * @throws InvocationTargetException
+     */
+    private static <E, R> String getMethodName(SerializableFunction<E, R> function) throws NoSuchMethodException, SecurityException, IllegalAccessException, IllegalArgumentException, InvocationTargetException {
+        Method method = function.getClass().getDeclaredMethod("writeReplace");
+        method.setAccessible(Boolean.TRUE);
+        SerializedLambda serializedLambda = (SerializedLambda) method.invoke(function);
+        String implMethodName = serializedLambda.getImplMethodName();
+
+        return implMethodName;
+    }
+
+    /**
+     * 获取类的函数
+     *
+     * @param clazz
+     * @param function
+     * @param <E>
+     * @param <R>
+     * @param <T>
+     * @return
+     * @throws NoSuchMethodException
+     * @throws SecurityException
+     * @throws IllegalAccessException
+     * @throws IllegalArgumentException
+     * @throws InvocationTargetException
+     */
+    private static <E, R, T> Method getMethod(Class<T> clazz, SerializableFunction<E, R> function) throws NoSuchMethodException, SecurityException, IllegalAccessException, IllegalArgumentException, InvocationTargetException {
+        String methodName = ContainerUtils.getMethodName(function);
+        return clazz.getMethod(methodName);
+    }
+
+    /**
+     * 根据对象列表中的对象的getXxxx()函数,取出成员
+     *
+     * @param objList
+     * @param clazz
+     * @param method  method是clazz的成员函数
+     * @param <K>
+     * @param <T>
+     * @return
+     */
+    private static <K, T> List<K> buildListByGetField(List<T> objList, Class<K> clazz, Method method) throws IllegalAccessException, IllegalArgumentException, InvocationTargetException {
+        List<K> keyList = new ArrayList<K>();
+        for (T obj : objList) {
+            // 接下来就该执行该方法了,第一个参数是具体调用该方法的对象, 第二个参数是执行该方法的具体参数
+            Object keyObject = method.invoke(obj);
+            if (clazz.isInstance(keyObject)) {
+                K key = clazz.cast(keyObject);
+                keyList.add(key);
+            }
+        }
+
+        return keyList;
+    }
+
+    public static <K, T> List<?> buildKeyList(List<T> objList, Method method) throws IllegalAccessException, IllegalArgumentException, InvocationTargetException {
+        List<K> keyList = new ArrayList<K>();
+        Class<?> returnType = method.getReturnType();
+        for (T obj : objList) {
+            // 接下来就该执行该方法了,第一个参数是具体调用该方法的对象, 第二个参数是执行该方法的具体参数
+            Object keyObject = method.invoke(obj);
+            if (returnType.isInstance(keyObject)) {
+                keyList.add((K) returnType.cast(keyObject));
+            }
+        }
+
+        return keyList;
+    }
+
+    /**
+     * 根据对象的getXxxx(),取出类型列表中的数据
+     *
+     * @param objList  AClass对象列表
+     * @param clazz    TClass AClass::getXxxx()中,TClass这样的成员
+     * @param function AClass::getXxxx 这样的函数
+     * @param <E>
+     * @param <R>
+     * @param <K>
+     * @param <T>
+     * @return
+     */
+    public static <E, R, K, T> List<K> buildListByGetField(List<T> objList, SerializableFunction<E, R> function, Class<K> clazz) {
+        if (objList.isEmpty()) {
+            return new ArrayList<K>();
+        }
+
+        try {
+            // 取得函数对应的方法
+            Method method = ContainerUtils.getMethod(objList.get(0).getClass(), function);
+
+            // 使用方法返回对应的数组
+            return ContainerUtils.buildListByGetField(objList, clazz, method);
+        } catch (NoSuchMethodException e) {
+            return new ArrayList<K>();
+        } catch (SecurityException e) {
+            return new ArrayList<K>();
+        } catch (IllegalAccessException e) {
+            return new ArrayList<K>();
+        } catch (IllegalArgumentException e) {
+            return new ArrayList<K>();
+        } catch (InvocationTargetException e) {
+            return new ArrayList<K>();
+        }
+    }
+
+    /**
+     * 根据Key生成Map,该方法是是具体类的函数,(不具备多态能力,不是反射,速度很快)
+     *
+     * @param objList
+     * @param clazz
+     * @param method  使用obj.getClass().getMethod("getTnlKey", new Class[0])获取Method
+     * @return
+     */
+    public static <K, T> Map<K, T> buildMapByKeyAndFinalMethod(List<T> objList, Class<K> clazz, Method method) {
+        try {
+            Map<K, T> uid2deviceMap = new HashMap<K, T>();
+            for (T obj : objList) {
+                // 接下来就该执行该方法了,第一个参数是具体调用该方法的对象, 第二个参数是执行该方法的具体参数
+                Object keyObject = method.invoke(obj);
+                if (clazz.isInstance(keyObject)) {
+                    K key = clazz.cast(keyObject);
+                    uid2deviceMap.put(key, obj);
+                }
+            }
+
+            return uid2deviceMap;
+        } catch (IllegalAccessException e) {
+            return null;
+        } catch (InvocationTargetException e) {
+            return null;
+        }
+    }
+
+
+    public static <E, R, K, T> Map<K, T> buildMapByKey(List<T> objList, SerializableFunction<E, R> function) {
+        if (objList.isEmpty()) {
+            return new HashMap<>();
+        }
+
+        try {
+            // 取得函数对应的方法
+            Method method = ContainerUtils.getMethod(objList.get(0).getClass(), function);
+
+            // 使用方法返回对应的数组
+            return ContainerUtils.buildMapByKey(objList, method);
+        } catch (NoSuchMethodException e) {
+            return new HashMap<K, T>();
+        } catch (SecurityException e) {
+            return new HashMap<K, T>();
+        } catch (IllegalAccessException e) {
+            return new HashMap<K, T>();
+        } catch (IllegalArgumentException e) {
+            return new HashMap<K, T>();
+        } catch (InvocationTargetException e) {
+            return new HashMap<K, T>();
+        }
+    }
+
+    public static <K, T> Map<K, T> buildMapByKey(List<T> objList, Method method) {
+        try {
+            Map<K, T> uid2deviceMap = new HashMap<K, T>();
+            for (T obj : objList) {
+                // 接下来就该执行该方法了,第一个参数是具体调用该方法的对象, 第二个参数是执行该方法的具体参数
+                Object keyObject = method.invoke(obj);
+                uid2deviceMap.put((K) keyObject, obj);
+            }
+
+            return uid2deviceMap;
+        } catch (IllegalAccessException e) {
+            return null;
+        } catch (InvocationTargetException e) {
+            return null;
+        }
+    }
+
+    /**
+     * 分类
+     *
+     * @param objList
+     * @param clazz
+     * @param method
+     * @param <K>
+     * @param <T>
+     * @return
+     */
+    public static <K, T> Map<K, List<T>> buildMapByTypeAndFinalMethod(List<T> objList, Class<K> clazz, Method method) {
+        try {
+            Map<K, List<T>> uid2deviceMap = new HashMap<>();
+            for (T obj : objList) {
+                // 接下来就该执行该方法了,第一个参数是具体调用该方法的对象, 第二个参数是执行该方法的具体参数
+                Object keyObject = method.invoke(obj);
+                if (clazz.isInstance(keyObject)) {
+                    K key = clazz.cast(keyObject);
+
+                    List<T> list = uid2deviceMap.get(key);
+                    if (list == null) {
+                        list = new ArrayList<>();
+                        uid2deviceMap.put(key, list);
+                    }
+
+                    list.add(obj);
+                }
+            }
+
+            return uid2deviceMap;
+        } catch (IllegalAccessException e) {
+            return null;
+        } catch (InvocationTargetException e) {
+            return null;
+        }
+    }
+
+    /**
+     * 根据Key生成Map,该方法是是具体类的函数,(不具备多态能力,不是反射,速度很快)
+     *
+     * @param objList  类型AClass的列表容器
+     * @param function 类型AClass中的某个成员getxxxx()
+     * @param clazz    类型AClass中的某个成员BClass getxxxx()中的BClass
+     * @param <E>
+     * @param <R>
+     * @param <K>
+     * @param <T>
+     * @return
+     */
+    public static <E, R, K, T> Map<K, T> buildMapByKeyAndFinalMethod(List<T> objList, SerializableFunction<E, R> function, Class<K> clazz) {
+        if (objList.isEmpty()) {
+            return new HashMap<>();
+        }
+
+        try {
+            // 取得函数对应的方法
+            Method method = ContainerUtils.getMethod(objList.get(0).getClass(), function);
+
+            // 使用方法返回对应的数组
+            return ContainerUtils.buildMapByKeyAndFinalMethod(objList, clazz, method);
+        } catch (NoSuchMethodException e) {
+            return new HashMap<K, T>();
+        } catch (SecurityException e) {
+            return new HashMap<K, T>();
+        } catch (IllegalAccessException e) {
+            return new HashMap<K, T>();
+        } catch (IllegalArgumentException e) {
+            return new HashMap<K, T>();
+        } catch (InvocationTargetException e) {
+            return new HashMap<K, T>();
+        }
+    }
+
+    public static <E, R, K, T> Map<K, List<T>> buildMapByTypeAndFinalMethod(List<T> objList, SerializableFunction<E, R> function, Class<K> clazz) {
+        if (objList.isEmpty()) {
+            return new HashMap<>();
+        }
+
+        try {
+            // 取得函数对应的方法
+            Method method = ContainerUtils.getMethod(objList.get(0).getClass(), function);
+
+            // 使用方法返回对应的数组
+            return ContainerUtils.buildMapByTypeAndFinalMethod(objList, clazz, method);
+        } catch (NoSuchMethodException e) {
+            return new HashMap<K, List<T>>();
+        } catch (SecurityException e) {
+            return new HashMap<K, List<T>>();
+        } catch (IllegalAccessException e) {
+            return new HashMap<K, List<T>>();
+        } catch (IllegalArgumentException e) {
+            return new HashMap<K, List<T>>();
+        } catch (InvocationTargetException e) {
+            return new HashMap<K, List<T>>();
+        }
+    }
+
+    /**
+     * 根据map中的某个元素,将列表转换成以这个元素为key的map
+     *
+     * @param objList
+     * @param mapKey
+     * @param clazz
+     * @param <K>
+     * @param <T>
+     * @return
+     */
+    public static <K, T> Map<K, Map<String, Object>> buildMapByMapAt(List<Map<String, Object>> objList, String mapKey, Class<K> clazz) {
+        if (objList.isEmpty()) {
+            return new HashMap<>();
+        }
+
+        Map<K, Map<String, Object>> result = new HashMap<>();
+        for (Map<String, Object> obj : objList) {
+            Object value = obj.get(mapKey);
+            if (!clazz.isInstance(value)) {
+                continue;
+            }
+
+            result.put((K) value, obj);
+        }
+
+        return result;
+    }
+
+    /**
+     * 从列表中获取:某个字段等于某个值的对象
+     *
+     * @param objList
+     * @param function
+     * @param key
+     * @param <E>
+     * @param <R>
+     * @param <K>
+     * @param <T>
+     * @return
+     */
+    public static <E, R, K, T> T getObjectByKey(List<T> objList, SerializableFunction<E, R> function, K key) {
+        try {
+            if (objList.isEmpty()) {
+                return null;
+            }
+
+            Method method = ContainerUtils.getMethod(objList.get(0).getClass(), function);
+
+            for (T obj : objList) {
+                // 先获取相应的method对象,getMethod第一个参数是方法名,第二个参数是该方法的参数类型,
+                //   Method method = obj.getClass().getMethod(getKeyMathName, new Class[0]);
+
+                // 接下来就该执行该方法了,第一个参数是具体调用该方法的对象, 第二个参数是执行该方法的具体参数
+                Object keyObject = method.invoke(obj);
+                if (key.getClass().isInstance(keyObject)) {
+                    if (keyObject.equals(key)) {
+                        return obj;
+                    }
+                }
+            }
+
+            return null;
+        } catch (NoSuchMethodException e) {
+            return null;
+        } catch (IllegalAccessException e) {
+            return null;
+        } catch (InvocationTargetException e) {
+            return null;
+        }
+    }
+
+    /**
+     * 交换Key-Value
+     *
+     * @param key2value key2value
+     * @return value2key
+     */
+    public static <K, V> Map<V, K> exchange(Map<K, V> key2value) {
+        Map<V, K> result = new HashMap<>();
+
+        for (Map.Entry<K, V> entry : key2value.entrySet()) {
+            result.put(entry.getValue(), entry.getKey());
+        }
+
+        return result;
+    }
+
+    public static <K, V> Map<V, List<K>> exchanges(Map<K, V> key2value) {
+        Map<V, List<K>> result = new HashMap<>();
+
+        for (Map.Entry<K, V> entry : key2value.entrySet()) {
+            List<K> list = result.get(entry.getValue());
+            if (list == null) {
+                list = new ArrayList<>();
+                result.put(entry.getValue(), list);
+            }
+
+            list.add(entry.getKey());
+        }
+
+        return result;
+    }
+
+    /**
+     * 根据Key提取出相关的值列表
+     *
+     * @param key2value key2value
+     * @param keyList   key列表
+     * @return 跟key相关的values
+     */
+    public static <K, V> List<V> buildValueListByKey(Map<K, V> key2value, Collection<K> keyList) {
+        List<V> resultList = new ArrayList<V>();
+        for (K key : keyList) {
+            V value = key2value.get(key);
+            if (value != null) {
+                resultList.add(value);
+            }
+        }
+
+        return resultList;
+    }
+
+    /**
+     * 从A类型列表转换成B类型列表:A/B是派生类关系
+     *
+     * @param aClazzList
+     * @param bClazz
+     * @return
+     */
+    public static <A, B> List<B> buildClassList(List<A> aClazzList, Class<B> bClazz) {
+        List<B> bInstanceList = new ArrayList<B>();
+
+        for (A aInstance : aClazzList) {
+            if (bClazz.isInstance(aInstance)) {
+                bInstanceList.add(bClazz.cast(aInstance));
+            }
+        }
+
+        return bInstanceList;
+    }
+
+    /**
+     * 定义一个函数接口
+     *
+     * @param <E>
+     * @param <R>
+     */
+    @FunctionalInterface
+    public interface SerializableFunction<E, R> extends Function<E, R>, Serializable {
+    }
+}

+ 143 - 0
iot-components/iot-DLT645-component/src/main/resources/DLT645-1997.csv

@@ -0,0 +1,143 @@
+di1h,di1l,di0h,di0l,format,length,unit,read,write,name
+9,0,1,0,XXXXXX.XX,4,kWh,TRUE,FALSE,(当前)正向有功总电能
+9,0,2,0,XXXXXX.XX,4,kWh,TRUE,FALSE,(当前)反向有功总电能
+9,1,1,0,XXXXXX.XX,4,kvarh,TRUE,FALSE,(当前)正向无功总电能
+9,1,2,0,XXXXXX.XX,4,kvarh,TRUE,FALSE,(当前)反向无功总电能
+9,1,3,0,XXXXXX.XX,4,kvarh,TRUE,FALSE,(当前)一象限无功总电能
+9,1,4,0,XXXXXX.XX,4,kvarh,TRUE,FALSE,(当前)四象限无功总电能
+9,1,5,0,XXXXXX.XX,4,kvarh,TRUE,FALSE,(当前)二象限无功总电能
+9,1,6,0,XXXXXX.XX,4,kvarh,TRUE,FALSE,(当前)三象限无功总电能
+9,4,1,0,XXXXXX.XX,4,kWh,TRUE,FALSE,(上月)正向有功总电能
+9,4,2,0,XXXXXX.XX,4,kWh,TRUE,FALSE,(上月)反向有功总电能
+9,5,1,0,XXXXXX.XX,4,kvarh,TRUE,FALSE,(上月)正向无功总电能
+9,5,2,0,XXXXXX.XX,4,kvarh,TRUE,FALSE,(上月)反向无功总电能
+9,5,3,0,XXXXXX.XX,4,kvarh,TRUE,FALSE,(上月)一象限无功总电能
+9,5,4,0,XXXXXX.XX,4,kvarh,TRUE,FALSE,(上月)四象限无功总电能
+9,5,5,0,XXXXXX.XX,4,kvarh,TRUE,FALSE,(上月)二象限无功总电能
+9,5,6,0,XXXXXX.XX,4,kvarh,TRUE,FALSE,(上月)三象限无功总电能
+9,8,1,0,XXXXXX.XX,4,kWh,TRUE,FALSE,(上上月)正向有功总电能
+9,8,2,0,XXXXXX.XX,4,kWh,TRUE,FALSE,(上上月)反向有功总电能
+9,9,1,0,XXXXXX.XX,4,kvarh,TRUE,FALSE,(上上月)正向无功总电能
+9,9,2,0,XXXXXX.XX,4,kvarh,TRUE,FALSE,(上上月)反向无功总电能
+9,9,3,0,XXXXXX.XX,4,kvarh,TRUE,FALSE,(上上月)一象限无功总电能
+9,9,4,0,XXXXXX.XX,4,kvarh,TRUE,FALSE,(上上月)四象限无功总电能
+9,9,5,0,XXXXXX.XX,4,kvarh,TRUE,FALSE,(上上月)二象限无功总电能
+9,9,6,0,XXXXXX.XX,4,kvarh,TRUE,FALSE,(上上月)三象限无功总电能
+A,0,1,0,XX.XXXX,3,kW,TRUE,FALSE,(当前)正向有功总最大需量
+A,0,2,0,XX.XXXX,3,kW,TRUE,FALSE,(当前)反向有功总最大需量
+A,1,1,0,XX.XXXX,3,kvar,TRUE,FALSE,(当前)正向无功总最大需量
+A,1,2,0,XX.XXXX,3,kvar,TRUE,FALSE,(当前)反向无功总最大需量
+A,1,3,0,XX.XXXX,3,kvar,TRUE,FALSE,(当前)一象限无功最大需量
+A,1,4,0,XX.XXXX,3,kvar,TRUE,FALSE,(当前)四象限无功最大需量
+A,1,5,0,XX.XXXX,3,kvar,TRUE,FALSE,(当前)二象限无功最大需量
+A,1,6,0,XX.XXXX,3,kvar,TRUE,FALSE,(当前)三象限无功最大需量
+A,4,1,0,XX.XXXX,3,kW,TRUE,FALSE,(上月)正向有功总最大需量
+A,4,2,0,XX.XXXX,3,kW,TRUE,FALSE,(上月)反向有功总最大需量
+A,5,1,0,XX.XXXX,3,kvar,TRUE,FALSE,(上月)正向无功总最大需量
+A,5,2,0,XX.XXXX,3,kvar,TRUE,FALSE,(上月)反向无功总最大需量
+A,5,3,0,XX.XXXX,3,kvar,TRUE,FALSE,(上月)一象限无功最大需量
+A,5,4,0,XX.XXXX,3,kvar,TRUE,FALSE,(上月)四象限无功最大需量
+A,5,5,0,XX.XXXX,3,kvar,TRUE,FALSE,(上月)二象限无功最大需量
+A,5,6,0,XX.XXXX,3,kvar,TRUE,FALSE,(上月)三象限无功最大需量
+A,8,1,0,XX.XXXX,3,kvar,TRUE,FALSE,(上上月)正向有功总最大需量
+A,8,2,0,XX.XXXX,3,kvar,TRUE,FALSE,(上上月)反向有功总最大需量
+A,9,1,0,XX.XXXX,3,kvar,TRUE,FALSE,(上上月)正向无功总最大需量
+A,9,2,0,XX.XXXX,3,kvar,TRUE,FALSE,(上上月)反向无功总最大需量
+A,9,3,0,XX.XXXX,3,kvar,TRUE,FALSE,(上上月)一象限无功最大需量
+A,9,4,0,XX.XXXX,3,kvar,TRUE,FALSE,(上上月)四象限无功最大需量
+A,9,5,0,XX.XXXX,3,kvar,TRUE,FALSE,(上上月)二象限无功最大需量
+A,9,6,0,XX.XXXX,3,kvar,TRUE,FALSE,(上上月)三象限无功最大需量
+B,0,1,0,MMDDHHmm,4,,TRUE,FALSE,(当前)正向有功总最大需量发生时间
+B,0,2,0,MMDDHHmm,4,,TRUE,FALSE,(当前)反向有功总最大需量发生时间
+B,1,1,0,MMDDHHmm,4,,TRUE,FALSE,(当前)正向无功总最大需量发生时间
+B,1,2,0,MMDDHHmm,4,,TRUE,FALSE,(当前)反向无功总最大需量发生时间
+B,1,3,0,MMDDHHmm,4,,TRUE,FALSE,(当前)一象限无功最大需量发生时间
+B,1,4,0,MMDDHHmm,4,,TRUE,FALSE,(当前)四象限无功最大需量发生时间
+B,1,5,0,MMDDHHmm,4,,TRUE,FALSE,(当前)二象限无功最大需量发生时间
+B,1,6,0,MMDDHHmm,4,,TRUE,FALSE,(当前)三象限无功最大需量发生时间
+B,4,1,0,MMDDHHmm,4,,TRUE,FALSE,(上月)正向有功总最大需量发生时间
+B,4,2,0,MMDDHHmm,4,,TRUE,FALSE,(上月)反向有功总最大需量发生时间
+B,5,1,0,MMDDHHmm,4,,TRUE,FALSE,(上月)正向无功总最大需量发生时间
+B,5,2,0,MMDDHHmm,4,,TRUE,FALSE,(上月)反向无功总最大需量发生时间
+B,5,3,0,MMDDHHmm,4,,TRUE,FALSE,(上月)一象限无功最大需量发生时间
+B,5,4,0,MMDDHHmm,4,,TRUE,FALSE,(上月)四象限无功最大需量发生时间
+B,5,5,0,MMDDHHmm,4,,TRUE,FALSE,(上月)二象限无功最大需量发生时间
+B,5,6,0,MMDDHHmm,4,,TRUE,FALSE,(上月)三象限无功最大需量发生时间
+B,8,1,0,MMDDHHmm,4,,TRUE,FALSE,(上上月)正向有功总最大需量发生时间
+B,8,2,0,MMDDHHmm,4,,TRUE,FALSE,(上上月)反向有功总最大需量发生时间
+B,9,1,0,MMDDHHmm,4,,TRUE,FALSE,(上上月)正向无功总最大需量发生时间
+B,9,2,0,MMDDHHmm,4,,TRUE,FALSE,(上上月)反向无功总最大需量发生时间
+B,9,3,0,MMDDHHmm,4,,TRUE,FALSE,(上上月)一象限无功最大需量发生时间
+B,9,4,0,MMDDHHmm,4,,TRUE,FALSE,(上上月)四象限无功最大需量发生时间
+B,9,5,0,MMDDHHmm,4,,TRUE,FALSE,(上上月)二象限无功最大需量发生时间
+B,9,6,0,MMDDHHmm,4,,TRUE,FALSE,(上上月)三象限无功最大需量发生时间
+B,2,1,0,MMDDHHmm,4,,TRUE,FALSE,最近一次编程时间
+B,2,1,1,MMDDHHmm,4,,TRUE,FALSE,最近一次最大需量清零时间
+B,2,1,2,NNNN,2,,TRUE,FALSE,编程次数
+B,2,1,3,NNNN,2,,TRUE,FALSE,最大需量清零次数
+B,2,1,4,NNNNNN,3,min,TRUE,FALSE,电池工作时间
+B,3,1,0,NNNN,2,,TRUE,FALSE,总断相次数
+B,3,1,1,NNNN,2,,TRUE,FALSE,A 相断相次数
+B,3,1,2,NNNN,2,,TRUE,FALSE,B 相断相次数
+B,3,1,3,NNNN,2,,TRUE,FALSE,C 相断相次数
+B,3,2,0,NNNNNN,3,min,TRUE,FALSE,断相时间累计值
+B,3,2,1,NNNNNN,3,min,TRUE,FALSE,A 断相时间累计值
+B,3,2,2,NNNNNN,3,min,TRUE,FALSE,B 断相时间累计值
+B,3,2,3,NNNNNN,3,min,TRUE,FALSE,C 断相时间累计值
+B,3,3,0,MMDDHHmm,4,,TRUE,FALSE,最近一次断相起始时刻
+B,3,3,1,MMDDHHmm,4,,TRUE,FALSE,A 相最近断相起始时刻
+B,3,3,2,MMDDHHmm,4,,TRUE,FALSE,B 相最近断相起始时刻
+B,3,3,3,MMDDHHmm,4,,TRUE,FALSE,C相最近断相起始时刻
+B,3,4,0,MMDDHHmm,4,,TRUE,FALSE,最近一次断相的结束时刻
+B,3,4,1,MMDDHHmm,4,,TRUE,FALSE,A 相最近一次断相的结束时刻
+B,3,4,2,MMDDHHmm,4,,TRUE,FALSE,B 相最近一次断相的结束时刻
+B,3,4,3,MMDDHHmm,4,,TRUE,FALSE,C 相最近一次断相的结束时刻
+B,6,1,1,XXX,2,V,TRUE,FALSE,A相电压
+B,6,1,2,XXX,2,V,TRUE,FALSE,B相电压
+B,6,1,3,XXX,2,V,TRUE,FALSE,C相电压
+B,6,2,1,XX.XX,2,A,TRUE,FALSE,A相电流
+B,6,2,2,XX.XX,2,A,TRUE,FALSE,B相电流
+B,6,2,3,XX.XX,2,A,TRUE,FALSE,C相电流
+B,6,3,0,XX.XXXX,3,kW,TRUE,FALSE,瞬时有功功率 
+B,6,3,1,XX.XXXX,3,kW,TRUE,FALSE,A相有功功率
+B,6,3,2,XX.XXXX,3,kW,TRUE,FALSE,B相有功功率
+B,6,3,3,XX.XXXX,3,kW,TRUE,FALSE,C相有功功率
+B,6,3,4,XX.XX,2,kW,TRUE,TRUE,正向有功功率上限值
+B,6,3,5,XX.XX,2,kW,TRUE,TRUE,反向有功功率上限值
+B,6,4,0,XX.XX,2,kvarh,TRUE,FALSE,瞬时无功功率
+B,6,4,1,XX.XX,2,kvarh,TRUE,FALSE,A相无功功率
+B,6,4,2,XX.XX,2,kvarh,TRUE,FALSE,B相无功功率
+B,6,4,3,XX.XX,2,kvarh,TRUE,FALSE,C相无功功率
+B,6,5,0,XX.XX,2,kvarh,TRUE,FALSE,总功率因数
+B,6,5,1,XX.XX,3,kvarh,TRUE,FALSE,A 相功率因数
+B,6,5,2,XX.XX,4,kvarh,TRUE,FALSE,B 相功率因数
+B,6,5,3,XX.XX,5,kvarh,TRUE,FALSE,C 相功率因数
+C,0,1,0,YYMMDDWW,4,,TRUE,TRUE,日期及周次
+C,0,1,1,hhmmss,3,,TRUE,TRUE,时间
+C,0,2,0,电表运行状态字,1,,TRUE,TRUE,电表运行状态字
+C,0,2,1,电网状态字,1,,TRUE,TRUE,电网状态字
+C,0,2,2,周休日状态字,1,,TRUE,TRUE,周休日状态字
+C,0,3,0,NNNNNN,3,p/(kWh),TRUE,TRUE,电表常数(有功)
+C,0,3,1,NNNNNN,3,p/(kvarh),TRUE,TRUE,电表常数(无功)
+C,0,3,2,NN...NN,6,,TRUE,TRUE,表号
+C,0,3,3,NN...NN,6,,TRUE,TRUE,用户号
+C,0,3,4,NN...NN,6,,TRUE,TRUE,设备码
+C,1,1,1,XX,1,min,TRUE,TRUE,最大需量周期
+C,1,1,2,XX,1,min,TRUE,TRUE,滑差时间
+C,1,1,3,XX,1,s,TRUE,TRUE,循显时间
+C,1,1,4,XX,1,s,TRUE,TRUE,停显时间
+C,1,1,5,NN,1,,TRUE,TRUE,显示电能小数位数
+C,1,1,6,NN,1,,TRUE,TRUE,显示功率(最大需量)小数位数
+C,1,1,7,DDhh,2,,TRUE,TRUE,自动抄表日期
+C,1,1,8,NN,1,,TRUE,TRUE,负荷代表日
+C,1,1,9,NNNNNN.N,4,kWh,TRUE,TRUE,有功电能起始读数
+C,1,1,A,NNNNNN.N,4,kvarh,TRUE,TRUE,无功电能起始读数
+C,2,1,1,NNNN,2,ms,TRUE,TRUE,输出脉冲宽度
+C,2,1,2,NNNNNNNN,4,,FALSE,TRUE,密码权限及密码
+C,3,1,0,NN,1,,TRUE,TRUE,年时区数
+C,3,1,1,NN,1,,TRUE,TRUE,日时段表数
+C,3,1,2,NN,1,,TRUE,TRUE,日时段(每日切换数)m≤10
+C,3,1,3,NN,1,,TRUE,TRUE,费率数 k≤14
+C,3,1,4,NN,1,,TRUE,TRUE,公共假日数
+C,5,1,0,MMDDhhmm,4,,TRUE,TRUE,负荷记录起始时间
+C,5,1,1,mmmm,2,min,TRUE,TRUE,负荷记录间隔时间

+ 74 - 0
iot-components/iot-DLT645-component/src/main/resources/component.js

@@ -0,0 +1,74 @@
+var mid=1;
+
+var gatewayPk="BRD3x4fkKxkaxXFt"
+var smartMeterPk="PjmkANSTDt85bZPj"
+
+function getMid(){
+	mid++;
+	if(mid>10000){
+		mid=1;
+	}
+	return mid;
+};
+function register(head){
+	var mac= head.mac;
+	return {
+		type:"register",
+		data:{
+			productKey:gatewayPk,
+			deviceName:mac,
+			model:""
+		}
+	};
+}
+
+function deviceStateChange(head,type){
+	var mac=head.mac;
+	return {
+		type:"state",
+		data:{
+			productKey:gatewayPk,
+			deviceName:mac,
+			state:type
+		}
+	}
+}
+
+function dltHandle(payload){
+	var dltData= JSON.parse(payload);
+	var identify= dltData.identify;
+	var content={};
+	content[identify]=dltData.data;
+	return {
+		type:"report",
+		data:{
+			productKey:smartMeterPk,
+			deviceName:dltData.deviceAddress,
+			mid:getMid(),
+			content:{
+				type:"property",
+				identifier: "report", //属性上报
+				occur: new Date().getTime(), //时间戳,设备上的事件或数据产生的本地时间
+				time: new Date().getTime(), //时间戳,消息上报时间
+				data: content
+			}
+		},
+	}
+}
+
+//必须提供onReceive方法
+this.onReceive=function(head,type,payload){
+	if("register"==type){
+		return register(head);
+	}else if("online"==type){
+		return deviceStateChange(head,type);
+	}else if("offline"==type){
+		return deviceStateChange(head,type);
+	}else if("dlt"==type){
+		return dltHandle(payload);
+	}
+};
+
+this.onRegistered=function (data,status) {
+	apiTool.log("onRegistered调用");
+}

+ 1 - 0
iot-components/iot-DLT645-component/src/main/resources/component.spi

@@ -0,0 +1 @@
+cc.iotkit.comp.DLT645.DLT645Component

+ 1 - 0
iot-components/iot-DLT645-component/src/main/resources/convert.spi

@@ -0,0 +1 @@
+cc.iotkit.comp.DLT645.analysis.DLT645Converter

+ 15 - 3
iot-components/iot-component-server/src/main/java/cc/iotkit/comps/DeviceComponentManager.java

@@ -20,14 +20,16 @@ import cc.iotkit.comp.IDeviceComponent;
 import cc.iotkit.comps.config.CacheKey;
 import cc.iotkit.comps.config.ComponentConfig;
 import cc.iotkit.comps.service.DeviceBehaviourService;
-import cc.iotkit.converter.*;
+import cc.iotkit.converter.Device;
+import cc.iotkit.converter.DeviceMessage;
+import cc.iotkit.converter.IConverter;
+import cc.iotkit.converter.IScriptConvertFactory;
 import cc.iotkit.data.IDeviceInfoData;
 import cc.iotkit.data.IProductData;
 import cc.iotkit.data.IProtocolComponentData;
 import cc.iotkit.data.IProtocolConverterData;
 import cc.iotkit.engine.IScriptEngine;
 import cc.iotkit.engine.IScriptEngineFactory;
-import cc.iotkit.engine.JsNashornScriptEngine;
 import cc.iotkit.model.device.DeviceInfo;
 import cc.iotkit.model.device.message.ThingModelMessage;
 import cc.iotkit.model.product.Product;
@@ -119,7 +121,17 @@ public class DeviceComponentManager {
         componentInstance.create(new CompConfig(300, component.getConfig()));
 
         try {
-            setScriptConvert(component, componentInstance);
+            if(component.CONVER_TYPE_STATIC.equals(component.getConverType())){
+                IConverter converterInstance;
+                try {
+                    converterInstance=ComponentClassLoader.getConverter(component.getId(), file);
+                } catch (Throwable e) {
+                    throw new BizException("get device convert instance error", e);
+                }
+                componentInstance.setConverter(converterInstance);
+            }else{
+                setScriptConvert(component, componentInstance);
+            }
 
             scriptEngine = scriptEngineFactory.getScriptEngine(component.getScriptTyp());
 

+ 36 - 16
iot-components/iot-websocket-component/src/main/java/cc/iotkit/comp/websocket/server/WebSocketServerVerticle.java

@@ -3,11 +3,8 @@ package cc.iotkit.comp.websocket.server;
 
 import cc.iotkit.common.exception.BizException;
 import cc.iotkit.common.utils.JsonUtil;
-import cc.iotkit.comp.model.ReceiveResult;
 import cc.iotkit.comp.websocket.AbstractDeviceVerticle;
 import cc.iotkit.converter.DeviceMessage;
-import com.fasterxml.jackson.databind.JsonNode;
-import com.fasterxml.jackson.databind.util.JSONPObject;
 import io.vertx.core.Future;
 import io.vertx.core.http.HttpServer;
 import io.vertx.core.http.HttpServerOptions;
@@ -63,7 +60,27 @@ public class WebSocketServerVerticle extends AbstractDeviceVerticle {
             wsClient.textMessageHandler(message -> {
                 HashMap<String,String> msg= JsonUtil.parse(message,HashMap.class);
                 if(wsClients.containsKey(deviceKey)){
-                    executor.onReceive(new HashMap<>(), "", message);
+                    if("ping".equals(msg.get("type"))){
+                        msg.put("type","pong");
+                        wsClient.writeTextMessage(JsonUtil.toJsonString(msg));
+                        return;
+                    }
+                    if("register".equals(msg.get("type"))){
+                        executor.onReceive(new HashMap<>(), "", message,(r) -> {
+                            if (r == null) {
+                                //注册失败
+                                Map<String,String> ret=new HashMap<>();
+                                ret.put("id",msg.get("id"));
+                                ret.put("type",msg.get("type"));
+                                ret.put("result","fail");
+                                wsClient.writeTextMessage(JsonUtil.toJsonString(ret));
+                                return;
+                            }else{
+                                msg.put("type","online");
+                                executor.onReceive(new HashMap<>(), "", JsonUtil.toJsonString(msg));
+                            }
+                        });
+                    }
                 }else if(msg!=null&&"auth".equals(msg.get("type"))){
                     Set<String> tokenKey=tokens.keySet();
                     for(String key:tokenKey){
@@ -87,15 +104,21 @@ public class WebSocketServerVerticle extends AbstractDeviceVerticle {
             });
             wsClient.closeHandler(c -> {
                 log.warn("client connection closed,deviceKey:{}", deviceKey);
-                executor.onReceive(new HashMap<>(), "disconnect", JsonUtil.toJsonString(deviceKeyObj), (r) -> {
-                    //删除设备与连接关系
-                    if(r!=null){
-                        wsClients.remove(getDeviceKey(r));
-                    }
-                });
+                if(wsClients.containsKey(deviceKey)){
+                    wsClients.remove(deviceKey);
+                    deviceKeyObj.put("type","offline");
+                    executor.onReceive(new HashMap<>(), "", JsonUtil.toJsonString(deviceKeyObj), (r) -> {
+                    });
+                }
             });
             wsClient.exceptionHandler(ex -> {
                 log.warn("webSocket client connection exception,deviceKey:{}", deviceKey);
+                if(wsClients.containsKey(deviceKey)){
+                    wsClients.remove(deviceKey);
+                    deviceKeyObj.put("type","offline");
+                    executor.onReceive(new HashMap<>(), "", JsonUtil.toJsonString(deviceKeyObj), (r) -> {
+                    });
+                }
             });
         }).listen(webSocketConfig.getPort(), server -> {
             if (server.succeeded()) {
@@ -115,23 +138,20 @@ public class WebSocketServerVerticle extends AbstractDeviceVerticle {
         for (String deviceKey : wsClients.keySet()) {
             Map<String,String> deviceKeyObj=new HashMap<>();
             deviceKeyObj.put("deviceKey",deviceKey);
-            executor.onReceive(null, "disconnect", JsonUtil.toJsonString(deviceKeyObj));
+            deviceKeyObj.put("type","offline");
+            executor.onReceive(null, "", JsonUtil.toJsonString(deviceKeyObj));
         }
         tokens.clear();
         httpServer.close(voidAsyncResult -> log.info("close webocket server..."));
     }
 
-    private String getDeviceKey(ReceiveResult result) {
-        return getDeviceKey(result.getProductKey(), result.getDeviceName());
-    }
-
     private String getDeviceKey(String productKey, String deviceName) {
         return String.format("%s_%s", productKey, deviceName);
     }
 
     @Override
     public DeviceMessage send(DeviceMessage message) {
-        ServerWebSocket wsClient = wsClients.get(getDeviceKey(message.getProductKey(), message.getDeviceName()));
+        ServerWebSocket wsClient = wsClients.get(getDeviceKey(message.getProductKey(),message.getDeviceName()));
         Object obj = message.getContent();
         if (!(obj instanceof Map)) {
             throw new BizException("message content is not Map");

+ 5 - 0
iot-data/iot-data-cache/src/main/java/cc/iotkit/data/service/DeviceInfoDataCache.java

@@ -124,6 +124,11 @@ public class DeviceInfoDataCache implements IDeviceInfoData, SmartInitializingSi
         return deviceInfoData.findByParentId(parentId);
     }
 
+    @Override
+    public List<Map<String, Object>> findByProductNodeType(String uid) {
+        return deviceInfoData.findByProductNodeType(uid);
+    }
+
     @Override
     public List<String> findSubDeviceIds(String parentId) {
         return deviceInfoData.findSubDeviceIds(parentId);

+ 5 - 0
iot-data/iot-data-cache/src/main/java/cc/iotkit/data/service/DeviceInfoPropertyDataCache.java

@@ -149,6 +149,11 @@ public class DeviceInfoPropertyDataCache implements IDeviceInfoData {
         deviceInfoData.removeGroup(groupId);
     }
 
+    @Override
+    public List<Map<String, Object>> findByProductNodeType(String uid) {
+        return deviceInfoData.findByProductNodeType(uid);
+    }
+
     @Override
     public List<DeviceInfo> findByUid(String uid) {
         return deviceInfoData.findByUid(uid);

+ 6 - 0
iot-data/iot-data-service/src/main/java/cc/iotkit/data/IDeviceInfoData.java

@@ -128,4 +128,10 @@ public interface IDeviceInfoData extends IOwnedData<DeviceInfo, String> {
      * @param groupId 分组ID
      */
     void removeGroup(String groupId);
+
+    /**
+     * 获取所有网关类型设备
+     * @return
+     */
+    List<Map<String, Object>> findByProductNodeType(String uid);
 }

+ 10 - 0
iot-data/iot-model/src/main/java/cc/iotkit/model/protocol/ProtocolComponent.java

@@ -19,6 +19,14 @@ public class ProtocolComponent implements Owned<String> {
     public static final String STATE_RUNNING = "running";
     public static final String TYPE_DEVICE = "device";
     public static final String TYPE_BIZ = "biz";
+    /**
+     * 转换器类型:静态
+     */
+    public static final String CONVER_TYPE_STATIC = "static";
+    /**
+     * 转换器类型:自定义
+     */
+    public static final String CONVER_TYPE_CUSTOM = "custom";
 
     public static final String SCRIPT_FILE_NAME = "component.js";
 
@@ -41,6 +49,8 @@ public class ProtocolComponent implements Owned<String> {
 
     private String converter;
 
+    private String converType = CONVER_TYPE_CUSTOM;
+
     private String state;
 
     private Long createAt;

+ 2 - 0
iot-data/iot-rdb-data-service/src/main/java/cc/iotkit/data/model/TbProtocolComponent.java

@@ -33,6 +33,8 @@ public class TbProtocolComponent {
 
     private String converter;
 
+    private String converType;
+
     private String state;
 
     private Long createAt;

+ 12 - 0
iot-data/iot-rdb-data-service/src/main/java/cc/iotkit/data/service/DeviceInfoDataImpl.java

@@ -164,6 +164,18 @@ public class DeviceInfoDataImpl implements IDeviceInfoData {
         return parseVoToDto(deviceInfoRepository.findByDeviceName(deviceName));
     }
 
+    @Override
+    public List<Map<String, Object>> findByProductNodeType(String uid) {
+        String sql = "SELECT\n" +
+                "a.id,\n" +
+                "a.device_name\n" +
+                "FROM device_info a JOIN product p ON p.node_type=0 AND a.product_key=p.id";
+        if (StringUtils.isNotBlank(uid)) {
+            sql += " WHERE a.uid='"+uid+"'";
+        }
+        return jdbcTemplate.queryForList(sql);
+    }
+
     @Override
     public Paging<DeviceInfo> findByConditions(String uid, String subUid,
                                                String productKey, String groupId,

+ 18 - 7
iot-standalone/src/main/java/cc/iotkit/manager/controller/DeviceController.java

@@ -23,18 +23,18 @@ import cc.iotkit.manager.model.query.DeviceQuery;
 import cc.iotkit.manager.service.DataOwnerService;
 import cc.iotkit.manager.service.DeferredDataConsumer;
 import cc.iotkit.manager.service.DeviceService;
-import cc.iotkit.model.device.DeviceConfig;
-import cc.iotkit.model.device.DeviceGroup;
-import cc.iotkit.temporal.IDevicePropertyData;
-import cc.iotkit.temporal.IThingModelMessageData;
-import cc.iotkit.utils.AuthUtil;
 import cc.iotkit.model.InvokeResult;
 import cc.iotkit.model.Paging;
+import cc.iotkit.model.device.DeviceConfig;
+import cc.iotkit.model.device.DeviceGroup;
 import cc.iotkit.model.device.DeviceInfo;
 import cc.iotkit.model.device.message.DeviceProperty;
 import cc.iotkit.model.device.message.ThingModelMessage;
 import cc.iotkit.model.product.Product;
 import cc.iotkit.model.product.ThingModel;
+import cc.iotkit.temporal.IDevicePropertyData;
+import cc.iotkit.temporal.IThingModelMessageData;
+import cc.iotkit.utils.AuthUtil;
 import lombok.extern.slf4j.Slf4j;
 import org.apache.commons.lang3.StringUtils;
 import org.springframework.beans.factory.annotation.Autowired;
@@ -129,7 +129,7 @@ public class DeviceController {
     }
 
     @PostMapping("/create")
-    public void createDevice(String productKey, String deviceName) {
+    public void createDevice(String productKey, String deviceName, String parentId) {
         Product product = productData.findById(productKey);
         if (product == null) {
             throw new BizException("the product does not exist");
@@ -152,7 +152,9 @@ public class DeviceController {
         device.setSecret(secret.toString());
         device.setState(new DeviceInfo.State(false, null, null));
         device.setCreateAt(System.currentTimeMillis());
-
+        if(StringUtils.isNotBlank(parentId)){
+            device.setParentId(parentId);
+        }
         deviceInfoData.save(device);
     }
 
@@ -167,6 +169,15 @@ public class DeviceController {
         return deviceInfoData.findByParentId(deviceId);
     }
 
+    @GetMapping("/parentDevices")
+    public List<Map<String, Object>> getParentDevices() {
+        String uid = "";
+        if (!AuthUtil.isAdmin()) {
+             uid = AuthUtil.getUserId();
+        }
+        return deviceInfoData.findByProductNodeType(uid);
+    }
+
     @GetMapping(Constants.API_DEVICE.DETAIL)
     public DeviceInfo getDetail(@PathVariable("deviceId") String deviceId) {
         DeviceInfo deviceInfo = deviceInfoData.findByDeviceId(deviceId);

+ 1 - 2
iot-standalone/src/main/java/cc/iotkit/manager/controller/ProtocolController.java

@@ -10,7 +10,6 @@
 package cc.iotkit.manager.controller;
 
 import cc.iotkit.common.exception.BizException;
-import cc.iotkit.common.utils.JsonUtil;
 import cc.iotkit.common.utils.ReflectUtil;
 import cc.iotkit.comps.ComponentManager;
 import cc.iotkit.comps.config.ComponentConfig;
@@ -328,7 +327,7 @@ public class ProtocolController {
     public void changeComponentState(@PathVariable("id") String id,
                                      @PathVariable("state") String state) {
         ProtocolComponent component = getAndCheckComponent(id);
-        if (ProtocolComponent.TYPE_DEVICE.equals(component.getType())) {
+        if (ProtocolComponent.TYPE_DEVICE.equals(component.getType())&&ProtocolComponent.CONVER_TYPE_CUSTOM.equals(component.getConverType())) {
             String converterId = component.getConverter();
             getAndCheckConverter(converterId);
         }