"""
Implementation of a Threaded Modbus Server
------------------------------------------

"""
from binascii import b2a_hex
import serial
from serial_asyncio import create_serial_connection
import ssl
import traceback

import asyncio
from pymodbus.compat import PYTHON_VERSION
from pymodbus.constants import Defaults
from pymodbus.utilities import hexlify_packets
from pymodbus.factory import ServerDecoder
from pymodbus.datastore import ModbusServerContext
from pymodbus.device import ModbusControlBlock
from pymodbus.device import ModbusDeviceIdentification
from pymodbus.transaction import *
from pymodbus.exceptions import NotImplementedException, NoSuchSlaveException
from pymodbus.pdu import ModbusExceptions as merror
from pymodbus.compat import socketserver, byte2int

# --------------------------------------------------------------------------- #
# Logging
# --------------------------------------------------------------------------- #
import logging
_logger = logging.getLogger(__name__)


# --------------------------------------------------------------------------- #
# Protocol Handlers
# --------------------------------------------------------------------------- #

class ModbusBaseRequestHandler(asyncio.BaseProtocol):
    """ Implements modbus slave wire protocol
    This uses the asyncio.Protocol to implement the client handler.

    When a connection is established, the asyncio.Protocol.connection_made
    callback is called. This callback will setup the connection and
    create and schedule an asyncio.Task and assign it to running_task.

    running_task will be canceled upon connection_lost event.
    """
    def __init__(self, owner):
        self.server = owner
        self.running = False
        self.receive_queue = asyncio.Queue()
        self.handler_task = None  # coroutine to be run on asyncio loop

    def _log_exception(self):
        if isinstance(self, ModbusConnectedRequestHandler):
            _logger.error(
                "Handler for stream [%s:%s] has "
                "been canceled" % self.client_address[:2])
        elif isinstance(self, ModbusSingleRequestHandler):
            _logger.error(
                "Handler for serial port has been cancelled")
        else:
            sock_name = self.protocol._sock.getsockname()
            _logger.error("Handler for UDP socket [%s] has "
                          "been canceled" % sock_name[1])

    def connection_made(self, transport):
        """
        asyncio.BaseProtocol callback for socket establish

        For streamed protocols (TCP) this will also correspond to an
        entire conversation; however for datagram protocols (UDP) this
        corresponds to the socket being opened
        """
        try:
            sockname = transport.get_extra_info('sockname')
            if sockname is not None:
                _logger.debug(
                    "Socket [%s:%s] opened" % transport.get_extra_info(
                        'sockname')[:2])
            else:
                if hasattr(transport, 'serial'):
                    _logger.debug(
                        "Serial connection opened on port: {}".format(
                            transport.serial.port)
                    )
            self.transport = transport
            self.running = True
            self.framer = self.server.framer(self.server.decoder, client=None)

            # schedule the connection handler on the event loop
            if PYTHON_VERSION >= (3, 7):
                self.handler_task = asyncio.create_task(self.handle())
            else:
                self.handler_task = asyncio.ensure_future(self.handle())
        except Exception as ex: # pragma: no cover
            _logger.error("Datastore unable to fulfill request: "
                          "%s; %s", ex, traceback.format_exc())

    def connection_lost(self, exc):
        """
        asyncio.BaseProtocol callback for socket tear down

        For streamed protocols any break in the network connection will
        be reported here; for datagram protocols, only a teardown of the
        socket itself will result in this call.
        """
        try:
            self.handler_task.cancel()
            if exc is None:
                self._log_exception()
            else:  # pragma: no cover
                if hasattr(self, "client_address"):  # TCP connection
                    _logger.debug("Client Disconnection {} due "
                                  "to {}".format(*self.client_address, exc))

            self.running = False

        except Exception as ex: # pragma: no cover
            _logger.error("Datastore unable to fulfill request: "
                          "%s; %s", ex, traceback.format_exc())

    async def handle(self):
        """Asyncio coroutine which represents a single conversation between
        the modbus slave and master

        Once the client connection is established, the data chunks will be
        fed to this coroutine via the asyncio.Queue object which is fed by
        the ModbusBaseRequestHandler class's callback Future.

        This callback future gets data from either
        asyncio.DatagramProtocol.datagram_received or
        from asyncio.BaseProtocol.data_received.

        This function will execute without blocking in the while-loop and
        yield to the asyncio event loop when the frame is exhausted.
        As a result, multiple clients can be interleaved without any
        interference between them.

        For ModbusConnectedRequestHandler, each connection will be given an
        instance of the handle() coroutine and this instance will be put in the
        active_connections dict. Calling server_close will individually cancel
        each running handle() task.

        For ModbusDisconnectedRequestHandler, a single handle() coroutine will
        be started and maintained. Calling server_close will cancel that task.

        """
        reset_frame = False
        while self.running:
            try:
                units = self.server.context.slaves()
                # this is an asyncio.Queue await, it will never fail
                data = await self._recv_()
                if isinstance(data, tuple):
                    # addr is populated when talking over UDP
                    data, *addr = data
                else:
                    addr = (None,)  # empty tuple

                if not isinstance(units, (list, tuple)):
                    units = [units]
                # if broadcast is enabled make sure to
                # process requests to address 0
                if self.server.broadcast_enable: # pragma: no cover
                    if 0 not in units:
                        units.append(0)

                if _logger.isEnabledFor(logging.DEBUG):
                    _logger.debug('Handling data: ' + hexlify_packets(data))

                single = self.server.context.single
                self.framer.processIncomingPacket(
                    data=data, callback=lambda x: self.execute(x, *addr),
                    unit=units, single=single)

            except asyncio.CancelledError:
                # catch and ignore cancelation errors
                self._log_exception()
            except Exception as e:
                # force TCP socket termination as processIncomingPacket
                # should handle applicaiton layer errors
                # for UDP sockets, simply reset the frame
                if isinstance(self, ModbusConnectedRequestHandler):
                    client_addr = self.client_address[:2]
                    _logger.error("Unknown exception '{}' on stream {} "
                                  "forcing disconnect".format(e, client_addr))
                    self.transport.close()
                else:
                    _logger.error("Unknown error occurred %s" % e)
                    reset_frame = True  # graceful recovery
            finally:
                if reset_frame:
                    self.framer.resetFrame()
                    reset_frame = False

    def execute(self, request, *addr):
        """ The callback to call with the resulting message

        :param request: The decoded request message
        """
        broadcast = False
        try:
            if self.server.broadcast_enable and request.unit_id == 0:
                broadcast = True
                # if broadcasting then execute on all slave contexts,
                # note response will be ignored
                for unit_id in self.server.context.slaves():
                    response = request.execute(self.server.context[unit_id])
            else:
                context = self.server.context[request.unit_id]
                response = request.execute(context)
        except NoSuchSlaveException as ex:
            _logger.error("requested slave does "
                          "not exist: %s" % request.unit_id)
            if self.server.ignore_missing_slaves:
                return  # the client will simply timeout waiting for a response
            response = request.doException(merror.GatewayNoResponse)
        except Exception as ex:
            _logger.error("Datastore unable to fulfill request: "
                          "%s; %s", ex, traceback.format_exc())
            response = request.doException(merror.SlaveFailure)
        # no response when broadcasting
        if not broadcast:
            response.transaction_id = request.transaction_id
            response.unit_id = request.unit_id
            skip_encoding = False
            if self.server.response_manipulator:
                response, skip_encoding = self.server.response_manipulator(response)
            self.send(response, *addr, skip_encoding=skip_encoding)

    def send(self, message, *addr, **kwargs):
        def __send(msg, *addr):
            if _logger.isEnabledFor(logging.DEBUG):
                _logger.debug('send: [%s]- %s' % (message, b2a_hex(msg)))
            if addr == (None,):
                self._send_(msg)
            else:
                self._send_(msg, *addr)
        skip_encoding = kwargs.get("skip_encoding", False)
        if skip_encoding:
            __send(message, *addr)
        elif message.should_respond:
            # self.server.control.Counter.BusMessage += 1
            pdu = self.framer.buildPacket(message)
            __send(pdu, *addr)
        else:
            _logger.debug("Skipping sending response!!")

    # ----------------------------------------------------------------------- #
    # Derived class implementations
    # ----------------------------------------------------------------------- #

    def _send_(self, data): # pragma: no cover
        """ Send a request (string) to the network

        :param message: The unencoded modbus response
        """
        raise NotImplementedException("Method not implemented "
                                      "by derived class")

    async def _recv_(self): # pragma: no cover
        """ Receive data from the network

        :return:
        """
        raise NotImplementedException("Method not implemented "
                                      "by derived class")


