CM160 owl energy monitor from serial - mk3 - updated

This flow queries the OWL energy monitor; I have used it on both Windows and ARM Debian linux, although for linux the CM210x driver may need to be modified to provide 250000 baud (search 'electric owl' for some details).

29/12/2016 - updated to fix a display issue on graph.

It generates both a real-time gauge and a graph of power usage for the day, or any day in the previous 30 days.

NOTE: this flow required fs to be exposed in global.

Notably, this loads the chart using 'restore' - applying the OWL's timestamps as the X axis; - it gathers the stored values at startup (in the 'store' function node', storing 30 days worth of values. Then one day's worth of values is send to the chart at a maximum of 10 second intervals. A slider allows selection of historical days, and the total kwh for the day is calculated from the selected days data. This avoids chart point overload, browser crashes, etc. as well as allowing the historical time data to be used.

historical data is written to daily json files in the current directory.

Credit to cm160server.py and other sources for data interpretation.

This is a work in progress, but hopefully will be of use to someone.

At first start, or start after a timeout, the OWL device will send a LOT of stored data. This will take a minute or two, and shows in the counts stats in the GUI. Only when this is over will the gauge fire up. During this time, the graph will show the historical data flowing in.

Usage (windows): Install the standard OWL software, but make sure the software is not running; we only need the usb drivers. Make sure you have node-red-dashboard (not the older contrib-ui). Import the below into your flow. Set the comport number in the flow correctly for your OWL device; make sure it takes the 250000 baud rate. See the ui at /ui

In linux, 250000 is not a standard baud rate. I modified my cp210x driver to set 250000 if asked for 57600 (so this flow is set to 57600....).

