KLEFR Electricity meter



  • Hi everyone,

    I had ordered successor to PRO380 meter and I had hard time connecting it to my smart home. I tried multiple things for 10 straight hours without any luck, until suddenly realised that the addresses in the manual are in HEX and not DEC that modbus works with. So for the future generations I would like to share my solution here.

    I have created flow in NodeRED to read most of the data (these that make sense to store in database and show in Grafana), I try to read the data using most efficient approach with just one modbus flex getter node that can handle it's queue correctly and then 3 functions to parse the data and format it into the object to pass to InfluxDB batch node. But you can alter it in any way that would work for you. The function also solves translating HEX data to correct floats, ASCII or signed int's.

    I have ended up using Modbus TCP x RTU gateway because I struggled to make my UniPi working at that day, but it should work exactly the same with RTU connection from any of UniPi devices running NodeRED.

    Here is the flow:

    image.png

    [{"id":"4193b02b.328318","type":"inject","z":"ae6fb191.56fce","name":"Series 4","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"30","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"{\"value\":4,\"fc\":3,\"unitid\":1,\"address\":16402,\"quantity\":5}","payloadType":"json","x":120,"y":240,"wires":[["38568550.7ebeba"]]},{"id":"deb14ad6.2a5b3","type":"inject","z":"ae6fb191.56fce","name":"Series 5","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"10","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"{\"value\":5,\"fc\":3,\"unitid\":1,\"address\":20480,\"quantity\":50}","payloadType":"json","x":120,"y":280,"wires":[["38568550.7ebeba"]]},{"id":"bd3e4c6b.f6afa8","type":"inject","z":"ae6fb191.56fce","name":"Series 6","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"15","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"{\"value\":6,\"fc\":3,\"unitid\":1,\"address\":24576,\"quantity\":75}","payloadType":"json","x":120,"y":320,"wires":[["38568550.7ebeba"]]},{"id":"38568550.7ebeba","type":"modbus-flex-getter","z":"ae6fb191.56fce","name":"","showStatusActivities":true,"showErrors":true,"logIOActivities":false,"server":"9a84ffbb.bc858","useIOFile":false,"ioFile":"","useIOForPayload":false,"emptyMsgOnFail":false,"keepMsgProperties":false,"x":320,"y":280,"wires":[["6adf031b.7abdec"],[]]},{"id":"6adf031b.7abdec","type":"switch","z":"ae6fb191.56fce","name":"Series Switcher","property":"modbusRequest.value","propertyType":"msg","rules":[{"t":"eq","v":"4","vt":"num"},{"t":"eq","v":"5","vt":"str"},{"t":"eq","v":"6","vt":"str"}],"checkall":"true","repair":false,"outputs":3,"x":520,"y":280,"wires":[["7f249c48.1dc5a4"],["671c07cd.4fc368"],["34c6686d.303148"]]},{"id":"7f249c48.1dc5a4","type":"function","z":"ae6fb191.56fce","name":"Series 4 DB parser","func":"function parseFloat(str) {\n    var float = 0, sign, order, mantissa, exp,\n    int = 0, multi = 1;\n    if (/^0x/.exec(str)) {\n        int = parseInt(str, 16);\n    }\n    else {\n        for (var i = str.length -1; i >=0; i -= 1) {\n            if (str.charCodeAt(i) > 255) {\n                console.log('Wrong string parameter');\n                return false;\n            }\n            int += str.charCodeAt(i) * multi;\n            multi *= 256;\n        }\n    }\n    sign = (int >>> 31) ? -1 : 1;\n    exp = (int >>> 23 & 0xff) - 127;\n    mantissa = ((int & 0x7fffff) + 0x800000).toString(2);\n    for (i=0; i<mantissa.length; i+=1) {\n        float += parseInt(mantissa[i]) ? Math.pow(2, exp) : 0;\n        exp--;\n    }\n    return float*sign;\n}\n\nfunction hexToAscii(str){\n    hexString = str;\n    strOut = '';\n        for (x = 0; x < hexString.length; x += 2) {\n            strOut += String.fromCharCode(parseInt(hexString.substr(x, 2), 16));\n        }\n    return strOut;    \n}\n\nmsg.payload = [\n    {\n        measurement: \"3F_main_meter\",\n        fields:{\n            l1_current_dir: hexToAscii(msg.payload[0].toString(16)),\n            l2_current_dir: hexToAscii(msg.payload[1].toString(16)),\n            l3_current_dir: hexToAscii(msg.payload[2].toString(16)),\n            //Error Code 3\n            powerdown_counter: msg.payload[4]\n            \n        },\n        timestamp: new Date()\n    }];\n\nreturn msg;","outputs":1,"noerr":0,"initialize":"","finalize":"","x":710,"y":240,"wires":[["1cd0236c.cf5a2d"]]},{"id":"671c07cd.4fc368","type":"function","z":"ae6fb191.56fce","name":"Series 5 DB parser","func":"function parseFloat(str) {\n    var float = 0, sign, order, mantissa, exp,\n    int = 0, multi = 1;\n    if (/^0x/.exec(str)) {\n        int = parseInt(str, 16);\n    }\n    else {\n        for (var i = str.length -1; i >=0; i -= 1) {\n            if (str.charCodeAt(i) > 255) {\n                console.log('Wrong string parameter');\n                return false;\n            }\n            int += str.charCodeAt(i) * multi;\n            multi *= 256;\n        }\n    }\n    sign = (int >>> 31) ? -1 : 1;\n    exp = (int >>> 23 & 0xff) - 127;\n    mantissa = ((int & 0x7fffff) + 0x800000).toString(2);\n    for (i=0; i<mantissa.length; i+=1) {\n        float += parseInt(mantissa[i]) ? Math.pow(2, exp) : 0;\n        exp--;\n    }\n    return float*sign;\n}\n\nfunction hexToAscii(str){\n    hexString = str;\n    strOut = '';\n        for (x = 0; x < hexString.length; x += 2) {\n            strOut += String.fromCharCode(parseInt(hexString.substr(x, 2), 16));\n        }\n    return strOut;    \n}\n\nmsg.payload = [\n    {\n        measurement: \"3F_main_meter\",\n        fields:{\n            //voltage 0,1\n            l1_voltage: parseFloat(\"0x\"+msg.payload[2].toString(16)+msg.payload[3].toString(16)),\n            l2_voltage: parseFloat(\"0x\"+msg.payload[4].toString(16)+msg.payload[5].toString(16)),\n            l3_voltage: parseFloat(\"0x\"+msg.payload[6].toString(16)+msg.payload[7].toString(16)),\n            grid_frequency: parseFloat(\"0x\"+msg.payload[8].toString(16)+msg.payload[9].toString(16)),\n            //current 10,11\n            l1_current: parseFloat(\"0x\"+msg.payload[12].toString(16)+msg.payload[13].toString(16)),\n            l2_current: parseFloat(\"0x\"+msg.payload[14].toString(16)+msg.payload[15].toString(16)),\n            l3_current: parseFloat(\"0x\"+msg.payload[16].toString(16)+msg.payload[17].toString(16)),\n            total_active_power: parseFloat(\"0x\"+msg.payload[18].toString(16)+msg.payload[19].toString(16)),\n            l1_active_power: parseFloat(\"0x\"+msg.payload[20].toString(16)+msg.payload[21].toString(16)),\n            l2_active_power: parseFloat(\"0x\"+msg.payload[22].toString(16)+msg.payload[23].toString(16)),\n            l3_active_power: parseFloat(\"0x\"+msg.payload[24].toString(16)+msg.payload[25].toString(16)),\n            total_reactive_power: parseFloat(\"0x\"+msg.payload[26].toString(16)+msg.payload[27].toString(16)),\n            l1_reactive_power: parseFloat(\"0x\"+msg.payload[28].toString(16)+msg.payload[29].toString(16)),\n            l2_reactive_power: parseFloat(\"0x\"+msg.payload[30].toString(16)+msg.payload[31].toString(16)),\n            l3_reactive_power: parseFloat(\"0x\"+msg.payload[32].toString(16)+msg.payload[33].toString(16)),\n            total_apparent_power: parseFloat(\"0x\"+msg.payload[34].toString(16)+msg.payload[35].toString(16)),\n            l1_apparent_power: parseFloat(\"0x\"+msg.payload[36].toString(16)+msg.payload[37].toString(16)),\n            l2_apparent_power: parseFloat(\"0x\"+msg.payload[38].toString(16)+msg.payload[39].toString(16)),\n            l3_apparent_power: parseFloat(\"0x\"+msg.payload[40].toString(16)+msg.payload[41].toString(16)),\n            total_power_factor: parseFloat(\"0x\"+msg.payload[42].toString(16)+msg.payload[43].toString(16)),\n            l1_power_factor: parseFloat(\"0x\"+msg.payload[44].toString(16)+msg.payload[45].toString(16)),\n            l2_power_factor: parseFloat(\"0x\"+msg.payload[46].toString(16)+msg.payload[47].toString(16)),\n            l3_power_factor: parseFloat(\"0x\"+msg.payload[48].toString(16)+msg.payload[49].toString(16)),\n            \n        },\n        timestamp: new Date()\n    }];\n\nreturn msg;","outputs":1,"noerr":0,"initialize":"","finalize":"","x":710,"y":280,"wires":[["1cd0236c.cf5a2d"]]},{"id":"34c6686d.303148","type":"function","z":"ae6fb191.56fce","name":"Series 6 DB parser","func":"function parseFloat(str) {\n    var float = 0, sign, order, mantissa, exp,\n    int = 0, multi = 1;\n    if (/^0x/.exec(str)) {\n        int = parseInt(str, 16);\n    }\n    else {\n        for (var i = str.length -1; i >=0; i -= 1) {\n            if (str.charCodeAt(i) > 255) {\n                console.log('Wrong string parameter');\n                return false;\n            }\n            int += str.charCodeAt(i) * multi;\n            multi *= 256;\n        }\n    }\n    sign = (int >>> 31) ? -1 : 1;\n    exp = (int >>> 23 & 0xff) - 127;\n    mantissa = ((int & 0x7fffff) + 0x800000).toString(2);\n    for (i=0; i<mantissa.length; i+=1) {\n        float += parseInt(mantissa[i]) ? Math.pow(2, exp) : 0;\n        exp--;\n    }\n    return float*sign;\n}\n\nfunction hexToAscii(str){\n    hexString = str;\n    strOut = '';\n        for (x = 0; x < hexString.length; x += 2) {\n            strOut += String.fromCharCode(parseInt(hexString.substr(x, 2), 16));\n        }\n    return strOut;    \n}\n\nmsg.payload = [\n    {\n        measurement: \"3F_main_meter\",\n        fields:{\n            total_active_energy: parseFloat(\"0x\"+msg.payload[0].toString(16)+msg.payload[1].toString(16)),\n            t1_active_energy: parseFloat(\"0x\"+msg.payload[2].toString(16)+msg.payload[3].toString(16)),\n            t2_active_energy: parseFloat(\"0x\"+msg.payload[4].toString(16)+msg.payload[5].toString(16)),\n            l1_active_energy: parseFloat(\"0x\"+msg.payload[6].toString(16)+msg.payload[7].toString(16)),\n            l2_active_energy: parseFloat(\"0x\"+msg.payload[8].toString(16)+msg.payload[9].toString(16)),\n            l3_active_energy: parseFloat(\"0x\"+msg.payload[10].toString(16)+msg.payload[11].toString(16)),\n            total_fwd_active_energy: parseFloat(\"0x\"+msg.payload[12].toString(16)+msg.payload[13].toString(16)),\n            t1_fwd_active_energy: parseFloat(\"0x\"+msg.payload[14].toString(16)+msg.payload[15].toString(16)),\n            t2_fwd_active_energy: parseFloat(\"0x\"+msg.payload[16].toString(16)+msg.payload[17].toString(16)),\n            l1_fwd_active_energy: parseFloat(\"0x\"+msg.payload[18].toString(16)+msg.payload[19].toString(16)),\n            l2_fwd_active_energy: parseFloat(\"0x\"+msg.payload[20].toString(16)+msg.payload[21].toString(16)),\n            l3_fwd_active_energy: parseFloat(\"0x\"+msg.payload[22].toString(16)+msg.payload[23].toString(16)),\n            total_rwd_active_energy: parseFloat(\"0x\"+msg.payload[24].toString(16)+msg.payload[25].toString(16)),\n            t1_rwd_active_energy: parseFloat(\"0x\"+msg.payload[26].toString(16)+msg.payload[27].toString(16)),\n            t2_rwd_active_energy: parseFloat(\"0x\"+msg.payload[28].toString(16)+msg.payload[29].toString(16)),\n            l1_rwd_active_energy: parseFloat(\"0x\"+msg.payload[30].toString(16)+msg.payload[31].toString(16)),\n            l2_rwd_active_energy: parseFloat(\"0x\"+msg.payload[32].toString(16)+msg.payload[33].toString(16)),\n            l3_rwd_active_energy: parseFloat(\"0x\"+msg.payload[34].toString(16)+msg.payload[35].toString(16)),\n            total_reactive_energy: parseFloat(\"0x\"+msg.payload[36].toString(16)+msg.payload[37].toString(16)),\n            t1_reactive_energy: parseFloat(\"0x\"+msg.payload[38].toString(16)+msg.payload[39].toString(16)),\n            t2_reactive_energy: parseFloat(\"0x\"+msg.payload[40].toString(16)+msg.payload[41].toString(16)),\n            l1_reactive_energy: parseFloat(\"0x\"+msg.payload[42].toString(16)+msg.payload[43].toString(16)),\n            l2_reactive_energy: parseFloat(\"0x\"+msg.payload[44].toString(16)+msg.payload[45].toString(16)),\n            l3_reactive_energy: parseFloat(\"0x\"+msg.payload[46].toString(16)+msg.payload[47].toString(16)),\n            total_fwd_reactive_energy: parseFloat(\"0x\"+msg.payload[48].toString(16)+msg.payload[49].toString(16)),\n            t1_fwd_reactive_energy: parseFloat(\"0x\"+msg.payload[50].toString(16)+msg.payload[51].toString(16)),\n            t2_fwd_reactive_energy: parseFloat(\"0x\"+msg.payload[52].toString(16)+msg.payload[53].toString(16)),\n            l1_fwd_reactive_energy: parseFloat(\"0x\"+msg.payload[54].toString(16)+msg.payload[55].toString(16)),\n            l2_fwd_reactive_energy: parseFloat(\"0x\"+msg.payload[56].toString(16)+msg.payload[57].toString(16)),\n            l3_fwd_reactive_energy: parseFloat(\"0x\"+msg.payload[58].toString(16)+msg.payload[59].toString(16)),\n            total_rwd_reactive_energy: parseFloat(\"0x\"+msg.payload[60].toString(16)+msg.payload[61].toString(16)),\n            t1_rwd_reactive_energy: parseFloat(\"0x\"+msg.payload[62].toString(16)+msg.payload[63].toString(16)),\n            t2_rwd_reactive_energy: parseFloat(\"0x\"+msg.payload[64].toString(16)+msg.payload[65].toString(16)),\n            l1_rwd_reactive_energy: parseFloat(\"0x\"+msg.payload[66].toString(16)+msg.payload[67].toString(16)),\n            l2_rwd_reactive_energy: parseFloat(\"0x\"+msg.payload[68].toString(16)+msg.payload[69].toString(16)),\n            l3_rwd_reactive_energy: parseFloat(\"0x\"+msg.payload[70].toString(16)+msg.payload[71].toString(16)),\n            tariff: msg.payload[72],\n            day_counter: parseFloat(\"0x\"+msg.payload[73].toString(16)+msg.payload[74].toString(16))\n            \n        },\n        timestamp: new Date()\n    }];\n\nreturn msg;","outputs":1,"noerr":0,"initialize":"","finalize":"","x":710,"y":320,"wires":[["1cd0236c.cf5a2d","b18c28e5.eb986"]]},{"id":"9a84ffbb.bc858","type":"modbus-client","name":"RTU2TCP_GW","clienttype":"tcp","bufferCommands":false,"stateLogEnabled":false,"queueLogEnabled":false,"tcpHost":"10.0.0.65","tcpPort":"502","tcpType":"DEFAULT","serialPort":"/dev/ttyUSB","serialType":"RTU-BUFFERD","serialBaudrate":"9600","serialDatabits":"8","serialStopbits":"1","serialParity":"none","serialConnectionDelay":"100","unit_id":1,"commandDelay":1,"clientTimeout":1000,"reconnectOnTimeout":true,"reconnectTimeout":2000,"parallelUnitIdsAllowed":true}]
    

    After visualising in Grafana it may give you results like this:

    Screenshot 2021-11-22 at 16.01.35.png

    One more thing

    The meter has resetable daily counter. Which you cen reset using this flow:

    [{"id":"24dc7abe.229b0e","type":"inject","z":"ae6fb191.56fce","name":"Reset daily meter","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"00 00 * * *","once":false,"onceDelay":0.1,"topic":"","payload":"[0,0]","payloadType":"json","x":150,"y":440,"wires":[["10b2b732.893979"]]},{"id":"10b2b732.893979","type":"modbus-write","z":"ae6fb191.56fce","name":"","showStatusActivities":false,"showErrors":false,"unitid":"1","dataType":"MHoldingRegisters","adr":"24649","quantity":"2","server":"9a84ffbb.bc858","emptyMsgOnFail":false,"keepMsgProperties":false,"x":340,"y":440,"wires":[[],[]]},{"id":"9a84ffbb.bc858","type":"modbus-client","name":"RTU2TCP_GW","clienttype":"tcp","bufferCommands":false,"stateLogEnabled":false,"queueLogEnabled":false,"tcpHost":"10.0.0.65","tcpPort":"502","tcpType":"DEFAULT","serialPort":"/dev/ttyUSB","serialType":"RTU-BUFFERD","serialBaudrate":"9600","serialDatabits":"8","serialStopbits":"1","serialParity":"none","serialConnectionDelay":"100","unit_id":1,"commandDelay":1,"clientTimeout":1000,"reconnectOnTimeout":true,"reconnectTimeout":2000,"parallelUnitIdsAllowed":true}]
    

    Hope this helps, and I will be happy for your feedback.


Log in to reply