class ModbusConnectedRequestHandler(ModbusBaseRequestHandler,asyncio.Protocol):
    """ Implements the modbus server protocol

    This uses asyncio.Protocol to implement
    the client handler for a connected protocol (TCP).
    """

    def connection_made(self, transport):
        """ asyncio.BaseProtocol:  Called when a connection is made. """
        super().connection_made(transport)

        self.client_address = transport.get_extra_info('peername')
        self.server.active_connections[self.client_address] = self
        _logger.debug("TCP client connection established "
                      "[%s:%s]" % self.client_address[:2])

    def connection_lost(self, exc):
        """
        asyncio.BaseProtocol: Called when the connection is lost or closed.
        """
        super().connection_lost(exc)
        client_addr = self.client_address[:2]
        _logger.debug("TCP client disconnected [%s:%s]" % client_addr)
        if self.client_address in self.server.active_connections:
            self.server.active_connections.pop(self.client_address)

    def data_received(self, data):
        """
        asyncio.Protocol: (TCP) Called when some data is received.
        data is a non-empty bytes object containing the incoming data.
        """
        self.receive_queue.put_nowait(data)

    async def _recv_(self):
        return await self.receive_queue.get()

    def _send_(self, data):
        """ tcp send """
        self.transport.write(data)


class ModbusDisconnectedRequestHandler(ModbusBaseRequestHandler,
                                       asyncio.DatagramProtocol):
    """ Implements the modbus server protocol

    This uses the socketserver.BaseRequestHandler to implement
    the client handler for a disconnected protocol (UDP). The
    only difference is that we have to specify who to send the
    resulting packet data to.
    """
    def __init__(self,owner):
        super().__init__(owner)
        _future = asyncio.get_event_loop().create_future()
        self.server.on_connection_terminated = _future

    def connection_lost(self,exc):
        super().connection_lost(exc)
        self.server.on_connection_terminated.set_result(True)

    def datagram_received(self,data, addr):
        """
        asyncio.DatagramProtocol: Called when a datagram is received.
         data is a bytes object containing the incoming data. addr
         is the address of the peer sending the data; the exact
         format depends on the transport.
        """
        self.receive_queue.put_nowait((data, addr))

    def error_received(self,exc): # pragma: no cover
        """
        asyncio.DatagramProtocol: Called when a previous send
        or receive operation raises an OSError. exc is the
        OSError instance.

        This method is called in rare conditions,
        when the transport (e.g. UDP) detects that a datagram could
        not be delivered to its recipient. In many conditions
        though, undeliverable datagrams will be silently dropped.
        """
        _logger.error("datagram connection error [%s]" % exc)

    async def _recv_(self):
        return await self.receive_queue.get()

    def _send_(self, data, addr):
        self.transport.sendto(data, addr=addr)


