class SerialReader(object): PORT_KEY = 'port' def __init__(self, device, serial_settings, telegram_specification): self.serial_settings = serial_settings self.serial_settings[self.PORT_KEY] = device self.telegram_parser = TelegramParser(telegram_specification) self.telegram_buffer = TelegramBuffer() def read(self): """ Read complete DSMR telegram's from the serial interface and parse it into CosemObject's and MbusObject's :rtype: generator """ with serial.Serial(**self.serial_settings) as serial_handle: while True: data = serial_handle.readline() self.telegram_buffer.append(data.decode('ascii')) for telegram in self.telegram_buffer.get_all(): try: yield self.telegram_parser.parse(telegram) except ParseError as e: logger.error('Failed to parse telegram: %s', e)
class DSMRProtocol(asyncio.Protocol): """Assemble and handle incoming data into complete DSM telegrams.""" transport = None telegram_callback = None def __init__(self, loop, telegram_parser, telegram_callback=None): """Initialize class.""" self.loop = loop self.log = logging.getLogger(__name__) self.telegram_parser = telegram_parser # callback to call on complete telegram self.telegram_callback = telegram_callback # buffer to keep incomplete incoming data self.telegram_buffer = TelegramBuffer() # keep a lock until the connection is closed self._closed = asyncio.Event() def connection_made(self, transport): """Just logging for now.""" self.transport = transport self.log.debug('connected') def data_received(self, data): """Add incoming data to buffer.""" data = data.decode('ascii') self.log.debug('received data: %s', data) self.telegram_buffer.append(data) for telegram in self.telegram_buffer.get_all(): self.handle_telegram(telegram) def connection_lost(self, exc): """Stop when connection is lost.""" if exc: self.log.exception('disconnected due to exception', exc_info=exc) else: self.log.info('disconnected because of close/abort.') self._closed.set() def handle_telegram(self, telegram): """Send off parsed telegram to handling callback.""" self.log.debug('got telegram: %s', telegram) try: parsed_telegram = self.telegram_parser.parse(telegram) except InvalidChecksumError as e: self.log.warning(str(e)) except ParseError: self.log.exception("failed to parse telegram") else: self.telegram_callback(parsed_telegram) @asyncio.coroutine def wait_closed(self): """Wait until connection is closed.""" yield from self._closed.wait()
class FileInputReader(object): """ Filereader to read and parse raw telegram strings from stdin or files specified at the commandline and instantiate Telegram objects for each read telegram. Usage python script "syphon_smartmeter_readings_stdin.py": from dsmr_parser import telegram_specifications from dsmr_parser.clients.filereader import FileInputReader if __name__== "__main__": fileinput_reader = FileReader( file = infile, telegram_specification = telegram_specifications.V4 ) for telegram in fileinput_reader.read_as_object(): print(telegram) Command line: tail -f /data/smartmeter/readings.txt | python3 syphon_smartmeter_readings_stdin.py """ def __init__(self, telegram_specification): self.telegram_parser = TelegramParser(telegram_specification) self.telegram_buffer = TelegramBuffer() self.telegram_specification = telegram_specification def read_as_object(self): """ Read complete DSMR telegram's from stdin of filearguments specified on teh command line and return a Telegram object. :rtype: generator """ with fileinput.input(mode='rb') as file_handle: while True: data = file_handle.readline() str = data.decode() self.telegram_buffer.append(str) for telegram in self.telegram_buffer.get_all(): try: yield Telegram(telegram, self.telegram_parser, self.telegram_specification) except InvalidChecksumError as e: logger.warning(str(e)) except ParseError as e: logger.error('Failed to parse telegram: %s', e)
class FileTailReader(object): """ Filereader to read and parse raw telegram strings from the tail of a given file and instantiate Telegram objects for each read telegram. Usage python script "syphon_smartmeter_readings_stdin.py": from dsmr_parser import telegram_specifications from dsmr_parser.clients.filereader import FileTailReader if __name__== "__main__": infile = '/data/smartmeter/readings.txt' filetail_reader = FileTailReader( file = infile, telegram_specification = telegram_specifications.V5 ) for telegram in filetail_reader.read_as_object(): print(telegram) """ def __init__(self, file, telegram_specification): self._file = file self.telegram_parser = TelegramParser(telegram_specification) self.telegram_buffer = TelegramBuffer() self.telegram_specification = telegram_specification def read_as_object(self): """ Read complete DSMR telegram's from a files tail and return a Telegram object. :rtype: generator """ with open(self._file, "rb") as file_handle: for data in tailer.follow(file_handle): str = data.decode() self.telegram_buffer.append(str) for telegram in self.telegram_buffer.get_all(): try: yield Telegram(telegram, self.telegram_parser, self.telegram_specification) except InvalidChecksumError as e: logger.warning(str(e)) except ParseError as e: logger.error('Failed to parse telegram: %s', e)
class FileReader(object): """ Filereader to read and parse raw telegram strings from a file and instantiate Telegram objects for each read telegram. Usage: from dsmr_parser import telegram_specifications from dsmr_parser.clients.filereader import FileReader if __name__== "__main__": infile = '/data/smartmeter/readings.txt' file_reader = FileReader( file = infile, telegram_specification = telegram_specifications.V4 ) for telegram in file_reader.read_as_object(): print(telegram) The file can be created like: from dsmr_parser import telegram_specifications from dsmr_parser.clients import SerialReader, SERIAL_SETTINGS_V5 if __name__== "__main__": outfile = '/data/smartmeter/readings.txt' serial_reader = SerialReader( device='/dev/ttyUSB0', serial_settings=SERIAL_SETTINGS_V5, telegram_specification=telegram_specifications.V4 ) for telegram in serial_reader.read_as_object(): f=open(outfile,"ab+") f.write(telegram._telegram_data.encode()) f.close() """ def __init__(self, file, telegram_specification): self._file = file self.telegram_parser = TelegramParser(telegram_specification) self.telegram_buffer = TelegramBuffer() self.telegram_specification = telegram_specification def read_as_object(self): """ Read complete DSMR telegram's from a file and return a Telegram object. :rtype: generator """ with open(self._file, "rb") as file_handle: while True: data = file_handle.readline() str = data.decode() self.telegram_buffer.append(str) for telegram in self.telegram_buffer.get_all(): try: yield Telegram(telegram, self.telegram_parser, self.telegram_specification) except InvalidChecksumError as e: logger.warning(str(e)) except ParseError as e: logger.error('Failed to parse telegram: %s', e)
class DSMRProtocol(asyncio.Protocol): """Assemble and handle incoming data into complete DSM telegrams.""" transport = None telegram_callback = None def __init__(self, loop, telegram_parser, telegram_callback=None, keep_alive_interval=None): """Initialize class.""" self.loop = loop self.log = logging.getLogger(__name__) self.telegram_parser = telegram_parser # callback to call on complete telegram self.telegram_callback = telegram_callback # buffer to keep incomplete incoming data self.telegram_buffer = TelegramBuffer() # keep a lock until the connection is closed self._closed = asyncio.Event() self._keep_alive_interval = keep_alive_interval self._active = True def connection_made(self, transport): """Just logging for now.""" self.transport = transport self.log.debug('connected') self._active = False if self.loop and self._keep_alive_interval: self.loop.call_later(self._keep_alive_interval, self.keep_alive) def data_received(self, data): """Add incoming data to buffer.""" # accept latin-1 (8-bit) on the line, to allow for non-ascii transport or padding data = data.decode("latin1") self._active = True self.log.debug('received data: %s', data) self.telegram_buffer.append(data) for telegram in self.telegram_buffer.get_all(): # ensure actual telegram is ascii (7-bit) only (ISO 646:1991 IRV required in section 5.5 of IEC 62056-21) telegram = telegram.encode("latin1").decode("ascii") self.handle_telegram(telegram) def keep_alive(self): if self._active: self.log.debug('keep-alive checked') self._active = False if self.loop: self.loop.call_later(self._keep_alive_interval, self.keep_alive) else: self.log.warning('keep-alive check failed') if self.transport: self.transport.close() def connection_lost(self, exc): """Stop when connection is lost.""" if exc: self.log.exception('disconnected due to exception', exc_info=exc) else: self.log.info('disconnected because of close/abort.') self._closed.set() def handle_telegram(self, telegram): """Send off parsed telegram to handling callback.""" self.log.debug('got telegram: %s', telegram) try: parsed_telegram = self.telegram_parser.parse(telegram) except InvalidChecksumError as e: self.log.warning(str(e)) except ParseError: self.log.exception("failed to parse telegram") else: self.telegram_callback(parsed_telegram) async def wait_closed(self): """Wait until connection is closed.""" await self._closed.wait()
class TelegramBufferTest(unittest.TestCase): def setUp(self): self.telegram_buffer = TelegramBuffer() def test_v22_telegram(self): self.telegram_buffer.append(TELEGRAM_V2_2) telegram = next(self.telegram_buffer.get_all()) self.assertEqual(telegram, TELEGRAM_V2_2) self.assertEqual(self.telegram_buffer._buffer, '') def test_v42_telegram(self): self.telegram_buffer.append(TELEGRAM_V4_2) telegram = next(self.telegram_buffer.get_all()) self.assertEqual(telegram, TELEGRAM_V4_2) self.assertEqual(self.telegram_buffer._buffer, '') def test_multiple_mixed_telegrams(self): self.telegram_buffer.append(''.join( (TELEGRAM_V2_2, TELEGRAM_V4_2, TELEGRAM_V2_2))) telegrams = list(self.telegram_buffer.get_all()) self.assertListEqual(telegrams, [TELEGRAM_V2_2, TELEGRAM_V4_2, TELEGRAM_V2_2]) self.assertEqual(self.telegram_buffer._buffer, '') def test_v42_telegram_preceded_with_unclosed_telegram(self): # There are unclosed telegrams at the start of the buffer. incomplete_telegram = TELEGRAM_V4_2[:-1] self.telegram_buffer.append(incomplete_telegram + TELEGRAM_V4_2) telegram = next(self.telegram_buffer.get_all()) self.assertEqual(telegram, TELEGRAM_V4_2) self.assertEqual(self.telegram_buffer._buffer, '') def test_v42_telegram_preceded_with_unopened_telegram(self): # There is unopened telegrams at the start of the buffer indicating that # the buffer was being filled while the telegram was outputted halfway. incomplete_telegram = TELEGRAM_V4_2[1:] self.telegram_buffer.append(incomplete_telegram + TELEGRAM_V4_2) telegram = next(self.telegram_buffer.get_all()) self.assertEqual(telegram, TELEGRAM_V4_2) self.assertEqual(self.telegram_buffer._buffer, '') def test_v42_telegram_trailed_by_unclosed_telegram(self): incomplete_telegram = TELEGRAM_V4_2[:-1] self.telegram_buffer.append(TELEGRAM_V4_2 + incomplete_telegram) telegram = next(self.telegram_buffer.get_all()) self.assertEqual(telegram, TELEGRAM_V4_2) self.assertEqual(self.telegram_buffer._buffer, incomplete_telegram) def test_v42_telegram_trailed_by_unopened_telegram(self): incomplete_telegram = TELEGRAM_V4_2[1:] self.telegram_buffer.append(TELEGRAM_V4_2 + incomplete_telegram) telegram = next(self.telegram_buffer.get_all()) self.assertEqual(telegram, TELEGRAM_V4_2) self.assertEqual(self.telegram_buffer._buffer, incomplete_telegram) def test_v42_telegram_adding_line_by_line(self): for line in TELEGRAM_V4_2.splitlines(keepends=True): self.telegram_buffer.append(line) telegram = next(self.telegram_buffer.get_all()) self.assertEqual(telegram, TELEGRAM_V4_2) self.assertEqual(self.telegram_buffer._buffer, '') def test_v42_telegram_adding_char_by_char(self): for char in TELEGRAM_V4_2: self.telegram_buffer.append(char) telegram = next(self.telegram_buffer.get_all()) self.assertEqual(telegram, TELEGRAM_V4_2) self.assertEqual(self.telegram_buffer._buffer, '')
class P1test(hass.Hass): def _logme(self, line): print(line) def initialize(self, *args, **kwargs): try: self.log("Using hass logger!") except Exception: self.log = self._logme self.log("Using test logger!") self.mode = 'tcp' self.host = 'homeassistant.fritz.box' self.port = 3333 self.device = 'COM3' self.dsmr_version = '5' self.terminal_name = 'test' self.stop = False self.transport = None self.log("Starting thread...") self.log("P1 test started") parser = self.test_serial # parser = self.tcp dsmr_version = self.dsmr_version if dsmr_version == '2.2': specification = telegram_specifications.V2_2 serial_settings = SERIAL_SETTINGS_V2_2 elif dsmr_version == '4': specification = telegram_specifications.V4 serial_settings = SERIAL_SETTINGS_V4 elif dsmr_version == '5': specification = telegram_specifications.V5 serial_settings = SERIAL_SETTINGS_V5 elif dsmr_version == '5B': specification = telegram_specifications.BELGIUM_FLUVIUS serial_settings = SERIAL_SETTINGS_V5 else: raise NotImplementedError( "No telegram parser found for version: %s", dsmr_version) self.telegram_parser = TelegramParser(specification) self.serial_settings = serial_settings # buffer to keep incomplete incoming data self.telegram_buffer = TelegramBuffer() self.thr = threading.Thread(target=parser, daemon=True) self.thr.start() self.log("Started!") # logging.basicConfig(level=logging.DEBUG) def dsmr_serial_callback(self, telegram): self.log('Telegram received') def terminate(self): self.stop = True self.log("Closing transport...") if self.transport: self.transport.close() self.thr.join(10) # Stopping loop the hard if self.thr.is_alive(): self.log( "Stopping the loop unfortunally did not stop the thread, waiting for completion..." ) else: self.log("Thread exited nicely") self.thr.join() self.log("Thread has stopped!") def test_serial(self): s = serial.Serial(port=self.device, **self.serial_settings) while not self.stop: data = s.read_until() print(f'{data}') self.data_received(data) s.close() def test_tcp(self): server_address = (self.host, self.port) sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) sock.connect(server_address) while not self.stop: data = sock.recv(1024) self.data_received(data) sock.close() def data_received(self, data): """Add incoming data to buffer.""" data = data.decode('ascii') self.telegram_buffer.append(data) for telegram in self.telegram_buffer.get_all(): self.handle_telegram(telegram) def handle_telegram(self, telegram): """Send off parsed telegram to handling callback.""" try: parsed_telegram = self.telegram_parser.parse(telegram) except InvalidChecksumError as e: self.log.warning(str(e)) except ParseError: self.log.exception("failed to parse telegram") else: self.dsmr_serial_callback(parsed_telegram)
class SocketReader(object): BUFFER_SIZE = 256 def __init__(self, host, port, telegram_specification): self.host = host self.port = port self.telegram_parser = TelegramParser(telegram_specification) self.telegram_buffer = TelegramBuffer() self.telegram_specification = telegram_specification def read(self): """ Read complete DSMR telegram's from remote interface and parse it into CosemObject's and MbusObject's :rtype: generator """ buffer = b"" with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as socket_handle: socket_handle.connect((self.host, self.port)) while True: buffer += socket_handle.recv(self.BUFFER_SIZE) lines = buffer.splitlines(keepends=True) if len(lines) == 0: continue for data in lines: self.telegram_buffer.append(data.decode('ascii')) for telegram in self.telegram_buffer.get_all(): try: yield self.telegram_parser.parse(telegram) except InvalidChecksumError as e: logger.warning(str(e)) except ParseError as e: logger.error('Failed to parse telegram: %s', e) buffer = b"" def read_as_object(self): """ Read complete DSMR telegram's from remote and return a Telegram object. :rtype: generator """ buffer = b"" with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as socket_handle: socket_handle.connect((self.host, self.port)) while True: buffer += socket_handle.recv(self.BUFFER_SIZE) lines = buffer.splitlines(keepends=True) if len(lines) == 0: continue for data in lines: self.telegram_buffer.append(data.decode('ascii')) for telegram in self.telegram_buffer.get_all(): try: yield Telegram(telegram, self.telegram_parser, self.telegram_specification) except InvalidChecksumError as e: logger.warning(str(e)) except ParseError as e: logger.error('Failed to parse telegram: %s', e) buffer = b""