[{"id":"ca46738b.b3449","type":"ui_gauge","z":"3080813a.09a2ce","name":"","group":"693e4c38.157824","order":2,"width":0,"height":0,"gtype":"gage","title":"KW","label":"KW","format":"{{value | number:2}}kw","min":0,"max":10,"colors":["#00b500","#e6e600","#ca3838"],"x":896,"y":100,"wires":[]},{"id":"c5dd387b.389868","type":"change","z":"3080813a.09a2ce","name":"kw->payload","rules":[{"t":"set","p":"payload","pt":"msg","to":"kw","tot":"msg"}],"action":"","property":"","from":"","to":"","reg":false,"x":730,"y":105,"wires":[["ca46738b.b3449","28a4109a.071ea"]]},{"id":"28a4109a.071ea","type":"ui_text","z":"3080813a.09a2ce","group":"693e4c38.157824","order":1,"width":0,"height":0,"name":"","label":"Last Time","format":"{{msg.orgtimestamp}}","layout":"row-spread","x":886,"y":140,"wires":[]},{"id":"f857b95f.2ca828","type":"serial in","z":"3080813a.09a2ce","name":"","serial":"d9dd2a8d.e718e8","x":76,"y":61,"wires":[["24e359df.057726"]]},{"id":"44d1fd5e.d59a34","type":"serial out","z":"3080813a.09a2ce","name":"","serial":"d9dd2a8d.e718e8","x":747,"y":36,"wires":[]},{"id":"f047f1b1.adba","type":"ui_template","z":"3080813a.09a2ce","group":"693e4c38.157824","name":"Counts","order":3,"width":0,"height":0,"format":"    <p>Ident: {{msg.payload.ident || '0'}} \n        Wait: {{msg.payload.wait || '0'}} \n        Realtime: {{msg.payload.realtime || '0'}} \n        Historic: {{msg.payload.totalhistoric || '0'}}</p>\n    <p>Duplicates: {{msg.payload.duplicates || '0'}} \n        Unknown: {{msg.payload.unknown || '0'}} \n        Outmsgs: {{msg.payload.outputmessages || '0'}}</p>\n    <p>Count today: {{msg.payload.daypoints || '0'}}</p>\n    <p>Curr Dayname: {{msg.payload.dayname || ''}}</p>\n    <p>Daysback: {{msg.payload.daysback || '0'}} \n    Displayed: {{msg.payload.daysbackdisplayed || '0'}}</p>\n","storeOutMessages":true,"fwdInMessages":true,"x":885,"y":183,"wires":[[]]},{"id":"1f7cc4e8.6a2d2b","type":"ui_button","z":"3080813a.09a2ce","name":"","group":"693e4c38.157824","order":4,"width":0,"height":0,"label":"Reset","color":"","icon":"","payload":"","payloadType":"str","topic":"Reset","x":66,"y":237,"wires":[["4e4ddd7.3450f24"]]},{"id":"4e4ddd7.3450f24","type":"function","z":"3080813a.09a2ce","name":"Reset","func":"var count = {};\nflow.set('count', count);\nmsg.payload = count;\nreturn msg;","outputs":1,"noerr":0,"x":300,"y":237,"wires":[["ff0a3862.eb5638"]]},{"id":"5c0afafb.cb30a4","type":"ui_chart","z":"3080813a.09a2ce","name":"chart2","group":"4673e918.d7c548","order":2,"width":0,"height":0,"label":"kw","chartType":"line","legend":"false","xformat":"%d-%m %H:%M","interpolate":"linear","nodata":"","ymin":"","ymax":"","removeOlder":"24","removeOlderUnit":"3600","x":895,"y":225,"wires":[[],[]]},{"id":"24e359df.057726","type":"function","z":"3080813a.09a2ce","name":"Process CM160","func":"// first output goes back to serial\n\n// second output carries update data and 'stored'\n// data more recent than last update data\n\n// third output is histroical data - lots at the start,\n// then every minute\n\n// fourth output is unknown messages\n\nvar counts = flow.get('count') || {};\nif (!counts.init){\n    flow.set('count', counts);\n    counts.init = true;\n}\n\nvar historical = flow.get('historical') || {};\nif (!historical.init){\n    flow.set('historical', historical);\n    historical.init = true;\n    historical.data = [];\n}\n\n\nvar procmess = function(a, msg, realtime){\n    msg.receivedat = new Date();\n    if (!realtime){\n        var year    = 2000 + a[1];\n        var month   = a[2] & 0x3f;\n        var day     = a[3];\n        var hour    = a[4];\n        var minute  = a[5];\n        \n        if ((year < 2050) && (month < 13) && (day < 32) && (hour < 24) && (minute < 60)){\n        } else {\n            year    = 2000 + (a[1] & 0x7f);\n            month   = a[2] & 0xf;\n            day     = a[3] & 0x1f;\n            hour    = a[4] & 0x1f;\n            minute  = a[5] & 0x3f;\n\n            var nmsg = {};\n            nmsg.payload = \"invalid date in \" + a;\n            node.send([null, null, null, null, nmsg]);\n        }\n        \n        msg.orgtimestamp = new Date(year, month - 1, day, hour, minute, 0, 0);\n        msg.realtime = false;\n        msg.islast = a[2] & 0x40;\n    } else {\n        msg.orgtimestamp = msg.receivedat;\n        msg.data_unit = a[2];\n        msg.realtime = true;\n    }\n    \n    msg.cost    = a[6] + (a[7] << 8);\n    msg.cost    = msg.cost/100;\n    if (a[2] & 0x80)\n        msg.cost = msg.cost*100;\n    msg.amps    = (a[8] + (a[9] << 8))  *0.07;\n    msg.kw      = Math.floor(msg.amps * 230 ) / 1000;\n}\n\nvar all = new Uint8Array(msg.payload);\n\n//console.log(all.length);\n\n\nvar process_historical = function(){\n    var histcount = historical.data.length;\n    \n    if (histcount){\n        var start = new Date();\n        //console.log(\"process \" + histcount + \" messages at \", start);\n        \n        var out = [];\n        \n        for (var i = 0; i < histcount; i++){\n            var a = historical.data[i];\n            var newmsg = {};\n    \n            newmsg.data = a;\n            newmsg.type = 'stored';\n            procmess(a, newmsg, false);\n            counts.stored = (counts.stored || 0) + 1;\n            \n            point = [];\n            point.push(newmsg.orgtimestamp.valueOf());\n            point.push(newmsg.kw);\n            \n            out.push(point);\n        }\n    \n        var end = new Date();\n        console.log(\"processed \" + histcount + \" messages in \" + (end - start) + \"ms\");\n    \n        var msg = {};\n        msg.payload = out;\n        // send on stored output only\n        node.send([null, null, null, msg, null]);\n    }\n    historical.data = [];\n}\n\n\nvar ProcessData = function(a){\n    switch (a[0]){\n    // if it is an update message\n        case 0x51:{ //'Q'\n            var newmsg = {};\n            newmsg.data = a;\n            newmsg.type = 'update';\n            procmess(a, newmsg, true);\n            context.lastrealtime = newmsg;\n            counts.realtime = (counts.realtime || 0) + 1;\n            // send on update output\n            node.send([null, newmsg, null, null, null]);\n            break;\n        }\n    \n        case 0x59:{ //'Y'\n            var process = true;\n            // ignore observed bad message\n            if (a[1] === 255){\n                if (a[2] === 255){\n                    if (a[3] === 255){\n                        process = false;    \n                    }\n                }\n            }\n            if (process){\n                historical.data.push(a);\n            }\n            break;\n        }\n    \n        case 0xA9:{//\n            switch(a[4]){\n                // if a waiting message\n                case 0x57:{ //'IDTWAITPC'\n                    // process gathered historical\n                    process_historical();\n                    var newmsg = {};\n                    newmsg.topic = \"\";\n                    // send a5 on serial\n                    newmsg.payload = Buffer([0xa5]); //\n                    counts.wait = (counts.wait || 0) + 1;\n                    // send back to serial\n                    node.send([newmsg, null, null, null, null]);\n                    break;\n                }\n            \n                // if an ident message\n                case 0x43:{ //'IDTCMV001'\n                    var newmsg = {payload:\"Z\", topic:\"msg1\"};\n                    // send 5a on serial\n                    counts.ident = (counts.ident || 0) + 1;\n                    //newmsg.payload = Buffer([0x5a]); //\n\n                    // send back to serial\n                    node.send([newmsg, null, null, null, null]);\n\n                    //var newmsg2 = {payload:\"Z\", topic:\"msg2\"};\n                    //console.log(\"send 2nd msg\");\n                    //node.send([newmsg2, null, null, null, null]);\n                    break;\n                }\n                \n                default:{\n                    var newmsg = {};\n                    newmsg.data = a;\n                    newmsg.type = a[4];\n                    counts.unknown = (counts.unknown || 0) + 1;\n                    // send 'unknown' output\n                    node.send([null, null, null, null, newmsg]);\n                break;\n                }\n            }\n            break;\n        }\n    \n        default:{\n            // we did not recognise it.\n            var newmsg = {};\n            newmsg.data = a;\n            newmsg.here = true;\n            counts.unknown = (counts.unknown || 0) + 1;\n            \n            // send 'unknown' output\n            node.send([null, null, null, null, newmsg]);\n            break;\n        }\n    }\n}\n\n\ncounts.unknown = (counts.unknown || 0);\n\nvar orgun  = counts.unknown;\nfor (var posn = 0; posn < all.length; posn += 11){\n    var a = all.slice(posn, posn+11);\n    ProcessData(a);\n}\n\n//console.log(\"Msgs: \" + all.length/11 + \" Unknown \" + (counts.unknown - orgun));\n","outputs":"5","noerr":0,"x":279,"y":61,"wires":[["ed848f81.cef76"],["47c4b6eb.4d0958","ef353b1f.4cd148"],[],["eaa2fb0b.3a1428"],["bf6c5600.eccb98"]]},{"id":"ff0a3862.eb5638","type":"function","z":"3080813a.09a2ce","name":"read flow counts","func":"var store = (flow.get('store') || []);\n\nmsg.payload = flow.get('count') || {};\nmsg.payload.storecount = store.length;\n\nreturn msg;","outputs":1,"noerr":0,"x":554,"y":183,"wires":[["f047f1b1.adba","162acd4.abefa33"]]},{"id":"19ac6228.d38aee","type":"ui_slider","z":"3080813a.09a2ce","name":"","label":"days back","group":"4673e918.d7c548","order":3,"width":0,"height":0,"passthru":true,"topic":"sendgraph","min":"0","max":"30","step":1,"x":83,"y":363,"wires":[["dd87ae63.71f7","c3afc616.741998"]]},{"id":"c3afc616.741998","type":"function","z":"3080813a.09a2ce","name":"storedaysback","func":"flow.set('daysback', msg.payload);\nvar count = flow.get('count')||{};\ncount.daysback = msg.payload;\n\nreturn msg;","outputs":1,"noerr":0,"x":298,"y":284,"wires":[["aeecc470.489b38"]]},{"id":"dd87ae63.71f7","type":"ui_text","z":"3080813a.09a2ce","group":"4673e918.d7c548","order":4,"width":0,"height":0,"name":"Days Back display","label":"","format":"{{msg.payload}}","layout":"row-center","x":853,"y":363,"wires":[]},{"id":"47daca54.a88d94","type":"debug","z":"3080813a.09a2ce","name":"","active":false,"console":"false","complete":"true","x":789,"y":69,"wires":[]},{"id":"47c4b6eb.4d0958","type":"delay","z":"3080813a.09a2ce","name":"","pauseType":"rate","timeout":"5","timeoutUnits":"seconds","rate":"1","nbRateUnits":"1","rateUnits":"second","randomFirst":"1","randomLast":"5","randomUnits":"seconds","drop":false,"x":553,"y":106,"wires":[["c5dd387b.389868"]]},{"id":"ed848f81.cef76","type":"function","z":"3080813a.09a2ce","name":"block if disabled","func":"if (context.disabled === undefined)\n    context.disabled = false;\n\nif (msg.topic){\n    if (msg.topic === 'serialout'){\n        context.disabled = msg.payload;\n    }\n}\n\nvar counts = flow.get('count') || {};\n\nif (context.disabled){\n    if (context.disabled === true){\n        counts.blockedmessages = counts.blockedmessages || 0;\n        counts.blockedmessages++;\n        return;\n    }    \n}\n\ncounts.outputmessages = counts.outputmessages || 0;\ncounts.outputmessages++;\n\n\n\nreturn msg;","outputs":1,"noerr":0,"x":497,"y":35,"wires":[["47daca54.a88d94","44d1fd5e.d59a34"]]},{"id":"ef353b1f.4cd148","type":"debug","z":"3080813a.09a2ce","name":"","active":false,"console":"false","complete":"true","x":528,"y":71,"wires":[]},{"id":"aeecc470.489b38","type":"delay","z":"3080813a.09a2ce","name":"","pauseType":"rate","timeout":"1","timeoutUnits":"seconds","rate":"1","nbRateUnits":"1","rateUnits":"second","randomFirst":"1","randomLast":"5","randomUnits":"seconds","drop":true,"x":494,"y":284,"wires":[["cf99d8d6.3c9348"]]},{"id":"97ca9837.7cd708","type":"inject","z":"3080813a.09a2ce","name":"1 Sec","topic":"","payload":"","payloadType":"date","repeat":"1","crontab":"","once":false,"x":101,"y":178,"wires":[["ff0a3862.eb5638","cf99d8d6.3c9348"]]},{"id":"162acd4.abefa33","type":"debug","z":"3080813a.09a2ce","name":"","active":false,"console":"false","complete":"false","x":878,"y":276,"wires":[]},{"id":"eaa2fb0b.3a1428","type":"function","z":"3080813a.09a2ce","name":"store values","func":"\nvar store = (flow.get('stores') || []);\nif (store.length === 0)\n    flow.set('stores', store);\nvar counts = flow.get('count') || {};\n\nvar maxtime = 1440; // 2 hours\nvar maxcount = 43200; // 30 days\n\nvar finddatetimefromend = function( data, t ){\n    var len = data.length;\n    var t1 = t.valueOf();\n    \n    for (var x = len-1; x >= 0; x--){\n        if (data[x][0] <= t1){\n            return x;\n        }\n    }\n    return len;\n}\n\n\n// payload is an array of points.\n\n// split it into days/runs of ordered dates\nvar oneday = 86400000;\n\n// if a new day, just assign.\n// else\n// if first is after last in the day found, just append\n// else \n// if last is before first in the day found, just pre-pend\n// else find time (expect 1 minute intervals, jump then search)\n// and insert checking for duplicates\n\n\nconsole.log(\"payload len = \");\nconsole.log(\" = \" + msg.payload.length);\n\nvar changeday = true;\nvar dayzero = 0;\nvar nextday = 0;\n\nvar day = { updated: false, data: [] };\ncounts.duplicates = (counts.duplicates || 0);\ncounts.totalhistoric = (counts.totalhistoric || 0);\ncounts.totalhistoric += msg.payload.length;\n\nfor (var i = 0; i < msg.payload.length; i++){\n    var point = msg.payload[i];\n\n    if ((point[0] < dayzero) || (point[0] >= nextday)){\n        var p0 = msg.payload[i];\n        var d0 = Math.floor(p0[0]/oneday);\n        d0 = d0 * oneday;\n        dayzero = d0;\n        nextday = dayzero + oneday;\n        changeday = true;\n    }\n\n    var fname = dayzero;\n    //console.log(\"then = \" + fname);\n    \n    if (changeday){\n        changeday = false;\n        day = { updated: false, data: [] };\n        if (store[fname]){\n            console.log(\"found \" + fname);\n            day = store[fname];\n        } else {\n            // create a new day\n            store[fname] = day;\n            console.log(\"new \" + fname);\n        }\n        \n        counts.dayname = fname;\n        \n        if (day.updated === undefined){\n            day.updated = false;\n        }\n        if (day.data === undefined){\n            day.data = [];\n        }\n        \n        var name = fname + \".json\";\n        //console.log(\"store file \" + name);        \n        \n        if (day.data.length === 0){\n            var fs = global.get('fs');\n            try{\n                fs.accessSync(name, fs.constants.R_OK);\n                console.log(\"store access file ok \" + name);        \n                var x = fs.readFileSync(name);\n                console.log(\"store read file ok \" + name);        \n                day = JSON.parse(x);\n                console.log(\"store parse file ok \" + name);        \n            } catch(e) {\n                console.log(\"store Failed (\"+ e + \") to access file \" + name);        \n            }\n        }\n        day.updated = true;\n        // mark it as recently used\n        day.age = 0;\n    }\n\n    var posn = finddatetimefromend(day.data, point[0]);\n    \n    if (posn === day.data.length){\n        day.data.push(point);\n        //console.log(\"set updated \" + name);        \n    } else {\n        //insert after posn\n        if (point[0] !== day.data[posn][0]){\n            day.data.splice(posn+1, 0, point);\n            //console.log(\"set updated \" + name);        \n        } else {\n            counts.duplicates++;\n            //console.log(\"duplicate \" + name);        \n        }\n    }\n}\n\n\ncounts.daypoints = day.data.length;\nreturn;\n","outputs":"1","noerr":0,"x":552,"y":142,"wires":[[]]},{"id":"cf99d8d6.3c9348","type":"function","z":"3080813a.09a2ce","name":"store","func":"var store = (flow.get('stores') || []);\nvar counts = flow.get('count') || {};\n\n\nvar maxtime = 1440; // 2 hours\nvar maxcount = 43200; // 30 days\n\nvar daysback = counts.daysback || 0;\n\nif (msg.topic === 'resetstore'){\n    store = [];\n    flow.set('stores', store);\n    console.log(\"stores reset\");\n    return;\n}\n\ncounts.daysrepeat = counts.daysrepeat || 0;\n\n// if we displayed this day, the only \n// re-display every 30 seconds\nif (daysback === counts.daysbackdisplayed){\n    counts.daysrepeat++;\n    if (counts.daysrepeat < 30){\n        return;\n    }\n}\ncounts.daysrepeat = 0;\n\n\nvar finddatetimefromend = function( data, t ){\n    var len = data.length;\n    var t1 = t.valueOf();\n    \n    for (var x = len-1; x >= 0; x--){\n        if (data[x][0] <= t1){\n            return x;\n        }\n    }\n    return len;\n}\n\nvar finddatetimefromstart = function( data, t ){\n    var len = data.length;\n    var t1 = t.valueOf();\n    \n    for (var x = 0; x < len; x++){\n        if (data[x][0] >= t1){\n            return x;\n        }\n    }\n    return len;\n}\n\n\nvar out = 1;\n\n\n\n\nvar now = new Date();\nnow.setDate(now.getDate() - daysback);\nvar then = new Date(now);\nnow.setHours(0,0,0,0);\nthen.setHours(23,59,59,999);\nvar fname = now.valueOf();\n\nvar day = { updated: false, data: [] };\nif (store[fname]){\n    day = store[fname];\n}\n\nif (day.updated === undefined){\n    day.updated = false;\n}\nif (day.data === undefined){\n    day.data = [];\n}\n\nvar name = fname + \".json\";\nconsole.log(\"to read file \" + name + \" because \" + now + \" - days \" + daysback);        \n\nif (day.data.length === 0){\n    var fs = global.get('fs');\n    try{\n        fs.accessSync(name, fs.constants.R_OK);\n        console.log(\"access file ok \" + name);        \n        var x = fs.readFileSync(name);\n        console.log(\"read file ok \" + name);        \n        day = JSON.parse(x);\n        console.log(\"parse file ok \" + name);        \n        day.updated = false;\n        console.log(\"update reset ok \" + name);        \n       // create a new day\n        store[fname] = day;\n    } catch(e) {\n        console.log(\"Failed (\"+ e + \") to access file \" + name);        \n    }\n}\n\n\nvar line2  = { values: [] };\nvar pfirst = [ now.valueOf(), 0 ];\nvar plast  = [ then.valueOf(), 0 ];\nline2.values.push(pfirst);\nline2.values.push(plast);\n\n// mark it as recently used\nday.age = 0;\n\n\nvar payload = {};\n\npayload.values = day.data;\n//console.log(payload.values.length);\npayload.key = 'KW';\n\nvar kwhtotal = 0;\n\nif (payload.values.length){\n    var lasttime = payload.values[0][0];\n    for(var i = 1; i < payload.values.length; i++)\n    {\n        kwhtotal = kwhtotal + \n            payload.values[i][1]*\n                (payload.values[i][0] - lasttime)/\n                    (1000*60*60);\n        lasttime = payload.values[i][0];\n    }\n}\n\n\n\nfunction convertDate(inputFormat) {\n  function pad(s) { return (s < 10) ? '0' + s : s; }\n  var d = new Date(inputFormat);\n  return [pad(d.getDate()), pad(d.getMonth()+1), d.getFullYear()].join('/');\n}\n\nvar kwt = Math.floor(kwhtotal * 10)/10;\nmsg1 = {};\nmsg1.payload = {kwhtotal: kwt, day: convertDate(now)};\n\nmsg.payload = [];\nmsg.payload.push(payload);\nmsg.payload.push(line2);\nmsg.topic = 'restore';\n\ncounts.daysbackdisplayed = daysback;\n\nreturn [msg, msg1];\n","outputs":"2","noerr":0,"x":662,"y":284,"wires":[["5c0afafb.cb30a4"],["162acd4.abefa33","791184e5.28b81c"]]},{"id":"b7156da9.d1787","type":"function","z":"3080813a.09a2ce","name":"read/write store","func":"var store = (flow.get('stores') || []);\n\nvar keys = Object.keys(store);\n\n//console.log(\"check \" + keys);\n\nfor (var i = 0; i < keys.length; i++){\n    //console.log(\"check \" + keys[i] + '.json');\n    if (store[keys[i]].updated){\n        var fs = global.get('fs');\n        store[keys[i]].updated = false;\n        // rest age if we ever get updated; don't want to delete the current day!\n        store[keys[i]].age = 0;\n        \n        fs.writeFile( keys[i] + '.json', JSON.stringify(store[keys[i]], null, 1), function(){});\n        console.log(\"wrote \" + keys[i] + '.json');\n        break;\n    }\n}\n\n// clear up days which have not been used for a while (100 calls here) \nfor (var i = 0; i < keys.length; i++){\n    if (store[keys[i]].age === undefined){\n        store[keys[i]].age = 0;\n    }\n    store[keys[i]].age++;\n    \n    if (store[keys[i]].age > 100){\n        delete store[keys[i]];\n        var msg = {payload: \"deleted memory for day \" + keys[i]};\n        node.send(msg);\n    }\n}\n\n\nreturn;","outputs":"1","noerr":0,"x":405,"y":466,"wires":[["bf6c5600.eccb98"]]},{"id":"e36a7072.1cba6","type":"inject","z":"3080813a.09a2ce","name":"5 Sec trigger store to disk","topic":"trigger","payload":"","payloadType":"date","repeat":"5","crontab":"","once":true,"x":150,"y":467,"wires":[["b7156da9.d1787"]]},{"id":"bf6c5600.eccb98","type":"debug","z":"3080813a.09a2ce","name":"","active":false,"console":"true","complete":"true","x":891,"y":463,"wires":[]},{"id":"db5d4c5d.0dbca","type":"comment","z":"3080813a.09a2ce","name":"Protocol notes","info":"OWL USB Serial Protocol:\n========================\n\nOnly after USB *cable* disconnect/reconnect is *all* historical data available.  At other times, only UNSENT historical data is available.\n\neach msg received is 11 bytes:\n\n\n['\\xa9', 'I', 'D', 'T', 'C', 'M', 'V', '0', '0', '1', '\\x01']\nIndicates that the device is present, and has not had a response from the PC for a while.\nRespond with 0x5a ('Z') - within 1 second (250ms seems to work ok).\nIF sent > 1s after the IDTC message, the 'Z' will be silently swallowed, then any subsequent 'Z' will work (leading to very confusing results).\n\nThis will cause the OWL to send historical data (lots of data, maybe as much as 32kbytes?)\nThe end of each chunk of historical data is indicated by\n['\\xa9', 'I', 'D', 'T', 'W', 'A', 'I', 'T', 'P', 'C', 'R']\nat which time the Computer can process the received data before returning \n0xa5\nWhich tells the owl to send more historical data.\n\neach 11 byte message consists of a type, plus 9 bytes of information, and a checksum\nthe last byte is the sum of the first 10 bytes.\n\nTypes:\n=====\n81 - Live data\n89 - historical data\n169 - IDTCMV001 or IDTWAITPC\n\nbasic processing:\n\nswitch (type)\n\tcase 81 (dec)\n\t\tstore/display live data\n\t\tbreak\n\tcase 89 (dec)\n\t\tif (message starts with 255,255,255)\n\t\t\tignore\n\t\telse\n\t\t\tplace historical data into historical buffer\n\t\tbreak\n\tcase 169 (dec)\n\t\tif msg contains 001\n\t\t\trespond 90 (dec)\n\t\t\tbreak\n\t\tif msg contains WAIT\n\t\t\tprocess historical buffer\n\t\t\tthen respond 165\n\t\t\tclear historical buffer\n\t\t\tbreak\n\t\tbreak\n}\n\n\n\n\nLive data format:\n=================\ne.g.\n['Q', '\\x10', '\\x0b', '\\x05', '\\n', '\\x15', '\\xe2', '\\x04', '\\x97', '\\x00', '\\r']\n\nAmps = ((data[9] << 8) | data[8])\nWatts = Amps * 0.7 * Voltage.\n\nCostI = (data[7] << 8) | data[6];\ndata[2] & 128 indicates cost should be multiplied by 100.\ndata[2] === 'Data Unit'.\ntime is take as time of reciept?  (time in message is not reliable - may be last time from last historical? - so ignore it).\n\n(note: 'data unit' replaces month.... hence why funny month values...)\n\n\nHistorical data format:\n=======================\ne.g.\n['Y', '\\x10', '\\n', '\\x06', '\\n', '&', '\\xe2', '\\x04', 'l', '\\x00', '\\xfb']\n\ndata[1] is year\ndata[2] & 63 is mon (1-12)\ndata[2] & 64 is 'Data available' if 0 (i.e. it's set on the last one?)\ndata[2] & 128 is 'Tarrif Units'\ndata[3] is day\ndata[4] is hour\ndata[5] is minute\n\nAmps = ((data[9] << 8) | data[8])\nWatts = Amps * 0.7 * Voltage.\n\nCostI = (data[7] << 8) | data[6];\ndata[2] & 128 indicates cost should be multiplied by 100.\n\nAlso observed the following 'bad' message:\n['Y', '\\xFF', '\\xFF', '\\xFF', '\\xFF', '\\xFF', '\\xFF', '\\xFF', '\\xFF', '\\xFF', '\\x80']\n","x":119,"y":133,"wires":[]},{"id":"791184e5.28b81c","type":"ui_template","z":"3080813a.09a2ce","group":"4673e918.d7c548","name":"KW in graph","order":1,"width":0,"height":0,"format":"<p><b>Total KWh for Day: {{msg.payload.kwhtotal}}</b></p>\n<p>Date Shown: {{msg.payload.day}}</p>\n","storeOutMessages":true,"fwdInMessages":true,"x":878,"y":317,"wires":[[]]},{"id":"693e4c38.157824","type":"ui_group","z":"","name":"Power Now","tab":"4603cecc.6ac38","disp":true,"width":"6"},{"id":"d9dd2a8d.e718e8","type":"serial-port","z":"","serialport":"/dev/ttyUSB0","serialbaud":"57600","databits":"8","parity":"none","stopbits":"1","newline":"250","bin":"bin","out":"time","addchar":false},{"id":"4673e918.d7c548","type":"ui_group","z":"","name":"Historical","tab":"4603cecc.6ac38","disp":true,"width":"8"},{"id":"4603cecc.6ac38","type":"ui_tab","z":"","name":"Home","icon":"dashboard"}]
btsimonh

Flow Info

created 7 months, 1 week ago

Node Types

Core
  • change (x1)
  • comment (x1)
  • debug (x4)
  • delay (x2)
  • function (x8)
  • inject (x2)
  • serial in (x1)
  • serial out (x1)
  • serial-port (x1)
Other
  • ui_button (x1)
  • ui_chart (x1)
  • ui_gauge (x1)
  • ui_group (x2)
  • ui_slider (x1)
  • ui_tab (x1)
  • ui_template (x2)
  • ui_text (x2)

Tags

  • CM160
  • OWL
  • fs
  • energy
Copy this flow JSON to your clipboard and then import into Node-RED using the Import From > Clipboard (Ctrl-I) menu option