====== CEM heatmeter ====== {{:custom_protocols:mbus:cem-heat-meter.png?direct&400|}} -- MAIN CODE STARTS AT LINE 118 function onScanStart () now = os.time() end POLL_DELAY = 60 * 15 -- poll rate constant in sec. HEX_NUMBERING = 16 -- numbering system conversion ------ convenient print function ----------- function tprint(t, indent) if not indent then indent = 0 end for k, v in pairs(t) do local formatting = string.rep(' ', indent) .. k .. ': ' if type(v) == "table" then ERROR(formatting) tprint(v, indent + 1) -- recursive call else if type(v) == "boolean" then v = v and "TRUE" or "FALSE" end ERROR(formatting .. v) end end end -- prints any parameters function DBG(...) for i = 1, #arg do local curArgument = arg[i] if type(curArgument) == 'table' then tprint(curArgument) else INFO(curArgument) end end end ; DEBUG = DBG ------ Table helpers ----------------------------- function table.findPattern(t, pttrn) for tabIndex, _ in ipairs(t) do local matchFlag = true for seqIndex, sequenceByte in ipairs(pttrn) do -- checking pttrn inside a t matchFlag = matchFlag and (sequenceByte == t[tabIndex + seqIndex - 1]) if (not matchFlag) then matchFlag = false break end end if matchFlag then return (tabIndex + #pttrn) end end return false end function table.sub(t, startIndex, endIndex) local tmpTable = {} for k = startIndex, endIndex do table.insert(tmpTable, t[k]) end return tmpTable end table.hexView = function(hextab, spacer) local hex = {} for _, hexbyte in ipairs(hextab) do table.insert(hex, getHexByteAsStr(hexbyte)) end return table.concat(hex, spacer) end -- converts tab to bcd number table.bcd = function (t) local hex = table.hexView(t) return tonumber(hex) end -- integer convertions table.int = function(t) return tonumber(table.hexView(t), HEX_NUMBERING) end -- gets 2 - char hex string of a byte function getHexByteAsStr(inputByte) local strByte = string.format("%X", inputByte) return (#strByte == 1 and '0' .. strByte) or strByte end table.reverse = function (tab) local outTable = {} for i = #tab, 1, -1 do table.insert(outTable, tab[i]) end return outTable end -- delimiters & control bytes SHORT_FRAME_STX = 0x10 SEND_NKE = 0x40 ACK = 0xE5 ETX = 0x16 CTRL_LONG_FRAME_STX = 0x68 REQ_UD2 = 0x7B -- Request for Class 2 Data FCB REQ_UD2_ = 0x5B -- Request for Class 2 Data Frame count bit BRDCAST_NET_LAYER_ADDR = 0xFD -- 253 BROADCAST_ADDR = 0xFE -- 254 -- prboe packet PROBE_PACKET = {SHORT_FRAME_STX, REQ_UD2_, BROADCAST_ADDR, 0x59, ETX} -- its indexes REQ_UD2_POS = 2 PROBE_PACKET_PAYLOAD_LEN = 2 PROBE_PACKET_CRC_POS = 4 data = {} dataHandlers = { energy = {pattern = {0xC, 0x06}, length = 4, func = table.bcd}, volume = {pattern = {0xC, 0x94, 0x3B}, length = 4, func = table.bcd}, tint = {pattern = {0xB, 0x59}, length = 3, func = table.bcd}, text = {pattern = {0xB, 0x5D}, length = 3, func = table.bcd}, power = {pattern = {0xC, 0x2D}, length = 4, func = table.bcd}, flow = {pattern = {0xc, 0x3B}, length = 4, func = table.bcd}, ontime = {pattern = {0xc, 0x22}, length = 4, func = table.bcd}, data = {pattern = {0x6, 0x6D}, length = 6, func = table.int} } function createDevices () addDevice{name = "E", shift = 0, base = 10, xtraFields = {'energy'} } addDevice{name = "V", shift = 0, base = 10, xtraFields = {'volume'} } addDevice{name = "TI", shift = 0, base = 10, xtraFields = {'temp. internal'} } addDevice{name = "TE", shift = 0, base = 10, xtraFields = {'temp. external'} } addDevice{name = "P", shift = 0, base = 10, xtraFields = {'power'} } addDevice{name = "Q", shift = 0, base = 10, xtraFields = {'flow'} } addDevice{name = "ON", shift = 0, base = 10, xtraFields = {'ontime'} } addDevice{name = "DT", shift = 0, base = 10, xtraFields = {'date time '} } end function readRegister (reg, device, unitId) if (not lastReadTimeStamp or (now - lastReadTimeStamp) >= POLL_DELAY ) then PROBE_PACKET[PROBE_PACKET_CRC_POS] = getCRC(PROBE_PACKET , REQ_UD2_POS , PROBE_PACKET_PAYLOAD_LEN) if (not sendBytes(PROBE_PACKET) ) then ERROR("Could not sent bytes for DATA_QUERY!") return false end local meterDataFrame = readUntil(ETX) if (#meterDataFrame > 10) then -- parsing for param, struc in pairs(dataHandlers) do local foundPos = table.findPattern(meterDataFrame, struc.pattern) if foundPos then DBG('found data for ' .. param ..' at: ', foundPos) local foundBytes = table.sub(meterDataFrame, foundPos , foundPos + struc.length - 1) DBG('found bytes: ', foundBytes) foundBytes = table.reverse(foundBytes) DBG("After reverse: ", foundBytes) local actualData = struc.func(foundBytes) DBG("actualData: ", actualData) data[param] = actualData lastReadTimeStamp = now else ERROR("failed to find data !") data[param] = false end end else ERROR("failed to read reply!") data = false end end if (device.name == 'E') then -- convert to Gcal -- 1 watt hour is equal to 8.604206500956E-7 local gcal = data.energy * 8.605206 * 10 ^ (-7) return gcal end if (device.name == 'V') then return data.volume end if (device.name == 'TI') then return data.tint end if (device.name == 'TE') then return data.text end if (device.name == 'P') then return data.power end if (device.name == 'Q') then return data.flow end if (device.name == 'ON') then return data.ontime end if (device.name == 'DT') then return data.data end end function writeRegister (reg, device, unitId, newValue) end -------------------------- Other helpers -------------------------- function readUntil(endByte) local ONE_BYTE, buf = 1, {} repeat local rx = readBytes(ONE_BYTE) if rx then rx = rx[1] ; table.insert(buf, rx) end until (not rx or (rx == endByte)) return ((#buf >= ONE_BYTE) and buf) or false end function getCRC(a, pos, len) local sum, mask = 0, 0xFF for i = pos, pos + len - 1 do sum = sum + a[i] sum = bit.band(sum, mask) end return sum end