wip
This commit is contained in:
parent
65b992de8a
commit
329848c468
235
tic2mqtt
235
tic2mqtt
@ -4,8 +4,14 @@ import sys
|
||||
import argparse
|
||||
import time
|
||||
import serial
|
||||
import logging
|
||||
import datetime
|
||||
import re
|
||||
from pprint import pprint
|
||||
|
||||
TIC_MODE_STD = 1
|
||||
TIC_MODE_HIS = 2
|
||||
|
||||
TIC_STX = b'\x02'
|
||||
TIC_ETX = b'\x03'
|
||||
TIC_SGR = b'\x0a'
|
||||
@ -13,27 +19,117 @@ TIC_SEP_STD = b'\x09'
|
||||
TIC_SEP_HIS = b'\x20'
|
||||
TIC_EGR = b'\x0d'
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
tic_msg_std = {
|
||||
'ADSC': { 'len': 12, 'value': 'adsc' },
|
||||
'VTIC': { 'len': 2, 'value': 'num' },
|
||||
'DATE': { 'len': 0 },
|
||||
'NGTF': { 'len': 16 },
|
||||
'LTARF': { 'len': 16 },
|
||||
'EAST': { 'len': 9, 'value': 'num' },
|
||||
'EASF01': { 'len': 9, 'value': 'num' },
|
||||
'EASF02': { 'len': 9, 'value': 'num' },
|
||||
'EASF03': { 'len': 9, 'value': 'num' },
|
||||
'EASF04': { 'len': 9, 'value': 'num' },
|
||||
'EASF05': { 'len': 9, 'value': 'num' },
|
||||
'EASF06': { 'len': 9, 'value': 'num' },
|
||||
'EASF07': { 'len': 9, 'value': 'num' },
|
||||
'EASF08': { 'len': 9, 'value': 'num' },
|
||||
'EASF09': { 'len': 9, 'value': 'num' },
|
||||
'EASF10': { 'len': 9, 'value': 'num' },
|
||||
'EASD01': { 'len': 9, 'value': 'num' },
|
||||
'EASD02': { 'len': 9, 'value': 'num' },
|
||||
'EASD03': { 'len': 9, 'value': 'num' },
|
||||
'EASD04': { 'len': 9, 'value': 'num' },
|
||||
'EAIT': { 'len': 9, 'value': 'num' },
|
||||
'ERQ1': { 'len': 9, 'value': 'num' },
|
||||
'ERQ2': { 'len': 9, 'value': 'num' },
|
||||
'ERQ3': { 'len': 9, 'value': 'num' },
|
||||
'ERQ4': { 'len': 9, 'value': 'num' },
|
||||
'IRMS1': { 'len': 3, 'value': 'num' },
|
||||
'IRMS2': { 'len': 3, 'value': 'num' },
|
||||
'IRMS3': { 'len': 3, 'value': 'num' },
|
||||
'URMS1': { 'len': 3, 'value': 'num' },
|
||||
'URMS2': { 'len': 3, 'value': 'num' },
|
||||
'URMS3': { 'len': 3, 'value': 'num' },
|
||||
'PREF': { 'len': 2, 'value': 'num' },
|
||||
'PCOUP': { 'len': 2, 'value': 'num' },
|
||||
'SINSTS': { 'len': 5, 'value': 'num' },
|
||||
'SINST1': { 'len': 5, 'value': 'num' },
|
||||
'SINST2': { 'len': 5, 'value': 'num' },
|
||||
'SINST3': { 'len': 5, 'value': 'num' },
|
||||
'SMAXSN': { 'len': 5, 'value': 'num' },
|
||||
'SMAXSN1': { 'len': 5, 'value': 'num' },
|
||||
'SMAXSN2': { 'len': 5, 'value': 'num' },
|
||||
'SMAXSN3': { 'len': 5, 'value': 'num' },
|
||||
'SMAXSN-1': { 'len': 5, 'value': 'num' },
|
||||
'SMAXSN1-1': { 'len': 5, 'value': 'num' },
|
||||
'SMAXSN2-1': { 'len': 5, 'value': 'num' },
|
||||
'SMAXSN3-1': { 'len': 5, 'value': 'num' },
|
||||
'SINSTI': { 'len': 5, 'value': 'num' },
|
||||
'SMAXIN': { 'len': 5, 'value': 'num' },
|
||||
'SMAXIN-1': { 'len': 5, 'value': 'num' },
|
||||
'CCASN': { 'len': 5, 'value': 'num' },
|
||||
'CCASN-1': { 'len': 5, 'value': 'num' },
|
||||
'CCAIN': { 'len': 5, 'value': 'num' },
|
||||
'CCAIN-1': { 'len': 5, 'value': 'num' },
|
||||
'UMOY1': { 'len': 3, 'value': 'num' },
|
||||
'UMOY2': { 'len': 3, 'value': 'num' },
|
||||
'UMOY3': { 'len': 3, 'value': 'num' },
|
||||
'STGE': { 'len': 8 },
|
||||
'DPM1': { 'len': 2 },
|
||||
'FPM1': { 'len': 2 },
|
||||
'DPM2': { 'len': 2 },
|
||||
'FPM2': { 'len': 2 },
|
||||
'DPM3': { 'len': 2 },
|
||||
'FPM3': { 'len': 2 },
|
||||
'MSG1': { 'len': 32 },
|
||||
'MSG2': { 'len': 16 },
|
||||
'PRM': { 'len': 14 },
|
||||
'RELAIS': { 'len': 3 },
|
||||
'NTARF': { 'len': 2 },
|
||||
'NJOURF': { 'len': 2 },
|
||||
'NJOURF+1': { 'len': 2 },
|
||||
'PJOURF+1': { 'len': 98 },
|
||||
'PPOINTE': { 'len': 98 },
|
||||
}
|
||||
|
||||
class TicDevice:
|
||||
def __init__(self, name):
|
||||
self._is_serial = False
|
||||
self.device = None
|
||||
try:
|
||||
self.device = serial.Serial(name,
|
||||
baudrate=9600,
|
||||
bytesize=serial.SEVENBITS,
|
||||
parity=serial.PARITY_EVEN,
|
||||
stopbits=serial.STOPBITS_ONE,
|
||||
timeout=2.0)
|
||||
self.device.baudrate = 9600
|
||||
self._is_serial = True
|
||||
except serial.serialutil.SerialException:
|
||||
except:
|
||||
if self.device:
|
||||
self.device.close()
|
||||
self.device = None
|
||||
if not self.device:
|
||||
self.device = open(name, 'rb')
|
||||
self._is_serial = False
|
||||
|
||||
def set_baudrate(self, rate):
|
||||
if self._is_serial:
|
||||
self.device.baudrate = rate
|
||||
|
||||
def get_char(self):
|
||||
return self.device.read(1)
|
||||
c = self.device.read(1)
|
||||
if not c:
|
||||
if self._is_serial:
|
||||
log.debug("RX Timeout")
|
||||
else:
|
||||
log.debug("EOF.")
|
||||
return c
|
||||
|
||||
def is_serial(self):
|
||||
return self._is_serial
|
||||
|
||||
class Tic:
|
||||
def __init__(self, input):
|
||||
@ -42,14 +138,14 @@ class Tic:
|
||||
self.mode = None
|
||||
|
||||
def detect_mode(self):
|
||||
print("tring speed 9600bps")
|
||||
self.device.set_baudrate(9600)
|
||||
if self._detect_mode():
|
||||
return
|
||||
print("tring speed 1200bps")
|
||||
self.device.set_baudrate(1200)
|
||||
if self._detect_mode():
|
||||
return
|
||||
if self.device.is_serial():
|
||||
log.debug("tring speed 1200bps")
|
||||
self.device.set_baudrate(1200)
|
||||
if self._detect_mode():
|
||||
return
|
||||
log.error("no TIC protocol detected")
|
||||
return None
|
||||
|
||||
def _detect_mode(self):
|
||||
@ -66,20 +162,124 @@ class Tic:
|
||||
if ok_mask or ok == 0x01:
|
||||
ok_mask = ok_mask | ok
|
||||
if ok_mask == 0x1f:
|
||||
print(f"Standard mode detected")
|
||||
return 'std'
|
||||
log.info(f"Standard mode detected")
|
||||
self.mode = TIC_MODE_STD
|
||||
return self.mode
|
||||
if ok_mask == 0x0f:
|
||||
print(f"Historiqual mode detected")
|
||||
return 'hist'
|
||||
log.info(f"Historiqual mode detected")
|
||||
self.mode = TIC_MODE_HIS
|
||||
return self.mode
|
||||
if not ok and (c < b' ' or c > b'\x7f'):
|
||||
#print(f"bad char: {c}")
|
||||
log.debug(f"bad char: {c}")
|
||||
ok_mask = 0x00
|
||||
nb_reset = nb_reset + 1
|
||||
if nb_reset >= 20:
|
||||
break
|
||||
print("Timeout/EOF")
|
||||
log.debug("Timeout/EOF")
|
||||
return None
|
||||
|
||||
def get_frame(self):
|
||||
frame = b''
|
||||
while True:
|
||||
c = self.device.get_char()
|
||||
if not c:
|
||||
return None
|
||||
if c == TIC_STX:
|
||||
break
|
||||
while True:
|
||||
c = self.device.get_char()
|
||||
if not c:
|
||||
return None
|
||||
if c == TIC_ETX:
|
||||
break
|
||||
frame = frame + c
|
||||
return frame
|
||||
|
||||
def split_frame(self, frame):
|
||||
groups = []
|
||||
start = 0
|
||||
while start <= len(frame):
|
||||
start = frame.find(TIC_SGR, start)
|
||||
if start < 0:
|
||||
return groups
|
||||
end = frame.find(TIC_EGR, start)
|
||||
if end < 0:
|
||||
return groups
|
||||
groups.append(frame[start+1:end])
|
||||
start = end + 1
|
||||
return groups
|
||||
|
||||
def check_sum(self, group):
|
||||
checksum = group[-1]
|
||||
end = -2 if self.mode == TIC_MODE_HIS else -1
|
||||
sum = 0
|
||||
for c in group[:end]:
|
||||
sum = sum + c
|
||||
sum = (sum & 0x3f) + 0x20
|
||||
return sum == checksum
|
||||
|
||||
def decode_group(self, group):
|
||||
sep = (TIC_SEP_HIS if self.mode == TIC_MODE_HIS else TIC_SEP_STD).decode('ascii')
|
||||
group = group[:-2].decode('ascii')
|
||||
fields = group.split(sep)
|
||||
msg = {}
|
||||
if len(fields) < 2 or len(fields) > 4:
|
||||
log.warning(f'corrupted group: {group}')
|
||||
return None
|
||||
label = fields[0]
|
||||
value = fields[-1]
|
||||
date = None
|
||||
if len(fields) == 3:
|
||||
date = self.parse_date(fields[1])
|
||||
|
||||
if not label in tic_msg_std:
|
||||
log.warning(f'unknown label: {group}')
|
||||
return None
|
||||
if len(value) != tic_msg_std[label]['len']:
|
||||
log.warning(f'bad value length: {group}')
|
||||
return None
|
||||
value_type = tic_msg_std[label].get('value', None)
|
||||
if value_type == 'num':
|
||||
try:
|
||||
value = int(value)
|
||||
except:
|
||||
log.warning(f'bad numerical value: {group}')
|
||||
return None
|
||||
else:
|
||||
value = re.sub('\s\s+', ' ', value).strip()
|
||||
|
||||
msg['label'] = label
|
||||
msg['value'] = value
|
||||
msg['date'] = date
|
||||
return msg
|
||||
|
||||
def parse_date(self, str):
|
||||
try:
|
||||
dy = int(str[1:3]) + 2000
|
||||
dm = int(str[3:5])
|
||||
dd = int(str[5:7])
|
||||
th = int(str[7:9])
|
||||
tm = int(str[9:11])
|
||||
ts = int(str[11:13])
|
||||
date = datetime.datetime(year=dy, month=dm, day=dd, hour=th, minute=tm, second=ts)
|
||||
return date
|
||||
except Exception as e:
|
||||
print(e)
|
||||
return None
|
||||
|
||||
def run(self):
|
||||
while True:
|
||||
frame = self.get_frame()
|
||||
if not frame:
|
||||
return
|
||||
groups = self.split_frame(frame)
|
||||
for group in groups:
|
||||
if not self.check_sum(group):
|
||||
continue
|
||||
msg = self.decode_group(group)
|
||||
print(msg)
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(prog="tic2mqtt",
|
||||
description="MQTT client for Linky Electricity Meters installed by Enedis in France.")
|
||||
@ -89,10 +289,15 @@ def main():
|
||||
help="input file or device to read TIC messages (stdin by default)")
|
||||
parser.add_argument('-m', '--mode', choices=['auto', 'historique', 'standard'], default='auto',
|
||||
help='select TIC mode')
|
||||
parser.add_argument('-l', '--log', choices=['debug', 'info', 'warning', 'error'], default='error',
|
||||
help='set the log level')
|
||||
args = parser.parse_args()
|
||||
|
||||
logging.basicConfig(filename='/dev/stderr', level=getattr(logging, args.log.upper()))
|
||||
|
||||
tic = Tic(args.input)
|
||||
tic.detect_mode()
|
||||
tic.run()
|
||||
|
||||
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user