class ModbusServerFactory:
    """
    Builder class for a modbus server

    This also holds the server datastore so that it is persisted between connections
    """

    def __init__(self, store, framer=None, identity=None, **kwargs):
        import warnings
        warnings.warn("deprecated API for asyncio. ServerFactory's are a "
                      "twisted construct and don't have an equivalent in "
                      "asyncio",
                      DeprecationWarning)


class ModbusSingleRequestHandler(ModbusBaseRequestHandler, asyncio.Protocol):
    """ Implements the modbus server protocol
    This uses asyncio.Protocol to implement
    the client handler for a serial connection.
    """
    def connection_made(self, transport):
        super().connection_made(transport)

        _logger.debug("Serial connection established")

    def connection_lost(self, exc):
        super().connection_lost(exc)
        _logger.debug("Serial conection lost")
        if hasattr(self.server, 'on_connection_lost'):
            self.server.on_connection_lost()

    def data_received(self, data):
        self.receive_queue.put_nowait(data)

    async def _recv_(self):
        return await self.receive_queue.get()

    def _send_(self, data):
        if self.transport is not None:
            self.transport.write(data)

# --------------------------------------------------------------------------- #
# Server Implementations
# --------------------------------------------------------------------------- #

class ModbusTcpServer:
    """
    A modbus threaded tcp socket server

    We inherit and overload the socket server so that we
    can control the client threads as well as have a single
    server context instance.
    """

    def __init__(self,
                 context,
                 framer=None,
                 identity=None,
                 address=None,
                 handler=None,
                 allow_reuse_address=False,
                 allow_reuse_port=False,
                 defer_start=False,
                 backlog=20,
                 loop=None,
                 **kwargs):
        """ Overloaded initializer for the socket server

        If the identify structure is not passed in, the ModbusControlBlock
        uses its own empty structure.

        :param context: The ModbusServerContext datastore
        :param framer: The framer strategy to use
        :param identity: An optional identify structure
        :param address: An optional (interface, port) to bind to.
        :param handler: A handler for each client session; default is
                        ModbusConnectedRequestHandler. The handler class
                        receives connection create/teardown events
        :param allow_reuse_address: Whether the server will allow the
                        reuse of an address.
        :param allow_reuse_port: Whether the server will allow the
                        reuse of a port.
        :param backlog:  is the maximum number of queued connections
                    passed to listen(). Defaults to 20, increase if many
                    connections are being made and broken to your Modbus slave
        :param loop: optional asyncio event loop to run in. Will default to
                        asyncio.get_event_loop() supplied value if None.
        :param ignore_missing_slaves: True to not send errors on a request
                        to a missing slave
        :param broadcast_enable: True to treat unit_id 0 as broadcast address,
                        False to treat 0 as any other unit_id
        :param response_manipulator: Callback method for manipulating the
                                        response
        """
        self.active_connections = {}
        self.loop = loop or asyncio.get_event_loop()
        self.allow_reuse_address = allow_reuse_address
        self.decoder = ServerDecoder()
        self.framer = framer or ModbusSocketFramer
        self.context = context or ModbusServerContext()
        self.control = ModbusControlBlock()
        self.address = address or ("", Defaults.Port)
        self.handler = handler or ModbusConnectedRequestHandler
        self.handler.server = self
        self.ignore_missing_slaves = kwargs.get('ignore_missing_slaves',
                                                Defaults.IgnoreMissingSlaves)
        self.broadcast_enable = kwargs.get('broadcast_enable',
                                           Defaults.broadcast_enable)
        self.response_manipulator = kwargs.get("response_manipulator", None)
        if isinstance(identity, ModbusDeviceIdentification):
            self.control.Identity.update(identity)

        # asyncio future that will be done once server has started
        self.serving = self.loop.create_future()
        # constructors cannot be declared async, so we have to
        # defer the initialization of the server
        self.server = None
        if PYTHON_VERSION >= (3, 7):
            # start_serving is new in version 3.7
            self.server_factory = self.loop.create_server(
                lambda: self.handler(self),
                *self.address,
                reuse_address=allow_reuse_address,
                reuse_port=allow_reuse_port,
                backlog=backlog,
                start_serving=not defer_start
            )
        else:
            self.server_factory = self.loop.create_server(
                lambda: self.handler(self),
                *self.address,
                reuse_address=allow_reuse_address,
                reuse_port=allow_reuse_port,
                backlog=backlog
            )

    async def serve_forever(self):
        if self.server is None:
            self.server = await self.server_factory
            self.serving.set_result(True)
            await self.server.serve_forever()
        else:
            raise RuntimeError("Can't call serve_forever on "
                               "an already running server object")

    def server_close(self):
        for k, v in self.active_connections.items():
            _logger.warning("aborting active session {}".format(k))
            v.handler_task.cancel()
        self.active_connections = {}
        self.server.close()


