Reads a line from the device.

        :param timeout: read timeout
        :type timeout: float
        :param purge_buffer: Indicates whether to purge the buffer prior to
                             reading.
        :type purge_buffer: bool

        :returns: line that was read
        :raises: :py:class:`~alarmdecoder.util.CommError`, :py:class:`~alarmdecoder.util.TimeoutError`

def read_line(self, timeout=0.0, purge_buffer=False):
        """
        Reads a line from the device.

        :param timeout: read timeout
        :type timeout: float
        :param purge_buffer: Indicates whether to purge the buffer prior to
                             reading.
        :type purge_buffer: bool

        :returns: line that was read
        :raises: :py:class:`~alarmdecoder.util.CommError`, :py:class:`~alarmdecoder.util.TimeoutError`
        """

        def timeout_event():
            """Handles read timeout event"""
            timeout_event.reading = False
        timeout_event.reading = True

        if purge_buffer:
            self._buffer = b''

        got_line, data = False, ''

        timer = threading.Timer(timeout, timeout_event)
        if timeout > 0:
            timer.start()

        leftovers = b''
        try:
            while timeout_event.reading and not got_line:
                read_ready, _, _ = select.select([self._device.fileno()], [], [], 0.5)
                if len(read_ready) == 0:
                    continue

                bytes_avail = 0
                if hasattr(self._device, "in_waiting"):
                    bytes_avail = self._device.in_waiting
                else:
                    bytes_avail = self._device.inWaiting()

                buf = self._device.read(bytes_avail)

                for idx in range(len(buf)):
                    c = buf[idx]

                    ub = bytes_hack(c)
                    if sys.version_info > (3,):
                        ub = bytes([ub])

                    # NOTE: AD2SERIAL and AD2PI apparently sends down \xFF on boot.
                    if ub != b'' and ub != b"\xff":
                        self._buffer += ub

                        if ub == b"\n":
                            self._buffer = self._buffer.strip(b"\r\n")

                            if len(self._buffer) > 0:
                                got_line = True
                                leftovers = buf[idx:]
                                break

        except (OSError, serial.SerialException) as err:
            raise CommError('Error reading from device: {0}'.format(str(err)), err)

        else:
            if got_line:
                data, self._buffer = self._buffer, leftovers

                self.on_read(data=data)

            else:
                raise TimeoutError('Timeout while waiting for line terminator.')

        finally:
            timer.cancel()

        return data.decode('utf-8')