From 329848c46817b0b5fef925e29b1e2187cb1793ab Mon Sep 17 00:00:00 2001 From: Gilles Grandou Date: Sun, 19 May 2024 21:37:31 +0200 Subject: [PATCH] wip --- tic2mqtt | 235 +++++++++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 220 insertions(+), 15 deletions(-) diff --git a/tic2mqtt b/tic2mqtt index ec3851d..5af1fc9 100755 --- a/tic2mqtt +++ b/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()