class ModbusTlsServer(ModbusTcpServer):
    """
    A modbus threaded tls socket server

    We inherit and overload the socket server so that we
    can control the client threads as well as have a single
    server context instance.
    """

    def __init__(self,
                 context,
                 framer=None,
                 identity=None,
                 address=None,
                 sslctx=None,
                 certfile=None,
                 keyfile=None,
                 handler=None,
                 allow_reuse_address=False,
                 allow_reuse_port=False,
                 defer_start=False,
                 backlog=20,
                 loop=None,
                 **kwargs):
        """ Overloaded initializer for the socket server

        If the identify structure is not passed in, the ModbusControlBlock
        uses its own empty structure.

        :param context: The ModbusServerContext datastore
        :param framer: The framer strategy to use
        :param identity: An optional identify structure
        :param address: An optional (interface, port) to bind to.
        :param sslctx: The SSLContext to use for TLS (default None and auto
                       create)
        :param certfile: The cert file path for TLS (used if sslctx is None)
        :param keyfile: The key file path for TLS (used if sslctx is None)
        :param handler: A handler for each client session; default is
                        ModbusConnectedRequestHandler. The handler class
                        receives connection create/teardown events
        :param allow_reuse_address: Whether the server will allow the
                        reuse of an address.
        :param allow_reuse_port: Whether the server will allow the
                        reuse of a port.
        :param backlog:  is the maximum number of queued connections
                    passed to listen(). Defaults to 20, increase if many
                    connections are being made and broken to your Modbus slave
        :param loop: optional asyncio event loop to run in. Will default to
                        asyncio.get_event_loop() supplied value if None.
        :param ignore_missing_slaves: True to not send errors on a request
                        to a missing slave
        :param broadcast_enable: True to treat unit_id 0 as broadcast address,
                        False to treat 0 as any other unit_id
        :param response_manipulator: Callback method for
                        manipulating the response
        """
        self.active_connections = {}
        self.loop = loop or asyncio.get_event_loop()
        self.allow_reuse_address = allow_reuse_address
        self.decoder = ServerDecoder()
        self.framer = framer or ModbusTlsFramer
        self.context = context or ModbusServerContext()
        self.control = ModbusControlBlock()
        self.address = address or ("", Defaults.Port)
        self.handler = handler or ModbusConnectedRequestHandler
        self.handler.server = self
        self.ignore_missing_slaves = kwargs.get('ignore_missing_slaves',
                                                Defaults.IgnoreMissingSlaves)
        self.broadcast_enable = kwargs.get('broadcast_enable',
                                           Defaults.broadcast_enable)
        self.response_manipulator = kwargs.get("response_manipulator", None)

        if isinstance(identity, ModbusDeviceIdentification):
            self.control.Identity.update(identity)

        self.sslctx = sslctx
        if self.sslctx is None:
            self.sslctx = ssl.create_default_context()
            self.sslctx.load_cert_chain(certfile=certfile, keyfile=keyfile)
            # According to MODBUS/TCP Security Protocol Specification, it is
            # TLSv2 at least
            self.sslctx.options |= ssl.OP_NO_TLSv1_1
            self.sslctx.options |= ssl.OP_NO_TLSv1
            self.sslctx.options |= ssl.OP_NO_SSLv3
            self.sslctx.options |= ssl.OP_NO_SSLv2
        self.sslctx.verify_mode = ssl.CERT_OPTIONAL
        self.sslctx.check_hostname = False
        # asyncio future that will be done once server has started
        self.serving = self.loop.create_future()
        # constructors cannot be declared async, so we have to
        # defer the initialization of the server
        self.server = None
        if PYTHON_VERSION >= (3, 7):
            # start_serving is new in version 3.7
            self.server_factory = self.loop.create_server(
                lambda: self.handler(self),
                *self.address,
                ssl=self.sslctx,
                reuse_address=allow_reuse_address,
                reuse_port=allow_reuse_port,
                backlog=backlog,
                start_serving=not defer_start
            )
        else:
            self.server_factory = self.loop.create_server(
                lambda: self.handler(self),
                *self.address,
                ssl=self.sslctx,
                reuse_address=allow_reuse_address,
                reuse_port=allow_reuse_port,
                backlog=backlog
            )


