{{ :custom_protocols:mbus:heat-meter-diehl-scylar-int-e.jpg?direct&200|}} ====== DIEHL INTE heat meter ====== The meter has integrated Mbus port. -- 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(tostring(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 function table.hexStr(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.hexStr(t) local out = tonumber(hex) ; DBG('entered bdc with', t, 'hex view: ' .. hex, 'out: ' .. tostring(out)) return out end -- integer convertions table.int = function(t) return tonumber(table.hexStr(t), HEX_NUMBERING) end table.float = function(t) return t 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 BROADCAST_ADDR = 0xFE -- 254 CRC_ = 0x59 -- prboe packet -- PROBE_PACKET = {SHORT_FRAME_STX, REQ_UD2_, BROADCAST_ADDR, CRC_, ETX} -- TxD:10 5B FE 59 16 PROBE_PACKET = {SHORT_FRAME_STX, REQ_UD2, BROADCAST_ADDR, CRC_, ETX} -- TxD:10 5B FE 59 16 -- its indexes REQ_UD2_POS = 2 PROBE_PCKT_ADDR_POS = 3 PROBE_PACKET_CRC_POS = 4 PROBE_PACKET_PAYLOAD_LEN = 2 data = {empty = {}} dataHandlers = { -- energy = {pattern = {0x4, 0x07}, length = 4, func = table.float}, energy = {pattern = {0x4, 0x07}, length = 4, func = table.float}, volume = {pattern = {0x4, 0x15}, length = 4, func = table.int}, power = {pattern = {0x5, 0x2E}, length = 4, func = table.float}, flow = {pattern = {0x5, 0x3E}, length = 4, func = table.float}, tint = {pattern = {0x2, 0x59}, length = 2, func = table.int}, -- fwd. temp. text = {pattern = {0x2, 0x5D}, length = 2, func = table.int}, -- return temp. err = {pattern = {0x2, 0xFD, 0x17}, length = 2, func = table.int}, upt = {pattern = {0x4, 0x20}, length = 4, func = table.int} -- uptime , sec. } function createDevices () -- xtraFields are used for comments addDevice{name = "E", shift = 0, base = 10, xtraFields = {'energy'} } addDevice{name = "V", shift = 0, base = 10, xtraFields = {'volume'} } addDevice{name = "P", shift = 0, base = 10, xtraFields = {'power'} } addDevice{name = "Q", shift = 0, base = 10, xtraFields = {'flow'} } addDevice{name = "FWD", shift = 0, base = 10, xtraFields = {'temp. internal'} } addDevice{name = "RET", shift = 0, base = 10, xtraFields = {'temp. external'} } addDevice{name = "ERR", shift = 0, base = 10, xtraFields = {'error flags'} } addDevice{name = "UPT", shift = 0, base = 10, xtraFields = {'error flags'} } end function readRegister (reg, device, unitId) local function initMeter() DBG("Etnered init packet for inte") local INTE_INIT_FRAME = {SHORT_FRAME_STX, SEND_NKE, BROADCAST_ADDR, 0x00 -- CRC #4 , ETX} INTE_INIT_FRAME[3] = unitId INTE_INIT_FRAME[4] = getCRC(INTE_INIT_FRAME, 2, 2) if (not sendBytes(INTE_INIT_FRAME) ) then ERROR("Could not sent bytes!") return false end if (readOneByte() ~= ACK) then ERROR("No ack after init packet!") ; return false else DBG("Ack received for INIT_PACKETS") end local CURRENT_PARAMS_SELECTION = { CTRL_LONG_FRAME_STX, 0x04, 0x04, CTRL_LONG_FRAME_STX , 0x53, BROADCAST_ADDR, 0x50, 0x0, 0x0 -- CRC #9 , ETX} CURRENT_PARAMS_SELECTION[6] = unitId CURRENT_PARAMS_SELECTION[9] = getCRC(CURRENT_PARAMS_SELECTION, 5, 4) if (not sendBytes(CURRENT_PARAMS_SELECTION) ) then ERROR("Could not send bytes for params selection!") ; return false end if (readOneByte() ~= ACK) then ERROR("No ack after cur params selection packet!") ; return false else DEBUG("Ack received for params selection") ; return true end end if (not lastReadTimeStamp or (now - lastReadTimeStamp) >= POLL_DELAY ) then DBG("Starting inte read by lastReadTimeStamp") if (not init and not initMeter() ) then ERROR("Could not init INTE meter!") init = false ; return false else init = true -- set global for init meter end PROBE_PACKET[PROBE_PCKT_ADDR_POS] = unitId 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 and (#meterDataFrame > 10) ) then DBG(table.hexStr(meterDataFrame, ' ')) -- parsing for param, struc in pairs(dataHandlers) do DBG('trying to read ' .. tostring(param)) local foundPos = table.findPattern(meterDataFrame, struc.pattern) if foundPos then DBG(' found data at: '.. foundPos) local foundBytes = table.sub(meterDataFrame , foundPos , foundPos + struc.length - 1) foundBytes = table.reverse(foundBytes) local actualData = struc.func(foundBytes) ; DBG("reversed actualData", '|', table.hexStr(foundBytes, ' ')) -- DBG('cur data structure: ', data) data[param] = actualData lastReadTimeStamp = now else ERROR("failed to find data for " .. param) data[param] = false end end else ERROR("failed to read long reply, meterDataFrame: " .. tostring(meterDataFrame)) return false end end DBG("going to return cached data: ", data ) if data then if (device.name == 'E') then DBG(device.xtraFields[1]) return data.energy end if (device.name == 'V') then return data.volume end if (device.name == 'P') then return data.power end if (device.name == 'Q') then return data.flow end if (device.name == 'FWD') then return data.tint end if (device.name == 'RET') then return data.text end if (device.name == 'ERR') then return data.err end if (device.name == 'UPT') then return data.upt end else ERROR("the data is empty ... ") return false end end function writeRegister (reg, device, unitId, newValue) end -------------------------- Other helpers -------------------------- function readUntil(endByte) local buf = {} repeat local b = readOneByte() if b then table.insert(buf, b) end until (not b or (b == endByte)) return ((#buf >= 1) 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 function readOneByte() local b = readBytes(1) return b and b[1] or false end