"""Asyncio protocol implementation for pyotgw""" import asyncio import logging import re from pyotgw import vars as v from pyotgw.commandprocessor import CommandProcessor from pyotgw.messageprocessor import MessageProcessor _LOGGER = logging.getLogger(__name__) class OpenThermProtocol( asyncio.Protocol ): # pylint: disable=too-many-instance-attributes """ Implementation of the Opentherm Gateway protocol to be used with asyncio connections. """ def __init__( self, status_manager, activity_callback, ): """Initialise the protocol object.""" self.transport = None self._readbuf = b"" self._received_lines = 0 self.activity_callback = activity_callback self.command_processor = CommandProcessor( self, status_manager, ) self._connected = False self.message_processor = MessageProcessor( self.command_processor, status_manager, ) self.status_manager = status_manager def connection_made(self, transport): """Gets called when a connection to the gateway is established.""" self.transport = transport self._received_lines = 0 self._connected = True def connection_lost(self, exc): """ Gets called when the connection to the gateway is lost. Tear down and clean up the protocol object. """ if self.active and self.connected: _LOGGER.error("Disconnected: %s", exc) self._received_lines = 0 self._connected = False self.command_processor.clear_queue() self.message_processor.connection_lost() self.status_manager.reset() @property def connected(self): """Return the connection status""" return self._connected async def cleanup(self): """Clean up""" self.disconnect() await self.message_processor.cleanup() def disconnect(self): """Disconnect gracefully.""" if self.transport.is_closing() or not self.connected: return self._connected = False self.transport.close() def data_received(self, data): """ Gets called when new data is received on the serial interface. Perform line buffering and call line_received() with complete lines. """ # DIY line buffering... newline = b"\r\n" eot = b"\x04" self._readbuf += data while newline in self._readbuf: line, _, self._readbuf = self._readbuf.partition(newline) if line: if eot in line: # Discard everything before EOT _, _, line = line.partition(eot) try: decoded = line.decode("ascii") except UnicodeDecodeError: _LOGGER.debug("Invalid data received, ignoring...") return self.line_received(decoded) def line_received(self, line): """ Gets called by data_received() when a complete line is received. Inspect the received line and process or queue accordingly. """ self._received_lines += 1 _LOGGER.debug("Received line %d: %s", self._received_lines, line) if self.activity_callback: asyncio.create_task(self.activity_callback()) pattern = r"^(T|B|R|A|E)([0-9A-F]{8})$" msg = re.match(pattern, line) if msg: self.message_processor.submit_matched_message(msg) elif re.match(r"^[0-9A-F]{1,8}$", line) and self._received_lines == 1: # Partial message on fresh connection. Ignore. self._received_lines = 0 _LOGGER.debug("Ignoring line: %s", line) else: _LOGGER.debug( "Submitting line %d to CommandProcessor", self._received_lines, ) self.command_processor.submit_response(line) @property def active(self): """Indicate that we have seen activity on the serial line.""" return self._received_lines > 0 async def init_and_wait_for_activity(self): """Wait for activity on the serial connection.""" await self.command_processor.issue_cmd(v.OTGW_CMD_SUMMARY, 0, retry=1) while not self.active: await asyncio.sleep(0)