class ModbusUdpServer:
    """
    A modbus threaded udp socket server

    We inherit and overload the socket server so that we
    can control the client threads as well as have a single
    server context instance.
    """

    def __init__(self, context, framer=None, identity=None, address=None,
                 handler=None, allow_reuse_address=False,
                 allow_reuse_port=False,
                 defer_start=False,
                 backlog=20,
                 loop=None,
                 **kwargs):
        """ Overloaded initializer for the socket server

        If the identify structure is not passed in, the ModbusControlBlock
        uses its own empty structure.

        :param context: The ModbusServerContext datastore
        :param framer: The framer strategy to use
        :param identity: An optional identify structure
        :param address: An optional (interface, port) to bind to.
        :param handler: A handler for each client session; default is
                            ModbusDisonnectedRequestHandler
        :param ignore_missing_slaves: True to not send errors on a request
                            to a missing slave
        :param broadcast_enable: True to treat unit_id 0 as broadcast address,
                            False to treat 0 as any other unit_id
        :param response_manipulator: Callback method for
                            manipulating the response
        """
        self.loop = loop or asyncio.get_event_loop()
        self.decoder = ServerDecoder()
        self.framer = framer  or ModbusSocketFramer
        self.context = context or ModbusServerContext()
        self.control = ModbusControlBlock()
        self.address = address or ("", Defaults.Port)
        self.handler = handler or ModbusDisconnectedRequestHandler
        self.ignore_missing_slaves = kwargs.get('ignore_missing_slaves',
                                                Defaults.IgnoreMissingSlaves)
        self.broadcast_enable = kwargs.get('broadcast_enable',
                                           Defaults.broadcast_enable)
        self.response_manipulator = kwargs.get("response_manipulator", None)

        if isinstance(identity, ModbusDeviceIdentification):
            self.control.Identity.update(identity)

        self.protocol = None
        self.endpoint = None
        self.on_connection_terminated = None
        self.stop_serving = self.loop.create_future()
        # asyncio future that will be done once server has started
        self.serving = self.loop.create_future()
        self.server_factory = self.loop.create_datagram_endpoint(
            lambda: self.handler(self),
            local_addr=self.address,
            reuse_address=allow_reuse_address,
            reuse_port=allow_reuse_port,
            allow_broadcast=True
        )

    async def serve_forever(self):
        if self.protocol is None:
            self.protocol, self.endpoint = await self.server_factory
            self.serving.set_result(True)
            await self.stop_serving
        else:
            raise RuntimeError("Can't call serve_forever on an "
                               "already running server object")

    def server_close(self):
        self.stop_serving.set_result(True)
        if self.endpoint.handler_task is not None:
            self.endpoint.handler_task.cancel()

        self.protocol.close()


