energymeter.py 25 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537
  1. #!/usr/bin/env python3
  2. """
  3. EnergyMeter: A Python module for interfacing with several energy meters.
  4. """
  5. __author__ = 'Stan Janssen'
  6. __email__ = 'stanjanssen@finetuned.nl'
  7. __url__ = 'https://finetuned.nl/'
  8. __license__ = 'Apache License, Version 2.0'
  9. __version__ = '1.2.0'
  10. __status__ = 'Beta'
  11. import random
  12. import socket
  13. import struct
  14. import asyncio
  15. import time
  16. import minimalmodbus
  17. import tinysbus
  18. from collections import Iterable
  19. DEBUG = False
  20. class ModbusTCPMeter:
  21. """
  22. Implementation for a Modbus TCP Energy Meter.
  23. """
  24. def __init__(self, port, tcp_port=502, slaveaddress=126, type=None, baudrate=None):
  25. self.port = port
  26. self.tcp_port = tcp_port
  27. self.device_id = slaveaddress
  28. def read(self, regnames=None):
  29. if regnames is None:
  30. registers = self.REGS
  31. return self._read_multiple(registers)
  32. if type(regnames) is str:
  33. registers = [register for register in self.REGS if register['name'] == regnames]
  34. return self._read_single(registers[0])
  35. if type(regnames) is list:
  36. registers = [register for register in self.REGS if register['name'] in regnames]
  37. return self._read_multiple(registers)
  38. def _read_single(self, register):
  39. message = self._modbus_message(start_reg=register['start'], num_regs=register['length'])
  40. data = self._perform_request(message)
  41. return self._convert_value(data, signed=register['signed'], decimals=register['decimals'])
  42. def _read_multiple(self, registers):
  43. registers.sort(key=lambda reg: reg['start'])
  44. results = {}
  45. for reg_range in self._split_ranges(registers):
  46. first_reg = min([register['start'] for register in reg_range])
  47. num_regs = max([register['start'] + register['length'] for register in reg_range]) - first_reg
  48. message = self._modbus_message(start_reg=first_reg, num_regs=num_regs)
  49. data = self._perform_request(message)
  50. results.update(self._interpret_result(data, reg_range))
  51. return results
  52. def _split_ranges(self, registers):
  53. """
  54. Generator that splits the registers list into continuous parts.
  55. """
  56. reg_list = []
  57. prev_end = registers[0]['start'] - 1
  58. for r in registers:
  59. if r['start'] - prev_end > 1:
  60. yield reg_list
  61. reg_list = []
  62. reg_list.append(r)
  63. prev_end = r['start'] + r['length']
  64. yield reg_list
  65. def _modbus_message(self, start_reg, num_regs):
  66. transaction_id = random.randint(1, 2**16 - 1)
  67. return struct.pack(">HHHBBHH", transaction_id,
  68. self.PROTOCOL_CODE,
  69. 6,
  70. self.device_id,
  71. self.FUNCTION_CODE,
  72. start_reg - self.REG_OFFSET,
  73. num_regs)
  74. def _perform_request(self, message):
  75. if self.device is None:
  76. self._connect()
  77. self.device.send(message)
  78. data = bytes()
  79. expect_bytes = 9 + 2 * struct.unpack(">H", message[-2:])[0]
  80. attempt = 1
  81. while len(data) is not expect_bytes:
  82. time.sleep(0.05)
  83. data += self.device.recv(2048)
  84. if attempt >= 10:
  85. return 2 * struct.unpack(">H", message[-2:])[0] * [0]
  86. attempt += 1
  87. return data[9:]
  88. def _interpret_result(self, data, registers):
  89. """
  90. Pull the returned string apart and package the data back to its
  91. intended form.
  92. Arguments:
  93. * data: list of register values returned from the device
  94. * registers: the original requested set of registers
  95. Returns:
  96. * A dict containing the register names and resulting values
  97. """
  98. first_reg = min([register['start'] for register in registers])
  99. results = {}
  100. for register in registers:
  101. regname = register['name']
  102. start = (register['start'] - first_reg) * 2
  103. end = start + register['length'] * 2
  104. values = data[start:end]
  105. results[regname] = self._convert_value(values=values,
  106. signed=register['signed'],
  107. decimals=register['decimals'],
  108. isFloat=register['isFloat'])
  109. if regname == "power_factor_total" and results[regname] == 0:
  110. results[regname] = 1 # The SMA will send out a 0 when the power factor is 100%
  111. return results
  112. def _convert_value(self, values, signed=False, decimals=0, isFloat=False):
  113. """
  114. Convert a list of returned integers to the intended value.
  115. Arguments:
  116. * bytestring: a list of integers that together represent the value
  117. * signed: whether the value is a signed value
  118. * decimals: number of decimals the return value should contain
  119. * isFloat: whether the valie is a float
  120. """
  121. numberOfBytes = len(values)
  122. formatcode_o = '>'
  123. if isFloat:
  124. formatcode_o += 'f'
  125. elif numberOfBytes == 1:
  126. if signed:
  127. formatcode_o += "b"
  128. else:
  129. formatcode_o += "B"
  130. elif numberOfBytes == 2:
  131. if signed:
  132. formatcode_o += "h"
  133. else:
  134. formatcode_o += "H"
  135. elif numberOfBytes == 4:
  136. if signed:
  137. formatcode_o += "l"
  138. else:
  139. formatcode_o += "L"
  140. value = struct.unpack(formatcode_o, bytes(values))[0]
  141. if value in self.NULLS:
  142. return None
  143. else:
  144. return float(value) / 10 ** decimals
  145. def _connect(self):
  146. self.device = socket.create_connection(address=(self.port, self.tcp_port))
  147. class SMAMeter(ModbusTCPMeter):
  148. def __init__(self, *args, **kwargs):
  149. super().__init__(*args, **kwargs)
  150. REGS = [
  151. {'name': 'current_ac', 'start': 40188, 'length': 1, 'signed': False, 'decimals': 1},
  152. {'name': 'current_l1', 'start': 40189, 'length': 1, 'signed': False, 'decimals': 1},
  153. {'name': 'current_l2', 'start': 40190, 'length': 1, 'signed': False, 'decimals': 1},
  154. {'name': 'current_l3', 'start': 40191, 'length': 1, 'signed': False, 'decimals': 1},
  155. {'name': 'voltage_l1_l2', 'start': 40193, 'length': 1, 'signed': False, 'decimals': 1},
  156. {'name': 'voltage_l2_l3', 'start': 40194, 'length': 1, 'signed': False, 'decimals': 1},
  157. {'name': 'voltage_l3_l1', 'start': 40195, 'length': 1, 'signed': False, 'decimals': 1},
  158. {'name': 'voltage_l1_n', 'start': 40196, 'length': 1, 'signed': False, 'decimals': 1},
  159. {'name': 'voltage_l2_n', 'start': 40197, 'length': 1, 'signed': False, 'decimals': 1},
  160. {'name': 'voltage_l3_n', 'start': 40198, 'length': 1, 'signed': False, 'decimals': 1},
  161. {'name': 'active_power_total', 'start': 40200, 'length': 1, 'signed': True, 'decimals': -1},
  162. {'name': 'frequency', 'start': 40202, 'length': 1, 'signed': False, 'decimals': 2},
  163. {'name': 'apparent_power_total', 'start': 40204, 'length': 1, 'signed': True, 'decimals': -1},
  164. {'name': 'reactive_power_total', 'start': 40206, 'length': 1, 'signed': True, 'decimals': -1},
  165. {'name': 'power_factor_total', 'start': 40208, 'length': 1, 'signed': True, 'decimals': 3},
  166. {'name': 'active_export', 'start': 40210, 'length': 2, 'signed': False, 'decimals': 3},
  167. {'name': 'dc_power', 'start': 40217, 'length': 1, 'signed': False, 'decimals': -1},
  168. {'name': 'temperature_internal', 'start': 40219, 'length': 1, 'signed': False, 'decimals': 0},
  169. {'name': 'temperature_other', 'start': 40222, 'length': 1, 'signed': False, 'decimals': 0},
  170. {'name': 'operating_status', 'start': 40224, 'length': 1, 'signed': False, 'decimals': 0}
  171. ]
  172. REG_OFFSET = 1
  173. PROTOCOL_CODE = 0
  174. FUNCTION_CODE = 3
  175. NULLS = [2**16 - 1, 2**15 - 1, 2**15, -2**15]
  176. class MulticubeMeter(ModbusTCPMeter):
  177. """
  178. Implementation for a Multicube energy meter over Modbus TCP.
  179. """
  180. def __init__(self, port, tcp_port=1502, slaveaddress=1, type=None, baudrate=None, auto_scale=True):
  181. super().__init__(port, tcp_port, slaveaddress, type, baudrate)
  182. self.device = socket.create_connection(address=(port, tcp_port))
  183. self.device_id = slaveaddress
  184. self.REGS = [
  185. {'name': 'energy_scale', 'start': 512, 'length': 2, 'decimals': 0, 'signed': True},
  186. {'name': 'active_net', 'start': 514, 'length': 2, 'decimals': 0, 'signed': True},
  187. {'name': 'apparent_net', 'start': 516, 'length': 2, 'decimals': 0, 'signed': True},
  188. {'name': 'reactive_net', 'start': 518, 'length': 2, 'decimals': 0, 'signed': True},
  189. {'name': 'active_power_total', 'start': 2816, 'length': 1, 'decimals': 0, 'signed': True},
  190. {'name': 'apparent_power_total', 'start': 2817, 'length': 1, 'decimals': 0, 'signed': True},
  191. {'name': 'reactive_power_total', 'start': 2818, 'length': 1, 'decimals': 0, 'signed': True},
  192. {'name': 'power_factor_total', 'start': 2819, 'length': 1, 'decimals': 3, 'signed': True},
  193. {'name': 'frequency', 'start': 2820, 'length': 1, 'decimals': 1, 'signed': True},
  194. {'name': 'voltage_l1_n', 'start': 2821, 'length': 1, 'decimals': 0, 'signed': True},
  195. {'name': 'current_l1', 'start': 2822, 'length': 1, 'decimals': 0, 'signed': True},
  196. {'name': 'active_power_l1', 'start': 2823, 'length': 1, 'decimals': 0, 'signed': True},
  197. {'name': 'voltage_l2_n', 'start': 2824, 'length': 1, 'decimals': 0, 'signed': True},
  198. {'name': 'current_l2', 'start': 2825, 'length': 1, 'decimals': 0, 'signed': True},
  199. {'name': 'active_power_l2', 'start': 2826, 'length': 1, 'decimals': 0, 'signed': True},
  200. {'name': 'voltage_l3_n', 'start': 2827, 'length': 1, 'decimals': 0, 'signed': True},
  201. {'name': 'current_l3', 'start': 2828, 'length': 1, 'decimals': 0, 'signed': True},
  202. {'name': 'active_power_l3', 'start': 2829, 'length': 1, 'decimals': 0, 'signed': True},
  203. {'name': 'power_factor_l1', 'start': 2830, 'length': 1, 'decimals': 0, 'signed': True},
  204. {'name': 'power_factor_l2', 'start': 2831, 'length': 1, 'decimals': 0, 'signed': True},
  205. {'name': 'power_factor_l3', 'start': 2832, 'length': 1, 'decimals': 0, 'signed': True},
  206. {'name': 'voltage_l1_l2', 'start': 2833, 'length': 1, 'decimals': 0, 'signed': True},
  207. {'name': 'voltage_l2_l3', 'start': 2834, 'length': 1, 'decimals': 0, 'signed': True},
  208. {'name': 'voltage_l3_l1', 'start': 2835, 'length': 1, 'decimals': 0, 'signed': True},
  209. {'name': 'current_n', 'start': 2836, 'length': 1, 'decimals': 0, 'signed': True},
  210. {'name': 'amps_scale', 'start': 2837, 'length': 1, 'decimals': 0, 'signed': True},
  211. {'name': 'phase_volts_scale', 'start': 2838, 'length': 1, 'decimals': 0, 'signed': True},
  212. {'name': 'line_volts_scale', 'start': 2839, 'length': 1, 'decimals': 0, 'signed': True},
  213. {'name': 'power_scale', 'start': 2840, 'length': 1, 'decimals': 0, 'signed': True},
  214. {'name': 'apparent_power_l1', 'start': 3072, 'length': 1, 'decimals': 0, 'signed': True},
  215. {'name': 'apparent_power_l2', 'start': 3073, 'length': 1, 'decimals': 0, 'signed': True},
  216. {'name': 'apparent_power_l3', 'start': 3074, 'length': 1, 'decimals': 0, 'signed': True},
  217. {'name': 'reactive_power_l1', 'start': 3075, 'length': 1, 'decimals': 0, 'signed': True},
  218. {'name': 'reactive_power_l2', 'start': 3076, 'length': 1, 'decimals': 0, 'signed': True},
  219. {'name': 'reactive_power_l3', 'start': 3077, 'length': 1, 'decimals': 0, 'signed': True},
  220. {'name': 'peak_current_l1', 'start': 3078, 'length': 1, 'decimals': 0, 'signed': True},
  221. {'name': 'peak_current_l2', 'start': 3079, 'length': 1, 'decimals': 0, 'signed': True},
  222. {'name': 'peak_current_l3', 'start': 3080, 'length': 1, 'decimals': 0, 'signed': True},
  223. {'name': 'current_l1_thd', 'start': 3081, 'length': 1, 'decimals': 3, 'signed': True},
  224. {'name': 'current_l2_thd', 'start': 3082, 'length': 1, 'decimals': 3, 'signed': True},
  225. {'name': 'current_l3_thd', 'start': 3083, 'length': 1, 'decimals': 3, 'signed': True}
  226. ]
  227. if auto_scale:
  228. self.set_scaling()
  229. def set_scaling(self):
  230. """
  231. Call this function before reading anything to set up the correct scaling for this meter.
  232. """
  233. decimals_mapping = {1: 2, 2: 1, 3: 0, 4: -1, 5: -2, 6: -3, 7: -4}
  234. a_registers = ['current_l1', 'current_l2', 'current_l3', 'current_n']
  235. scale = int(self.read('amps_scale'))
  236. for r in self.REGS:
  237. if r['name'] in a_registers:
  238. r['decimals'] = decimals_mapping[scale]
  239. pv_registers = ['voltage_l1_n', 'voltage_l2_n', 'voltage_l3_n']
  240. scale = int(self.read('phase_volts_scale'))
  241. for r in self.REGS:
  242. if r['name'] in pv_registers:
  243. r['decimals'] = decimals_mapping[scale]
  244. lv_registers = ['voltage_l1_l2', 'voltage_l2_l3', 'voltage_l3_l1']
  245. scale = int(self.read('line_volts_scale'))
  246. for r in self.REGS:
  247. if r['name'] in lv_registers:
  248. r['decimals'] = decimals_mapping[scale]
  249. p_registers = ['active_power_total',
  250. 'reactive_power_total',
  251. 'apparent_power_total',
  252. 'active_power_l1',
  253. 'active_power_l2',
  254. 'active_power_l3',
  255. 'apparent_power_l1',
  256. 'apparent_power_l2',
  257. 'apparent_power_l3',
  258. 'reactive_power_l1',
  259. 'reactive_power_l2',
  260. 'reactive_power_l3']
  261. scale = int(self.read('power_scale'))
  262. for r in self.REGS:
  263. if r['name'] in p_registers:
  264. r['decimals'] = decimals_mapping[scale]
  265. e_registers = ['active_net', 'apparent_net', 'reactive_net']
  266. decimals_mapping = {3: 3, 4: 2, 5: 1, 6: 0, 7: -1}
  267. scale = int(self.read('energy_scale'))
  268. for r in self.REGS:
  269. if r['name'] in e_registers:
  270. r['decimals'] = decimals_mapping[scale]
  271. REG_OFFSET = 0
  272. PROTOCOL_CODE = 0
  273. FUNCTION_CODE = 3
  274. NULLS = [2**16 - 1, 2**15 - 1, 2**15, -2**15]
  275. class SaiaMeter:
  276. def __init__(self, port, baudrate=38400, slaveaddress=1, type=None, **kwargs):
  277. """ Initialize the ABBMeter object.
  278. Arguments:
  279. * port: a serial port (string)
  280. * baudrate: the baudrate to use (integer)
  281. * slaveaddress: the address of the modbus device (integer)
  282. * Specification of the type. Used to limit the registers to a specific set.
  283. Returns:
  284. * An ABBMeter object
  285. """
  286. self.instrument = tinysbus.Instrument(address=slaveaddress,
  287. serial_port=port,
  288. baudrate=baudrate,
  289. **kwargs)
  290. def read(self, regnames=None):
  291. """ Read one, many or all registers from the device
  292. Args:
  293. * regnames (str or list). If None, read all. If string, read
  294. single register. If list, read all registers from list.
  295. Returns:
  296. * If single register, it returns a single value. If all or list,
  297. return a dict with the keys and values.
  298. Raises:
  299. * KeyError, TypeError, IOError
  300. """
  301. if regnames is None:
  302. return self._batch_read(self.REGS)
  303. if type(regnames) is list:
  304. registers = [register for register in self.REGS if register['name'] in regnames]
  305. if len(registers) < len(regnames):
  306. regs_not_available = [regname for regname in regnames if regname not in \
  307. [register['name'] for register in self.REGS]]
  308. print("Warning: the following registers are not available on this device: " +
  309. ", ".join(regs_not_available))
  310. print("The available registers are: %s" +
  311. ", ".join(register['name'] for register in self.REGS))
  312. if len(registers) == 0:
  313. return {}
  314. registers.sort(key=lambda reg: reg['start'])
  315. return self._batch_read(registers)
  316. elif type(regnames) is str:
  317. registers = [register for register in self.REGS if register['name'] == regnames]
  318. if len(registers) == 0:
  319. raise ValueError("Register not found on device.")
  320. return self._read_single(registers[0])
  321. else:
  322. raise TypeError
  323. def _read_single(self, register):
  324. """
  325. Read a single register and return the value. Not to be called directly.
  326. Arguments:
  327. * register: a 'register' dict that contains info on the register.
  328. Returns:
  329. * The interpreted value from the meter.
  330. """
  331. if register['length'] == 1:
  332. return self.instrument.read_register(register_address=register['start'],
  333. number_of_decimals=register['decimals'],
  334. signed=register['signed'])
  335. if register['length'] == 2:
  336. value = self.instrument.read_long(register_address=register['start'],
  337. signed=register['signed'])
  338. return value / 10 ** register['decimals']
  339. if register['length'] == 4:
  340. value = self.instrument.read_registers(register_address=register['start'],
  341. number_of_registers=register['length'])
  342. return self._convert_value(values=value,
  343. signed=register['signed'],
  344. number_of_decimals=register['decimals'])
  345. def _read_multiple(self, registers):
  346. """
  347. Read multiple registers from the slave device and return their values as a dict.
  348. Arguments:
  349. * A list of registers (complete structs)
  350. Returns:
  351. * A dict containing all keys and values
  352. """
  353. first_reg = min([register['start'] for register in registers])
  354. num_regs = max([register['start'] + register['length'] for register in registers]) - first_reg
  355. values = self.instrument.read_registers(register_address=first_reg,
  356. num_registers=num_regs)
  357. return self._interpret_result(values, registers)
  358. def _batch_read(self, registers):
  359. """
  360. Read multiple registers in batches, limiting each batch to at most 10 registers.
  361. Arguments:
  362. * A list of registers (complete structs)
  363. Returns:
  364. * A dict containing all keys and values
  365. """
  366. # Count up to at most 10 registers:
  367. start_reg = registers[0]['start']
  368. batch = []
  369. results = {}
  370. for register in registers:
  371. if register['start'] + register['length'] - start_reg <= 10:
  372. batch.append(register)
  373. else:
  374. results.update(self._read_multiple(batch))
  375. batch = []
  376. batch.append(register)
  377. start_reg = register['start']
  378. results.update(self._read_multiple(batch))
  379. return results
  380. def _interpret_result(self, data, registers):
  381. """
  382. Pull the returned string apart and package the data back to its
  383. intended form.
  384. Arguments:
  385. * data: list of register values returned from the device
  386. * registers: the original requested set of registers
  387. Returns:
  388. * A dict containing the register names and resulting values
  389. """
  390. first_reg = min([register['start'] for register in registers])
  391. results = {}
  392. for register in registers:
  393. regname = register['name']
  394. start = register['start'] - first_reg
  395. end = start + register['length']
  396. values = data[start:end]
  397. results[regname] = self._convert_value(values=values,
  398. signed=register['signed'],
  399. number_of_decimals=register['decimals'])
  400. return results
  401. def _convert_value(self, values, signed=False, number_of_decimals=0):
  402. """
  403. Convert a list of returned integers to the intended value.
  404. Arguments:
  405. * bytestring: a list of integers that together represent the value
  406. * signed: whether the value is a signed value
  407. * decimals: number of decimals the return value should contain
  408. """
  409. number_of_registers = len(values)
  410. formatcode_i = '>'
  411. formatcode_o = '>'
  412. if number_of_registers == 1:
  413. formatcode_i += "L"
  414. if signed:
  415. formatcode_o += "l"
  416. else:
  417. formatcode_o += "L"
  418. if number_of_registers == 2:
  419. formatcode_i += 'LL'
  420. if signed:
  421. formatcode_o += "q"
  422. else:
  423. formatcode_o += "Q"
  424. bytestring = struct.pack(formatcode_i, *values)
  425. value = struct.unpack(formatcode_o, bytestring)[0]
  426. return float(value) / 10 ** number_of_decimals
  427. REGS = [{'name': 'firmware_version', 'start': 0, 'length': 1, 'signed': True, 'decimals': 0},
  428. {'name': 'num_registers', 'start': 1, 'length': 1, 'signed': False, 'decimals': 0},
  429. {'name': 'num_flags', 'start': 2, 'length': 1, 'signed': False, 'decimals': 0},
  430. {'name': 'baudrate', 'start': 3, 'length': 1, 'signed': False, 'decimals': 0},
  431. {'name': 'serial_number', 'start': 11, 'length': 2, 'signed': False, 'decimals': 0},
  432. {'name': 'status_protect', 'start': 14, 'length': 1, 'signed': False, 'decimals': 0},
  433. {'name': 'sbus_timeout', 'start': 15, 'length': 1, 'signed': False, 'decimals': 0},
  434. {'name': 'sbus_address', 'start': 16, 'length': 1, 'signed': False, 'decimals': 0},
  435. {'name': 'error_flags', 'start': 17, 'length': 1, 'signed': False, 'decimals': 0},
  436. {'name': 'tariff', 'start': 19, 'length': 1, 'signed': False, 'decimals': 0},
  437. {'name': 'active_import_tariff_1', 'start': 20, 'length': 1, 'signed': False, 'decimals': 2},
  438. {'name': 'resettable_active_import_t1','start': 21, 'length': 1, 'signed': False, 'decimals': 2},
  439. {'name': 'active_import_tariff_2', 'start': 22, 'length': 1, 'signed': False, 'decimals': 2},
  440. {'name': 'resettable_active_import_t2','start': 23, 'length': 1, 'signed': False, 'decimals': 2},
  441. {'name': 'voltage_l1_n', 'start': 24, 'length': 1, 'signed': False, 'decimals': 0},
  442. {'name': 'current_l1', 'start': 25, 'length': 1, 'signed': False, 'decimals': 1},
  443. {'name': 'active_power_l1', 'start': 26, 'length': 1, 'signed': True, 'decimals': 2},
  444. {'name': 'reactive_power_l1', 'start': 27, 'length': 1, 'signed': True, 'decimals': 2},
  445. {'name': 'power_factor_l1', 'start': 28, 'length': 1, 'signed': True, 'decimals': 2},
  446. {'name': 'voltage_l2_n', 'start': 29, 'length': 1, 'signed': False, 'decimals': 0},
  447. {'name': 'current_l2', 'start': 30, 'length': 1, 'signed': False, 'decimals': 1},
  448. {'name': 'active_power_l2', 'start': 31, 'length': 1, 'signed': True, 'decimals': 2},
  449. {'name': 'reactive_power_l2', 'start': 32, 'length': 1, 'signed': True, 'decimals': 2},
  450. {'name': 'power_factor_l2', 'start': 33, 'length': 1, 'signed': True, 'decimals': 2},
  451. {'name': 'voltage_l3_n', 'start': 34, 'length': 1, 'signed': False, 'decimals': 0},
  452. {'name': 'current_l3', 'start': 35, 'length': 1, 'signed': False, 'decimals': 1},
  453. {'name': 'active_power_l3', 'start': 36, 'length': 1, 'signed': True, 'decimals': 2},
  454. {'name': 'reactive_power_l3', 'start': 37, 'length': 1, 'signed': True, 'decimals': 2},
  455. {'name': 'power_factor_l3', 'start': 38, 'length': 1, 'signed': True, 'decimals': 2},
  456. {'name': 'active_power_total', 'start': 39, 'length': 1, 'signed': True, 'decimals': 2},
  457. {'name': 'reactive_power_total', 'start': 40, 'length': 1, 'signed': True, 'decimals': 2}]