Logic Machine Forum
Norwegian HAN-interface - Kamstrup meter - Printable Version

+- Logic Machine Forum (https://forum.logicmachine.net)
+-- Forum: LogicMachine eco-system (https://forum.logicmachine.net/forumdisplay.php?fid=1)
+--- Forum: Gateway (https://forum.logicmachine.net/forumdisplay.php?fid=10)
+--- Thread: Norwegian HAN-interface - Kamstrup meter (/showthread.php?tid=1881)



Norwegian HAN-interface - Kamstrup meter - oveh - 31.01.2019

Hello

I am planing to make a script that receives the power data from a Kamstrup Power meter.
In Norway all the power meters has been changed to smart meters, and one of the requirement from the government, was that customer should have access to the data. 
Google translate this site for more info: 
https://www.nek.no/info-ams-han-utviklere/


As far as i know there is 3 types of power meters used, Kamstrup, Aidon and Kaifa.

They are all sending (pushing) data on a port (Physical interface is MBUS), and the data can be read with a Mbus to serial converter (slave type). I am using this, to connect LM to my Kamstrup meter:
https://www.aliexpress.com/item/USB-transfer-MBUS-module-slave-module-communication-debug-alternative-TSS721/32719562958.html


Since i have a Kamstrup meter, i will focus on that type in this thread.
More info about Kamstrup HAN interface
https://www.nek.no/wp-content/uploads/2018/11/Kamstrup_V0001-181022.pdf
https://www.nek.no/wp-content/uploads/2018/10/Kamstrup-HAN-NVE-interface-description_rev_3_1.pdf

Code:
-- Check if port is open, open serial port if not
if not port then
 require('serial')
 port = serial.open('/dev/ttyUSB0', { baudrate = 2400 })
 port:flush()
 data = ''
end

-- Read on byte, or time out after 1 sec
byte = port:read(1, 1)


if byte then
 -- if byte read, add byte to data array
 data = data .. byte
else
 -- read timeout: No more data in this package
 if string.len(data) > 1 then
   loghex(data)
 end
 data = ""
end
Here is a residential script that reads the data, pushed from the kamstrup meter.

There is two types of packages:
- every 10 sec we get the basic data
- every 1 hr we get the basic + a little bit more.

Example of a basic data (ID replaced with XX):
* hexstring [228]: 7E A0 E2 2B 21 13 23 9A E6 E7 00 0F 00 00 00 00 0C 07 E3 01 1F 04 0D 24 0A FF 80 00 00 02 19 0A 0E 4B 61 6D 73 74 72 75 70 5F 56 30 30 30 31 09 06 01 01 00 00 05 FF 0A 10 XX XX XX XX XX XX XX XX XX XX XX XX XX XX XX XX 09 06 01 01 60 01 01 FF 0A 12 36 38 34 31 31 32 31 42 4E 32 34 33 31 30 31 30 34 30 09 06 01 01 01 07 00 FF 06 00 00 0E BB 09 06 01 01 02 07 00 FF 06 00 00 00 00 09 06 01 01 03 07 00 FF 06 00 00 01 C4 09 06 01 01 04 07 00 FF 06 00 00 00 00 09 06 01 01 1F 07 00 FF 06 00 00 05 C1 09 06 01 01 33 07 00 FF 06 00 00 03 27 09 06 01 01 47 07 00 FF 06 00 00 03 91 09 06 01 01 20 07 00 FF 12 00 DC 09 06 01 01 34 07 00 FF 12 00 DE 09 06 01 01 48 07 00 FF 12 00 DD F7 9B 7E


Code:
Start flag      7E
               A0 E2 2B 21 13 23 9A E6 E7 00

Date/time       0F 00 00 00 00      0C 07 E3 01 1F 04 0D 24 0A FF 80 00 00

               02 19
               0E 4B  

Kamstrup_V0001  61 6D 73 74 72 75 70 5F 56 30 30 30 31

ID              09 06 01 01 00 00 05 FF    0A 10 XX XX XX XX XX XX XX XX XX XX XX XX XX XX XX XX

Type            09 06 01 01 60 01 01 FF    0A 12 36 38 34 31 31 32 31 42 4E 32 34 33 31 30 31 30 34 30
(6841121BN243101040)

Act Pow+        09 06 01 01 01 07 00 FF    06 00 00 0E BB
Act Pow-        09 06 01 01 02 07 00 FF    06 00 00 00 00

Rea Pow+        09 06 01 01 03 07 00 FF    06 00 00 01 C4
Rea Pow-        09 06 01 01 04 07 00 FF    06 00 00 00 00

Current         09 06 01 01 1F 07 00 FF    06 00 00 05 C1
Current         09 06 01 01 33 07 00 FF    06 00 00 03 27
Current         09 06 01 01 47 07 00 FF    06 00 00 03 91

Voltage         09 06 01 01 20 07 00 FF    12 00 DC
Voltage         09 06 01 01 34 07 00 FF    12 00 DE
Voltage         09 06 01 01 48 07 00 FF    12 00 DD

CRC             F7 9B
Stop flag       7E


CRC
The checksum is the same as the CRC-16/X-25 on this site: https://crccalc.com/
Remove start and end byte (7E) + CRC bytes.

Both of the following functions should work:

Code:
function crc16(data)
 local crc = 0xFFFF
for i = 1, #data do
   crc = bit.bxor(crc, data:byte(i))
   for j = 1, 8 do
     local k = bit.band(crc, 1)
     crc = bit.rshift(crc, 1)
     if k ~= 0 then
       crc = bit.bxor(crc, 0x8408)
       end
     end
   end
   --return bit.bxor(crc, 0xFFFF)
   return string.format("%X",bit.bxor(crc, 0xFFFF))
end


Code:
local crcTable = {
       0x0000, 0x1189, 0x2312, 0x329b, 0x4624, 0x57ad, 0x6536, 0x74bf,
       0x8c48, 0x9dc1, 0xaf5a, 0xbed3, 0xca6c, 0xdbe5, 0xe97e, 0xf8f7,
       0x1081, 0x0108, 0x3393, 0x221a, 0x56a5, 0x472c, 0x75b7, 0x643e,
       0x9cc9, 0x8d40, 0xbfdb, 0xae52, 0xdaed, 0xcb64, 0xf9ff, 0xe876,
       0x2102, 0x308b, 0x0210, 0x1399, 0x6726, 0x76af, 0x4434, 0x55bd,
       0xad4a, 0xbcc3, 0x8e58, 0x9fd1, 0xeb6e, 0xfae7, 0xc87c, 0xd9f5,
       0x3183, 0x200a, 0x1291, 0x0318, 0x77a7, 0x662e, 0x54b5, 0x453c,
       0xbdcb, 0xac42, 0x9ed9, 0x8f50, 0xfbef, 0xea66, 0xd8fd, 0xc974,
       0x4204, 0x538d, 0x6116, 0x709f, 0x0420, 0x15a9, 0x2732, 0x36bb,
       0xce4c, 0xdfc5, 0xed5e, 0xfcd7, 0x8868, 0x99e1, 0xab7a, 0xbaf3,
       0x5285, 0x430c, 0x7197, 0x601e, 0x14a1, 0x0528, 0x37b3, 0x263a,
       0xdecd, 0xcf44, 0xfddf, 0xec56, 0x98e9, 0x8960, 0xbbfb, 0xaa72,
       0x6306, 0x728f, 0x4014, 0x519d, 0x2522, 0x34ab, 0x0630, 0x17b9,
       0xef4e, 0xfec7, 0xcc5c, 0xddd5, 0xa96a, 0xb8e3, 0x8a78, 0x9bf1,
       0x7387, 0x620e, 0x5095, 0x411c, 0x35a3, 0x242a, 0x16b1, 0x0738,
       0xffcf, 0xee46, 0xdcdd, 0xcd54, 0xb9eb, 0xa862, 0x9af9, 0x8b70,
       0x8408, 0x9581, 0xa71a, 0xb693, 0xc22c, 0xd3a5, 0xe13e, 0xf0b7,
       0x0840, 0x19c9, 0x2b52, 0x3adb, 0x4e64, 0x5fed, 0x6d76, 0x7cff,
       0x9489, 0x8500, 0xb79b, 0xa612, 0xd2ad, 0xc324, 0xf1bf, 0xe036,
       0x18c1, 0x0948, 0x3bd3, 0x2a5a, 0x5ee5, 0x4f6c, 0x7df7, 0x6c7e,
       0xa50a, 0xb483, 0x8618, 0x9791, 0xe32e, 0xf2a7, 0xc03c, 0xd1b5,
       0x2942, 0x38cb, 0x0a50, 0x1bd9, 0x6f66, 0x7eef, 0x4c74, 0x5dfd,
       0xb58b, 0xa402, 0x9699, 0x8710, 0xf3af, 0xe226, 0xd0bd, 0xc134,
       0x39c3, 0x284a, 0x1ad1, 0x0b58, 0x7fe7, 0x6e6e, 0x5cf5, 0x4d7c,
       0xc60c, 0xd785, 0xe51e, 0xf497, 0x8028, 0x91a1, 0xa33a, 0xb2b3,
       0x4a44, 0x5bcd, 0x6956, 0x78df, 0x0c60, 0x1de9, 0x2f72, 0x3efb,
       0xd68d, 0xc704, 0xf59f, 0xe416, 0x90a9, 0x8120, 0xb3bb, 0xa232,
       0x5ac5, 0x4b4c, 0x79d7, 0x685e, 0x1ce1, 0x0d68, 0x3ff3, 0x2e7a,
       0xe70e, 0xf687, 0xc41c, 0xd595, 0xa12a, 0xb0a3, 0x8238, 0x93b1,
       0x6b46, 0x7acf, 0x4854, 0x59dd, 0x2d62, 0x3ceb, 0x0e70, 0x1ff9,
       0xf78f, 0xe606, 0xd49d, 0xc514, 0xb1ab, 0xa022, 0x92b9, 0x8330,
       0x7bc7, 0x6a4e, 0x58d5, 0x495c, 0x3de3, 0x2c6a, 0x1ef1, 0x0f78
}

function crc16(v)
    local crc = 0xffff
    
    for i = 1, #v do
        
   local j = bit.band( bit.bxor( v:byte(i), crc ),  0xFF )
        
   crc = bit.band( bit.bxor(crcTable[j+1], bit.rshift(crc, 8)), 0xffff)
    end
    
    return bit.bxor(crc, 0xffff)
end


I am new to LUA, so please help optimize the code  Smile

Will update this post when i have more info..


RE: Norwegian HAN-interface - Kamstrup meter - admin - 31.01.2019

Basic receive logic:
Receive 1 byte with 1 second timeout
  • In case of timeout, check if buffer is not empty and parse it
  • Otherwise add byte to buffer
The third byte seems to be packet length without first/last 0x7E so you can use that to parse buffer with multiple messages in case they arrive together (10 second and 1 hour data).


RE: Norwegian HAN-interface - Kamstrup meter - oveh - 31.01.2019

(31.01.2019, 13:58)admin Wrote: Basic receive logic:
Receive 1 byte with 1 second timeout
  • In case of timeout, check if buffer is not empty and parse it
  • Otherwise add byte to buffer
Can you please provide an example?


(31.01.2019, 13:58)admin Wrote: The third byte seems to be packet length without first/last 0x7E so you can use that to parse buffer with multiple messages in case they arrive together (10 second and 1 hour data).

Yes, actually it is both 3rd and lower part on 2nd, but the 1 hr and 10 sec message should not come together:
Quote:Push interval: Fixed at 3600s, The 10 second timer and the 1 hour timer must not collide since then one of the lists will not be pushed. So the 10 sec timer will be sent at xx:xx:x0 (hh:mmConfuseds) and the 1 hour timer is offset 5 seconds and sent at xx:00:05 (hh:mmConfuseds)




Some more sample data. ID-bytes have been replaced with 0x01, and checksum bytes have been updated accordingly
Code:
# 3 examples of 10 sec message. ID changed to 1111... and CRC updated

7EA0E22B2113239AE6E7000F000000000C07E3011501172B0AFF80000002190A0E4B616D73747275705F563030303109060101000005FF0A100101010101010101010101010101010109060101600101FF0A1236383431313231424E32343331303130343009060101010700FF0600000B0809060101020700FF060000000009060101030700FF06000001D109060101040700FF0600000000090601011F0700FF060000044309060101330700FF06000002EE09060101470700FF060000022D09060101200700FF1200DC09060101340700FF1200DD09060101480700FF1200DFA98A7E

7EA0E22B2113239AE6E7000F000000000C07E3011D02010E28FF80000002190A0E4B616D73747275705F563030303109060101000005FF0A100101010101010101010101010101010109060101600101FF0A1236383431313231424E32343331303130343009060101010700FF0600000E5909060101020700FF060000000009060101030700FF060000011509060101040700FF0600000000090601011F0700FF060000058A09060101330700FF060000033D09060101470700FF060000036209060101200700FF1200DA09060101340700FF1200DA09060101480700FF1200DF187E7E

7EA0E22B2113239AE6E7000F000000000C07E3011D0209391EFF80000002190A0E4B616D73747275705F563030303109060101000005FF0A100101010101010101010101010101010109060101600101FF0A1236383431313231424E32343331303130343009060101010700FF060000153009060101020700FF060000000009060101030700FF060000013809060101040700FF0600000000090601011F0700FF06000008AB09060101330700FF06000007B209060101470700FF060000023709060101200700FF1200D609060101340700FF1200DA09060101480700FF1200DDE3777E



# 3 examples of 1 hr message. ID changed to 1111... and CRC updated

7EA12C2B2113FC04E6E7000F000000000C07E3011D02010005FF80000002230A0E4B616D73747275705F563030303109060101000005FF0A100101010101010101010101010101010109060101600101FF0A1236383431313231424E32343331303130343009060101010700FF060000105B09060101020700FF060000000009060101030700FF060000017809060101040700FF0600000000090601011F0700FF06000005D809060101330700FF060000038A09060101470700FF060000041509060101200700FF1200DB09060101340700FF1200DB09060101480700FF1200DF09060001010000FF090C07E3011D02010005FF80000009060101010800FF0600437C9009060101020800FF060000000009060101030800FF06000A35EE09060101040800FF0600012C8CFA257E

7EA12C2B2113FC04E6E7000F000000000C07E3011D020A0005FF80000002230A0E4B616D73747275705F563030303109060101000005FF0A100101010101010101010101010101010109060101600101FF0A1236383431313231424E32343331303130343009060101010700FF060000153509060101020700FF060000000009060101030700FF060000015009060101040700FF0600000000090601011F0700FF06000008A409060101330700FF06000007AB09060101470700FF060000023A09060101200700FF1200D609060101340700FF1200DB09060101480700FF1200DE09060001010000FF090C07E3011D020A0005FF80000009060101010800FF060043895409060101020800FF060000000009060101030800FF06000A370E09060101040800FF0600012C8C4B157E

7EA12C2B2113FC04E6E7000F000000000C07E3011D020B0005FF80000002230A0E4B616D73747275705F563030303109060101000005FF0A100101010101010101010101010101010109060101600101FF0A1236383431313231424E32343331303130343009060101010700FF060000153209060101020700FF060000000009060101030700FF06000000E909060101040700FF0600000000090601011F0700FF06000008B909060101330700FF06000007C409060101470700FF060000023809060101200700FF1200D309060101340700FF1200DB09060101480700FF1200DB09060001010000FF090C07E3011D020B0005FF80000009060101010800FF0600438ABD09060101020800FF060000000009060101030800FF06000A372B09060101040800FF0600012C8CA93B7E



RE: Norwegian HAN-interface - Kamstrup meter - admin - 01.02.2019

Your example already does what I've suggested. The only slight improvement is to use a table to buffer data.
Code:
-- Check if port is open, open serial port if not
if not port then
  require('serial')
  port = serial.open('/dev/ttyUSB0', { baudrate = 2400 })
  port:flush()
  buffer = {}
end

-- Read on byte, or time out after 1 sec
char = port:read(1, 1)

if char then
  buffer[ #buffer + 1 ] = char
elseif #buffer > 0 then
  data = table.concat(buffer)
  -- parse(data)
  loghex(data)
  buffer = {}
end



RE: Norwegian HAN-interface - Kamstrup meter - Tokatubs - 08.02.2019

(31.01.2019, 16:24)oveh Wrote:
(31.01.2019, 13:58)admin Wrote: Basic receive logic:
Receive 1 byte with 1 second timeout
  • In case of timeout, check if buffer is not empty and parse it
  • Otherwise add byte to buffer
Can you please provide an example?


(31.01.2019, 13:58)admin Wrote: The third byte seems to be packet length without first/last 0x7E so you can use that to parse buffer with multiple messages in case they arrive together (10 second and 1 hour data).

Yes, actually it is both 3rd and lower part on 2nd, but the 1 hr and 10 sec message should not come together:
Quote:Push interval: Fixed at 3600s, The 10 second timer and the 1 hour timer must not collide since then one of the lists will not be pushed. So the 10 sec timer will be sent at xx:xx:x0 (hh:mmConfuseds) and the 1 hour timer is offset 5 seconds and sent at xx:00:05 (hh:mmConfuseds)




Some more sample data. ID-bytes have been replaced with 0x01, and checksum bytes have been updated accordingly
Code:
# 3 examples of 10 sec message. ID changed to 1111... and CRC updated

7EA0E22B2113239AE6E7000F000000000C07E3011501172B0AFF80000002190A0E4B616D73747275705F563030303109060101000005FF0A100101010101010101010101010101010109060101600101FF0A1236383431313231424E32343331303130343009060101010700FF0600000B0809060101020700FF060000000009060101030700FF06000001D109060101040700FF0600000000090601011F0700FF060000044309060101330700FF06000002EE09060101470700FF060000022D09060101200700FF1200DC09060101340700FF1200DD09060101480700FF1200DFA98A7E

7EA0E22B2113239AE6E7000F000000000C07E3011D02010E28FF80000002190A0E4B616D73747275705F563030303109060101000005FF0A100101010101010101010101010101010109060101600101FF0A1236383431313231424E32343331303130343009060101010700FF0600000E5909060101020700FF060000000009060101030700FF060000011509060101040700FF0600000000090601011F0700FF060000058A09060101330700FF060000033D09060101470700FF060000036209060101200700FF1200DA09060101340700FF1200DA09060101480700FF1200DF187E7E

7EA0E22B2113239AE6E7000F000000000C07E3011D0209391EFF80000002190A0E4B616D73747275705F563030303109060101000005FF0A100101010101010101010101010101010109060101600101FF0A1236383431313231424E32343331303130343009060101010700FF060000153009060101020700FF060000000009060101030700FF060000013809060101040700FF0600000000090601011F0700FF06000008AB09060101330700FF06000007B209060101470700FF060000023709060101200700FF1200D609060101340700FF1200DA09060101480700FF1200DDE3777E



# 3 examples of 1 hr message. ID changed to 1111... and CRC updated

7EA12C2B2113FC04E6E7000F000000000C07E3011D02010005FF80000002230A0E4B616D73747275705F563030303109060101000005FF0A100101010101010101010101010101010109060101600101FF0A1236383431313231424E32343331303130343009060101010700FF060000105B09060101020700FF060000000009060101030700FF060000017809060101040700FF0600000000090601011F0700FF06000005D809060101330700FF060000038A09060101470700FF060000041509060101200700FF1200DB09060101340700FF1200DB09060101480700FF1200DF09060001010000FF090C07E3011D02010005FF80000009060101010800FF0600437C9009060101020800FF060000000009060101030800FF06000A35EE09060101040800FF0600012C8CFA257E

7EA12C2B2113FC04E6E7000F000000000C07E3011D020A0005FF80000002230A0E4B616D73747275705F563030303109060101000005FF0A100101010101010101010101010101010109060101600101FF0A1236383431313231424E32343331303130343009060101010700FF060000153509060101020700FF060000000009060101030700FF060000015009060101040700FF0600000000090601011F0700FF06000008A409060101330700FF06000007AB09060101470700FF060000023A09060101200700FF1200D609060101340700FF1200DB09060101480700FF1200DE09060001010000FF090C07E3011D020A0005FF80000009060101010800FF060043895409060101020800FF060000000009060101030800FF06000A370E09060101040800FF0600012C8C4B157E

7EA12C2B2113FC04E6E7000F000000000C07E3011D020B0005FF80000002230A0E4B616D73747275705F563030303109060101000005FF0A100101010101010101010101010101010109060101600101FF0A1236383431313231424E32343331303130343009060101010700FF060000153209060101020700FF060000000009060101030700FF06000000E909060101040700FF0600000000090601011F0700FF06000008B909060101330700FF06000007C409060101470700FF060000023809060101200700FF1200D309060101340700FF1200DB09060101480700FF1200DB09060001010000FF090C07E3011D020B0005FF80000009060101010800FF0600438ABD09060101020800FF060000000009060101030800FF06000A372B09060101040800FF0600012C8CA93B7E
Hi , was just wondering did you get this working?


RE: Norwegian HAN-interface - Kamstrup meter - oveh - 10.02.2019

Thanks for improvement suggestion, Admin. I didn't know that successive concatenations to a single string have poor performance. Smile I am new to LUA, so i appreciate feedback, how to optimize the code.

Tokatubs.
Working away from home at the moment, and will not be able to test before end of February.
However, i did bring some samples of the data packages sent from the meter with me, and have been playing a little bit with them on my Laptop.

My first plan was to iterate through the whole package, and parse all the obis data and values. But since the packages always have the data i need in the same position every time, i decided that verifying the data package, and read out the data from the known position, would be both easier for me and less demanding for the LM.  

Here is the code i have so far, works fine in ZeroBrane Studio:
Code:
------------------------------------------------------------------------------------
-- Sample data from 3 phase Kamstrup power meter
-- Type: 6841121BN243101040
-- Tested on ZeroBrane Studio, with Lua Interpreter: Lua (LuaJit)
------------------------------------------------------------------------------------


-- LuaJIT BitOp, same as LM bit lib?
local bit = require("bit")

-- 10 sec sample telegrams
sample_1 = string.char( 0x7e, 0xa0, 0xe2, 0x2b, 0x21, 0x13, 0x23, 0x9a, 0xe6, 0xe7, 0x00, 0x0f, 0x00, 0x00, 0x00, 0x00, 0x0c, 0x07, 0xe3, 0x01, 0x15, 0x01, 0x17, 0x2b, 0x0a, 0xff, 0x80, 0x00, 0x00, 0x02, 0x19, 0x0a, 0x0e, 0x4b, 0x61, 0x6d, 0x73, 0x74, 0x72, 0x75, 0x70, 0x5f, 0x56, 0x30, 0x30, 0x30, 0x31, 0x09, 0x06, 0x01, 0x01, 0x00, 0x00, 0x05, 0xff, 0x0a, 0x10, 0x31, 0x31, 0x31, 0x31, 0x31, 0x31, 0x31, 0x31, 0x31, 0x31, 0x31, 0x31, 0x31, 0x31, 0x31, 0x31, 0x09, 0x06, 0x01, 0x01, 0x60, 0x01, 0x01, 0xff, 0x0a, 0x12, 0x36, 0x38, 0x34, 0x31, 0x31, 0x32, 0x31, 0x42, 0x4e, 0x32, 0x34, 0x33, 0x31, 0x30, 0x31, 0x30, 0x34, 0x30, 0x09, 0x06, 0x01, 0x01, 0x01, 0x07, 0x00, 0xff, 0x06, 0x00, 0x00, 0x0b, 0x08, 0x09, 0x06, 0x01, 0x01, 0x02, 0x07, 0x00, 0xff, 0x06, 0x00, 0x00, 0x00, 0x00, 0x09, 0x06, 0x01, 0x01, 0x03, 0x07, 0x00, 0xff, 0x06, 0x00, 0x00, 0x01, 0xd1, 0x09, 0x06, 0x01, 0x01, 0x04, 0x07, 0x00, 0xff, 0x06, 0x00, 0x00, 0x00, 0x00, 0x09, 0x06, 0x01, 0x01, 0x1f, 0x07, 0x00, 0xff, 0x06, 0x00, 0x00, 0x04, 0x43, 0x09, 0x06, 0x01, 0x01, 0x33, 0x07, 0x00, 0xff, 0x06, 0x00, 0x00, 0x02, 0xee, 0x09, 0x06, 0x01, 0x01, 0x47, 0x07, 0x00, 0xff, 0x06, 0x00, 0x00, 0x02, 0x2d, 0x09, 0x06, 0x01, 0x01, 0x20, 0x07, 0x00, 0xff, 0x12, 0x00, 0xdc, 0x09, 0x06, 0x01, 0x01, 0x34, 0x07, 0x00, 0xff, 0x12, 0x00, 0xdd, 0x09, 0x06, 0x01, 0x01, 0x48, 0x07, 0x00, 0xff, 0x12, 0x00, 0xdf, 0x6b, 0xaa, 0x7e )

sample_2 = string.char( 0x7e, 0xa0, 0xe2, 0x2b, 0x21, 0x13, 0x23, 0x9a, 0xe6, 0xe7, 0x00, 0x0f, 0x00, 0x00, 0x00, 0x00, 0x0c, 0x07, 0xe3, 0x01, 0x1d, 0x02, 0x09, 0x39, 0x1e, 0xff, 0x80, 0x00, 0x00, 0x02, 0x19, 0x0a, 0x0e, 0x4b, 0x61, 0x6d, 0x73, 0x74, 0x72, 0x75, 0x70, 0x5f, 0x56, 0x30, 0x30, 0x30, 0x31, 0x09, 0x06, 0x01, 0x01, 0x00, 0x00, 0x05, 0xff, 0x0a, 0x10, 0x31, 0x31, 0x31, 0x31, 0x31, 0x31, 0x31, 0x31, 0x31, 0x31, 0x31, 0x31, 0x31, 0x31, 0x31, 0x31, 0x09, 0x06, 0x01, 0x01, 0x60, 0x01, 0x01, 0xff, 0x0a, 0x12, 0x36, 0x38, 0x34, 0x31, 0x31, 0x32, 0x31, 0x42, 0x4e, 0x32, 0x34, 0x33, 0x31, 0x30, 0x31, 0x30, 0x34, 0x30, 0x09, 0x06, 0x01, 0x01, 0x01, 0x07, 0x00, 0xff, 0x06, 0x00, 0x00, 0x15, 0x30, 0x09, 0x06, 0x01, 0x01, 0x02, 0x07, 0x00, 0xff, 0x06, 0x00, 0x00, 0x00, 0x00, 0x09, 0x06, 0x01, 0x01, 0x03, 0x07, 0x00, 0xff, 0x06, 0x00, 0x00, 0x01, 0x38, 0x09, 0x06, 0x01, 0x01, 0x04, 0x07, 0x00, 0xff, 0x06, 0x00, 0x00, 0x00, 0x00, 0x09, 0x06, 0x01, 0x01, 0x1f, 0x07, 0x00, 0xff, 0x06, 0x00, 0x00, 0x08, 0xab, 0x09, 0x06, 0x01, 0x01, 0x33, 0x07, 0x00, 0xff, 0x06, 0x00, 0x00, 0x07, 0xb2, 0x09, 0x06, 0x01, 0x01, 0x47, 0x07, 0x00, 0xff, 0x06, 0x00, 0x00, 0x02, 0x37, 0x09, 0x06, 0x01, 0x01, 0x20, 0x07, 0x00, 0xff, 0x12, 0x00, 0xd6, 0x09, 0x06, 0x01, 0x01, 0x34, 0x07, 0x00, 0xff, 0x12, 0x00, 0xda, 0x09, 0x06, 0x01, 0x01, 0x48, 0x07, 0x00, 0xff, 0x12, 0x00, 0xdd, 0x21, 0x57, 0x7e )

-- Table dump / pretty print function found online:
-- http://lua-users.org/wiki/TableSerialization
function print_r (t, indent) -- alt version, abuse to http://richard.warburton.it
 local indent=indent or ''
 for key,value in pairs(t) do
   io.write(indent,'[',tostring(key),']')
   if type(value)=="table" then io.write(':\n') print_r(value,indent..'\t')
   else io.write(' = ',tostring(value),'\n') end
 end
end


--  Return CRC-16/X-25
function crc16(data)
 local crc = 0xFFFF
for i = 1, #data do
   crc = bit.bxor(crc, data:byte(i))
   for j = 1, 8 do
     local k = bit.band(crc, 1)
     crc = bit.rshift(crc, 1)
     if k ~= 0 then
       crc = bit.bxor(crc, 0x8408)
       end
     end
   end
   return bit.bxor(crc, 0xFFFF)
end


-- Convert 2 bytes to number
function uint16(b)
 if( #b ~= 2) then
   return
 else
   return bit.lshift(b:byte(1),8) + b:byte(2)
 end
end


-- Convert 4 bytes to number
 function uint32(b)
   if( #b ~= 4) then
     return
   else
   --Workaround needed...  lshift(XX,24) gives som strange results on numbers bigger than 0x7f....
   --return bit.lshift(b:byte(1),24) + bit.lshift(b:byte(2),16) + bit.lshift(b:byte(3),8) + b:byte(4)
     return b:byte(1) * 2 ^ 24       + bit.lshift(b:byte(2),16) + bit.lshift(b:byte(3),8) + b:byte(4)
   end
 end  


-- Convert 12 byte octet-string to date/time
function date_time(b)
 if (#b ~= 12) then
   return
 end
 local weekdays = {"Mon","Tue","Wed","Thu","Fri","Sat","Sun"}
 local t = {}
 t.year = uint16(b:sub(1,2))
 t.month = b:byte(3)
 t.day = b:byte(4)
 t.wday = weekdays[b:byte(5)]
 t.hour = b:byte(6)
 t.min = b:byte(7)
 t.sec = b:byte(8)
 return  t
end



function parse(b)
 
  -- Check package length
 if (#b ~= 228) then
   print("Error: Wrong package length, 10 sec package should be 228 bytes")
   return
 end

-- Check start and stop flag
 if ( b:byte(1) ~= 0x7E or b:byte(-1) ~= 0x7E) then
   print("Error: Wrong start and/or stop flag, both must be 0x7E on valid package")
   return
 end
 
 -- Parse checksum bytes -> 0xAA, 0xBB -> 0xBBAA
 check_sum= bit.lshift(b:byte(-2),8) + b:byte(-3)
 
 -- Compare checksum bytes with calculated cheksum
 if ( check_sum ~= crc16( b:sub(2,-4))) then
   print("Error: CRC mismatch")
   return
 end
 
 -- Verify list version
 if ( b:sub(34,47) ~= "Kamstrup_V0001") then
   print("Error: Not Kamstrup_V0001 list")
   return
 end
 
 -- All looks good so far. Read out data from package
 local t = {}
 t.time    = date_time(b:sub(18,29))
 t.version = "Kamstrup_V0001"
 t.id      = b:sub(58,73)
 t.type    = b:sub(84,101)
 -- P: Active power in Watt
 -- Q: Reactive power in kVar
 -- I: Current in Amp
 -- U: Voltage in Volt
 t.P       = { imp = uint32(b:sub(111,114)), exp = uint32(b:sub(124,127)) }
 t.Q       = { imp = uint32(b:sub(137,140)), exp = uint32(b:sub(150,153)) }
 t.I       = { uint32(b:sub(163,166)) / 100, uint32(b:sub(176,179)) / 100, uint32(b:sub(189,192)) / 100 }
 t.U       = { uint16(b:sub(202,203))      , uint16(b:sub(213,214))      , uint16(b:sub(224,225)) }
 
 return t
end


result = parse(sample_1)
--result = parse(sample_2)


if (result) then
 
 print_r(result)
 
 --update_influxDB(result)
 --update_kamstrup_grp(result)
 
end

Some more info how to parse the data:

Code:
Datatypes:

structure        02 LL            (LL elements)
octet-string     09 LL   XX ..       (LL bytes)
visible-string   0A LL   XX ..       (LL bytes)
unsigned32       06      XX XX XX XX    (04 bytes)
unsigned16       12      XX XX        (02 bytes)



OBIS CODE - octet-string[06]
09 06    01 01 1F 07 00 FF  =  1.1.31.07.00.255



Date/time - octet-string[0C]
09 0C    07 E3 01 1D 02 0B 00 05 FF 80 00 00
          2019/01/29 Tu 11:00:05

Byte[1-2]    Year
Byte[3]      Month
Byte[4]      Day of month

Byte[5]      Day of week, 1= Mon, 7 = Sun
Byte[6]      Hours
Byte[7]      Min
Byte[8]      Sec

Byte[9-0C]   ....

This date/time format is also used in the header,
but without the octet-string identifier 0x09



RE: Norwegian HAN-interface - Kamstrup meter - admin - 11.02.2019

Looks good. The issue with uint32 function using bit shifts is caused by bit lib operating on signed 32-bit integers, so your solution with multiplication is correct.