class ModbusSerialServer(object):
    """
    A modbus threaded serial socket server
    We inherit and overload the socket server so that we
    can control the client threads as well as have a single
    server context instance.
    """

    handler = None

    def __init__(self, context, framer=None, identity=None, **kwargs):  # pragma: no cover
        """ Overloaded initializer for the socket server
        If the identity structure is not passed in, the ModbusControlBlock
        uses its own empty structure.
        :param context: The ModbusServerContext datastore
        :param framer: The framer strategy to use
        :param identity: An optional identify structure
        :param port: The serial port to attach to
        :param stopbits: The number of stop bits to use
        :param bytesize: The bytesize of the serial messages
        :param parity: Which kind of parity to use
        :param baudrate: The baud rate to use for the serial device
        :param timeout: The timeout to use for the serial device
        :param ignore_missing_slaves: True to not send errors on a request
                            to a missing slave
        :param broadcast_enable: True to treat unit_id 0 as broadcast address,
                            False to treat 0 as any other unit_id
        :param autoreonnect: True to enable automatic reconnection,
                            False otherwise
        :param reconnect_delay: reconnect delay in seconds
        :param response_manipulator: Callback method for
                    manipulating the response
        """
        self.device = kwargs.get('port', 0)
        self.stopbits = kwargs.get('stopbits', Defaults.Stopbits)
        self.bytesize = kwargs.get('bytesize', Defaults.Bytesize)
        self.parity = kwargs.get('parity', Defaults.Parity)
        self.baudrate = kwargs.get('baudrate', Defaults.Baudrate)
        self.timeout = kwargs.get('timeout', Defaults.Timeout)
        self.ignore_missing_slaves = kwargs.get('ignore_missing_slaves',
                                                Defaults.IgnoreMissingSlaves)
        self.broadcast_enable = kwargs.get('broadcast_enable',
                                           Defaults.broadcast_enable)
        self.auto_reconnect = kwargs.get('auto_reconnect', False)
        self.reconnect_delay = kwargs.get('reconnect_delay', 2)
        self.reconnecting_task = None

        self.handler = kwargs.get("handler") or ModbusSingleRequestHandler
        self.framer = framer or ModbusRtuFramer
        self.decoder = ServerDecoder()
        self.context = context or ModbusServerContext()
        self.response_manipulator = kwargs.get("response_manipulator", None)
        self.control = ModbusControlBlock()
        if isinstance(identity, ModbusDeviceIdentification):
            self.control.Identity.update(identity)

        self.protocol = None
        self.transport = None

    async def start(self):
        await self._connect()

    def _protocol_factory(self):
        return self.handler(self)

    async def _delayed_connect(self):
        await asyncio.sleep(self.reconnect_delay)
        await self._connect()

    async def _connect(self):
        if self.reconnecting_task is not None:
            self.reconnecting_task = None

        try:
            self.transport, self.protocol = await create_serial_connection(
                asyncio.get_event_loop(),
                self._protocol_factory,
                self.device,
                baudrate=self.baudrate,
                bytesize=self.bytesize,
                parity=self.parity,
                stopbits=self.stopbits,
                timeout=self.timeout
            )
        except serial.serialutil.SerialException as e:
            _logger.debug("Failed to open serial port: {}".format(self.device))
            if not self.auto_reconnect:
                raise e

            self._check_reconnect()

        except Exception as e:
            _logger.debug("Exception while create - {}".format(e))

    def on_connection_lost(self):
        if self.transport is not None:
            self.transport.close()
            self.transport = None
            self.protocol = None

        self._check_reconnect()

    def _check_reconnect(self):
        _logger.debug("checkking autoreconnect {} {}".format(
            self.auto_reconnect, self.reconnecting_task))
        if self.auto_reconnect and (self.reconnecting_task is None):
            _logger.debug("Scheduling serial connection reconnect")
            loop = asyncio.get_event_loop()
            self.reconnecting_task = loop.create_task(self._delayed_connect())

    async def serve_forever(self):
        while True:
            await asyncio.sleep(360)


# --------------------------------------------------------------------------- #
# Creation Factories
# --------------------------------------------------------------------------- #
async def StartTcpServer(context=None, identity=None, address=None,
                         custom_functions=[], defer_start=True, **kwargs):
    """ A factory to start and run a tcp modbus server

    :param context: The ModbusServerContext datastore
    :param identity: An optional identify structure
    :param address: An optional (interface, port) to bind to.
    :param custom_functions: An optional list of custom function classes
        supported by server instance.
    :param defer_start: if set, a coroutine which can be started and stopped
            will be returned. Otherwise, the server will be immediately spun
            up without the ability to shut it off from within the asyncio loop
    :param ignore_missing_slaves: True to not send errors on a request to a
                                      missing slave
    :return: an initialized but inactive server object coroutine
    """
    framer = kwargs.pop("framer", ModbusSocketFramer)
    server = ModbusTcpServer(context, framer, identity, address, **kwargs)

    for f in custom_functions:
        server.decoder.register(f)  # pragma: no cover

    if not defer_start:
        await server.serve_forever()

    return server


async def StartTlsServer(context=None, identity=None, address=None,
                         sslctx=None,
                         certfile=None, keyfile=None,
                         allow_reuse_address=False,
                         allow_reuse_port=False,
                         custom_functions=[],
                         defer_start=True, **kwargs):
    """ A factory to start and run a tls modbus server

    :param context: The ModbusServerContext datastore
    :param identity: An optional identify structure
    :param address: An optional (interface, port) to bind to.
    :param sslctx: The SSLContext to use for TLS (default None and auto create)
    :param certfile: The cert file path for TLS (used if sslctx is None)
    :param keyfile: The key file path for TLS (used if sslctx is None)
    :param allow_reuse_address: Whether the server will allow the reuse of an
                                address.
    :param allow_reuse_port: Whether the server will allow the reuse of a port.
    :param custom_functions: An optional list of custom function classes
        supported by server instance.
    :param defer_start: if set, a coroutine which can be started and stopped
            will be returned. Otherwise, the server will be immediately spun
            up without the ability to shut it off from within the asyncio loop
    :param ignore_missing_slaves: True to not send errors on a request to a
                                      missing slave
    :return: an initialized but inactive server object coroutine
    """
    framer = kwargs.pop("framer", ModbusTlsFramer)
    server = ModbusTlsServer(context, framer, identity, address, sslctx,
                             certfile, keyfile,
                             allow_reuse_address=allow_reuse_address,
                             allow_reuse_port=allow_reuse_port, **kwargs)

    for f in custom_functions:
        server.decoder.register(f) # pragma: no cover

    if not defer_start:
        await server.serve_forever()

    return server


async def StartUdpServer(context=None, identity=None, address=None,
                         custom_functions=[], defer_start=True, **kwargs):
    """ A factory to start and run a udp modbus server

    :param context: The ModbusServerContext datastore
    :param identity: An optional identify structure
    :param address: An optional (interface, port) to bind to.
    :param custom_functions: An optional list of custom function classes
        supported by server instance.
    :param framer: The framer to operate with (default ModbusSocketFramer)
    :param ignore_missing_slaves: True to not send errors on a request
                                    to a missing slave
    """
    framer = kwargs.pop('framer', ModbusSocketFramer)
    server = ModbusUdpServer(context, framer, identity, address, **kwargs)

    for f in custom_functions:
        server.decoder.register(f) # pragma: no cover

    if not defer_start:
        await server.serve_forever() # pragma: no cover

    return server


async def StartSerialServer(context=None, identity=None,
                      custom_functions=[], **kwargs):  # pragma: no cover
    """ A factory to start and run a serial modbus server

    :param context: The ModbusServerContext datastore
    :param identity: An optional identify structure
    :param custom_functions: An optional list of custom function classes
        supported by server instance.
    :param framer: The framer to operate with (default ModbusAsciiFramer)
    :param port: The serial port to attach to
    :param stopbits: The number of stop bits to use
    :param bytesize: The bytesize of the serial messages
    :param parity: Which kind of parity to use
    :param baudrate: The baud rate to use for the serial device
    :param timeout: The timeout to use for the serial device
    :param ignore_missing_slaves: True to not send errors on a request to a
                                  missing slave
    """
    framer = kwargs.pop('framer', ModbusAsciiFramer)
    server = ModbusSerialServer(context, framer, identity, **kwargs)
    for f in custom_functions:
        server.decoder.register(f)
    await server.start()
    await server.serve_forever()


def StopServer():
    """
    Helper method to stop Async Server
    """
    import warnings
    warnings.warn("deprecated API for asyncio. Call server_close() on "
                  "server object returned by StartXxxServer",
                  DeprecationWarning)


# --------------------------------------------------------------------------- #
# Exported symbols
# --------------------------------------------------------------------------- #


__all__ = [

    "StartTcpServer", "StartTlsServer", "StartUdpServer", "StartSerialServer"

]
