Repository: tayler6000/pyVoIP (language: python)

Issue:
Check Black
Now that files are black'd, add a check to ensure this remains so. 

Output of CI https://github.com/balloob/pyVoIP/actions/runs/3041686250/jobs/4899071334
Unable to perform username/password registration
The realm passed in response to Register and Invite requests was not reflected in the response.  This bug makes it so no one can register with any username/password-protected accounts that aren't hosted on Asterisk.
Proposal for function/method naming standardization.
After seeing some of the programming style of @Input-BDF, I thought it would be a good idea to set a solid standard for function/method naming conventions.  [The documentation](https://pyvoip.readthedocs.io/en/v1.5.1/) states the phrase "This [function] should not be called by the user." Over 20 times.  This phrase will also be added quite a few more times in the recent updates in 1.6.0.  In other programming languages, these are referred to as "private functions" and are usually not even accessible to someone using the API/library.  Python does not have this functionality.

I propose that all function names should be in lowerCamelCase. 
For these purposes I define `lowerCamelCase` as the following:
```
No spaces or underscores.
The first word is all lowercase.
The first letter of each subsequent word is capitalized.
Acronyms and abbreviations maintain their standard capitalization.
```

If the function is a "private function", it should have a single underscore proceeding the function's name. For example, a private function with the desired name of "Hello World API" should be `_helloWorldAPI`, whereas a public function with the same name would be `helloWorldAPI`.

I propose that all class names should be in UpperCamelCase. 
For these purposes I define `UpperCamelCase` as the following:
```
No spaces or underscores.
The first letter of every word is capitalized.
Acronyms and abbreviations maintain their standard capitalization.
```

For example, Voice over Internet Protocol's standard acronym is VoIP, therefore the class for a Voice over Internet Protocol Call is called `VoIPCall`.  If you wanted a class to have the name of "foo bar" it would be called `FooBar`.

Any thoughts would be appreciated!
SIPMessage parse_header
In the class SIPMessage and the function parse_header there are two points to improve the code for use with different SIP server.
First in the part From or To.  The part contact = raw.split('<sip:') makes problems sometimes. 
Here is the code part from the branche v1.6.0-dev.
```
        elif header == "From" or header == "To":
            info = data.split(';tag=')
            tag = ''
            if len(info) >= 2:
                tag = info[1]
            raw = info[0]
            contact = raw.split('<sip:')
            contact[0] = contact[0].strip('"').strip("'")
            address = contact[1].strip('>')
```
I've changed this to contact = re.split(r"<?sip:", raw)
```
        elif header == "From" or header == "To":
            info = data.split(';tag=')
            tag = ''
            if len(info) >= 2:
                tag = info[1]
            raw = info[0]
            # For some header from / to are only sip:xxxx@xxxxxx not <sip:xxxxxxx@xxxxxxxxx>
            # so changed raw.split to the regex version re.split
            contact = re.split(r"<?sip:", raw)
            contact[0] = contact[0].strip('"').strip("'")
```

The second part is in  WWW-Authenticate. You can have the element qop="auth,auth-int" and with info = data.split(",") this causes trouble. 
Here is the code part from the branche v1.6.0-dev.
```
        elif header == "WWW-Authenticate" or header == "Authorization":
            data = data.replace("Digest", "")
            info = data.split(",")
            header_data = {}
```
I've changed this to info = data.split(", "). 
```
        elif header == 'WWW-Authenticate' or header == "Authorization":
            data = data.replace("Digest", "")
            # add blank to avoid the split of qop="auth,auth-int"
            info = data.split(", ")
            header_data = {}
```

Incompatibility whith Python 3.6.8 of pyVoIP-1.6.0.dev0
When run pyVoIP-1.6.0.dev0 on Python 3.6.8 get error:
```
Traceback (most recent call last):
  File "/home/user/projects/calls/pyvoip.py", line 1, in <module>
    from pyVoIP.VoIP import VoIPPhone, InvalidStateError, CallState
  File "<fstring>", line 1
    (call_id=)
            ^
SyntaxError: invalid syntax
```
On Python 3.10.4 - goes normal.

Problem whith f strings at VoIP.py  on lines 756 and 762
```
                    debug(f"{call_id=} {self.calls=}")
```
and 
```
                    debug(f"{thread=} {self.threadLookup=}")
```
Pylance message: Unexpected token at end of expression

May be you need add correct `python_requires='>` in `setup.py` or fix compatibility whith python 3.6


Discussion / hints:

Fix patch:
diff --git a/.gitignore b/.gitignore
index 2b677df..678ba9e 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,3 +1,4 @@
 dist/
 pyVoIP.egg-info
 __pycache__
+venv
diff --git a/docs/Examples.rst b/docs/Examples.rst
index 03a2d40..2a842a6 100644
--- a/docs/Examples.rst
+++ b/docs/Examples.rst
@@ -6,7 +6,7 @@ Here we will go over a few basic phone setups.
 Setup
 *****
 
-PyVoIP uses callback functions to initiate phone calls.  In the example below, our callback function is named answer.  The callback takes one argument, which is a :ref:`VoIPCall` class.
+PyVoIP uses callback functions to initiate phone calls.  In the example below, our callback function is named ``answer``.  The callback takes one argument, which is a :ref:`VoIPCall` instance.
 
 We are also importing :ref:`VoIPPhone` and :ref:`InvalidStateError<invalidstateerror>`.  VoIPPhone is the main class for our `softphone <https://en.wikipedia.org/wiki/Softphone>`_.  An InvalidStateError is thrown when you try to perform an impossible command.  For example, denying the call when the phone is already answered, answering when it's already answered, etc.
 
@@ -17,22 +17,22 @@ The following will create a phone that answers and automatically hangs up:
   from pyVoIP.VoIP import VoIPPhone, InvalidStateError
 
   def answer(call):
-    try:
-      call.answer()
-      call.hangup()
-    except InvalidStateError:
-      pass
+      try:
+          call.answer()
+          call.hangup()
+      except InvalidStateError:
+          pass
   
-  if __name__ == '__main__':
-    phone = VoIPPhone(<SIP Server IP>, <SIP Server Port>, <SIP Server Username>, <SIP Server Password>, callCallback=answer, myIP=<Your computer's local IP>)
-    phone.start()
-    input('Press enter to disable the phone')
-    phone.stop()
+    if __name__ == "__main__":
+        phone = VoIPPhone(<SIP server IP>, <SIP server port>, <SIP server username>, <SIP server password>, myIP=<Your computer's local IP>, callCallback=answer)
+        phone.start()
+        input('Press enter to disable the phone')
+        phone.stop()
     
 Announcement Board
 ******************
 
-Let's say you want to make a phone that when you call it, it plays an announcement message, then hangs up.  We can accomplish this with the builtin libraries `wave <https://docs.python.org/3/library/wave.html>`_, `audioop <https://docs.python.org/3/library/audioop.html>`_, `time <https://docs.python.org/3/library/time.html>`_ and by importing :ref:`CallState<callstate>`.
+Let's say you want to make a phone that when you call it, it plays an announcement message, then hangs up.  We can accomplish this with the builtin libraries `wave <https://docs.python.org/3/library/wave.html>`_, `audioop <https://docs.python.org/3/library/audioop.html>`_, `time <https://docs.python.org/3/library/time.html>`_, and by importing :ref:`CallState<callstate>`.
 
 .. code-block:: python
 
@@ -41,48 +41,49 @@ Let's say you want to make a phone that when you call it, it plays an announceme
   import wave
   
   def answer(call):
-    try:
-      f = wave.open('announcment.wav', 'rb')
-      frames = f.getnframes()
-      data = f.readframes(frames)
-      f.close()
+      try:
+          f = wave.open('announcment.wav', 'rb')
+          frames = f.getnframes()
+          data = f.readframes(frames)
+          f.close()
       
-      call.answer()
-      call.writeAudio(data) #This writes the audio data to the transmit buffer, this must be bytes.
+          call.answer()
+          call.write_audio(data)  # This writes the audio data to the transmit buffer, this must be bytes.
       
-      stop = time.time()+(frames/8000) #The number of frames/8000 is the length of the audio in seconds.
+          stop = time.time() + (frames / 8000)  # frames/8000 is the length of the audio in seconds. 8000 is the hertz of PCMU.
       
-      while time.time() <= stop and call.state == CallState.ANSWERED:
-        time.sleep(1) 
-      call.hangup()
-    except InvalidStateError:
-      pass
-    except:
-      call.hangup()
+          while time.time() <= stop and call.state == CallState.ANSWERED:
+              time.sleep(0.1)
+          call.hangup()
+      except InvalidStateError:
+          pass
+      except:
+          call.hangup()
+  
       
-  if __name__ == '__main__':
-    phone=VoIPPhone(<SIP Server IP>, <SIP Server Port>, <SIP Server Username>, <SIP Server Password>, callCallback=answer, myIP=<Your computers local IP>)
-    phone.start()
-    input('Press enter to disable the phone')
-    phone.stop()
+  if __name__ == "__main__":
+      phone = VoIPPhone(<SIP Server IP>, <SIP Server Port>, <SIP Server Username>, <SIP Server Password>, myIP=<Your computers local IP>, callCallback=answer)
+      phone.start()
+      input('Press enter to disable the phone')
+      phone.stop()
 
 Something important to note is our wait function.  We are currently using:
 
 .. code-block:: python
 
-  stop = time.time()+(frames/8000) #The number of frames/8000 is the length of the audio in seconds.
+  stop = time.time() + (frames / 8000)  # The number of frames/8000 is the length of the audio in seconds.
       
   while time.time() <= stop and call.state == CallState.ANSWERED:
-    time.sleep(1)
+      time.sleep(0.1)
 
-This could be replaced with ``time.sleep(frames/8000)``.  However, doing so will not cause the thread to automatically close if the user hangs up, or if ``VoIPPhone().stop()`` is called; using the while loop method will fix this issue.  The ``time.sleep(1)`` inside the while loop is also important.  Supplementing ``time.sleep(1)`` for ``pass`` will cause your CPU to ramp up while running the loop, making the RTP (audio being sent out) lag.  This can make the voice audibly slow or choppy.
+This could be replaced with ``time.sleep(frames / 8000)``.  However, doing so will not cause the thread to automatically close if the user hangs up, or if ``VoIPPhone().stop()`` is called; using the while loop method will fix this issue.  The ``time.sleep(0.1)`` inside the while loop is also important.  Supplementing ``time.sleep(0.1)`` for ``pass`` will cause your CPU to ramp up while running the loop, making the RTP (audio being sent out and received) lag.  This can make the voice audibly slow or choppy.
 
-*Note: Audio must be in 8 bit, 8000Hz, and Mono/1 channel.  You can accomplish this in a free program called* `Audacity <https://www.audacityteam.org/>`_.  *To make an audio recording Mono, go to Tracks > Mix > Mix Stereo Down to Mono.  To make an audio recording 8000 Hz, go to Tracks > Resample... and select 8000, then ensure that your 'Project Rate' in the bottom left is also set to 8000.  To make an audio recording 8 bit, go to File > Export > Export as WAV, then change 'Save as type:' to 'Other uncompressed files', then set 'Header:' to 'WAV (Microsoft)', then set the 'Encoding:' to 'Unsigned 8-bit PCM'*
+*Note: Audio must be 8 bit, 8000Hz, and Mono/1 channel.  You can accomplish this in a free program called* `Audacity <https://www.audacityteam.org/>`_.  *To make an audio recording Mono, go to Tracks > Mix > Mix Stereo Down to Mono.  To make an audio recording 8000 Hz, go to Tracks > Resample... and select 8000, then ensure that your 'Project Rate' in the bottom left is also set to 8000.  To make an audio recording 8 bit, go to File > Export > Export as WAV, then change 'Save as type:' to 'Other uncompressed files', then set 'Header:' to 'WAV (Microsoft)', then set the 'Encoding:' to 'Unsigned 8-bit PCM'*
 
 IVR/Phone Menus
 ****************
 
-We can use the above code to create `IVR Menus <https://en.wikipedia.org/wiki/Interactive_voice_response>`_.  Currently, we cannot make 'breaking' IVR menus.  Breaking IVR menus in this context means, a user selecting an option mid-prompt will cancel the prompt, and start the next action.  Support for breaking IVR's will be made in the future.  For now, here is the code for a non-breaking IVR:
+We can use the following code to create `IVR Menus <https://en.wikipedia.org/wiki/Interactive_voice_response>`_.  Currently, we cannot make 'breaking' IVR menus.  Breaking IVR menus in this context means, a user selecting an option mid-prompt will cancel the prompt, and start the next action.  Support for breaking IVR's will be made in the future.  For now, here is the code for a non-breaking IVR:
 
 .. code-block:: python
 
@@ -91,34 +92,34 @@ We can use the above code to create `IVR Menus <https://en.wikipedia.org/wiki/In
   import wave
   
   def answer(call):
-    try:
-      f = wave.open('prompt.wav', 'rb')
-      frames = f.getnframes()
-      data = f.readframes(frames)
-      f.close()
+      try:
+          f = wave.open('prompt.wav', 'rb')
+          frames = f.getnframes()
+          data = f.readframes(frames)
+          f.close()
       
-      call.answer()
-      call.writeAudio(data)
+          call.answer()
+          call.write_audio(data)
       
-      while call.state == CallState.ANSWERED:
-        dtmf = call.getDTMF()
-        if dtmf == "1":
-          #Do Something
-          call.hangup()
-        elif dtmf == "2":
-          #Do Something Else
+          while call.state == CallState.ANSWERED:
+              dtmf = call.get_dtmf()
+              if dtmf == "1":
+                  # Do something
+                  call.hangup()
+              elif dtmf == "2":
+                  # Do something else
+                  call.hangup()
+              time.sleep(0.1)
+      except InvalidStateError:
+          pass
+      except:
           call.hangup()
-        time.sleep(1)
-    except InvalidStateError:
-      pass
-    except:
-      call.hangup()
       
   if __name__ == '__main__':
-    phone=VoIPPhone(<SIP Server IP>, <SIP Server Port>, <SIP Server Username>, <SIP Server Password>, callCallback=answer, myIP=<Your computers local IP>)
-    phone.start()
-    input('Press enter to disable the phone')
-    phone.stop()
+      phone = VoIPPhone(<SIP Server IP>, <SIP Server Port>, <SIP Server Username>, <SIP Server Password>, myIP=<Your computers local IP>, callCallback=answer)
+      phone.start()
+      input('Press enter to disable the phone')
+      phone.stop()
 
-Please note that ``getDTMF()`` is actually ``getDTMF(length=1)``, and as it is technically a ``io.StringBuffer()``, it will return ``''`` instead of ``None``.  This may be important if you wanted an 'if anything else, do that' clause.  Lastly, VoIPCall stores all DTMF keys pressed since the call was established; meaning, users can technically press any key they want before the prompt even finishes, or may press a wrong key before the prompt even starts.
+Please note that ``get_dtmf()`` is actually ``get_dtmf(length=1)``, and as it is technically an ``io.StringBuffer()``, it will return ``""`` instead of ``None``.  This may be important if you wanted an 'if anything else, do that' clause.  Lastly, VoIPCall stores all DTMF keys pressed since the call was established; meaning, users can press any key they want before the prompt even finishes, or may press a wrong key before the prompt even starts.
 
diff --git a/docs/Globals.rst b/docs/Globals.rst
new file mode 100644
index 0000000..20a0577
--- /dev/null
+++ b/docs/Globals.rst
@@ -0,0 +1,10 @@
+Global Variables
+########
+
+There are a few global variables that may assist you if you're having problems with the library.
+
+pyVoIP.\ **DEBUG** = False
+    If set to true, pyVoIP will print debug messages that may be useful if you need to open a GitHub issue.  Otherwise, does nothing.
+
+pyVoIP.\ **TRANSMIT_DELAY_REDUCTION** = 0.0
+    The higher this variable is, the more often RTP packets are sent.  This *should* only ever need to be 0.0.  However, when testing on Windows, there has sometimes been jittering, setting this to 0.75 fixed this in testing, but you may need to tinker with this number on a per-system basis.
diff --git a/docs/RTP.rst b/docs/RTP.rst
index 092de36..235a46b 100644
--- a/docs/RTP.rst
+++ b/docs/RTP.rst
@@ -1,14 +1,14 @@
 RTP - Real-time Transport Protocol
 ###################################
 
-The RTP module recives and transmits, sound and phone-event data for a particular phone call.
+The RTP module recives and transmits sound and phone-event data for a particular phone call.
 
 The RTP module has two methods that are used by various classes for packet parsing.
 
-RTP.\ **byte_to_bits**\ (byte)
+RTP.\ **byte_to_bits**\ (byte: bytes) -> str
   This method converts a single byte into an eight character string of ones and zeros.  The *byte* argument must be a single byte.
   
-RTP.\ **add_bytes**\ (bytes)
+RTP.\ **add_bytes**\ (bytes: bytes) -> int
   This method takes multiple bytes and adds them together into an integer.
   
 Errors
@@ -18,7 +18,7 @@ Errors
   This may be thrown when you try to int cast a dynamic PayloadType.  Most PayloadTypes have a number assigned in `RFC 3551 Section 6 <https://tools.ietf.org/html/rfc3551#section-6>`_.  However, some are considered to be 'dynamic' meaning the PBX/VoIP server will pick an available number, and define it.
   
 *exception* RTP.\ **RTPParseError**
-  This is thrown by :ref:`RTPMessage` is unable to parse a RTP message.  It may also be thrown by `RTPClient` when it's unable to encode or decode the RTP packet payload.
+  This is thrown by :ref:`RTPMessage` when unable to parse a RTP message.  It may also be thrown by `RTPClient` when it's unable to encode or decode the RTP packet payload.
 
 Enums
 *******
@@ -88,7 +88,7 @@ RTP.\ **PayloadType**
     This variation of the L16 Codec has the attributes: value 11, rate 44100, channel 2, description "L16"
     
   PayloadType.\ **EVENT**
-    This is the dynamic non-codec 'telephone-event'.  Telephone-event is used for sending and recieving DTMF codes.
+    This is the dynamic non-codec 'telephone-event'.  Telephone-event is used for sending and receiving DTMF codes.
     
 Classes
 *********
@@ -102,13 +102,13 @@ The RTPPacketManager class utilizes an ``io.ByteIO`` that stores either received
 
 RTP.\ **RTPPacketManager**\ ()
   
-  **read**\ (length=160)
-    Reads *length* bytes from the ByteIO.  This will always return the length requested, and will append ``b'\x00'``'s onto the end of the available bytes to achieve this length.
+  **read**\ (length=160) -> bytes
+    Reads *length* bytes from the ByteIO.  This will always return the length requested, and will append ``b'\x80'``'s onto the end of the available bytes to achieve this length.
     
-  **rebuild**\ (reset, offset=0, data=b'')
-    This rebuilds the ByteIO if packets are sent out of order.  Setting the argument *reset* to true will wipe all data in the ByteIO and insert in the data in the argument *data* at the position in the argument *offset*.
+  **rebuild**\ (reset: bool, offset=0, data=b'') -> None
+    This rebuilds the ByteIO if packets are sent out of order.  Setting the argument *reset* to ``True`` will wipe all data in the ByteIO and insert in the data in the argument *data* at the position in the argument *offset*.
     
-  **write**\ (offset, data)
+  **write**\ (offset: int, data: bytes) -> None
     Writes the data in the argument *data* to the ByteIO at the position in the argument *offset*.  RTP data comes with a timestamp that is passed as the offset in this case.  This makes it so a hole left by delayed packets can be filled later.  If a packet with a timestamp sooner than any other timestamp received, it will rebuild the ByteIO with the new data.  If this new position is over 100,000 bytes before the earliest byte, the ByteIO is completely wiped and starts over.  This is to prevent Overflow errors.
 
 .. _RTPMessage:
@@ -118,11 +118,11 @@ RTPMessage
 
 The RTPMessage class is used to parse RTP packets and makes them easily processed by the :ref:`RTPClient`.
 
-RTP.\ **RTPMessage**\ (data, assoc)
+RTP.\ **RTPMessage**\ (data: bytes, assoc: dict[int, :ref:`PayloadType<payload-type>`])
     
     The *data* argument is the received RTP packet in bytes.
     
-    The *assoc* argument is a dictionary, using the payload number as a key and a :ref:`PayloadType<payload-type>` as the value.  This way RTPMessage can determine what a number a dynamic payload is.  This association dictionary is generated by :ref:`VoIPCall`.
+    The *assoc* argument is a dictionary, using the payload number as a key and a :ref:`PayloadType<payload-type>` as the value.  This way RTPMessage can determine what number a dynamic payload is.  This association dictionary is generated by :ref:`VoIPCall`.
     
   RTPMessage has attributes that come from `RFC 3550 Section 5.1 <https://tools.ietf.org/html/rfc3550#section-5.1>`_. RTPMessage has the following attributes:
     
@@ -159,10 +159,10 @@ RTP.\ **RTPMessage**\ (data, assoc)
     RTPMessage.\ **raw**
       This attribute is the unparsed version of the *data* argument, in bytes.
   
-  **summary**\ ()
+  **summary**\ () -> str
     This method returns a string representation of the RTP packet excluding the payload.
     
-  **parse**\ (data)
+  **parse**\ (data: bytes) -> None
     This method is called by the initialization of the class.  It determines the RTP version, whether the packet has padding, has a header extension, and other information about the backet.
 
 .. _RTPClient:
@@ -172,7 +172,7 @@ RTPClient
 
 The RTPClient is used to send and receive RTP packets and encode/decode the audio codecs.
 
-*class* RTP.\ **RTPClient**\ (assoc, inIP, inPort, outIP, outPort, sendrecv, dtmf = None):
+*class* RTP.\ **RTPClient**\ (assoc: dict[int, :ref:`PayloadType<payload-type>`], inIP: str, inPort: int, outIP: str, outPort: int, sendrecv: :ref:`TransmitType<transmittype>`, dtmf: Optional[Callable[[str], None] = None):
     
     The *assoc* argument is a dictionary, using the payload number as a key and a :ref:`PayloadType<payload-type>` as the value.  This way, RTPMessage can determine what a number a dynamic payload is.  This association dictionary is generated by :ref:`VoIPCall`.
     
@@ -184,46 +184,61 @@ The RTPClient is used to send and receive RTP packets and encode/decode the audi
     
     The *outPort* argument is used to transmit RTP packets.
     
-    The *sendrecv* argument describes how the RTPClient should act.  Please reference `TransmitType<enums>` for more details.
+    The *sendrecv* argument describes how the RTPClient should act.  Please reference :ref:`TransmitType<transmittype>` for more details.
     
     The *dtmf* argument is set to the callback :ref:`VoIPCall`.dtmfCallback().
     
-  **start**\ ()
+  **start**\ () -> None
     This method is called by :ref:`VoIPCall`.answer(). It starts the recv() and trans() threads. It is also what initiates the bound port. **This should not be called by the** :term:`user`.
     
-  **stop**\ ()
+  **stop**\ () -> None
     This method is called by :ref:`VoIPCall`.hangup() and :ref:`VoIPCall`.bye().  It stops the recv() and trans() threads.  It will also close the bound port.  **This should not be called by the** :term:`user`.
     
-  **read**\ (length=160, blocking=True)
+  **read**\ (length=160, blocking=True) -> bytes
     This method is called by :ref:`VoIPCall`.readAudio().  It reads linear/raw audio data from the received buffer. Returns *length* amount of bytes. Default length is 160 as that is the amount of bytes sent per PCMU/PCMA packet.  When *blocking* is set to true, this function will not return until data is available.  When *blocking* is set to false and data is not available, this function will return bytes(length).
     
-  **write**\ (data)
+  **write**\ (data: bytes) -> None
     This method is called by :ref:`VoIPCall`.writeAudio().  It queues the data written to be sent to the :term:`client`.
     
-  **recv**\ ()
+  **recv**\ () -> None
     This method is called by RTPClient.start() and is responsible for receiving and parsing through RTP packets. **This should not be called by the** :term:`user`.
     
-  **trans**\ ()
+  **trans**\ () -> None
     This method is called by RTPClient.start() and is responsible for transmitting RTP packets. **This should not be called by the** :term:`user`.
     
-  **parsePacket**\ (packet)
+  **parsePacket**\ (packet: bytes) -> None
+    *Deprecated.* Please use ``parse_packet`` instead.
+    
+  **parse_packet**\ (packet: bytes) -> None
     This method is called by the recv() thread.  It converts the argument *packet* into a :ref:`RTPMessage`, then sends it to the proper parse function depending on the :ref:`PayloadType<payload-type>`.
     
-  **encodePacket**\ (payload)
+  **encodePacket**\ (payload: bytes) -> bytes
+    *Deprecated.* Please use ``encode_packet`` instead.
+    
+  **encode_packet**\ (payload: bytes) -> bytes
     This method is called by the trans() thread.  It encoded the argument *payload* into the prefered codec.  Currently, PCMU is the hardcoded prefered codec.  The trans() thread will use the payload to create the RTP packet before transmitting.
     
-  **parsePCMU**\ (packet)
-    This method is called by parsePacket().  It will decode the *packet*'s payload from PCMU to linear/raw audio and write it to the incoming :ref:`RTPPacketManager`.
+  **parsePCMU**\ (packet: :ref:`RTPMessage`) -> None
+    *Deprecated.* Please use ``parse_pcmu`` instead.
+    
+  **parse_pcmu**\ (packet: :ref:`RTPMessage`) -> None
+    This method is called by parse_packet().  It will decode the *packet*'s payload from PCMU to linear/raw audio and write it to the incoming :ref:`RTPPacketManager`.
+    
+  **encodePCMU**\ (payload: bytes) -> bytes
+    This method is called by encode_packet().  It will encode the *payload* into the PCMU audio codec.
+    
+  **parsePCMA**\ (packet: :ref:`RTPMessage`) -> None
+    This method is called by parse_packet().  It will decode the *packet*'s payload from PCMA to linear/raw audio and write it to the incoming :ref:`RTPPacketManager`.
     
-  **encodePCMU**\ (payload)
-    This method is called by encodePacket().  It will encode the *payload* into the PCMU audio codec.
+  **encodePCMA**\ (payload: bytes) -> bytes
+    *Deprecated.* Please use ``encode_pcma`` instead.
     
-  **parsePCMA**\ (packet)
-    This method is called by parsePacket().  It will decode the *packet*'s payload from PCMA to linear/raw audio and write it to the incoming :ref:`RTPPacketManager`.
+  **encode_pcma**\ (payload: bytes) -> bytes
+    This method is called by encode_packet().  It will encode the *payload* into the PCMA audio codec.
     
-  **encodePCMA**\ (payload)
-    This method is called by encodePacket().  It will encode the *payload* into the PCMA audio codec.
+  **parseTelephoneEvent**\ (packet: :ref:`RTPMessage`) -> None
+    *Deprecated* Please use ``parse_telephone_event`` instead.
     
-  **parseTelephoneEvent**\ (packet)
-    This method is called by parsePacket().  It will decode the *packet*'s payload from the telephone-event non-codec to the string representation of the event.  It will then call :ref:`VoIPCall`.dtmfCallback().
+  **parse_telephone_event**\ (packet: :ref:`RTPMessage`) -> None
+    This method is called by parse_packet().  It will decode the *packet*'s payload from the telephone-event non-codec to the string representation of the event.  It will then call :ref:`VoIPCall`.dtmf_callback().
     
diff --git a/docs/SIP.rst b/docs/SIP.rst
index cc4effc..b0fb78f 100644
--- a/docs/SIP.rst
+++ b/docs/SIP.rst
@@ -1,7 +1,7 @@
 SIP - Session Initiation Protocol
 ##################################
 
-The SIP module receives, parses, and responds to all incoming SIP requests/messages.  If appropriate, it then forwards them to the *callback* method of :ref:`VoIPPhone`.
+The SIP module receives, parses, and responds to incoming SIP requests/messages.  If appropriate, it then forwards them to the *callback* method of :ref:`VoIPPhone`.
 
 Errors
 *******
@@ -24,7 +24,7 @@ Enums
 ******
 
 SIP.\ **SIPMessageType**
-  SIPMessageType is an IntEnum with two attributes.  It's stored in ``SIPMessage.type`` to effectivly parse the message.
+  SIPMessageType is an IntEnum with two attributes.  It's stored in ``SIPMessage.type`` to effectively parse the message.
   
   SIPMessageType.\ **MESSAGE**
     This SIPMessageType is used to signify the message was a SIP request.
@@ -68,61 +68,112 @@ SIPClient
 
 The SIPClient class is used to communicate with the PBX/VoIP server.  It is responsible for registering with the server, and receiving phone calls.
 
-*class* SIP.\ **SIPClient**\ (server, port, username, password, myIP=None, myPort=5060, callCallback=None)
-    The *server* argument is your PBX/VoIP server’s IP, represented as a string.
+*class* SIP.\ **SIPClient**\ (server: str, port: int, username: str, password: str, myIP="0.0.0.0", myPort=5060, callCallback: Optional[Callable[[SIPMessage], None]] = None)
+    The *server* argument is your PBX/VoIP server's IP.
     
-    The *port* argument is your PBX/VoIP server’s port, represented as an integer.
+    The *port* argument is your PBX/VoIP server's port.
 
-    The *username* argument is your SIP account username on the PBX/VoIP server, represented as a string.
+    The *username* argument is your SIP account username on the PBX/VoIP server.
 
-    The *password* argument is your SIP account password on the PBX/VoIP server, represented as a string.
-    
-    The *myIP* argument is used to receive incoming SIP requests and responses. If left as None, the SIPClient will bind to 0.0.0.0.
+    The *password* argument is your SIP account password on the PBX/VoIP server.
+
+    The *myIP* argument is used to bind a socket and receive incoming SIP requests and responses.
 
     The *myPort* argument is the port SIPClient will bind to, to receive incoming SIP requests and responses. The default for this protocol is port 5060, but any port can be used.
 
     The *callCallback* argument is the callback function for :ref:`VoIPPhone`.  VoIPPhone will process the SIP request, and perform the appropriate actions.
 
-  **recv**\ ()
+  **recv**\ () -> None
     This method is called by SIPClient.start() and is responsible for receiving and parsing through SIP requests.  **This should not be called by the** :term:`user`.
     
-  **parseMessage**\ (message)
+  **parseMessage**\ (message: :ref:`SIPMessage`) -> None
+    *Deprecated.* Please use ``parse_message`` instead.
+
+  **parse_message**\ (message: :ref:`SIPMessage`) -> None
     This method is called by SIPClient.recv() and is responsible for parsing through SIP responses.  **This should not be called by the** :term:`user`.
     
-  **start**\ ()
+  **start**\ () -> None
     This method is called by :ref:`VoIPPhone`.start().  It starts the REGISTER and recv() threads.  It is also what initiates the bound port.  **This should not be called by the** :term:`user`.
     
-  **stop**\ ()
+  **stop**\ () -> None
     This method is called by :ref:`VoIPPhone`.stop(). It stops the REGISTER and recv() threads.  It will also close the bound port.  **This should not be called by the** :term:`user`.
     
-  **genCallID**\ ()
+  **genCallID**\ () -> str
+    *Deprecated.* **This should not be called by the** :term:`user`.
+
+  **gen_call_id**\ () -> str
     This method is called by other 'gen' methods when a new Call-ID header is needed.  See `RFC 3261 Section 20.8 <https://tools.ietf.org/html/rfc3261#section-20.8>`_.  **This should not be called by the** :term:`user`.
+
+  **lastCallID**\ () -> str
+    *Deprecated.*  **This should not be called by the** :term:`user`.
+
+  **last_call_id**\ () -> str
+    This method is called by other 'gen' methods when the last Call-ID header is needed.  See `RFC 3261 Section 20.8 <https://tools.ietf.org/html/rfc3261#section-20.8>`_.  **This should not be called by the** :term:`user`.
     
-  **genTag**\ ()
+  **genTag**\ () -> str
+    *Deprecated.* **This should not be called by the** :term:`user`.
+
+  **gen_tag**\ () -> str
     This method is called by other 'gen' methods when a new tag is needed. See `RFC 3261 Section 8.2.6.2 <https://tools.ietf.org/html/rfc3261#section-8.2.6.2>`_.  **This should not be called by the** :term:`user`.
     
-  **getSIPVersoinNotSupported**\ ()
+  **genSIPVersionNotSupported**\ () -> str
+    *Deprecated.* **This should not be called by the** :term:`user`.
+
+  **gen_sip_version_not_supported**\ () -> str
     This method is called by the recv() thread when it has received a SIP message that is not SIP version 2.0.
     
-  **genAuthorization**\ (request):
+  **genAuthorization**\ (request: :ref:`SIPMessage`) -> bytes
+    *Deprecated.* **This should not be called by the** :term:`user`.
+
+  **gen_authorization**\ (request: :ref:`SIPMessage`) -> bytes
     This calculates the authroization hash in response to the WWW-Authenticate header.  See `RFC 3261 Section 20.7 <https://tools.ietf.org/html/rfc3261#section-20.7>`_.  The *request* argument should be a 401 Unauthorized response.  **This should not be called by the** :term:`user`.
     
-  **genRegister**\ (request)
-    This method generates a SIP REGISTER request. The *request* argument should be a 401 Unauthorized response.   **This should not be called by the** :term:`user`.
+  **genRegister**\ (request: :ref:`SIPMessage`, deregister: bool = False) -> str
+    *Deprecated.* **This should not be called by the** :term:`user`.
+
+  **gen_register**\ (request: :ref:`SIPMessage`, deregister: bool = False) -> str
+    This method generates a SIP REGISTER request. The *request* argument should be a 401 Unauthorized response.  If *deregister* is set to true, a SIP DE-REGISTER request is generated instead.  **This should not be called by the** :term:`user`.
     
-  **genBusy**\ (request)
+  **genBusy**\ (request: :ref:`SIPMessage`) -> str
+    *Deprecated.* **This should not be called by the** :term:`user`.
+
+  **gen_busy**\ (request: :ref:`SIPMessage`) -> str
     This method generates a SIP 486 'Busy Here' response.  The *request* argument should be a SIP INVITE request.
     
-  **genOk**\ (request)
+  **genOk**\ (request: :ref:`SIPMessage`) -> str
+    *Deprecated.* **This should not be called by the** :term:`user`.
+
+  **gen_ok**\ (request: :ref:`SIPMessage`) -> str
     This method generates a SIP 200 'Ok' response.  The *request* argument should be a SIP BYE request.
     
-  **genInvite**\ (number, sess_id, ms, sendtype, branch, call_id)
-    This method generates a SIP INVITE request.  This is called by SIPClient.invite().  The *number* argument must be the number being called as a string.  The *sess_id* argument must be a unique number. The *ms* argument is a dictionary of the media types to be used.  Currently only PCMU and telephone-event is supported.  The *sendtype* argument must be an instance of :ref:`TransmitType`.  The *branch* argument must be a unique string starting with "z9hG4bK".  See `RFC 3261 Section 8.1.1.7 <https://tools.ietf.org/html/rfc3261#section-8.1.1.7>`_.  The *call_id* argument must be a unique string.  See `RFC 3261 Section 8.1.1.4 <https://tools.ietf.org/html/rfc3261#section-8.1.1.4>`_.
+  **genInvite**\ (number: str, sess_id: str, ms: dict[int, dict[str, RTP.\ :ref:`PayloadType<payload-type>`]], sendtype: RTP.\ :ref:`TransmitType`, branch: str, call_id: str) -> str
+    *Deprecated.* **This should not be called by the** :term:`user`.
+
+  **gen_invite**\ (number: str, sess_id: str, ms: dict[int, dict[str, RTP.\ :ref:`PayloadType<payload-type>`]], sendtype: RTP.\ :ref:`TransmitType`, branch: str, call_id: str) -> str
+    This method generates a SIP INVITE request.  This is called by SIPClient.invite().
+
+    The *number* argument must be the number being called as a string.
+
+    The *sess_id* argument must be a unique number.
+
+    The *ms* argument is a dictionary of the media types to be used.  Currently only PCMU and telephone-event is supported.
+
+    The *sendtype* argument must be an instance of :ref:`TransmitType`.
+
+    The *branch* argument must be a unique string starting with "z9hG4bK".  See `RFC 3261 Section 8.1.1.7 <https://tools.ietf.org/html/rfc3261#section-8.1.1.7>`_.
+
+    The *call_id* argument must be a unique string.  See `RFC 3261 Section 8.1.1.4 <https://tools.ietf.org/html/rfc3261#section-8.1.1.4>`_.
     
-  **genRinging**\ (request)
+  **genRinging**\ (request: :ref:`SIPMessage`) -> str
+    *Deprecated.* **This should not be called by the** :term:`user`.
+
+  **gen_ringing**\ (request: :ref:`SIPMessage`) -> str
     This method generates a SIP 180 'Ringing' response.  The *request* argument should be a SIP INVITE request.
     
-  **genAnswer**\ (request, sess_id, ms, sendtype)
+  **genAnswer**\ (request: :ref:`SIPMessage`, sess_id: str, ms: list[dict[str, Any]], sendtype: RTP.\ :ref:`TransmitType`)
+    *Deprecated.* **This should not be called by the** :term:`user`.
+
+  **gen_answer**\ (request: :ref:`SIPMessage`, sess_id: str, ms: list[dict[str, Any]], sendtype: RTP.\ :ref:`TransmitType`)
     This method generates a SIP 200 'OK' response.  Which, when in reply to an INVITE request, tells the server the :term:`user` has answered.  **This should not be called by the** :term:`user`.
     
     The *request* argument should be a SIP INVITE request.
@@ -131,24 +182,36 @@ The SIPClient class is used to communicate with the PBX/VoIP server.  It is resp
     
     The *ms* argument should be a list of parsed SDP m tags, found in the :ref:`SIPMessage`.body attribute.  This is used to generate the response SDP m tags.   See `RFC 4566 Section 5.14 <https://tools.ietf.org/html/rfc4566#section-5.14>`_.
     
-    The *sendtype* argument should be a RTP.\ :ref:`TransmitType<transmittype>` enum.  This will be used to generate the SDP a tag.   See `RFC 4566 Section 6 <https://tools.ietf.org/html/rfc4566#section-6>`_.
+    The *sendtype* argument should be a RTP.\ :ref:`TransmitType<transmittype>` enum.  This will be used to generate the SDP a tag.   See `RFC 4567 Section 6 <https://tools.ietf.org/html/rfc4567#section-6>`_.
     
-  **genBye**\ (request)
+  **genBye**\ (request: :ref:`SIPMessage`) -> str
+    *Deprecated.* **This should not be called by the** :term:`user`.
+
+  **gen_bye**\ (request: :ref:`SIPMessage`) -> str
     This method generates a SIP BYE request.  This is used to end a call. The *request* argument should be a SIP INVITE request.  **This should not be called by the** :term:`user`.
     
-  **genAck**\ (request)
+  **genAck**\ (request: :ref:`SIPMessage`) -> str
+    *Deprecated.* **This should not be called by the** :term:`user`.
+
+  **gen_ack**\ (request: :ref:`SIPMessage`) -> str
     This method generates a SIP ACK response.  The *request* argument should be a SIP 401 response.
     
-  **invite**\ (number, ms, sendtype)
-    This method generates a SIP INVITE request.  This method is called by :ref:`VoIPPhone`.call().  The *number* argument must be the number being called as a string.  The *ms* argument is a dictionary of the media types to be used.  Currently only PCMU and telephone-event is supported.  The *sendtype* argument must be an instance of :ref:`TransmitType`.  
+  **invite**\ (number: str, ms: dict[int, dict[str, RTP.\ :ref:`PayloadType<payload-type>`]], sendtype: RTP.\ :ref:`TransmitType`)
+    This method generates a SIP INVITE request.  This method is called by :ref:`VoIPPhone`.call().
+
+    The *number* argument must be the number being called as a string.
+
+    The *ms* argument is a dictionary of the media types to be used.  Currently only PCMU and telephone-event is supported.
+
+    The *sendtype* argument must be an instance of :ref:`TransmitType`.
     
-  **bye**\ (request)
+  **bye**\ (request: :ref:`SIPMessage`) -> None
     This method is called by :ref:`VoIPCall`.hangup().  It calls genBye(), and then transmits the generated request.  **This should not be called by the** :term:`user`.
     
-  **deregister**\ ()
+  **deregister**\ () -> bool
     This method is called by SIPClient.stop() after the REGISTER thread is stopped.  It will generate and transmit a REGISTER request with an Expiration of zero.  Telling the PBX/VoIP server it is turning off.  **This should not be called by the** :term:`user`.
     
-  **register**\ ()
+  **register**\ () -> bool
     This method is called by the REGISTER thread.  It will generate and transmit a REGISTER request telling the PBX/VoIP server that it will be online for at least 300 seconds.  The REGISTER thread will call this function every 295 seconds.  **This should not be called by the** :term:`user`.
     
 .. _SIPMessage:
@@ -158,7 +221,7 @@ SIPMessage
 
 The SIPMessage class is used to parse SIP requests and responses and makes them easily processed by other classes.
 
-*class* SIP.\ **SIPMessage**\ (data)
+*class* SIP.\ **SIPMessage**\ (data: bytes)
     The *data* argument is the SIP message in bytes.  It is then passed to SIPMessage.parse().
   
   SIPMessage has the following attributes:
@@ -187,20 +250,32 @@ The SIPMessage class is used to parse SIP requests and responses and makes them
     SIPMessage.\ **raw**
       This attribute is an unparsed version of the *data* argument, in bytes.
       
-  **summary**\ ()
+  **summary**\ () -> str
     This method returns a string representation of the SIP request.
     
-  **parse**\ (data)
+  **parse**\ (data: bytes) -> None
     This method is called by the initialization of the class.  It decides the SIPMessageType, and sends it to the corresponding parse function.  *Data* is the original *data* argument in the initialization of the class.  **This should not be called by the** :term:`user`.
 
-  **parseSIPResponse**\ (data)
+  **parseSIPResponse**\ (data: bytes) -> None
+    *Deprecated.* **This should not be called by the** :term:`user`.
+
+  **parse_sip_response**\ (data: bytes) -> None
     This method is called by parse().  It sets the *header*, *version*, and *status* attributes and may raise a :ref:`SIPParseError<sip-parse-error>` if the SIP response is an unsupported SIP version.  It then calls parseHeader() for each header in the request. *Data* is the original *data* argument in the initialization of the class.  **This should not be called by the** :term:`user`.
     
-  **parseSIPMessage**\ (data)
+  **parseSIPMessage**\ (data: bytes) -> None
+    *Deprecated.* **This should not be called by the** :term:`user`.
+
+  **parse_sip_message**\ (data: bytes) -> None
     This method is called by parse().  It sets the *header*, *version*, and *method* attributes and may raise a :ref:`SIPParseError<sip-parse-error>` if the SIP request is an unsupported SIP version.  It then calls parseHeader() and parseBody() for each header or tag in the request respectively. *Data* is the original *data* argument in the initialization of the class.  **This should not be called by the** :term:`user`.
     
-  **parseHeader**\ (header, data)
+  **parseHeader**\ (header: str, data: str) -> None
+    *Deprecated.* **This should not be called by the** :term:`user`.
+
+  **parse_header**\ (header: str, data: str) -> None
     This method is called by parseSIPResponse() and parseSIPMessage().  The *header* argument is the name of the header, i.e. 'Call-ID' or 'CSeq', represented as a string.  The *data* argument is the value of the header, i.e. 'Ogq-T7iBmNozoUu3GL9Lvg..' or '1 INVITE', represented as a string.  **This should not be called by the** :term:`user`.
     
-  **parseBody**\ (header, data)
+  **parseBody**\ (header: str, data: str) -> None
+    *Deprecated.* **This should not be called by the** :term:`user`.
+
+  **parse_body**\ (header: str, data: str) -> None
     This method is called by parseSIPResponse() and parseSIPMessage().  The *header* argument is the name of the SDP tag, i.e. 'm' or 'a', represented as a string.  The *data* argument is the value of the header, i.e. 'audio 56704 RTP/AVP 0' or 'sendrecv', represented as a string.  **This should not be called by the** :term:`user`.
diff --git a/docs/VoIP.rst b/docs/VoIP.rst
index 7a36d94..f8cf0ce 100644
--- a/docs/VoIP.rst
+++ b/docs/VoIP.rst
@@ -11,10 +11,10 @@ There are two errors under ``pyVoIP.VoIP``.
 .. _invalidstateerror:
 
 *exception* VoIP.\ **InvalidStateError**
-  This is thrown when you try to run :ref:`VoIPCall` when you try to perform an action that cannot be performed during the current :ref:`CallState<callstate>`.  For example denying a call that has already been answered, hanging up a call that hasn't been answered yet, or has already been ended.
+  This is thrown by :ref:`VoIPCall` when you try to perform an action that cannot be performed during the current :ref:`CallState<callstate>`.  For example denying a call that has already been answered, hanging up a call that hasn't been answered yet, or has already been ended.
   
 *exception* VoIP.\ **InvalidRangeError**
-  This is thrown by :ref:`VoIPPhone` when you define the rtpPort ranges as rtpPortLow > rtpPortHigh.  However this is not checked by :ref:`VoIPCall`, so if you are using your own class instead of VoIPPhone, make sure these ranges are correct.
+  This is thrown by :ref:`VoIPPhone` when you define the rtpPort ranges as rtpPortLow > rtpPortHigh.  However, this is not checked by :ref:`VoIPCall`, so if you are using your own class instead of VoIPPhone, make sure these ranges are correct.
   
 *exception* VoIP.\ **NoPortsAvailableError**
   This is thrown when a call is attempting to be initiated but no ports are available.
@@ -45,6 +45,26 @@ VoIP.\ **CallState**
     
     In this state, you can not use any functions.
 
+.. _phonestatus
+
+VoIP.\ **PhoneStatus**
+  PhoneStatus is an Enum with five attributes.
+
+  PhoneStatus.\ **INACTIVE**
+    This PhoneStatus is used when ``VoIPPhone.start()`` has not been called, or after the phone has fully stopped after calling ``VoIPPhone.stop()``.
+
+  PhoneStatus.\ **REGISTERING**
+    This PhoneStatus is used when ``VoIPPhone.start()`` has been called, but has not finished starting.
+
+  PhoneStatus.\ **REGISTERED**
+    This PhoneStatus is used when ``VoIPPhone`` has finished starting successfully, and is ready for use.
+
+  PhoneStatus.\ **DEREGISTERING**
+    This PhoneStatus is used when ``VoIPPhone.stop()`` has been called, but has not finished stopping.
+
+  PhoneStatus.\ **FAILED**
+    This PhoneStatus is used when ``VoIPPhone.start()`` has been called, but failed to start due to an error.
+
 Classes
 ********
 
@@ -55,7 +75,7 @@ VoIPCall
 
 The VoIPCall class is used to represent a single VoIP Session, which may be to multiple :term:`clients<client>`.
 
-*class* VoIP.\ **VoIPCall**\ (phone, callstate, request, session_id, myIP, ms)
+*class* VoIP.\ **VoIPCall**\ (phone: :ref:`VoIPPhone`, request: :ref:`SIPMessage`, session_id: int, myIP: str, rtpPortLow: int, rtpPortHigh: int)
       The *phone* argument is the initating instance of :ref:`VoIPPhone`.
      
       The *callstate* arguement is the initiating :ref:`CallState<callstate>`.
@@ -69,43 +89,55 @@ The VoIPCall class is used to represent a single VoIP Session, which may be to m
       The *ms* arguement is a dictionary with int as the key and a :ref:`PayloadType<payload-type>` as the value.  This is only used when originating the call.
      
      
-    **dtmfCallback**\ (code)
+    **dtmfCallback**\ (code: str) -> None
+      *Deprecated.* Please use ``dtmf_callback`` instead.
+
+    **dtmf_callback**\ (code: str) -> None
       This method is called by :ref:`RTPClient`'s when a telephone-event DTMF message is received.  The *code* argument is a string.  It should be an Event in complinace with `RFC 4733 Section 3.2 <https://tools.ietf.org/html/rfc4733#section-3.2>`_.
        
-    **getDTMF**\ (length=1)
+    **getDTMF**\ (length=1) -> str
+      *Deprecated.* Please use ``get_dtmf`` instead.
+
+    **get_dtmf**\ (length=1) -> str
       This method can be called get the next pressed DTMF key.  DTMF's are stored in an ``io.StringIO`` and act as a stack.  Meaning if the :term:`client` presses the numbers 1-9-5 you'll have the following output:
        
       .. code-block:: python
        
-        VoIPCall.getDTMF()
+        VoIPCall.get_dtmf()
         >>> '1'
-        VoIPCall.getDTMF(length=2)
+        VoIPCall.get_dtmf(length=2)
         >>> '95'
-        VoIPCall.getDTMF()
+        VoIPCall.get_dtmf()
         >>> ''
       
       As you can see, calling this method when there a key has not been pressed returns an empty string.
       
-    **answer**\ ()
+    **answer**\ () -> None
       Answers the call if the phone's state is CallState.RINGING.
       
-    **answered**\ (request)
+    **answered**\ (request: :ref:`SIPMessage`) -> None
       This function is called by :ref:`SIPClient` when a call originated by the :term:`user` has been answered by the :term:`client`.
       
-    **deny**\ ()
+    **deny**\ () -> None
       Denies the call if the phone's state is CallState.RINGING.
       
-    **hangup**\ ()
+    **hangup**\ () -> None
       Ends the call if the phone's state is CallState.ANSWRED.
     
-    **bye**\ ()
+    **bye**\ () -> None
       Ends the call but does not send a SIP BYE message to the SIP server.  This function is used to end the call on the server side when the client ended the call.  **THE** :term:`USER<user>` **SHOUND NOT CALL THIS FUNCTION OR THE** :term:`CLIENT<client>` **WILL BE LEFT ON THE LINE WITH NO RESPONSE. CALL HANGUP() INSTEAD.**
       
-    **writeAudio**\ (data)
+    **writeAudio**\ (data: bytes) -> None
+      *Deprecated.* Please use ``write_audio`` instead.
+
+    **write_audio**\ (data: bytes) -> None
       Writes linear/raw audio data to the transmit buffer before being encoded and sent.  The *data* argument MUST be bytes.  **This audio must be linear/not encoded,** :ref:`RTPClient` **will encode it before transmitting.**
       
-    **readAudio**\ (length=160, blocking=True)
-      Reads linear/raw audio data from the received buffer.  Returns *length* amount of bytes.  Default length is 160 as that is the amount of bytes sent per PCMU/PCMA packet.  When *blocking* is set to true, this function will not return until data is available.  When *blocking* is set to false and data is not available, this function will return bytes(length).
+    **readAudio**\ (length=160, blocking=True) -> bytes
+      *Deprecated.* Please use ``read_audio`` instead.
+
+    **read_audio**\ (length=160, blocking=True) -> bytes
+      Reads linear/raw audio data from the received buffer.  Returns *length* amount of bytes.  Default length is 160 as that is the amount of bytes sent per PCMU/PCMA packet.  When *blocking* is set to true, this function will not return until data is available.  When *blocking* is set to false and data is not available, this function will return ``b"\x80" * length``.
     
 .. _VoIPPhone:
 
@@ -114,7 +146,7 @@ VoIPPhone
 
 The VoIPPhone class is used to manage the :ref:`SIPClient` class and create :ref:`VoIPCall`'s when there is an incoming call.  It then passes the VoIPCall as the argument in the callback.
 
-*class* VoIP.\ **VoIPPhone**\ (server, port, username, password, callCallback=None, myIP=None, sipPort=5060, rtpPortLow=10000, rtpPortHigh=20000)
+*class* VoIP.\ **VoIPPhone**\ (server: str, port: int, username: str, password: str, callCallback: Optional[Callable] = None, myIP: Optional[str] = None, sipPort=5060, rtpPortLow=10000, rtpPortHigh=20000)
     The *server* argument is your PBX/VoIP server's IP, represented as a string.
     
     The *port* argument is your PBX/VoIP server's port, represented as an integer.
@@ -131,21 +163,27 @@ The VoIPPhone class is used to manage the :ref:`SIPClient` class and create :ref
     
     The *rtpPortLow* and *rtpPortHigh* arguments are used to generate random ports to use for audio transfer.  Per RFC 4566 Sections `5.7 <https://tools.ietf.org/html/rfc4566#section-5.7>`_ and `5.14 <https://tools.ietf.org/html/rfc4566#section-5.14>`_, it can take multiple ports to fully communicate with other :term:`clients<client>`, as such a large range is recommended.  If an invalid range is given, a :ref:`InvalidStateError<invalidstateerror>` will be thrown.
     
-  **callback**\ (request)
-    This method is called by the :ref:`SIPClient` when an INVITE or BYE request is received.  This method then creates a :ref:`VoIPCall` or terminates it respectively.  When a VoIPCall is created, it will then pass it to the *callCallback* function as an argument.  If *callCallback* is set to None, this function replies as BUSY. **This function should not be called by the** :term:`user`.
+  **callback**\ (request: :ref:`SIPMessage`) -> None
+    This method is called by the :ref:`SIPClient` when an INVITE or BYE request is received.  This function then creates a :ref:`VoIPCall` or terminates it respectively.  When a VoIPCall is created, it will then pass it to the *callCallback* function as an argument.  If *callCallback* is set to None, this function replies as BUSY. **This function should not be called by the** :term:`user`.
+
+  **getStatus**\ () -> PhoneStatus
+    *Deprecated.* Please use ``get_status`` instead.
+
+  **get_status**\ () -> PhoneStatus
+    This method returns the :ref:`PhoneStatus<phonestatus>`.
     
-  **request_port**\ (blocking=True)
+  **request_port**\ (blocking=True) -> int
     This method is called when a new port is needed to use in a :ref:`VoIPCall`.  If blocking is set to True, this will wait until a port is available.  Otherwise, it will raise NoPortsAvailableError.
     
-  **release_ports**\ (call=None)
+  **release_ports**\ (call: Optional[VoIPCall] = None) -> None
     This method is called when a call ends.  If call is provided, it will only release the ports used by that :ref:`VoIPCall`.  Otherwise, it will iterate through all active calls, and release all ports that are no longer in use.
     
-  **start**\ ()
+  **start**\ () -> None
     This method starts the :ref:`SIPClient` class.  On failure, this will automatically call stop().
     
-  **stop**\ ()
+  **stop**\ () -> None
     This method ends all currently ongoing calls, then stops the :ref:`SIPClient` class
   
-  **call**\ (number)
+  **call**\ (number: str) -> :ref:`VoIPCall`
     Originates a call using PCMU and telephone-event. The *number* argument must be a string, and it returns a :ref:`VoIPCall` class in CallState.DIALING.  You should use a while loop to wait until the CallState is ANSWRED.
   
diff --git a/docs/conf.py b/docs/conf.py
index fcdf47a..85c4518 100644
--- a/docs/conf.py
+++ b/docs/conf.py
@@ -20,11 +20,11 @@
 # -- Project information -----------------------------------------------------
 
 project = 'pyVoIP'
-copyright = '2021, Tayler Porter'
+copyright = '2022, Tayler Porter'
 author = 'Tayler J Porter'
 
 # The full version, including alpha/beta/rc tags
-release = '1.5.6'
+release = '1.6.0'
 
 master_doc = 'index'
 
diff --git a/docs/index.rst b/docs/index.rst
index be8c0b4..3da0a8b 100644
--- a/docs/index.rst
+++ b/docs/index.rst
@@ -27,6 +27,7 @@ In this documentation we will use the following terms:
    :caption: Contents:
 
    Examples
+   Globals
    VoIP
    SIP
    RTP
diff --git a/pyVoIP/RTP.py b/pyVoIP/RTP.py
index a040b6a..90e4208 100644
--- a/pyVoIP/RTP.py
+++ b/pyVoIP/RTP.py
@@ -1,5 +1,6 @@
-from enum import Enum, IntEnum
+from enum import Enum
 from threading import Timer
+from typing import Callable, Dict, Optional, Union
 import audioop
 import io
 import pyVoIP
@@ -7,356 +8,557 @@
 import socket
 import threading
 import time
+import warnings
+
+
+__all__ = [
+    "add_bytes",
+    "byte_to_bits",
+    "DynamicPayloadType",
+    "PayloadType",
+    "RTPParseError",
+    "RTPProtocol",
+    "RTPPacketManager",
+    "RTPClient",
+    "TransmitType",
+]
+
 
-__all__ = ['add_bytes', 'byte_to_bits', 'DynamicPayloadType', 'PayloadType', 'RTPParseError', 'RTPProtocol', 'RTPPacketManager', 'RTPClient', 'TransmitType']
 debug = pyVoIP.debug
 
-def byte_to_bits(byte):
-  byte = bin(ord(byte)).lstrip('-0b')
-  byte = ("0"*(8-len(byte)))+byte
-  return byte
-    
-def add_bytes(byte_string):
-  binary = ""
-  for byte in byte_string:
-    byte = bin(byte).lstrip('-0b')
-    byte = ("0"*(8-len(byte)))+byte
-    binary += byte
-  return int(binary, 2)
+
+def byte_to_bits(byte: bytes) -> str:
+    nbyte = bin(ord(byte)).lstrip("-0b")
+    nbyte = ("0" * (8 - len(nbyte))) + nbyte
+    return nbyte
+
+
+def add_bytes(byte_string: bytes) -> int:
+    binary = ""
+    for byte in byte_string:
+        nbyte = bin(byte).lstrip("-0b")
+        nbyte = ("0" * (8 - len(nbyte))) + nbyte
+        binary += nbyte
+    return int(binary, 2)
+
 
 class DynamicPayloadType(Exception):
-  pass
-  
+    pass
+
+
 class RTPParseError(Exception):
-  pass
+    pass
+
 
 class RTPProtocol(Enum):
-  UDP = 'udp'
-  AVP = 'RTP/AVP'
-  SAVP = 'RTP/SAVP'
+    UDP = "udp"
+    AVP = "RTP/AVP"
+    SAVP = "RTP/SAVP"
+
 
 class TransmitType(Enum):
-  RECVONLY = 'recvonly'
-  SENDRECV = 'sendrecv'
-  SENDONLY = 'sendonly'
-  INACTIVE = 'inactive'
-  
-  def __str__(self):
-    return self.value
+    RECVONLY = "recvonly"
+    SENDRECV = "sendrecv"
+    SENDONLY = "sendonly"
+    INACTIVE = "inactive"
+
+    def __str__(self):
+        return self.value
+
 
 class PayloadType(Enum):
-  def __new__(cls, value, clock, channel, description):
-    obj = object.__new__(cls)
-    obj._value_ = value
-    obj.rate = clock
-    obj.channel = channel
-    obj.description = description
-    return obj
-  
-  def __int__(self):
-    try:
-      return int(self.value)
-    except ValueError:
-      pass
-    raise DynamicPayloadType(self.description + " is a dynamically assigned payload")
-    
-  def __str__(self):
-    if type(self.value)==int:
-      return self.description
-    return str(self.value)
-  
-  #Audio
-  PCMU = 0, 8000, 1, "PCMU"
-  GSM = 3, 8000, 1, "GSM"
-  G723 = 4, 8000, 1, "G723"
-  DVI4_8000 = 5, 8000, 1, "DVI4"
-  DVI4_16000 = 6, 16000, 1, "DVI4"
-  LPC = 7, 8000, 1, "LPC"
-  PCMA = 8, 8000, 1, "PCMA"
-  G722 = 9, 8000, 1, "G722"
-  L16_2 = 10, 44100, 2, "L16"
-  L16 = 11, 44100, 1, "L16"
-  QCELP = 12, 8000, 1, "QCELP"
-  CN = 13, 8000, 1, "CN"
-  MPA = 14, 90000, 0, "MPA" #MPA channel varries, should be defined in the RTP packet.
-  G728 = 15, 8000, 1, "G728"
-  DVI4_11025 = 16, 11025, 1, "DVI4"
-  DVI4_22050 = 17, 22050, 1, "DVI4"
-  G729 = 18, 8000, 1, "G729"
-  
-  #Video
-  CELB = 25, 90000, 0, "CelB"
-  JPEG = 26, 90000, 0, "JPEG"
-  NV = 28, 90000, 0, "nv"
-  H261 = 31, 90000, 0, "H261"
-  MPV = 32, 90000, 0, "MPV"
-  MP2T = 33, 90000, 1, "MP2T" #MP2T is both audio and video per RFC 3551 July 2003 5.7
-  H263 = 34, 90000, 0, "H263"
-  
-  #Non-codec
-  EVENT = "telephone-event", 8000, 0, "telephone-event"
-  UNKOWN = "UNKOWN", 0, 0, "UNKOWN CODEC"
-
-class RTPPacketManager(): 
-  def __init__(self):
-    self.offset = 4294967296 #The largest number storable in 4 bytes + 1.  This will ensure the offset adjustment in self.write(offset, data) works.
-    self.buffer = io.BytesIO()
-    self.bufferLock = threading.Lock()
-    self.log = {}
-    self.rebuilding = False
-    
-  def read(self, length=160):
-    while self.rebuilding: #This acts functionally as a lock while the buffer is being rebuilt.
-      time.sleep(0.01)
-    self.bufferLock.acquire()
-    packet = self.buffer.read(length)
-    if len(packet)<length:
-      packet = packet + (b'\x80' * (length-len(packet)))
-    self.bufferLock.release()
-    return packet
-    
-  def rebuild(self, reset, offset=0, data=b''):
-    self.rebuilding = True
-    if reset:
-      self.log={}
-      self.log[offset] = data
-      self.buffer = io.BytesIO(data)
-    else:
-      bufferloc = self.buffer.tell()
-      self.buffer = io.BytesIO()
-      for pkt in self.log:
-        self.write(pkt, self.log[pkt])
-      self.buffer.seek(bufferloc, 0)
-    self.rebuilding = False
-    
-  def write(self, offset, data):
-    self.bufferLock.acquire()
-    self.log[offset] = data
-    bufferloc = self.buffer.tell()
-    if offset<self.offset:
-      reset = (abs(offset - self.offset)>=100000) #If the new timestamp is over 100,000 bytes before the earliest, erase the buffer.  This will stop memory errors.
-      self.offset = offset
-      self.bufferLock.release()
-      self.rebuild(reset, offset, data) #Rebuilds the buffer if something before the earliest timestamp comes in, this will stop overwritting.
-      return
-    offset = offset - self.offset
-    self.buffer.seek(offset, 0)
-    self.buffer.write(data)
-    self.buffer.seek(bufferloc, 0)
-    self.bufferLock.release()
-    
-class RTPMessage():
-  def __init__(self, data, assoc):
-    self.RTPCompatibleVersions = pyVoIP.RTPCompatibleVersions
-    self.assoc = assoc
-    
-    self.parse(data)
-  
-  def summary(self):
-    data = ""
-    data += "Version: "+str(self.version)+"\n"
-    data += "Padding: "+str(self.padding)+"\n"
-    data += "Extension: "+str(self.extension)+"\n"
-    data += "CC: "+str(self.CC)+"\n"
-    data += "Marker: "+str(self.marker)+"\n"
-    data += "Payload Type: "+str(self.payload_type)+" ("+str(self.payload_type.value)+")"+"\n"
-    data += "Sequence Number: "+str(self.sequence)+"\n"
-    data += "Timestamp: "+str(self.timestamp)+"\n"
-    data += "SSRC: "+str(self.SSRC)+"\n"
-    return data
-  
-  def parse(self, packet):
-    byte = byte_to_bits(packet[0:1])
-    self.version = int(byte[0:2], 2)
-    if not self.version in self.RTPCompatibleVersions:
-      raise RTPParseError("RTP Version {} not compatible.".format(self.version))
-    self.padding = bool(int(byte[2], 2))
-    self.extension = bool(int(byte[3], 2)) 
-    self.CC = int(byte[4:], 2)
-    
-    byte = byte_to_bits(packet[1:2])
-    self.marker = bool(int(byte[0], 2))
-    
-    pt = int(byte[1:], 2)
-    if pt in self.assoc:
-      self.payload_type = self.assoc[pt]
-    else:
-      try:
-        self.payload_type = PayloadType(pt)
-        e = False
-      except ValueError:
-        e = True
-      if e:
-        raise RTPParseError("RTP Payload type {} not found.".format(str(pt)))
-        
-    self.sequence = add_bytes(packet[2:4])
-    self.timestamp = add_bytes(packet[4:8])
-    self.SSRC = add_bytes(packet[8:12])
-    
-    self.CSRC = []
-    
-    i = 12
-    for x in range(self.CC):
-      self.CSRC.append(packet[i:i+4])
-      i += 4
-    
-    if self.extension:
-      pass
-      
-    
-    self.payload = packet[i:]
-    
-class RTPClient():
-  def __init__(self, assoc, inIP, inPort, outIP, outPort, sendrecv, dtmf = None):
-    self.NSD = True
-    self.assoc = assoc # Example: {0: PayloadType.PCMU, 101: PayloadType.EVENT}
-    debug("Selecting audio codec for transmission")
-    for m in assoc:
-      try:
-        if int(assoc[m]) is not None:
-          debug(f"Selected {assoc[m]}")
-          self.preference = assoc[m] #Select the first available actual codec to encode with.  TODO: will need to change if video codecs are ever implemented.
-          break
-      except Exception:
-        debug(f"{assoc[m]} cannot be selected as an audio codec")
-    
-    self.inIP = inIP
-    self.inPort = inPort
-    self.outIP = outIP
-    self.outPort = outPort
-    
-    self.dtmf = dtmf
-    
-    self.pmout = RTPPacketManager() #To Send
-    self.pmin = RTPPacketManager() #Received
-    self.outOffset = random.randint(1,5000)
-    
-    self.outSequence = random.randint(1,100)
-    self.outTimestamp = random.randint(1,10000)
-    self.outSSRC = random.randint(1000,65530)
-    
-  def start(self):
-    self.sin = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
-    self.sout = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
-    self.sin.bind((self.inIP, self.inPort))
-    self.sin.setblocking(False)
-    
-    r = Timer(1, self.recv)
-    r.name = "RTP Receiver"
-    r.start()
-    t = Timer(1, self.trans)
-    t.name = "RTP Transmitter"
-    t.start()
-    
-  def stop(self):
-    self.NSD = False
-    self.sin.close()
-    self.sout.close()
-    
-  def read(self, length=160, blocking=True):
-    if not blocking:
-      return self.pmin.read(length)
-    packet = self.pmin.read(length)
-    while packet == (b'\x80'*length) and self.NSD:
-      time.sleep(0.01)
-      packet = self.pmin.read(length)
-    return packet
-    
-  def write(self, data):
-    self.pmout.write(self.outOffset, data)
-    self.outOffset += len(data)
-    
-  def recv(self):
-    while self.NSD:
-      try:
-        packet = self.sin.recv(8192)
-        self.parsePacket(packet)
-      except BlockingIOError:
-        time.sleep(0.01)
-      except RTPParseError as e:
-        debug(str(e))
-      except OSError:
-        pass
-    
-  def trans(self):
-    while self.NSD:
-      payload = self.pmout.read()
-      payload = self.encodePacket(payload)
-      packet = b"\x80" #RFC 1889 V2 No Padding Extension or CC.
-      packet += chr(int(self.preference)).encode('utf8')
-      try:
-        packet += self.outSequence.to_bytes(2, byteorder='big')
-      except OverflowError:
-        self.outSequence = 0
-      try:
-        packet += self.outTimestamp.to_bytes(4, byteorder='big')
-      except OverflowError:
-        self.outTimestamp = 0
-      packet += self.outSSRC.to_bytes(4, byteorder='big')
-      packet += payload
-      
-      #debug(payload)
-      
-      try:
-        self.sout.sendto(packet, (self.outIP, self.outPort))
-      except OSError:
-        pass
-      
-      self.outSequence += 1
-      self.outTimestamp += len(payload)
-      time.sleep((1/self.preference.rate)*160) #1/8000 *160
-    
-  def parsePacket(self, packet):
-    packet = RTPMessage(packet, self.assoc)
-    if packet.payload_type == PayloadType.PCMU:
-      self.parsePCMU(packet)
-    elif packet.payload_type == PayloadType.PCMA:
-      self.parsePCMA(packet)
-    elif packet.payload_type == PayloadType.EVENT:
-      self.parseTelephoneEvent(packet)
-    else:
-      raise RTPParseError("Unsupported codec (parse): "+str(packet.payload_type))
-  
-  def encodePacket(self, payload):
-    if self.preference == PayloadType.PCMU:
-      return self.encodePCMU(payload)
-    elif self.preference == PayloadType.PCMA:
-      return self.encodePCMA(payload)
-    else:
-      raise RTPParseError("Unsupported codec (encode): "+str(self.preference))
-  
-  def parsePCMU(self, packet):
-    data = audioop.ulaw2lin(packet.payload, 1)
-    data = audioop.bias(data, 1, 128)
-    self.pmin.write(packet.timestamp, data)
-    
-  def encodePCMU(self, packet):
-    packet = audioop.bias(packet, 1, -128)
-    packet = audioop.lin2ulaw(packet, 1)
-    return packet
-    
-  def parsePCMA(self, packet):
-    data = audioop.alaw2lin(packet.payload, 1)
-    data = audioop.bias(data, 1, 128)
-    self.pmin.write(packet.timestamp, data)
-    
-  def encodePCMA(self, packet):
-    packet = audioop.bias(packet, 1, -128)
-    packet = audioop.lin2alaw(packet, 1)
-    return packet
-
-  def parseTelephoneEvent(self, packet):
-    key = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '*', '#', 'A', 'B', 'C', 'D']
-    end = False
-
-    payload = packet.payload
-    event = key[payload[0]]
-    byte = byte_to_bits(payload[1:2])
-    if byte[0]=='1':
-      end = True
-    volume = int(byte[2:], 2)
-    
-    if packet.marker:
-      if self.dtmf != None:
-        self.dtmf(event)
-      
+    def __new__(
+        cls,
+        value: Union[int, str],
+        clock: int = 0,
+        channel: int = 0,
+        description: str = "",
+    ):
+        obj = object.__new__(cls)
+        obj._value_ = value
+        obj.rate = clock
+        obj.channel = channel
+        obj.description = description
+        return obj
+
+    @property
+    def rate(self) -> int:
+        return self._rate
+
+    @rate.setter
+    def rate(self, value: int) -> None:
+        self._rate = value
+
+    @property
+    def channel(self) -> int:
+        return self._channel
+
+    @channel.setter
+    def channel(self, value: int) -> None:
+        self._channel = value
+
+    @property
+    def description(self) -> str:
+        return self._description
+
+    @description.setter
+    def description(self, value: str) -> None:
+        self._description = value
+
+    def __int__(self) -> int:
+        try:
+            return int(self.value)
+        except ValueError:
+            pass
+        raise DynamicPayloadType(
+            self.description + " is a dynamically assigned payload"
+        )
+
+    def __str__(self) -> str:
+        if isinstance(self.value, int):
+            return self.description
+        return str(self.value)
+
+    # Audio
+    PCMU = 0, 8000, 1, "PCMU"
+    GSM = 3, 8000, 1, "GSM"
+    G723 = 4, 8000, 1, "G723"
+    DVI4_8000 = 5, 8000, 1, "DVI4"
+    DVI4_16000 = 6, 16000, 1, "DVI4"
+    LPC = 7, 8000, 1, "LPC"
+    PCMA = 8, 8000, 1, "PCMA"
+    G722 = 9, 8000, 1, "G722"
+    L16_2 = 10, 44100, 2, "L16"
+    L16 = 11, 44100, 1, "L16"
+    QCELP = 12, 8000, 1, "QCELP"
+    CN = 13, 8000, 1, "CN"
+    # MPA channel varries, should be defined in the RTP packet.
+    MPA = 14, 90000, 0, "MPA"
+    G728 = 15, 8000, 1, "G728"
+    DVI4_11025 = 16, 11025, 1, "DVI4"
+    DVI4_22050 = 17, 22050, 1, "DVI4"
+    G729 = 18, 8000, 1, "G729"
+
+    # Video
+    CELB = 25, 90000, 0, "CelB"
+    JPEG = 26, 90000, 0, "JPEG"
+    NV = 28, 90000, 0, "nv"
+    H261 = 31, 90000, 0, "H261"
+    MPV = 32, 90000, 0, "MPV"
+    # MP2T is both audio and video per RFC 3551 July 2003 5.7
+    MP2T = 33, 90000, 1, "MP2T"
+    H263 = 34, 90000, 0, "H263"
+
+    # Non-codec
+    EVENT = "telephone-event", 8000, 0, "telephone-event"
+    UNKNOWN = "UNKNOWN", 0, 0, "UNKNOWN CODEC"
+
+
+class RTPPacketManager:
+    def __init__(self):
+        self.offset = 4294967296
+        """
+        The largest number storable in 4 bytes + 1. This will ensure the
+        offset adjustment in self.write(offset, data) works.
+        """
+        self.buffer = io.BytesIO()
+        self.bufferLock = threading.Lock()
+        self.log = {}
+        self.rebuilding = False
+
+    def read(self, length: int = 160) -> bytes:
+        # This acts functionally as a lock while the buffer is being rebuilt.
+        while self.rebuilding:
+            time.sleep(0.01)
+        self.bufferLock.acquire()
+        packet = self.buffer.read(length)
+        if len(packet) < length:
+            packet = packet + (b"\x80" * (length - len(packet)))
+        self.bufferLock.release()
+        return packet
+
+    def rebuild(self, reset: bool, offset: int = 0, data: bytes = b"") -> None:
+        self.rebuilding = True
+        if reset:
+            self.log = {}
+            self.log[offset] = data
+            self.buffer = io.BytesIO(data)
+        else:
+            bufferloc = self.buffer.tell()
+            self.buffer = io.BytesIO()
+            for pkt in self.log:
+                self.write(pkt, self.log[pkt])
+            self.buffer.seek(bufferloc, 0)
+        self.rebuilding = False
+
+    def write(self, offset: int, data: bytes) -> None:
+        self.bufferLock.acquire()
+        self.log[offset] = data
+        bufferloc = self.buffer.tell()
+        if offset < self.offset:
+            """
+            If the new timestamp is over 100,000 bytes before the
+            earliest, erase the buffer.  This will stop memory errors.
+            """
+            reset = abs(offset - self.offset) >= 100000
+            self.offset = offset
+            self.bufferLock.release()
+            """
+            Rebuilds the buffer if something before the earliest
+            timestamp comes in, this will stop overwritting.
+            """
+            self.rebuild(reset, offset, data)
+            return
+        offset = offset - self.offset
+        self.buffer.seek(offset, 0)
+        self.buffer.write(data)
+        self.buffer.seek(bufferloc, 0)
+        self.bufferLock.release()
+
+
+class RTPMessage:
+    def __init__(self, data: bytes, assoc: Dict[int, PayloadType]):
+        self.RTPCompatibleVersions = pyVoIP.RTPCompatibleVersions
+        self.assoc = assoc
+        # Setting defaults to stop mypy from complaining
+        self.version = 0
+        self.padding = False
+        self.extension = False
+        self.CC = 0
+        self.marker = False
+        self.payload_type = PayloadType.UNKNOWN
+        self.sequence = 0
+        self.timestamp = 0
+        self.SSRC = 0
+
+        self.parse(data)
+
+    def summary(self) -> str:
+        data = ""
+        data += f"Version: {self.version}\n"
+        data += f"Padding: {self.padding}\n"
+        data += f"Extension: {self.extension}\n"
+        data += f"CC: {self.CC}\n"
+        data += f"Marker: {self.marker}\n"
+        data += (
+            f"Payload Type: {self.payload_type} "
+            + f"({self.payload_type.value})\n"
+        )
+        data += f"Sequence Number: {self.sequence}\n"
+        data += f"Timestamp: {self.timestamp}\n"
+        data += f"SSRC: {self.SSRC}\n"
+        return data
+
+    def parse(self, packet: bytes) -> None:
+        byte = byte_to_bits(packet[0:1])
+        self.version = int(byte[0:2], 2)
+        if self.version not in self.RTPCompatibleVersions:
+            raise RTPParseError(f"RTP Version {self.version} not compatible.")
+        self.padding = bool(int(byte[2], 2))
+        self.extension = bool(int(byte[3], 2))
+        self.CC = int(byte[4:], 2)
+
+        byte = byte_to_bits(packet[1:2])
+        self.marker = bool(int(byte[0], 2))
+
+        pt = int(byte[1:], 2)
+        if pt in self.assoc:
+            self.payload_type = self.assoc[pt]
+        else:
+            try:
+                self.payload_type = PayloadType(pt)
+                e = False
+            except ValueError:
+                e = True
+            if e:
+                raise RTPParseError(f"RTP Payload type {pt} not found.")
+
+        self.sequence = add_bytes(packet[2:4])
+        self.timestamp = add_bytes(packet[4:8])
+        self.SSRC = add_bytes(packet[8:12])
+
+        self.CSRC = []
+
+        i = 12
+        for x in range(self.CC):
+            self.CSRC.append(packet[i : i + 4])
+            i += 4
+
+        if self.extension:
+            pass
+
+        self.payload = packet[i:]
+
+
+class RTPClient:
+    def __init__(
+        self,
+        assoc: Dict[int, PayloadType],
+        inIP: str,
+        inPort: int,
+        outIP: str,
+        outPort: int,
+        sendrecv: TransmitType,
+        dtmf: Optional[Callable[[str], None]] = None,
+    ):
+        self.NSD = True
+        # Example: {0: PayloadType.PCMU, 101: PayloadType.EVENT}
+        self.assoc = assoc
+        debug("Selecting audio codec for transmission")
+        for m in assoc:
+            try:
+                if int(assoc[m]) is not None:
+                    debug(f"Selected {assoc[m]}")
+                    """
+                    Select the first available actual codec to encode with.
+                    TODO: will need to change if video codecs
+                    are ever implemented.
+                    """
+                    self.preference = assoc[m]
+                    break
+            except Exception:
+                debug(f"{assoc[m]} cannot be selected as an audio codec")
+
+        self.inIP = inIP
+        self.inPort = inPort
+        self.outIP = outIP
+        self.outPort = outPort
+
+        self.dtmf = dtmf
+
+        self.pmout = RTPPacketManager()  # To Send
+        self.pmin = RTPPacketManager()  # Received
+        self.outOffset = random.randint(1, 5000)
+
+        self.outSequence = random.randint(1, 100)
+        self.outTimestamp = random.randint(1, 10000)
+        self.outSSRC = random.randint(1000, 65530)
+
+    def start(self) -> None:
+        self.sin = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
+        self.sout = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
+        self.sin.bind((self.inIP, self.inPort))
+        self.sin.setblocking(False)
+
+        r = Timer(0, self.recv)
+        r.name = "RTP Receiver"
+        r.start()
+        t = Timer(0, self.trans)
+        t.name = "RTP Transmitter"
+        t.start()
+
+    def stop(self) -> None:
+        self.NSD = False
+        self.sin.close()
+        self.sout.close()
+
+    def read(self, length: int = 160, blocking: bool = True) -> bytes:
+        if not blocking:
+            return self.pmin.read(length)
+        packet = self.pmin.read(length)
+        while packet == (b"\x80" * length) and self.NSD:
+            time.sleep(0.01)
+            packet = self.pmin.read(length)
+        return packet
+
+    def write(self, data: bytes) -> None:
+        self.pmout.write(self.outOffset, data)
+        self.outOffset += len(data)
+
+    def recv(self) -> None:
+        while self.NSD:
+            try:
+                packet = self.sin.recv(8192)
+                self.parsePacket(packet)
+            except BlockingIOError:
+                time.sleep(0.01)
+            except RTPParseError as e:
+                debug(str(e))
+            except OSError:
+                pass
+
+    def trans(self) -> None:
+        while self.NSD:
+            last_sent = time.monotonic_ns()
+            payload = self.pmout.read()
+            payload = self.encodePacket(payload)
+            packet = b"\x80"  # RFC 1889 V2 No Padding Extension or CC.
+            packet += chr(int(self.preference)).encode("utf8")
+            try:
+                packet += self.outSequence.to_bytes(2, byteorder="big")
+            except OverflowError:
+                self.outSequence = 0
+            try:
+                packet += self.outTimestamp.to_bytes(4, byteorder="big")
+            except OverflowError:
+                self.outTimestamp = 0
+            packet += self.outSSRC.to_bytes(4, byteorder="big")
+            packet += payload
+
+            # debug(payload)
+
+            try:
+                self.sout.sendto(packet, (self.outIP, self.outPort))
+            except OSError:
+                warnings.warn(
+                    "RTP Packet failed to send!",
+                    RuntimeWarning,
+                    stacklevel=2,
+                )
+
+            self.outSequence += 1
+            self.outTimestamp += len(payload)
+            # Calculate how long it took to generate this packet.
+            # Then how long we should wait to send the next, then devide by 2.
+            delay = (1 / self.preference.rate) * 160
+            sleep_time = max(
+                0, delay - ((time.monotonic_ns() - last_sent) / 1000000000)
+            )
+            time.sleep(sleep_time / self.trans_delay_reduction)
+
+    @property
+    def trans_delay_reduction(self) -> float:
+        reduction = pyVoIP.TRANSMIT_DELAY_REDUCTION + 1
+        return reduction if reduction else 1.0
+
+    def parsePacket(self, packet: bytes) -> None:
+        warnings.warn(
+            "parsePacket is deprecated due to PEP8 compliance. "
+            + "Use parse_packet instead.",
+            DeprecationWarning,
+            stacklevel=2,
+        )
+        return self.parse_packet(packet)
+
+    def parse_packet(self, packet: bytes) -> None:
+        msg = RTPMessage(packet, self.assoc)
+        if msg.payload_type == PayloadType.PCMU:
+            self.parsePCMU(msg)
+        elif msg.payload_type == PayloadType.PCMA:
+            self.parsePCMA(msg)
+        elif msg.payload_type == PayloadType.EVENT:
+            self.parseTelephoneEvent(msg)
+        else:
+            raise RTPParseError(
+                "Unsupported codec (parse): " + str(msg.payload_type)
+            )
+
+    def encodePacket(self, payload: bytes) -> bytes:
+        warnings.warn(
+            "encodePacket is deprecated due to PEP8 compliance. "
+            + "Use encode_packet instead.",
+            DeprecationWarning,
+            stacklevel=2,
+        )
+        return self.encode_packet(payload)
+
+    def encode_packet(self, payload: bytes) -> bytes:
+        if self.preference == PayloadType.PCMU:
+            return self.encodePCMU(payload)
+        elif self.preference == PayloadType.PCMA:
+            return self.encodePCMA(payload)
+        else:
+            raise RTPParseError(
+                "Unsupported codec (encode): " + str(self.preference)
+            )
+
+    def parsePCMU(self, packet: RTPMessage) -> None:
+        warnings.warn(
+            "parsePCMU is deprecated due to PEP8 compliance. "
+            + "Use parse_pcmu instead.",
+            DeprecationWarning,
+            stacklevel=2,
+        )
+        return self.parse_pcmu(packet)
+
+    def parse_pcmu(self, packet: RTPMessage) -> None:
+        data = audioop.ulaw2lin(packet.payload, 1)
+        data = audioop.bias(data, 1, 128)
+        self.pmin.write(packet.timestamp, data)
+
+    def encodePCMU(self, packet: bytes) -> bytes:
+        warnings.warn(
+            "encodePCMU is deprecated due to PEP8 compliance. "
+            + "Use encode_pcmu instead.",
+            DeprecationWarning,
+            stacklevel=2,
+        )
+        return self.encode_pcmu(packet)
+
+    def encode_pcmu(self, packet: bytes) -> bytes:
+        packet = audioop.bias(packet, 1, -128)
+        packet = audioop.lin2ulaw(packet, 1)
+        return packet
+
+    def parsePCMA(self, packet: RTPMessage) -> None:
+        warnings.warn(
+            "parsePCMA is deprecated due to PEP8 compliance. "
+            + "Use parse_pcma instead.",
+            DeprecationWarning,
+            stacklevel=2,
+        )
+        return self.parse_pcma(packet)
+
+    def parse_pcma(self, packet: RTPMessage) -> None:
+        data = audioop.alaw2lin(packet.payload, 1)
+        data = audioop.bias(data, 1, 128)
+        self.pmin.write(packet.timestamp, data)
+
+    def encodePCMA(self, packet: bytes) -> bytes:
+        warnings.warn(
+            "encodePCMA is deprecated due to PEP8 compliance. "
+            + "Use encode_pcma instead.",
+            DeprecationWarning,
+            stacklevel=2,
+        )
+        return self.encode_pcma(packet)
+
+    def encode_pcma(self, packet: bytes) -> bytes:
+        packet = audioop.bias(packet, 1, -128)
+        packet = audioop.lin2alaw(packet, 1)
+        return packet
+
+    def parseTelephoneEvent(self, packet: RTPMessage) -> None:
+        warnings.warn(
+            "parseTelephoneEvent "
+            + "is deprecated due to PEP8 compliance. "
+            + "Use parse_telephone_event instead.",
+            DeprecationWarning,
+            stacklevel=2,
+        )
+        return self.parse_telephone_event(packet)
+
+    def parse_telephone_event(self, packet: RTPMessage) -> None:
+        key = [
+            "0",
+            "1",
+            "2",
+            "3",
+            "4",
+            "5",
+            "6",
+            "7",
+            "8",
+            "9",
+            "*",
+            "#",
+            "A",
+            "B",
+            "C",
+            "D",
+        ]
+
+        payload = packet.payload
+        event = key[payload[0]]
+        """
+        Commented out the following due to F841 (Unused variable).
+        Might use at some point though, so I'm saving the logic.
+
+        byte = byte_to_bits(payload[1:2])
+        end = (byte[0] == '1')
+        volume = int(byte[2:], 2)
+        """
+
+        if packet.marker:
+            if self.dtmf is not None:
+                self.dtmf(event)
diff --git a/pyVoIP/SIP.py b/pyVoIP/SIP.py
index 669dad2..9b9fffb 100644
--- a/pyVoIP/SIP.py
+++ b/pyVoIP/SIP.py
@@ -1,948 +1,1831 @@
-from enum import IntEnum
+from enum import Enum, IntEnum
 from threading import Timer, Lock
+from typing import Any, Callable, Dict, List, Optional, Tuple, TYPE_CHECKING
 import pyVoIP
 import hashlib
 import socket
 import random
 import re
 import time
+import uuid
+import select
+import warnings
+
+
+if TYPE_CHECKING:
+    from pyVoIP import RTP
+
+
+__all__ = [
+    "Counter",
+    "InvalidAccountInfoError",
+    "SIPClient",
+    "SIPMessage",
+    "SIPMessageType",
+    "SIPParseError",
+    "SIPStatus",
+]
+
 
-__all__ = ['Counter', 'InvalidAccountInfoError', 'SIPClient', 'SIPMessage', 'SIPMessageType', 'SIPParseError', 'SIPStatus']
 debug = pyVoIP.debug
 
+
 class InvalidAccountInfoError(Exception):
-  pass
+    pass
+
 
 class SIPParseError(Exception):
-  pass
-
-class Counter():
-  def __init__(self, start=1):
-    self.x = start
-    
-  def count(self):
-    x = self.x
-    self.x += 1
-    return x
-    
-  def next(self):
-    return self.count()
-
-class SIPStatus(IntEnum):
-    def __new__(cls, value, phrase, description=''):
-        obj = int.__new__(cls, value)
+    pass
+
+
+class Counter:
+    def __init__(self, start: int = 1):
+        self.x = start
+
+    def count(self) -> int:
+        x = self.x
+        self.x += 1
+        return x
+
+    def next(self) -> int:
+        return self.count()
+
+    def current(self) -> int:
+        return self.x
+
+
+class SIPStatus(Enum):
+    def __new__(cls, value: int, phrase: str = "", description: str = ""):
+        obj = object.__new__(cls)
         obj._value_ = value
 
         obj.phrase = phrase
         obj.description = description
         return obj
 
-    # informational
-    TRYING = 100, 'Trying', 'Extended search being performed, may take a significant time'
-    RINGING = (180, 'Ringing',
-            'Destination user agent received INVITE, and is alerting user of call')
-    FORWARDED = 181, 'Call is Being Forwarded'
-    QUEUED = 182, 'Queued'
-    SESSION_PROGRESS = 183, 'Session Progress'
-    TERMINATED = 199, 'Early Dialog Terminated'
-
-    # success
-    OK = 200, 'OK', 'Request successful'
-    ACCEPTED = (202, 'Accepted',
-        'Request accepted, processing continues (Deprecated.)')
-    NO_NOTIFICATION = 204, 'No Notification', 'Request fulfilled, nothing follows'
-
-    # redirection
-    MULTIPLE_CHOICES = (300, 'Multiple Choices',
-        'Object has several resources -- see URI list')
-    MOVED_PERMANENTLY = (301, 'Moved Permanently',
-        'Object moved permanently -- see URI list')
-    MOVED_TEMPORARILY = 302, 'Moved Temporarily', 'Object moved temporarily -- see URI list'
-    USE_PROXY = (305, 'Use Proxy',
-        'You must use proxy specified in Location to access this resource')
-    ALTERNATE_SERVICE = (380, 'Alternate Service',
-        'The call failed, but alternatives are available -- see URI list')
-
-    # client error
-    BAD_REQUEST = (400, 'Bad Request',
-        'Bad request syntax or unsupported method')
-    UNAUTHORIZED = (401, 'Unauthorized',
-        'No permission -- see authorization schemes')
-    PAYMENT_REQUIRED = (402, 'Payment Required',
-        'No payment -- see charging schemes')
-    FORBIDDEN = (403, 'Forbidden',
-        'Request forbidden -- authorization will not help')
-    NOT_FOUND = (404, 'Not Found',
-        'Nothing matches the given URI')
-    METHOD_NOT_ALLOWED = (405, 'Method Not Allowed',
-        'Specified method is invalid for this resource')
-    NOT_ACCEPTABLE = (406, 'Not Acceptable',
-        'URI not available in preferred format')
-    PROXY_AUTHENTICATION_REQUIRED = (407,
-        'Proxy Authentication Required',
-        'You must authenticate with this proxy before proceeding')
-    REQUEST_TIMEOUT = (408, 'Request Timeout',
-        'Request timed out; try again later')
-    CONFLICT = 409, 'Conflict', 'Request conflict'
-    GONE = (410, 'Gone',
-        'URI no longer exists and has been permanently removed')
-    LENGTH_REQUIRED = (411, 'Length Required',
-        'Client must specify Content-Length')
-    CONDITIONAL_REQUEST_FAILED = (412, 'Conditional Request Failed')
-    REQUEST_ENTITY_TOO_LARGE = (413, 'Request Entity Too Large',
-        'Entity is too large')
-    REQUEST_URI_TOO_LONG = (414, 'Request-URI Too Long',
-        'URI is too long')
-    UNSUPPORTED_MEDIA_TYPE = (415, 'Unsupported Media Type',
-        'Entity body in unsupported format')
-    UNSUPPORTED_URI_SCHEME = (416,
-        'Unsupported URI Scheme',
-        'Cannot satisfy request')
-    UNKOWN_RESOURCE_PRIORITY = (417, 'Unkown Resource-Priority',
-        'There was a resource-priority option tag, but no Resource-Priority header')
-    BAD_EXTENSION = (420, 'Bad Extension', 
-        'Bad SIP Protocol Extension used, not understood by the server.')
-    EXTENSION_REQUIRED = (421, 'Extension Required',
-        'Server requeires a specific extension to be listed in the Supported header.')
-    SESSION_INTERVAL_TOO_SMALL = 422, 'Session Interval Too Small'
-    SESSION_INTERVAL_TOO_BRIEF = 423, 'Session Interval Too Breif'
-    BAD_LOCATION_INFORMATION = 424, 'Bad Location Information'
-    USE_IDENTITY_HEADER = (428, 'Use Identity Header',
-        'The server requires an Identity header, and one has not been provided.')
-    PROVIDE_REFERRER_IDENTITY = (429, 'Provide Referrer Identity')
-    FLOW_FAILED = (430, 'Flow Failed',
-        'A specific flow to a user agent has failed, although other flows may succeed.') #This response is intended for use between proxy devices, and should not be seen by an endpoint (and if it is seen by one, should be treated as a 400 Bad Request response).
-    ANONYMITY_DISALLOWED = (433,
-        'Anonymity Disallowed')
-    BAD_IDENTITY_INFO = (436, 'Bad Identity-Info')
-    UNSUPPORTED_CERTIFICATE = (437, 'Unsupported Certificate')
-    INVALID_IDENTITY_HEADER = (438, 'Invalid Identity Header')
-    FIRST_HOP_LACKS_OUTBOUND_SUPPORT = (439, 'First Hop Lacks Outbound Support')
-    MAX_BREADTH_EXCEEDED = (440, 'Max-Breadth Exceeded')
-    BAD_INFO_PACKAGE = (469, 'Bad Info Package')
-    CONSENT_NEEDED = (470, 'Consent Needed')
-    TEMPORARILY_UNAVAILABLE = (480, 'Temporarily Unavailable')
-    CALL_OR_TRANSACTION_DOESNT_EXIST = (481, 'Call/Transaction Does Not Exist')
-    LOOP_DETECTED = 482, 'Loop Detected'
-    TOO_MANY_HOPS = (483, 'Too Many Hops')
-    ADDRESS_INCOMPLETE = (484, 'Address Incomplete')
-    AMBIGUOUS = (485, 'Ambiguous')
-    BUSY_HERE = (486, 'Busy Here', 'Callee is busy')
-    REQUEST_TERMINATED = (487, 'Request Terminated')
-    NOT_ACCEPTABLE_HERE = (488, 'Not Acceptable Here')
-    BAD_EVENT = (489, 'Bad Event')
-    REQUEST_PENDING = (491, 'Request Pending')
-    UNDECIPHERABLE = (493, 'Undecipherable')
-    SECURITY_AGREEMENT_REQUIRED = (494, 'Security Agreement Required')
-    
-    # server errors
-    INTERNAL_SERVER_ERROR = (500, 'Internal Server Error',
-        'Server got itself in trouble')
-    NOT_IMPLEMENTED = (501, 'Not Implemented',
-        'Server does not support this operation')
-    BAD_GATEWAY = (502, 'Bad Gateway',
-        'Invalid responses from another server/proxy')
-    SERVICE_UNAVAILABLE = (503, 'Service Unavailable',
-        'The server cannot process the request due to a high load')
-    GATEWAY_TIMEOUT = (504, 'Server Timeout',
-        'The server did not receive a timely response')
-    SIP_VERSION_NOT_SUPPORTED = (505, 'SIP Version Not Supported',
-        'Cannot fulfill request')
-    MESSAGE_TOO_LONG = (513, 'Message Too Long')
-    PUSH_NOTIFICATION_SERVICE_NOT_SUPPORTED = (555, 'Push Notification Service Not Supported')
-    PRECONDITION_FAILURE = (580, 'Precondition Failure')
-
-    #Global Failure Responses
-    BUSY_EVERYWHERE = 600, 'Busy Everywhere'
-    DECLINE = 603, 'Decline'
-    DOES_NOT_EXIST_ANYWHERE = 604, 'Does Not Exist Anywhere'
-    GLOBAL_NOT_ACCEPTABLE = 606, 'Not Acceptable'
-    UNWANTED = 607, 'Unwanted'
-    REJECTED = 608, 'Rejected'
+    def __int__(self) -> int:
+        return self._value_
+
+    def __str__(self) -> str:
+        return f"{self._value_} {self.phrase}"
+
+    @property
+    def phrase(self) -> str:
+        return self._phrase
+
+    @phrase.setter
+    def phrase(self, value: str) -> None:
+        self._phrase = value
+
+    @property
+    def description(self) -> str:
+        return self._description
+
+    @description.setter
+    def description(self, value: str) -> None:
+        self._description = value
+
+    # Informational
+    TRYING = (
+        100,
+        "Trying",
+        "Extended search being performed, may take a significant time",
+    )
+    RINGING = (
+        180,
+        "Ringing",
+        "Destination user agent received INVITE, "
+        + "and is alerting user of call",
+    )
+    FORWARDED = 181, "Call is Being Forwarded"
+    QUEUED = 182, "Queued"
+    SESSION_PROGRESS = 183, "Session Progress"
+    TERMINATED = 199, "Early Dialog Terminated"
+
+    # Success
+    OK = 200, "OK", "Request successful"
+    ACCEPTED = (
+        202,
+        "Accepted",
+        "Request accepted, processing continues (Deprecated.)",
+    )
+    NO_NOTIFICATION = (
+        204,
+        "No Notification",
+        "Request fulfilled, nothing follows",
+    )
+
+    # Redirection
+    MULTIPLE_CHOICES = (
+        300,
+        "Multiple Choices",
+        "Object has several resources -- see URI list",
+    )
+    MOVED_PERMANENTLY = (
+        301,
+        "Moved Permanently",
+        "Object moved permanently -- see URI list",
+    )
+    MOVED_TEMPORARILY = (
+        302,
+        "Moved Temporarily",
+        "Object moved temporarily -- see URI list",
+    )
+    USE_PROXY = (
+        305,
+        "Use Proxy",
+        "You must use proxy specified in Location to "
+        + "access this resource",
+    )
+    ALTERNATE_SERVICE = (
+        380,
+        "Alternate Service",
+        "The call failed, but alternatives are available -- see URI list",
+    )
+
+    # Client Error
+    BAD_REQUEST = (
+        400,
+        "Bad Request",
+        "Bad request syntax or unsupported method",
+    )
+    UNAUTHORIZED = (
+        401,
+        "Unauthorized",
+        "No permission -- see authorization schemes",
+    )
+    PAYMENT_REQUIRED = (
+        402,
+        "Payment Required",
+        "No payment -- see charging schemes",
+    )
+    FORBIDDEN = (
+        403,
+        "Forbidden",
+        "Request forbidden -- authorization will not help",
+    )
+    NOT_FOUND = (404, "Not Found", "Nothing matches the given URI")
+    METHOD_NOT_ALLOWED = (
+        405,
+        "Method Not Allowed",
+        "Specified method is invalid for this resource",
+    )
+    NOT_ACCEPTABLE = (
+        406,
+        "Not Acceptable",
+        "URI not available in preferred format",
+    )
+    PROXY_AUTHENTICATION_REQUIRED = (
+        407,
+        "Proxy Authentication Required",
+        "You must authenticate with this proxy before proceeding",
+    )
+    REQUEST_TIMEOUT = (
+        408,
+        "Request Timeout",
+        "Request timed out; try again later",
+    )
+    CONFLICT = 409, "Conflict", "Request conflict"
+    GONE = (
+        410,
+        "Gone",
+        "URI no longer exists and has been permanently removed",
+    )
+    LENGTH_REQUIRED = (
+        411,
+        "Length Required",
+        "Client must specify Content-Length",
+    )
+    CONDITIONAL_REQUEST_FAILED = 412, "Conditional Request Failed"
+    REQUEST_ENTITY_TOO_LARGE = (
+        413,
+        "Request Entity Too Large",
+        "Entity is too large",
+    )
+    REQUEST_URI_TOO_LONG = 414, "Request-URI Too Long", "URI is too long"
+    UNSUPPORTED_MEDIA_TYPE = (
+        415,
+        "Unsupported Media Type",
+        "Entity body in unsupported format",
+    )
+    UNSUPPORTED_URI_SCHEME = (
+        416,
+        "Unsupported URI Scheme",
+        "Cannot satisfy request",
+    )
+    UNKOWN_RESOURCE_PRIORITY = (
+        417,
+        "Unkown Resource-Priority",
+        "There was a resource-priority option tag, "
+        + "but no Resource-Priority header",
+    )
+    BAD_EXTENSION = (
+        420,
+        "Bad Extension",
+        "Bad SIP Protocol Extension used, not understood by the server.",
+    )
+    EXTENSION_REQUIRED = (
+        421,
+        "Extension Required",
+        "Server requeires a specific extension to be "
+        + "listed in the Supported header.",
+    )
+    SESSION_INTERVAL_TOO_SMALL = 422, "Session Interval Too Small"
+    SESSION_INTERVAL_TOO_BRIEF = 423, "Session Interval Too Breif"
+    BAD_LOCATION_INFORMATION = 424, "Bad Location Information"
+    USE_IDENTITY_HEADER = (
+        428,
+        "Use Identity Header",
+        "The server requires an Identity header, "
+        + "and one has not been provided.",
+    )
+    PROVIDE_REFERRER_IDENTITY = 429, "Provide Referrer Identity"
+    """
+    This response is intended for use between proxy devices,
+    and should not be seen by an endpoint. If it is seen by one,
+    it should be treated as a 400 Bad Request response.
+    """
+    FLOW_FAILED = (
+        430,
+        "Flow Failed",
+        "A specific flow to a user agent has failed, "
+        + "although other flows may succeed.",
+    )
+    ANONYMITY_DISALLOWED = 433, "Anonymity Disallowed"
+    BAD_IDENTITY_INFO = 436, "Bad Identity-Info"
+    UNSUPPORTED_CERTIFICATE = 437, "Unsupported Certificate"
+    INVALID_IDENTITY_HEADER = 438, "Invalid Identity Header"
+    FIRST_HOP_LACKS_OUTBOUND_SUPPORT = 439, "First Hop Lacks Outbound Support"
+    MAX_BREADTH_EXCEEDED = 440, "Max-Breadth Exceeded"
+    BAD_INFO_PACKAGE = 469, "Bad Info Package"
+    CONSENT_NEEDED = 470, "Consent Needed"
+    TEMPORARILY_UNAVAILABLE = 480, "Temporarily Unavailable"
+    CALL_OR_TRANSACTION_DOESNT_EXIST = 481, "Call/Transaction Does Not Exist"
+    LOOP_DETECTED = 482, "Loop Detected"
+    TOO_MANY_HOPS = 483, "Too Many Hops"
+    ADDRESS_INCOMPLETE = 484, "Address Incomplete"
+    AMBIGUOUS = 485, "Ambiguous"
+    BUSY_HERE = 486, "Busy Here", "Callee is busy"
+    REQUEST_TERMINATED = 487, "Request Terminated"
+    NOT_ACCEPTABLE_HERE = 488, "Not Acceptable Here"
+    BAD_EVENT = 489, "Bad Event"
+    REQUEST_PENDING = 491, "Request Pending"
+    UNDECIPHERABLE = 493, "Undecipherable"
+    SECURITY_AGREEMENT_REQUIRED = 494, "Security Agreement Required"
+
+    # Server Errors
+    INTERNAL_SERVER_ERROR = (
+        500,
+        "Internal Server Error",
+        "Server got itself in trouble",
+    )
+    NOT_IMPLEMENTED = (
+        501,
+        "Not Implemented",
+        "Server does not support this operation",
+    )
+    BAD_GATEWAY = (
+        502,
+        "Bad Gateway",
+        "Invalid responses from another server/proxy",
+    )
+    SERVICE_UNAVAILABLE = (
+        503,
+        "Service Unavailable",
+        "The server cannot process the request due to a high load",
+    )
+    GATEWAY_TIMEOUT = (
+        504,
+        "Server Timeout",
+        "The server did not receive a timely response",
+    )
+    SIP_VERSION_NOT_SUPPORTED = (
+        505,
+        "SIP Version Not Supported",
+        "Cannot fulfill request",
+    )
+    MESSAGE_TOO_LONG = 513, "Message Too Long"
+    PUSH_NOTIFICATION_SERVICE_NOT_SUPPORTED = (
+        555,
+        "Push Notification Service Not Supported",
+    )
+    PRECONDITION_FAILURE = 580, "Precondition Failure"
+
+    # Global Failure Responses
+    BUSY_EVERYWHERE = 600, "Busy Everywhere"
+    DECLINE = 603, "Decline"
+    DOES_NOT_EXIST_ANYWHERE = 604, "Does Not Exist Anywhere"
+    GLOBAL_NOT_ACCEPTABLE = 606, "Not Acceptable"
+    UNWANTED = 607, "Unwanted"
+    REJECTED = 608, "Rejected"
+
 
 class SIPMessageType(IntEnum):
-  def __new__(cls, value):
-    obj = int.__new__(cls, value)
-    obj._value_ = value
-    return obj
-    
-  MESSAGE = 1
-  RESPONSE = 0
-
-class SIPMessage():
-  def __init__(self, data):
-    self.SIPCompatibleVersions = pyVoIP.SIPCompatibleVersions
-    self.SIPCompatibleMethods = pyVoIP.SIPCompatibleMethods
-    self.heading = ""
-    self.type = None
-    self.status = 0
-    self.headers = {}
-    self.body = {}
-    self.authentication = {}
-    self.raw = data
-    self.parse(data)
-    
-  def summary(self):
-    data = ""
-    if self.type == SIPMessageType.RESPONSE:
-      data += "Status: "+str(int(self.status))+" "+str(self.status.phrase)+"\n\n"
-    else:
-      data += "Method: "+str(self.method)+"\n\n"
-    data += "Headers:\n"
-    for x in self.headers:
-      data += x+": "+str(self.headers[x])+"\n"
-    data += "\n"
-    data += "Body:\n"
-    for x in self.body:
-      data += x+": "+str(self.body[x])+"\n"
-    
-    return data
-    
-  def parse(self, data):
-    headers = data.split(b'\r\n\r\n')[0]
-    body = data.split(b'\r\n\r\n')[1]
-    
-    headers_raw = headers.split(b'\r\n')
-    heading = headers_raw.pop(0)
-    check = str(heading.split(b" ")[0], 'utf8')
-    
-    if check in self.SIPCompatibleVersions:
-      self.type = SIPMessageType.RESPONSE
-      self.parseSIPResponse(data)
-    elif check in self.SIPCompatibleMethods:
-      self.type = SIPMessageType.MESSAGE
-      self.parseSIPMessage(data)
-    else:
-      raise SIPParseError("Unable to decipher SIP request: "+str(heading, 'utf8'))
-    
-  def parseHeader(self, header, data):
-    if header=="Via":
-      info = re.split(" |;", data)
-      self.headers['Via'] = {'type': info[0], 'address':(info[1].split(':')[0], info[1].split(':')[1])}
-      for x in info[2:]: #Sets branch, maddr, ttl, received, and rport if defined as per RFC 3261 20.7
-        if '=' in x:
-          self.headers['Via'][x.split('=')[0]] = x.split('=')[1]
+    def __new__(cls, value: int):
+        obj = int.__new__(cls, value)
+        obj._value_ = value
+        return obj
+
+    MESSAGE = 1
+    RESPONSE = 0
+
+
+class SIPMessage:
+    def __init__(self, data: bytes):
+        self.SIPCompatibleVersions = pyVoIP.SIPCompatibleVersions
+        self.SIPCompatibleMethods = pyVoIP.SIPCompatibleMethods
+        self.heading = b""
+        self.type: Optional[SIPMessageType] = None
+        self.status = SIPStatus(491)
+        self.headers: Dict[str, Any] = {"Via": []}
+        self.body: Dict[str, Any] = {}
+        self.authentication: Dict[str, str] = {}
+        self.raw = data
+        self.auth_match = re.compile(",?[a-zA-Z0-9]*=")
+        self.parse(data)
+
+    def summary(self) -> str:
+        data = ""
+        if self.type == SIPMessageType.RESPONSE:
+            data += f"Status: {int(self.status)} {self.status.phrase}\n\n"
         else:
-          self.headers['Via'][x] = None
-    elif header=="From" or header=="To":
-      info = data.split(';tag=')
-      tag = ''
-      if len(info) >= 2:
-        tag = info[1]
-      raw = info[0]
-      contact = raw.split('<sip:')
-      contact[0] = contact[0].strip('"').strip("'")
-      address = contact[1].strip('>')
-      if len(address.split('@')) == 2:
-        number = address.split('@')[0]
-        host = address.split('@')[1]
-      else:
-        number = None
-        host = address
-      
-      self.headers[header] = {'raw': raw, 'tag': tag, 'address': address, 'number': number, 'caller': contact[0], 'host': host}
-    elif header=="CSeq":
-      self.headers[header] = {'check': data.split(" ")[0], 'method': data.split(" ")[1]}
-    elif header=="Allow" or header=="Supported":
-      self.headers[header] = data.split(", ")
-    elif header=="Content-Length":
-      self.headers[header] = int(data)
-    elif header=='WWW-Authenticate' or header=="Authorization":
-      data = data.replace("Digest", "")
-      info = data.split(",")
-      header_data = {}
-      for x in info:
-        x = x.strip()
-        header_data[x.split('=')[0]] = x.split('=')[1].strip('"')
-      self.headers[header] = header_data
-      self.authentication = header_data
-    else:
-      self.headers[header] = data
-      
-  def parseBody(self, header, data):
-    if 'Content-Encoding' in self.headers:
-      raise SIPParseError("Unable to parse encoded content.")
-    if self.headers['Content-Type'] == 'application/sdp':
-      #Referenced RFC 4566 July 2006
-      if header == "v":
-        #SDP 5.1 Version
-        self.body[header] = int(data)
-      elif header == "o":
-        #SDP 5.2 Origin
-        #o=<username> <sess-id> <sess-version> <nettype> <addrtype> <unicast-address>
-        data = data.split(' ')
-        self.body[header] = {'username': data[0], 'id': data[1], 'version': data[2], 'network_type': data[3], 'address_type': data[4], 'address': data[5]}
-      elif header == "s":
-        #SDP 5.3 Session Name
-        #s=<session name>
-        self.body[header] = data
-      elif header  == "i":
-        #SDP 5.4 Session Information
-        #i=<session-description>
-        self.body[header] = data
-      elif header == "u":
-        #SDP 5.5 URI
-        #u=<uri>
-        self.body[header] = data
-      elif header == "e" or header == "p":
-        #SDP 5.6 Email Address and Phone Number of person responsible for the conference
-        #e=<email-address>
-        #p=<phone-number>
-        self.body[header] = data
-      elif header == "c":
-        #SDP 5.7 Connection Data
-        #c=<nettype> <addrtype> <connection-address>
-        if not 'c' in self.body:
-          self.body['c'] = []
-        data = data.split(' ')
-        #TTL Data and Multicast addresses may be specified.
-        #For IPv4 its listed as addr/ttl/number of addresses.
-        #c=IN IP4 224.2.1.1/127/3 means:
-        #c=IN IP4 224.2.1.1/127
-        #c=IN IP4 224.2.1.2/127
-        #c=IN IP4 224.2.1.3/127
-        #With the TTL being 127.  IPv6 does not support time to live so you will only see a / for multicast addresses.
-        if '/' in data[2]:
-          if data[1] == "IP6":
-            self.body[header].append({'network_type': data[0], 'address_type': data[1], 'address': data[2].split('/')[0], 'ttl': None, 'address_count': int(data[2].split('/')[1])})
-          else:
-            address_data=data[2].split('/')
-            if len(address_data) == 2:
-              self.body[header].append({'network_type': data[0], 'address_type': data[1], 'address': address_data[0], 'ttl': int(address_data[1]), 'address_count': 1})
-            else:
-              self.body[header].append({'network_type': data[0], 'address_type': data[1], 'address': address_data[0], 'ttl': int(address_data[1]), 'address_count': int(address_data[2])})
+            data += f"Method: {self.method}\n\n"
+        data += "Headers:\n"
+        for x in self.headers:
+            data += f"{x}: {self.headers[x]}\n"
+        data += "\n"
+        data += "Body:\n"
+        for x in self.body:
+            data += f"{x}: {self.body[x]}\n"
+
+        return data
+
+    def parse(self, data: bytes) -> None:
+        try:
+            headers, body = data.split(b"\r\n\r\n")
+        except ValueError as ve:
+            debug(f"Error unpacking data, only using header: {ve}")
+            headers = data.split(b"\r\n\r\n")[0]
+
+        headers_raw = headers.split(b"\r\n")
+        heading = headers_raw.pop(0)
+        check = str(heading.split(b" ")[0], "utf8")
+
+        if check in self.SIPCompatibleVersions:
+            self.type = SIPMessageType.RESPONSE
+            self.parseSIPResponse(data)
+        elif check in self.SIPCompatibleMethods:
+            self.type = SIPMessageType.MESSAGE
+            self.parseSIPMessage(data)
         else:
-          self.body[header].append({'network_type': data[0], 'address_type': data[1], 'address': data[2], 'ttl': None, 'address_count': 1})
-      elif header == "b":
-        #SDP 5.8 Bandwidth
-        #b=<bwtype>:<bandwidth>
-        #A bwtype of CT means Conference Total between all medias and all devices in the conference.
-        #A bwtype of AS means Applicaton Specific total for this media and this device.
-        #The bandwidth is given in kilobits per second.  As this was written in 2006, this could be Kibibits.
-        #TODO: Implement Bandwidth restrictions
-        data = data.split(':')
-        self.body[header] = {'type': data[0], 'bandwidth': data[1]}
-      elif header == "t":
-        #SDP 5.9 Timing
-        #t=<start-time> <stop-time>
-        data = data.split(' ')
-        self.body[header] = {'start': data[0], 'stop': data[1]}
-      elif header == "r":
-        #SDP 5.10 Repeat Times
-        #r=<repeat interval> <active duration> <offsets from start-time>
-        data = data.split(' ')
-        self.body[header] = {'repeat': data[0], 'duration': data[1], 'offset1': data[2], 'offset2': data[3]}
-      elif header == "z":
-        #SDP 5.11 Time Zones
-        #z=<adjustment time> <offset> <adjustment time> <offset> ....
-        #Used for change in timezones such as day light savings time.
-        data = data.split(0)
-        amount = len(data)/2
-        self.body[header] = {}
-        for x in range(amount):
-          self.body[header]['adjustment-time'+str(x)]  = data[x*2]
-          self.body[header]['offset'+str(x)]  = data[x*2+1]
-      elif header == "k":
-        #SDP 5.12 Encryption Keys
-        #k=<method>
-        #k=<method>:<encryption key>
-        if ':' in data:
-          data = data.split(':')
-          self.body[header] = {'method': data[0], 'key': data[1]}
+            raise SIPParseError(
+                "Unable to decipher SIP request: " + str(heading, "utf8")
+            )
+
+    def parseHeader(self, header: str, data: str) -> None:
+        warnings.warn(
+            "parseHeader is deprecated due to PEP8 compliance. "
+            + "Use parse_header instead.",
+            DeprecationWarning,
+            stacklevel=2,
+        )
+        return self.parse_header(header, data)
+
+    def parse_header(self, header: str, data: str) -> None:
+        if header == "Via":
+            for d in data:
+                info = re.split(" |;", d)
+                _type = info[0]  # SIP Method
+                _address = info[1].split(":")  # Tuple: address, port
+                _ip = _address[0]
+
+                """
+                If no port is provided in via header assume default port.
+                Needs to be str. Check response build for better str creation
+                """
+                _port = info[1].split(":")[1] if len(_address) > 1 else "5060"
+                _via = {"type": _type, "address": (_ip, _port)}
+
+                """
+                Sets branch, maddr, ttl, received, and rport if defined
+                as per RFC 3261 20.7
+                """
+                for x in info[2:]:
+                    if "=" in x:
+                        _via[x.split("=")[0]] = x.split("=")[1]
+                    else:
+                        _via[x] = None
+                self.headers["Via"].append(_via)
+        elif header == "From" or header == "To":
+            info = data.split(";tag=")
+            tag = ""
+            if len(info) >= 2:
+                tag = info[1]
+            raw = info[0]
+            # fix issue 41 part 1
+            contact = re.split(r"<?sip:", raw)
+            contact[0] = contact[0].strip('"').strip("'")
+            address = contact[1].strip(">")
+            if len(address.split("@")) == 2:
+                number = address.split("@")[0]
+                host = address.split("@")[1]
+            else:
+                number = None
+                host = address
+
+            self.headers[header] = {
+                "raw": raw,
+                "tag": tag,
+                "address": address,
+                "number": number,
+                "caller": contact[0],
+                "host": host,
+            }
+        elif header == "CSeq":
+            self.headers[header] = {
+                "check": data.split(" ")[0],
+                "method": data.split(" ")[1],
+            }
+        elif header == "Allow" or header == "Supported":
+            self.headers[header] = data.split(", ")
+        elif header == "Content-Length":
+            self.headers[header] = int(data)
+        elif header == "WWW-Authenticate" or header == "Authorization":
+            data = data.replace("Digest ", "")
+            vars = [
+                x.lstrip(",").rstrip("=")
+                for x in self.auth_match.findall(data)
+            ]
+            row_data = self.auth_match.split(data)
+            row_data.pop(0)
+            row_data = [x.strip('"') for x in row_data]
+            header_data = {}
+            for var, data in zip(vars, row_data):
+                header_data[var] = data
+            self.headers[header] = header_data
+            self.authentication = header_data
         else:
-          self.body[header] = {'method': data}
-      elif header == "m":
-        #SDP 5.14 Media Descriptions
-        #m=<media> <port>/<number of ports> <proto> <fmt> ...
-        #<port> should be even, and <port>+1 should be the RTCP port.
-        #<number of ports> should coinside with number of addresses in SDP 5.7 c=
-        if not 'm' in self.body:
-          self.body['m'] = []
-        data = data.split(' ')
-        
-        if '/' in data[1]:
-          ports_raw = data[1].split('/')
-          port = ports_raw[0]
-          count = ports_raw[1]
+            self.headers[header] = data
+
+    def parseBody(self, header: str, data: str) -> None:
+        warnings.warn(
+            "parseBody is deprecated due to PEP8 compliance. "
+            + "Use parse_body instead.",
+            DeprecationWarning,
+            stacklevel=2,
+        )
+        return self.parse_body(header, data)
+
+    def parse_body(self, header: str, data: str) -> None:
+        if "Content-Encoding" in self.headers:
+            raise SIPParseError("Unable to parse encoded content.")
+        if self.headers["Content-Type"] == "application/sdp":
+            # Referenced RFC 4566 July 2006
+            if header == "v":
+                # SDP 5.1 Version
+                self.body[header] = int(data)
+            elif header == "o":
+                # SDP 5.2 Origin
+                # o=<username> <sess-id> <sess-version> <nettype> <addrtype> <unicast-address> # noqa: E501
+                d = data.split(" ")
+                self.body[header] = {
+                    "username": d[0],
+                    "id": d[1],
+                    "version": d[2],
+                    "network_type": d[3],
+                    "address_type": d[4],
+                    "address": d[5],
+                }
+            elif header == "s":
+                # SDP 5.3 Session Name
+                # s=<session name>
+                self.body[header] = data
+            elif header == "i":
+                # SDP 5.4 Session Information
+                # i=<session-description>
+                self.body[header] = data
+            elif header == "u":
+                # SDP 5.5 URI
+                # u=<uri>
+                self.body[header] = data
+            elif header == "e" or header == "p":
+                # SDP 5.6 Email Address and Phone Number of person
+                # responsible for the conference
+                # e=<email-address>
+                # p=<phone-number>
+                self.body[header] = data
+            elif header == "c":
+                # SDP 5.7 Connection Data
+                # c=<nettype> <addrtype> <connection-address>
+                if "c" not in self.body:
+                    self.body["c"] = []
+                d = data.split(" ")
+                # TTL Data and Multicast addresses may be specified.
+                # For IPv4 its listed as addr/ttl/number of addresses.
+                # c=IN IP4 224.2.1.1/127/3 means:
+                # c=IN IP4 224.2.1.1/127
+                # c=IN IP4 224.2.1.2/127
+                # c=IN IP4 224.2.1.3/127
+                # With the TTL being 127.
+                # IPv6 does not support time to live so you will only see a '/'
+                # for multicast addresses.
+                if "/" in d[2]:
+                    if d[1] == "IP6":
+                        self.body[header].append(
+                            {
+                                "network_type": d[0],
+                                "address_type": d[1],
+                                "address": d[2].split("/")[0],
+                                "ttl": None,
+                                "address_count": int(d[2].split("/")[1]),
+                            }
+                        )
+                    else:
+                        address_data = d[2].split("/")
+                        if len(address_data) == 2:
+                            self.body[header].append(
+                                {
+                                    "network_type": d[0],
+                                    "address_type": d[1],
+                                    "address": address_data[0],
+                                    "ttl": int(address_data[1]),
+                                    "address_count": 1,
+                                }
+                            )
+                        else:
+                            self.body[header].append(
+                                {
+                                    "network_type": d[0],
+                                    "address_type": d[1],
+                                    "address": address_data[0],
+                                    "ttl": int(address_data[1]),
+                                    "address_count": int(address_data[2]),
+                                }
+                            )
+                else:
+                    self.body[header].append(
+                        {
+                            "network_type": d[0],
+                            "address_type": d[1],
+                            "address": d[2],
+                            "ttl": None,
+                            "address_count": 1,
+                        }
+                    )
+            elif header == "b":
+                # SDP 5.8 Bandwidth
+                # b=<bwtype>:<bandwidth>
+                # A bwtype of CT means Conference Total between all medias
+                # and all devices in the conference.
+                # A bwtype of AS means Applicaton Specific total for this
+                # media and this device.
+                # The bandwidth is given in kilobits per second.
+                # As this was written in 2006, this could be Kibibits.
+                # TODO: Implement Bandwidth restrictions
+                d = data.split(":")
+                self.body[header] = {"type": d[0], "bandwidth": d[1]}
+            elif header == "t":
+                # SDP 5.9 Timing
+                # t=<start-time> <stop-time>
+                d = data.split(" ")
+                self.body[header] = {"start": d[0], "stop": d[1]}
+            elif header == "r":
+                # SDP 5.10 Repeat Times
+                # r=<repeat interval> <active duration> <offsets from start-time> # noqa: E501
+                d = data.split(" ")
+                self.body[header] = {
+                    "repeat": d[0],
+                    "duration": d[1],
+                    "offset1": d[2],
+                    "offset2": d[3],
+                }
+            elif header == "z":
+                # SDP 5.11 Time Zones
+                # z=<adjustment time> <offset> <adjustment time> <offset> ....
+                # Used for change in timezones such as day light savings time.
+                d = data.split()
+                amount = len(d) / 2
+                self.body[header] = {}
+                for x in range(int(amount)):
+                    self.body[header]["adjustment-time" + str(x)] = d[x * 2]
+                    self.body[header]["offset" + str(x)] = d[x * 2 + 1]
+            elif header == "k":
+                # SDP 5.12 Encryption Keys
+                # k=<method>
+                # k=<method>:<encryption key>
+                if ":" in data:
+                    d = data.split(":")
+                    self.body[header] = {"method": d[0], "key": d[1]}
+                else:
+                    self.body[header] = {"method": d}
+            elif header == "m":
+                # SDP 5.14 Media Descriptions
+                # m=<media> <port>/<number of ports> <proto> <fmt> ...
+                # <port> should be even, and <port>+1 should be the RTCP port.
+                # <number of ports> should coinside with number of
+                # addresses in SDP 5.7 c=
+                if "m" not in self.body:
+                    self.body["m"] = []
+                d = data.split(" ")
+
+                if "/" in d[1]:
+                    ports_raw = d[1].split("/")
+                    port = ports_raw[0]
+                    count = int(ports_raw[1])
+                else:
+                    port = d[1]
+                    count = 1
+                methods = d[3:]
+
+                self.body["m"].append(
+                    {
+                        "type": d[0],
+                        "port": int(port),
+                        "port_count": count,
+                        "protocol": pyVoIP.RTP.RTPProtocol(d[2]),
+                        "methods": methods,
+                        "attributes": {},
+                    }
+                )
+                for x in self.body["m"][-1]["methods"]:
+                    self.body["m"][-1]["attributes"][x] = {}
+            elif header == "a":
+                # SDP 5.13 Attributes & 6.0 SDP Attributes
+                # a=<attribute>
+                # a=<attribute>:<value>
+
+                if "a" not in self.body:
+                    self.body["a"] = {}
+
+                if ":" in data:
+                    d = data.split(":")
+                    attribute = d[0]
+                    value = d[1]
+                else:
+                    attribute = data
+                    value = None
+
+                if value is not None:
+                    if attribute == "rtpmap":
+                        # a=rtpmap:<payload type> <encoding name>/<clock rate> [/<encoding parameters>] # noqa: E501
+                        v = re.split(" |/", value)
+                        for t in self.body["m"]:
+                            if v[0] in t["methods"]:
+                                index = int(self.body["m"].index(t))
+                                break
+                        if len(v) == 4:
+                            encoding = v[3]
+                        else:
+                            encoding = None
+
+                        self.body["m"][index]["attributes"][v[0]]["rtpmap"] = {
+                            "id": v[0],
+                            "name": v[1],
+                            "frequency": v[2],
+                            "encoding": encoding,
+                        }
+
+                    elif attribute == "fmtp":
+                        # a=fmtp:<format> <format specific parameters>
+                        d = value.split(" ")
+                        for t in self.body["m"]:
+                            if d[0] in t["methods"]:
+                                index = int(self.body["m"].index(t))
+                                break
+
+                        self.body["m"][index]["attributes"][d[0]]["fmtp"] = {
+                            "id": d[0],
+                            "settings": d[1:],
+                        }
+                    else:
+                        self.body["a"][attribute] = value
+                else:
+                    if (
+                        attribute == "recvonly"
+                        or attribute == "sendrecv"
+                        or attribute == "sendonly"
+                        or attribute == "inactive"
+                    ):
+                        self.body["a"][
+                            "transmit_type"
+                        ] = pyVoIP.RTP.TransmitType(
+                            attribute
+                        )  # noqa: E501
+            else:
+                self.body[header] = data
+
         else:
-          port = data[1]
-          count = 1
-        methods = data[3:]
-        
-        self.body['m'].append({'type': data[0], 'port': int(port), 'port_count': int(count), 'protocol': pyVoIP.RTP.RTPProtocol(data[2]), 'methods': methods, 'attributes': {}})
-        for x in self.body['m'][-1]['methods']:
-          self.body['m'][-1]['attributes'][x] = {}
-      elif header == "a":
-        #SDP 5.13 Attributes & 6.0 SDP Attributes
-        #a=<attribute>
-        #a=<attribute>:<value>
-        
-        if not "a" in self.body:
-          self.body['a'] = {}
-        
-        if ':' in data:
-          data = data.split(':')
-          attribute = data[0]
-          value = data[1]
+            self.body[header] = data
+
+    @staticmethod
+    def parse_raw_header(
+        headers_raw: List[bytes], handle: Callable[[str, str], None]
+    ) -> None:
+        headers: Dict[str, Any] = {"Via": []}
+        # Only use first occurance of VIA header field;
+        # got second VIA from Kamailio running in DOCKER
+        # According to RFC 3261 these messages should be
+        # discarded in a response
+        for x in headers_raw:
+            i = str(x, "utf8").split(": ")
+            if i[0] == "Via":
+                headers["Via"].append(i[1])
+            if i[0] not in headers.keys():
+                headers[i[0]] = i[1]
+
+        for key, val in headers.items():
+            handle(key, val)
+
+    @staticmethod
+    def parse_raw_body(
+        body: bytes, handle: Callable[[str, str], None]
+    ) -> None:
+        if len(body) > 0:
+            body_raw = body.split(b"\r\n")
+            for x in body_raw:
+                i = str(x, "utf8").split("=")
+                if i != [""]:
+                    handle(i[0], i[1])
+
+    def parseSIPResponse(self, data: bytes) -> None:
+        warnings.warn(
+            "parseSIPResponse is deprecated "
+            + "due to PEP8 compliance. Use parse_sip_response "
+            + "instead.",
+            DeprecationWarning,
+            stacklevel=2,
+        )
+        return self.parse_sip_response(data)
+
+    def parse_sip_response(self, data: bytes) -> None:
+        headers, body = data.split(b"\r\n\r\n")
+
+        headers_raw = headers.split(b"\r\n")
+        self.heading = headers_raw.pop(0)
+        self.version = str(self.heading.split(b" ")[0], "utf8")
+        if self.version not in self.SIPCompatibleVersions:
+            raise SIPParseError(f"SIP Version {self.version} not compatible.")
+
+        self.status = SIPStatus(int(self.heading.split(b" ")[1]))
+
+        self.parse_raw_header(headers_raw, self.parseHeader)
+
+        self.parse_raw_body(body, self.parseBody)
+
+    def parseSIPMessage(self, data: bytes) -> None:
+        warnings.warn(
+            "parseSIPMessage is deprecated due to PEP8 compliance."
+            + " Use parse_sip_message instead.",
+            DeprecationWarning,
+            stacklevel=2,
+        )
+        return self.parse_sip_message(data)
+
+    def parse_sip_message(self, data: bytes) -> None:
+        headers, body = data.split(b"\r\n\r\n")
+
+        headers_raw = headers.split(b"\r\n")
+        self.heading = headers_raw.pop(0)
+        self.version = str(self.heading.split(b" ")[2], "utf8")
+        if self.version not in self.SIPCompatibleVersions:
+            raise SIPParseError(f"SIP Version {self.version} not compatible.")
+
+        self.method = str(self.heading.split(b" ")[0], "utf8")
+
+        self.parse_raw_header(headers_raw, self.parseHeader)
+
+        self.parse_raw_body(body, self.parseBody)
+
+
+class SIPClient:
+    def __init__(
+        self,
+        server: str,
+        port: int,
+        username: str,
+        password: str,
+        myIP="0.0.0.0",
+        myPort=5060,
+        callCallback: Optional[Callable[[SIPMessage], None]] = None,
+    ):
+        self.NSD = False
+        self.server = server
+        self.port = port
+        self.myIP = myIP
+        self.username = username
+        self.password = password
+
+        self.callCallback = callCallback
+
+        self.tags: List[str] = []
+        self.tagLibrary = {"register": self.genTag()}
+
+        self.myPort = myPort
+
+        self.default_expires = 120
+        self.register_timeout = 30
+
+        self.inviteCounter = Counter()
+        self.registerCounter = Counter()
+        self.subscribeCounter = Counter()
+        self.byeCounter = Counter()
+        self.callID = Counter()
+        self.sessID = Counter()
+
+        self.urnUUID = self.gen_urn_uuid()
+
+        self.registerThread: Optional[Timer] = None
+        self.recvLock = Lock()
+
+    def recv(self) -> None:
+        while self.NSD:
+            self.recvLock.acquire()
+            self.s.setblocking(False)
+            try:
+                raw = self.s.recv(8192)
+                if raw != b"\x00\x00\x00\x00":
+                    try:
+                        message = SIPMessage(raw)
+                        debug(message.summary())
+                        self.parseMessage(message)
+                    except Exception as ex:
+                        debug(f"Error on header parsing: {ex}")
+            except BlockingIOError:
+                self.s.setblocking(True)
+                self.recvLock.release()
+                time.sleep(0.01)
+                continue
+            except SIPParseError as e:
+                if "SIP Version" in str(e):
+                    request = self.genSIPVersionNotSupported(message)
+                    self.out.sendto(
+                        request.encode("utf8"), (self.server, self.port)
+                    )
+                else:
+                    debug(f"SIPParseError in SIP.recv: {type(e)}, {e}")
+            except Exception as e:
+                debug(f"SIP.recv error: {type(e)}, {e}\n\n{str(raw, 'utf8')}")
+                if pyVoIP.DEBUG:
+                    self.s.setblocking(True)
+                    self.recvLock.release()
+                    raise
+            self.s.setblocking(True)
+            self.recvLock.release()
+
+    def parseMessage(self, message: SIPMessage) -> None:
+        warnings.warn(
+            "parseMessage is deprecated due to PEP8 compliance. "
+            + "Use parse_message instead.",
+            DeprecationWarning,
+            stacklevel=2,
+        )
+        return self.parse_message(message)
+
+    def parse_message(self, message: SIPMessage) -> None:
+        if message.type != SIPMessageType.MESSAGE:
+            if message.status == SIPStatus.OK:
+                if self.callCallback is not None:
+                    self.callCallback(message)
+            elif message.status == SIPStatus.NOT_FOUND:
+                if self.callCallback is not None:
+                    self.callCallback(message)
+            elif message.status == SIPStatus.SERVICE_UNAVAILABLE:
+                if self.callCallback is not None:
+                    self.callCallback(message)
+            elif (
+                message.status == SIPStatus.TRYING
+                or message.status == SIPStatus.RINGING
+            ):
+                pass
+            else:
+                debug(
+                    "TODO: Add 500 Error on Receiving SIP Response:\r\n"
+                    + message.summary(),
+                    "TODO: Add 500 Error on Receiving SIP Response",
+                )
+            self.s.setblocking(True)
+            return
+        elif message.method == "INVITE":
+            if self.callCallback is None:
+                request = self.genBusy(message)
+                self.out.sendto(
+                    request.encode("utf8"), (self.server, self.port)
+                )
+            else:
+                self.callCallback(message)
+        elif message.method == "BYE":
+            # TODO: If callCallback is None, the call doesn't exist, 481
+            self.callCallback(message)  # type: ignore
+            response = self.genOk(message)
+            try:
+                # BYE comes from client cause server only acts as mediator
+                (_sender_adress, _sender_port) = message.headers["Via"][0][
+                    "address"
+                ]
+                self.out.sendto(
+                    response.encode("utf8"),
+                    (_sender_adress, int(_sender_port)),
+                )
+            except Exception:
+                debug("BYE Answer failed falling back to server as target")
+                self.out.sendto(
+                    response.encode("utf8"), (self.server, self.port)
+                )
+        elif message.method == "ACK":
+            return
+        elif message.method == "CANCEL":
+            # TODO: If callCallback is None, the call doesn't exist, 481
+            self.callCallback(message)  # type: ignore
+            response = self.genOk(message)
+            self.out.sendto(response.encode("utf8"), (self.server, self.port))
         else:
-          attribute = data
-          value = None
-        
-        if value != None:
-          if attribute == "rtpmap":
-            #a=rtpmap:<payload type> <encoding name>/<clock rate> [/<encoding parameters>]
-            value = re.split(" |/", value)
-            for x in self.body['m']:
-              if value[0] in x['methods']:
-                index = self.body['m'].index(x)
-                break
-            if len(value) == 4:
-              encoding = value[3]
+            debug("TODO: Add 400 Error on non processable request")
+
+    def start(self) -> None:
+        if self.NSD:
+            raise RuntimeError("Attempted to start already started SIPClient")
+        self.NSD = True
+        self.s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
+        # self.out = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
+        self.s.bind((self.myIP, self.myPort))
+        self.out = self.s
+        self.register()
+        t = Timer(1, self.recv)
+        t.name = "SIP Recieve"
+        t.start()
+
+    def stop(self) -> None:
+        self.NSD = False
+        if self.registerThread:
+            # Only run if registerThread exists
+            self.registerThread.cancel()
+            self.deregister()
+        self._close_sockets()
+
+    def _close_sockets(self) -> None:
+        if hasattr(self, "s"):
+            if self.s:
+                self.s.close()
+        if hasattr(self, "out"):
+            if self.out:
+                self.out.close()
+
+    def genCallID(self) -> str:
+        warnings.warn(
+            "genCallID is deprecated due to PEP8 compliance. "
+            + "Use gen_call_id instead.",
+            DeprecationWarning,
+            stacklevel=2,
+        )
+        return self.gen_call_id()
+
+    def gen_call_id(self) -> str:
+        hash = hashlib.sha256(str(self.callID.next()).encode("utf8"))
+        hhash = hash.hexdigest()
+        return f"{hhash[0:32]}@{self.myIP}:{self.myPort}"
+
+    def lastCallID(self) -> str:
+        warnings.warn(
+            "lastCallID is deprecated due to PEP8 compliance. "
+            + "Use gen_last_call_id instead.",
+            DeprecationWarning,
+            stacklevel=2,
+        )
+        return self.gen_last_call_id()
+
+    def gen_last_call_id(self) -> str:
+        hash = hashlib.sha256(str(self.callID.current() - 1).encode("utf8"))
+        hhash = hash.hexdigest()
+        return f"{hhash[0:32]}@{self.myIP}:{self.myPort}"
+
+    def genTag(self) -> str:
+        warnings.warn(
+            "genTag is deprecated due to PEP8 compliance. "
+            + "Use gen_tag instead.",
+            DeprecationWarning,
+            stacklevel=2,
+        )
+        return self.gen_tag()
+
+    def gen_tag(self) -> str:
+        # Keep as True instead of NSD so it can generate a tag on deregister.
+        while True:
+            rand = str(random.randint(1, 4294967296)).encode("utf8")
+            tag = hashlib.md5(rand).hexdigest()[0:8]
+            if tag not in self.tags:
+                self.tags.append(tag)
+                return tag
+        return ""
+
+    def genSIPVersionNotSupported(self, request: SIPMessage) -> str:
+        warnings.warn(
+            "genSIPVersionNotSupported is deprecated "
+            + "due to PEP8 compliance. "
+            + "Use gen_sip_version_not_supported instead.",
+            DeprecationWarning,
+            stacklevel=2,
+        )
+        return self.gen_sip_version_not_supported(request)
+
+    def gen_sip_version_not_supported(self, request: SIPMessage) -> str:
+        # TODO: Add Supported
+        response = "SIP/2.0 505 SIP Version Not Supported\r\n"
+        response += self._gen_response_via_header(request)
+        response += (
+            f"From: {request.headers['From']['raw']};tag="
+            + f"{request.headers['From']['tag']}\r\n"
+        )
+        response += (
+            f"To: {request.headers['To']['raw']};tag=" + f"{self.genTag()}\r\n"
+        )
+        response += f"Call-ID: {request.headers['Call-ID']}\r\n"
+        response += (
+            f"CSeq: {request.headers['CSeq']['check']} "
+            + f"{request.headers['CSeq']['method']}\r\n"
+        )
+        response += f"Contact: {request.headers['Contact']}\r\n"
+        response += f"User-Agent: pyVoIP {pyVoIP.__version__}\r\n"
+        response += 'Warning: 399 GS "Unable to accept call"\r\n'
+        response += f"Allow: {(', '.join(pyVoIP.SIPCompatibleMethods))}\r\n"
+        response += "Content-Length: 0\r\n\r\n"
+
+        return response
+
+    def genAuthorization(self, request: SIPMessage) -> bytes:
+        warnings.warn(
+            "genAuthorization is deprecated "
+            + "due to PEP8 compliance. Use gen_authorization instead.",
+            DeprecationWarning,
+            stacklevel=2,
+        )
+        return self.gen_authorization(request)
+
+    def gen_authorization(self, request: SIPMessage) -> bytes:
+        realm = request.authentication["realm"]
+        HA1 = self.username + ":" + realm + ":" + self.password
+        HA1 = hashlib.md5(HA1.encode("utf8")).hexdigest()
+        HA2 = (
+            ""
+            + request.headers["CSeq"]["method"]
+            + ":sip:"
+            + self.server
+            + ";transport=UDP"
+        )
+        HA2 = hashlib.md5(HA2.encode("utf8")).hexdigest()
+        nonce = request.authentication["nonce"]
+        response = (HA1 + ":" + nonce + ":" + HA2).encode("utf8")
+        response = hashlib.md5(response).hexdigest().encode("utf8")
+
+        return response
+
+    def genBranch(self, length=32) -> str:
+        """
+        Generate unique branch id according to
+        https://datatracker.ietf.org/doc/html/rfc3261#section-8.1.1.7
+        """
+        warnings.warn(
+            "genBranch is deprecated due to PEP8 compliance. "
+            + "Use gen_branch instead.",
+            DeprecationWarning,
+            stacklevel=2,
+        )
+        return self.gen_branch(length)
+
+    def gen_branch(self, length=32) -> str:
+        """
+        Generate unique branch id according to
+        https://datatracker.ietf.org/doc/html/rfc3261#section-8.1.1.7
+        """
+        branchid = uuid.uuid4().hex[: length - 7]
+        return f"z9hG4bK{branchid}"
+
+    def gen_urn_uuid(self) -> str:
+        """
+        Generate client instance specific urn:uuid
+        """
+        return str(uuid.uuid4()).upper()
+
+    def genFirstRequest(self, deregister=False) -> str:
+        warnings.warn(
+            "genFirstResponse is deprecated "
+            + "due to PEP8 compliance. "
+            + "Use gen_first_response instead.",
+            DeprecationWarning,
+            stacklevel=2,
+        )
+        return self.gen_first_response(deregister)
+
+    def gen_first_response(self, deregister=False) -> str:
+        regRequest = f"REGISTER sip:{self.server} SIP/2.0\r\n"
+        regRequest += (
+            f"Via: SIP/2.0/UDP {self.myIP}:{self.myPort};"
+            + f"branch={self.genBranch()};rport\r\n"
+        )
+        regRequest += (
+            f'From: "{self.username}" '
+            + f"<sip:{self.username}@{self.server}>;tag="
+            + f'{self.tagLibrary["register"]}\r\n'
+        )
+        regRequest += (
+            f'To: "{self.username}" '
+            + f"<sip:{self.username}@{self.server}>\r\n"
+        )
+        regRequest += f"Call-ID: {self.genCallID()}\r\n"
+        regRequest += f"CSeq: {self.registerCounter.next()} REGISTER\r\n"
+        regRequest += (
+            "Contact: "
+            + f"<sip:{self.username}@{self.myIP}:{self.myPort};"
+            + "transport=UDP>;+sip.instance="
+            + f'"<urn:uuid:{self.urnUUID}>"\r\n'
+        )
+        regRequest += f'Allow: {(", ".join(pyVoIP.SIPCompatibleMethods))}\r\n'
+        regRequest += "Max-Forwards: 70\r\n"
+        regRequest += "Allow-Events: org.3gpp.nwinitdereg\r\n"
+        regRequest += f"User-Agent: pyVoIP {pyVoIP.__version__}\r\n"
+        # Supported: 100rel, replaces, from-change, gruu
+        regRequest += (
+            "Expires: "
+            + f"{self.default_expires if not deregister else 0}\r\n"
+        )
+        regRequest += "Content-Length: 0"
+        regRequest += "\r\n\r\n"
+
+        return regRequest
+
+    def genSubscribe(self, response: SIPMessage) -> str:
+        warnings.warn(
+            "genSubscribe is deprecated due to PEP8 compliance. "
+            + "Use gen_subscribe instead.",
+            DeprecationWarning,
+            stacklevel=2,
+        )
+        return self.gen_subscribe(response)
+
+    def gen_subscribe(self, response: SIPMessage) -> str:
+        subRequest = f"SUBSCRIBE sip:{self.username}@{self.server} SIP/2.0\r\n"
+        subRequest += (
+            f"Via: SIP/2.0/UDP {self.myIP}:{self.myPort};"
+            + f"branch={self.genBranch()};rport\r\n"
+        )
+        subRequest += (
+            f'From: "{self.username}" '
+            + f"<sip:{self.username}@{self.server}>;tag="
+            + f"{self.genTag()}\r\n"
+        )
+        subRequest += f"To: <sip:{self.username}@{self.server}>\r\n"
+        subRequest += f'Call-ID: {response.headers["Call-ID"]}\r\n'
+        subRequest += f"CSeq: {self.subscribeCounter.next()} SUBSCRIBE\r\n"
+        # TODO: check if transport is needed
+        subRequest += (
+            "Contact: "
+            + f"<sip:{self.username}@{self.myIP}:{self.myPort};"
+            + "transport=UDP>;+sip.instance="
+            + f'"<urn:uuid:{self.urnUUID}>"\r\n'
+        )
+        subRequest += "Max-Forwards: 70\r\n"
+        subRequest += f"User-Agent: pyVoIP {pyVoIP.__version__}\r\n"
+        subRequest += f"Expires: {self.default_expires * 2}\r\n"
+        subRequest += "Event: message-summary\r\n"
+        subRequest += "Accept: application/simple-message-summary"
+        subRequest += "Content-Length: 0"
+        subRequest += "\r\n\r\n"
+
+        return subRequest
+
+    def genRegister(self, request: SIPMessage, deregister=False) -> str:
+        warnings.warn(
+            "genRegister is deprecated due to PEP8 compliance. "
+            + "Use gen_register instead.",
+            DeprecationWarning,
+            stacklevel=2,
+        )
+        return self.gen_register(request, deregister)
+
+    def gen_register(self, request: SIPMessage, deregister=False) -> str:
+        response = str(self.genAuthorization(request), "utf8")
+        nonce = request.authentication["nonce"]
+        realm = request.authentication["realm"]
+
+        regRequest = f"REGISTER sip:{self.server} SIP/2.0\r\n"
+        regRequest += (
+            f"Via: SIP/2.0/UDP {self.myIP}:{self.myPort};branch="
+            + f"{self.genBranch()};rport\r\n"
+        )
+        regRequest += (
+            f'From: "{self.username}" '
+            + f"<sip:{self.username}@{self.server}>;tag="
+            + f'{self.tagLibrary["register"]}\r\n'
+        )
+        regRequest += (
+            f'To: "{self.username}" '
+            + f"<sip:{self.username}@{self.server}>\r\n"
+        )
+        regRequest += f"Call-ID: {self.genCallID()}\r\n"
+        regRequest += f"CSeq: {self.registerCounter.next()} REGISTER\r\n"
+        regRequest += (
+            "Contact: "
+            + f"<sip:{self.username}@{self.myIP}:{self.myPort};"
+            + "transport=UDP>;+sip.instance="
+            + f'"<urn:uuid:{self.urnUUID}>"\r\n'
+        )
+        regRequest += f'Allow: {(", ".join(pyVoIP.SIPCompatibleMethods))}\r\n'
+        regRequest += "Max-Forwards: 70\r\n"
+        regRequest += "Allow-Events: org.3gpp.nwinitdereg\r\n"
+        regRequest += f"User-Agent: pyVoIP {pyVoIP.__version__}\r\n"
+        regRequest += (
+            "Expires: "
+            + f"{self.default_expires if not deregister else 0}\r\n"
+        )
+        regRequest += (
+            f'Authorization: Digest username="{self.username}",'
+            + f'realm="{realm}",nonce="{nonce}",'
+            + f'uri="sip:{self.server};transport=UDP",'
+            + f'response="{response}",algorithm=MD5\r\n'
+        )
+        regRequest += "Content-Length: 0"
+        regRequest += "\r\n\r\n"
+
+        return regRequest
+
+    def genBusy(self, request: SIPMessage) -> str:
+        warnings.warn(
+            "genBusy is deprecated due to PEP8 compliance. "
+            + "Use gen_busy instead.",
+            DeprecationWarning,
+            stacklevel=2,
+        )
+        return self.gen_busy(request)
+
+    def gen_busy(self, request: SIPMessage) -> str:
+        response = "SIP/2.0 486 Busy Here\r\n"
+        response += self._gen_response_via_header(request)
+        response += (
+            f"From: {request.headers['From']['raw']};tag="
+            + f"{request.headers['From']['tag']}\r\n"
+        )
+        response += (
+            f"To: {request.headers['To']['raw']};tag=" + f"{self.genTag()}\r\n"
+        )
+        response += f"Call-ID: {request.headers['Call-ID']}\r\n"
+        response += (
+            f"CSeq: {request.headers['CSeq']['check']} "
+            + f"{request.headers['CSeq']['method']}\r\n"
+        )
+        response += f"Contact: {request.headers['Contact']}\r\n"
+        # TODO: Add Supported
+        response += f"User-Agent: pyVoIP {pyVoIP.__version__}\r\n"
+        response += 'Warning: 399 GS "Unable to accept call"\r\n'
+        response += f"Allow: {(', '.join(pyVoIP.SIPCompatibleMethods))}\r\n"
+        response += "Content-Length: 0\r\n\r\n"
+
+        return response
+
+    def genOk(self, request: SIPMessage) -> str:
+        warnings.warn(
+            "genOk is deprecated due to PEP8 compliance. "
+            + "Use gen_ok instead.",
+            DeprecationWarning,
+            stacklevel=2,
+        )
+        return self.gen_ok(request)
+
+    def gen_ok(self, request: SIPMessage) -> str:
+        okResponse = "SIP/2.0 200 OK\r\n"
+        okResponse += self._gen_response_via_header(request)
+        okResponse += (
+            f"From: {request.headers['From']['raw']};tag="
+            + f"{request.headers['From']['tag']}\r\n"
+        )
+        okResponse += (
+            f"To: {request.headers['To']['raw']};tag=" + f"{self.genTag()}\r\n"
+        )
+        okResponse += f"Call-ID: {request.headers['Call-ID']}\r\n"
+        okResponse += (
+            f"CSeq: {request.headers['CSeq']['check']} "
+            + f"{request.headers['CSeq']['method']}\r\n"
+        )
+        okResponse += f"User-Agent: pyVoIP {pyVoIP.__version__}\r\n"
+        okResponse += f"Allow: {(', '.join(pyVoIP.SIPCompatibleMethods))}\r\n"
+        okResponse += "Content-Length: 0\r\n\r\n"
+
+        return okResponse
+
+    def genRinging(self, request: SIPMessage) -> str:
+        warnings.warn(
+            "genRinging is deprecated due to PEP8 compliance. "
+            + "Use gen_ringing instead.",
+            DeprecationWarning,
+            stacklevel=2,
+        )
+        return self.gen_ringing(request)
+
+    def gen_ringing(self, request: SIPMessage) -> str:
+        tag = self.genTag()
+        regRequest = "SIP/2.0 180 Ringing\r\n"
+        regRequest += self._gen_response_via_header(request)
+        regRequest += (
+            f"From: {request.headers['From']['raw']};tag="
+            + f"{request.headers['From']['tag']}\r\n"
+        )
+        regRequest += f"To: {request.headers['To']['raw']};tag={tag}\r\n"
+        regRequest += f"Call-ID: {request.headers['Call-ID']}\r\n"
+        regRequest += (
+            f"CSeq: {request.headers['CSeq']['check']} "
+            + f"{request.headers['CSeq']['method']}\r\n"
+        )
+        regRequest += f"Contact: {request.headers['Contact']}\r\n"
+        # TODO: Add Supported
+        regRequest += f"User-Agent: pyVoIP {pyVoIP.__version__}\r\n"
+        regRequest += f"Allow: {(', '.join(pyVoIP.SIPCompatibleMethods))}\r\n"
+        regRequest += "Content-Length: 0\r\n\r\n"
+
+        self.tagLibrary[request.headers["Call-ID"]] = tag
+
+        return regRequest
+
+    def genAnswer(
+        self,
+        request: SIPMessage,
+        sess_id: str,
+        ms: Dict[int, Dict[int, "RTP.PayloadType"]],
+        sendtype: "RTP.TransmitType",
+    ) -> str:
+        warnings.warn(
+            "genAnswer is deprecated due to PEP8 compliance. "
+            + "Use gen_answer instead.",
+            DeprecationWarning,
+            stacklevel=2,
+        )
+        return self.gen_answer(request, sess_id, ms, sendtype)
+
+    def gen_answer(
+        self,
+        request: SIPMessage,
+        sess_id: str,
+        ms: Dict[int, Dict[int, "RTP.PayloadType"]],
+        sendtype: "RTP.TransmitType",
+    ) -> str:
+        # Generate body first for content length
+        body = "v=0\r\n"
+        # TODO: Check IPv4/IPv6
+        body += f"o=pyVoIP {sess_id} {int(sess_id)+2} IN IP4 {self.myIP}\r\n"
+        body += f"s=pyVoIP {pyVoIP.__version__}\r\n"
+        # TODO: Check IPv4/IPv6
+        body += f"c=IN IP4 {self.myIP}\r\n"
+        body += "t=0 0\r\n"
+        for x in ms:
+            # TODO: Check AVP mode from request
+            body += f"m=audio {x} RTP/AVP"
+            for m in ms[x]:
+                body += f" {m}"
+        body += "\r\n"  # m=audio <port> RTP/AVP <codecs>\r\n
+        for x in ms:
+            for m in ms[x]:
+                body += f"a=rtpmap:{m} {ms[x][m]}/{ms[x][m].rate}\r\n"
+                if str(ms[x][m]) == "telephone-event":
+                    body += f"a=fmtp:{m} 0-15\r\n"
+        body += "a=ptime:20\r\n"
+        body += "a=maxptime:150\r\n"
+        body += f"a={sendtype}\r\n"
+
+        tag = self.tagLibrary[request.headers["Call-ID"]]
+
+        regRequest = "SIP/2.0 200 OK\r\n"
+        regRequest += self._gen_response_via_header(request)
+        regRequest += (
+            f"From: {request.headers['From']['raw']};tag="
+            + f"{request.headers['From']['tag']}\r\n"
+        )
+        regRequest += f"To: {request.headers['To']['raw']};tag={tag}\r\n"
+        regRequest += f"Call-ID: {request.headers['Call-ID']}\r\n"
+        regRequest += (
+            f"CSeq: {request.headers['CSeq']['check']} "
+            + f"{request.headers['CSeq']['method']}\r\n"
+        )
+        regRequest += (
+            "Contact: "
+            + f"<sip:{self.username}@{self.myIP}:{self.myPort}>\r\n"
+        )
+        # TODO: Add Supported
+        regRequest += f"User-Agent: pyVoIP {pyVoIP.__version__}\r\n"
+        regRequest += f"Allow: {(', '.join(pyVoIP.SIPCompatibleMethods))}\r\n"
+        regRequest += "Content-Type: application/sdp\r\n"
+        regRequest += f"Content-Length: {len(body)}\r\n\r\n"
+        regRequest += body
+
+        return regRequest
+
+    def genInvite(
+        self,
+        number: str,
+        sess_id: str,
+        ms: Dict[int, Dict[str, "RTP.PayloadType"]],
+        sendtype: "RTP.TransmitType",
+        branch: str,
+        call_id: str,
+    ) -> str:
+        warnings.warn(
+            "genInvite is deprecated due to PEP8 compliance. "
+            + "Use gen_invite instead.",
+            DeprecationWarning,
+            stacklevel=2,
+        )
+        return self.gen_invite(number, sess_id, ms, sendtype, branch, call_id)
+
+    def gen_invite(
+        self,
+        number: str,
+        sess_id: str,
+        ms: Dict[int, Dict[str, "RTP.PayloadType"]],
+        sendtype: "RTP.TransmitType",
+        branch: str,
+        call_id: str,
+    ) -> str:
+        # Generate body first for content length
+        body = "v=0\r\n"
+        # TODO: Check IPv4/IPv6
+        body += f"o=pyVoIP {sess_id} {int(sess_id)+2} IN IP4 {self.myIP}\r\n"
+        body += f"s=pyVoIP {pyVoIP.__version__}\r\n"
+        body += f"c=IN IP4 {self.myIP}\r\n"  # TODO: Check IPv4/IPv6
+        body += "t=0 0\r\n"
+        for x in ms:
+            # TODO: Check AVP mode from request
+            body += f"m=audio {x} RTP/AVP"
+            for m in ms[x]:
+                body += f" {m}"
+        body += "\r\n"  # m=audio <port> RTP/AVP <codecs>\r\n
+        for x in ms:
+            for m in ms[x]:
+                body += f"a=rtpmap:{m} {ms[x][m]}/{ms[x][m].rate}\r\n"
+                if str(ms[x][m]) == "telephone-event":
+                    body += f"a=fmtp:{m} 0-15\r\n"
+        body += "a=ptime:20\r\n"
+        body += "a=maxptime:150\r\n"
+        body += f"a={sendtype}\r\n"
+
+        tag = self.genTag()
+        self.tagLibrary[call_id] = tag
+
+        invRequest = f"INVITE sip:{number}@{self.server} SIP/2.0\r\n"
+        invRequest += (
+            f"Via: SIP/2.0/UDP {self.myIP}:{self.myPort};branch="
+            + f"{branch}\r\n"
+        )
+        invRequest += "Max-Forwards: 70\r\n"
+        invRequest += (
+            "Contact: "
+            + f"<sip:{self.username}@{self.myIP}:{self.myPort}>\r\n"
+        )
+        invRequest += f"To: <sip:{number}@{self.server}>\r\n"
+        invRequest += f"From: <sip:{self.username}@{self.myIP}>;tag={tag}\r\n"
+        invRequest += f"Call-ID: {call_id}\r\n"
+        invRequest += f"CSeq: {self.inviteCounter.next()} INVITE\r\n"
+        invRequest += f"Allow: {(', '.join(pyVoIP.SIPCompatibleMethods))}\r\n"
+        invRequest += "Content-Type: application/sdp\r\n"
+        invRequest += f"User-Agent: pyVoIP {pyVoIP.__version__}\r\n"
+        invRequest += f"Content-Length: {len(body)}\r\n\r\n"
+        invRequest += body
+
+        return invRequest
+
+    def genBye(self, request: SIPMessage) -> str:
+        warnings.warn(
+            "genBye is deprecated due to PEP8 compliance. "
+            + "Use gen_bye instead.",
+            DeprecationWarning,
+            stacklevel=2,
+        )
+        return self.gen_bye(request)
+
+    def gen_bye(self, request: SIPMessage) -> str:
+        tag = self.tagLibrary[request.headers["Call-ID"]]
+        c = request.headers["Contact"].strip("<").strip(">")
+        byeRequest = f"BYE {c} SIP/2.0\r\n"
+        byeRequest += self._gen_response_via_header(request)
+        fromH = request.headers["From"]["raw"]
+        toH = request.headers["To"]["raw"]
+        if request.headers["From"]["tag"] == tag:
+            byeRequest += f"From: {fromH};tag={tag}\r\n"
+            if request.headers["To"]["tag"] != "":
+                to = toH + ";tag=" + request.headers["To"]["tag"]
             else:
-              encoding = None
-            self.body['m'][int(index)]['attributes'][value[0]]['rtpmap'] = {'id': value[0], 'name': value[1], 'frequency': value[2], 'encoding': encoding}
-          elif attribute == "fmtp":
-            #a=fmtp:<format> <format specific parameters>
-            value = value.split(' ')
-            for x in self.body['m']:
-              if value[0] in x['methods']:
-                index = self.body['m'].index(x)
-                break
-                
-            self.body['m'][int(index)]['attributes'][value[0]]['fmtp'] = {'id': value[0], 'settings': value[1:]}
-          else:
-            self.body['a'][attribute] = value
+                to = toH
+            byeRequest += f"To: {to}\r\n"
         else:
-          if attribute == "recvonly" or attribute == "sendrecv" or attribute == "sendonly" or attribute == "inactive":
-            self.body['a']['transmit_type'] = pyVoIP.RTP.TransmitType(attribute)
-      else:
-        self.body[header] = data
-      
-    else:
-      self.body[header] = data    
-      
-  def parseSIPResponse(self, data):
-    headers = data.split(b'\r\n\r\n')[0]
-    body = data.split(b'\r\n\r\n')[1]
-    
-    headers_raw = headers.split(b'\r\n')
-    self.heading = headers_raw.pop(0)
-    self.version = str(self.heading.split(b" ")[0], 'utf8')
-    if self.version not in self.SIPCompatibleVersions:
-      raise SIPParseError("SIP Version {} not compatible.".format(self.version))
-      
-    self.status = SIPStatus(int(self.heading.split(b" ")[1]))
-    
-    headers = {}
-    
-    for x in headers_raw:
-      i = str(x, 'utf8').split(': ')
-      headers[i[0]] = i[1]
-      
-    for x in headers:
-      self.parseHeader(x, headers[x])
-    
-    if len(body)>0:
-      body_raw = body.split(b'\r\n')
-      body_tags={}
-      for x in body_raw:
-        i = str(x, 'utf8').split('=')
-        if i != ['']:
-          self.parseBody(i[0], i[1])
-    
-  def parseSIPMessage(self, data):
-    headers = data.split(b'\r\n\r\n')[0]
-    body = data.split(b'\r\n\r\n')[1]
-    
-    headers_raw = headers.split(b'\r\n')
-    self.heading = headers_raw.pop(0)
-    self.version = str(self.heading.split(b" ")[2], 'utf8')
-    if self.version not in self.SIPCompatibleVersions:
-      raise SIPParseError("SIP Version {} not compatible.".format(self.version))
-      
-    self.method = str(self.heading.split(b" ")[0], 'utf8')
-    
-    headers = {}
-    
-    for x in headers_raw:
-      i = str(x, 'utf8').split(': ')
-      headers[i[0]] = i[1]
-      
-    for x in headers:
-      self.parseHeader(x, headers[x])
-      
-    if len(body)>0:
-      body_raw = body.split(b'\r\n')
-      body_tags={}
-      for x in body_raw:
-        i = str(x, 'utf8').split('=')
-        if i != ['']:
-          self.parseBody(i[0], i[1])
-      
-class SIPClient():
-  def __init__(self, server, port, username, password, myIP=None, myPort=5060, callCallback=None):
-    self.NSD = False
-    self.server = server
-    self.port = port
-    self.hostname = socket.gethostname()
-    self.myIP = socket.gethostbyname(self.hostname)
-    if myIP!=None:
-      self.myIP = myIP
-    self.username = username
-    self.password = password
-    
-    self.callCallback=callCallback
-    
-    self.tags = []
-    self.tagLibrary = {}
-    
-    self.myPort = myPort
-    
-    self.inviteCounter = Counter()
-    self.registerCounter = Counter()
-    self.byeCounter = Counter()
-    self.callID = Counter()
-    self.sessID = Counter()
-    
-    self.registerThread = None
-    self.recvLock = Lock()
-  
-  def recv(self):
-    while self.NSD:
-      self.recvLock.acquire()
-      self.s.setblocking(False)
-      try:
-        raw = self.s.recv(8192)
-        message = SIPMessage(raw)
-        debug(message.summary())
-        self.parseMessage(message)
-      except BlockingIOError:
-        self.s.setblocking(True)
+            byeRequest += (
+                f"To: {fromH};tag=" + f"{request.headers['From']['tag']}\r\n"
+            )
+            byeRequest += f"From: {toH};tag={tag}\r\n"
+        byeRequest += f"Call-ID: {request.headers['Call-ID']}\r\n"
+        cseq = int(request.headers["CSeq"]["check"]) + 1
+        byeRequest += f"CSeq: {cseq} BYE\r\n"
+        byeRequest += (
+            "Contact: "
+            + f"<sip:{self.username}@{self.myIP}:{self.myPort}>\r\n"
+        )
+        byeRequest += f"User-Agent: pyVoIP {pyVoIP.__version__}\r\n"
+        byeRequest += f"Allow: {(', '.join(pyVoIP.SIPCompatibleMethods))}\r\n"
+        byeRequest += "Content-Length: 0\r\n\r\n"
+
+        return byeRequest
+
+    def genAck(self, request: SIPMessage) -> str:
+        warnings.warn(
+            "genAck is deprecated due to PEP8 compliance. "
+            + "Use gen_ack instead.",
+            DeprecationWarning,
+            stacklevel=2,
+        )
+        return self.gen_ack(request)
+
+    def gen_ack(self, request: SIPMessage) -> str:
+        tag = self.tagLibrary[request.headers["Call-ID"]]
+        t = request.headers["To"]["raw"].strip("<").strip(">")
+        ackMessage = f"ACK {t} SIP/2.0\r\n"
+        ackMessage += self._gen_response_via_header(request)
+        ackMessage += "Max-Forwards: 70\r\n"
+        ackMessage += (
+            f"To: {request.headers['To']['raw']};tag=" + f"{self.genTag()}\r\n"
+        )
+        ackMessage += f"From: {request.headers['From']['raw']};tag={tag}\r\n"
+        ackMessage += f"Call-ID: {request.headers['Call-ID']}\r\n"
+        ackMessage += f"CSeq: {request.headers['CSeq']['check']} ACK\r\n"
+        ackMessage += f"User-Agent: pyVoIP {pyVoIP.__version__}\r\n"
+        ackMessage += "Content-Length: 0\r\n\r\n"
+
+        return ackMessage
+
+    def _gen_response_via_header(self, request: SIPMessage) -> str:
+        via = ""
+        for h_via in request.headers["Via"]:
+            v_line = (
+                "Via: SIP/2.0/UDP "
+                + f'{h_via["address"][0]}:{h_via["address"][1]}'
+            )
+            if "branch" in h_via.keys():
+                v_line += f';branch={h_via["branch"]}'
+            if "rport" in h_via.keys():
+                if h_via["rport"] is not None:
+                    v_line += f';rport={h_via["rport"]}'
+                else:
+                    v_line += ";rport"
+            if "received" in h_via.keys():
+                v_line += f';received={h_via["received"]}'
+            v_line += "\r\n"
+            via += v_line
+        return via
+
+    def invite(
+        self,
+        number: str,
+        ms: Dict[int, Dict[str, "RTP.PayloadType"]],
+        sendtype: "RTP.TransmitType",
+    ) -> Tuple[SIPMessage, str, int]:
+        branch = "z9hG4bK" + self.genCallID()[0:25]
+        call_id = self.genCallID()
+        sess_id = self.sessID.next()
+        invite = self.genInvite(
+            number, str(sess_id), ms, sendtype, branch, call_id
+        )
+        self.recvLock.acquire()
+        self.out.sendto(invite.encode("utf8"), (self.server, self.port))
+        debug("Invited")
+        response = SIPMessage(self.s.recv(8192))
+
+        while (
+            response.status != SIPStatus(401)
+            and response.status != SIPStatus(100)
+            and response.status != SIPStatus(180)
+        ) or response.headers["Call-ID"] != call_id:
+            if not self.NSD:
+                break
+            self.parseMessage(response)
+            response = SIPMessage(self.s.recv(8192))
+
+        if response.status == SIPStatus(100) or response.status == SIPStatus(
+            180
+        ):
+            return SIPMessage(invite.encode("utf8")), call_id, sess_id
+        debug(f"Received Response: {response.summary()}")
+        ack = self.genAck(response)
+        self.out.sendto(ack.encode("utf8"), (self.server, self.port))
+        debug("Acknowledged")
+        authhash = self.genAuthorization(response)
+        nonce = response.authentication["nonce"]
+        realm = response.authentication["realm"]
+        auth = (
+            f'Authorization: Digest username="{self.username}",realm='
+            + f'"{realm}",nonce="{nonce}",uri="sip:{self.server};'
+            + f'transport=UDP",response="{str(authhash, "utf8")}",'
+            + "algorithm=MD5\r\n"
+        )
+
+        invite = self.genInvite(
+            number, str(sess_id), ms, sendtype, branch, call_id
+        )
+        invite = invite.replace(
+            "\r\nContent-Length", f"\r\n{auth}Content-Length"
+        )
+
+        self.out.sendto(invite.encode("utf8"), (self.server, self.port))
+
         self.recvLock.release()
-        time.sleep(0.01)
-        continue
-      except SIPParseError as e:
-        if "SIP Version" in str(e):
-          request = self.genSIPVersionNotSupported(message)
-          self.out.sendto(request.encode('utf8'), (self.server, self.port))
+
+        return SIPMessage(invite.encode("utf8")), call_id, sess_id
+
+    def bye(self, request: SIPMessage) -> None:
+        message = self.genBye(request)
+        # TODO: Handle bye to server vs. bye to connected client
+        self.out.sendto(message.encode("utf8"), (self.server, self.port))
+
+    def deregister(self) -> bool:
+        self.recvLock.acquire()
+        firstRequest = self.genFirstRequest(deregister=True)
+        self.out.sendto(firstRequest.encode("utf8"), (self.server, self.port))
+
+        self.out.setblocking(False)
+
+        ready = select.select([self.out], [], [], self.register_timeout)
+        if ready[0]:
+            resp = self.s.recv(8192)
         else:
-          debug(f"SIPParseError in SIP.recv: {type(e)}, {e}")
-      except Exception as e:
-        debug(f"SIP.recv error: {type(e)}, {e}\n\n{str(raw, 'utf8')}")
-        if pyVoIP.DEBUG:
-          self.s.setblocking(True)
-          self.recvLock.release()
-          raise
-      self.s.setblocking(True)
-      self.recvLock.release()
-  
-  def parseMessage(self, message):
-    if message.type != SIPMessageType.MESSAGE:
-      if message.status == SIPStatus.OK:
-        if self.callCallback != None:
-          self.callCallback(message)
-      elif message.status == SIPStatus.NOT_FOUND:
-        if self.callCallback != None:
-          self.callCallback(message)
-      elif message.status == SIPStatus.SERVICE_UNAVAILABLE:
-        if self.callCallback != None:
-          self.callCallback(message)
-      elif message.status == SIPStatus.TRYING or message.status == SIPStatus.RINGING:
-        pass
-      else:
-        debug("TODO: Add 500 Error on Receiving SIP Response:\r\n"+message.summary(), "TODO: Add 500 Error on Receiving SIP Response")
-      self.s.setblocking(True)
-      return
-    elif message.method == "INVITE":
-      if self.callCallback == None:
-        request = self.genBusy(message)
-        self.out.sendto(request.encode('utf8'), (self.server, self.port))
-      else:
-        self.callCallback(message)
-    elif message.method == "BYE":
-      self.callCallback(message)
-      response = self.genOk(message)
-      self.out.sendto(response.encode('utf8'), (self.server, self.port))
-    elif message.method == "ACK":
-      return
-    else:
-      debug("TODO: Add 400 Error on non processable request")
-  
-  def start(self):
-    if self.NSD == True:
-      raise RuntimeError("Attempted to start already started SIPClient")
-    self.NSD = True
-    self.s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
-    #self.out = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
-    self.s.bind((self.myIP, self.myPort))
-    self.out = self.s
-    register = self.register()
-    t = Timer(1, self.recv)
-    t.name = "SIP Recieve"
-    t.start()
-     
-  def stop(self):
-    self.NSD = False
-    if self.registerThread:
-      self.registerThread.cancel()
-      self.deregister()
-    self._close_sockets()
-
-  def _close_sockets(self):
-    if hasattr(self, 's') and self.s:
-        self.s.close()
-    if hasattr(self, 'out') and self.out:
-        self.out.close()
-    
-  def genCallID(self):
-    return hashlib.sha256(str(self.callID.next()).encode('utf8')).hexdigest()[0:32]+"@"+self.myIP+":"+str(self.myPort)
-  
-  def genTag(self):
-    while True:  # Keep as True instead of NSD so it can generate a tag on deregister.
-      tag = hashlib.md5(str(random.randint(1, 4294967296)).encode('utf8')).hexdigest()[0:8]
-      if tag not in self.tags:
-        self.tags.append(tag)
-        return tag
-  
-  def genSIPVersionNotSupported(self, request):
-    response = "SIP/2.0 505 SIP Version Not Supported\r\n"
-    response += "Via: SIP/2.0/UDP "+request.headers['Via']['address'][0]+":"+request.headers['Via']['address'][1]+";branch="+request.headers['Via']['branch']+"\r\n"
-    response += "From: "+request.headers['From']['raw']+";tag="+request.headers['From']['tag']+"\r\n"
-    response += "To: "+request.headers['To']['raw']+";tag="+self.genTag()+"\r\n"
-    response += "Call-ID: "+request.headers['Call-ID']+"\r\n"
-    response += "CSeq: "+request.headers['CSeq']['check']+" "+request.headers['CSeq']['method']+"\r\n"
-    response += "Contact: "+request.headers['Contact']+"\r\n" #TODO: Add Supported
-    response += "User-Agent: pyVoIP """+pyVoIP.__version__+"\r\n"
-    response += "Warning: 399 GS \"Unable to accept call\"\r\n"
-    response += "Allow: "+(", ".join(pyVoIP.SIPCompatibleMethods))+"\r\n"
-    response += "Content-Length: 0\r\n\r\n"
-
-    return response
-  
-  def genAuthorization(self, request):
-    HA1 = hashlib.md5(self.username.encode('utf8')+b':'+request.authentication['realm'].encode('utf8')+b':'+self.password.encode('utf8')).hexdigest().encode('utf8')
-    HA2 = hashlib.md5(request.headers['CSeq']['method'].encode('utf8')+b':sip:'+self.server.encode('utf8')+b';transport=UDP').hexdigest().encode('utf8')
-    nonce = request.authentication['nonce'].encode('utf8')
-    response = hashlib.md5(HA1+b':'+nonce+b':'+HA2).hexdigest().encode('utf8')
-    
-    return response
-  
-  def genRegister(self, request):
-    response = self.genAuthorization(request)
-    nonce = request.authentication['nonce']
-    realm = request.authentication['realm']
-    
-    regRequest = "REGISTER sip:"+self.server
-    regRequest += " SIP/2.0\r\nVia: SIP/2.0/UDP "+self.myIP+":"+str(self.myPort)
-    regRequest += "\r\nMax-Forwards: 70\r\nContact: <sip:"
-    regRequest += self.username+"@"+self.myIP+":"+str(self.myPort)
-    regRequest += ";transport=UDP>\r\nTo: <sip:"""+self.username+"@"
-    regRequest += self.server+";transport=UDP>\r\nFrom: <sip:"+self.username
-    regRequest += "@"+self.server+";transport=UDP>;tag="+self.genTag()
-    regRequest += "\r\nCall-ID: "+request.headers['Call-ID']
-    regRequest += "\r\nCSeq: "+str(self.registerCounter.next())+" REGISTER"
-    regRequest +="\r\nExpires: 300\r\nAllow: "+(", ".join(pyVoIP.SIPCompatibleMethods))+"\r\nUser-Agent: pyVoIP """+pyVoIP.__version__+"\r\n"
-    regRequest += 'Authorization: Digest username="'+self.username
-    regRequest += '",realm="'+realm+'",nonce="'+nonce
-    regRequest += '",uri="sip:'+self.server
-    regRequest += ';transport=UDP",response="'+str(response, 'utf8')
-    regRequest += '",algorithm=MD5\r\n'+"Content-Length: 0\r\n\r\n"
-    
-    return regRequest
-    
-  def genBusy(self, request):
-    regRequest = "SIP/2.0 486 Busy Here\r\n"
-    regRequest += "Via: SIP/2.0/UDP "+request.headers['Via']['address'][0]+":"+request.headers['Via']['address'][1]+";branch="+request.headers['Via']['branch']+"\r\n"
-    regRequest += "From: "+request.headers['From']['raw']+";tag="+request.headers['From']['tag']+"\r\n"
-    regRequest += "To: "+request.headers['To']['raw']+";tag="+self.genTag()+"\r\n"
-    regRequest += "Call-ID: "+request.headers['Call-ID']+"\r\n"
-    regRequest += "CSeq: "+request.headers['CSeq']['check']+" "+request.headers['CSeq']['method']+"\r\n"
-    regRequest += "Contact: "+request.headers['Contact']+"\r\n" #TODO: Add Supported
-    regRequest += "User-Agent: pyVoIP """+pyVoIP.__version__+"\r\n"
-    regRequest += "Warning: 399 GS \"Unable to accept call\"\r\n"
-    regRequest += "Allow: "+(", ".join(pyVoIP.SIPCompatibleMethods))+"\r\n"
-    regRequest += "Content-Length: 0\r\n\r\n"
-    
-    return regRequest
-    
-  def genOk(self, request):
-    okResponse = "SIP/2.0 200 OK\r\n"
-    okResponse += "Via: SIP/2.0/UDP "+request.headers['Via']['address'][0]+":"+request.headers['Via']['address'][1]+";branch="+request.headers['Via']['branch']+"\r\n"
-    okResponse += "From: "+request.headers['From']['raw']+";tag="+request.headers['From']['tag']+"\r\n"
-    okResponse += "To: "+request.headers['To']['raw']+";tag="+request.headers['To']['tag']+"\r\n"
-    okResponse += "Call-ID: "+request.headers['Call-ID']+"\r\n"
-    okResponse += "CSeq: "+request.headers['CSeq']['check']+" "+request.headers['CSeq']['method']+"\r\n"
-    okResponse += "User-Agent: pyVoIP """+pyVoIP.__version__+"\r\n"
-    okResponse += "Allow: "+(", ".join(pyVoIP.SIPCompatibleMethods))+"\r\n"
-    okResponse += "Content-Length: 0\r\n\r\n"
-    
-    return okResponse
-  
-  def genRinging(self, request):
-    tag = self.genTag()
-    regRequest = "SIP/2.0 180 Ringing\r\n"
-    regRequest += "Via: SIP/2.0/UDP "+request.headers['Via']['address'][0]+":"+request.headers['Via']['address'][1]+";branch="+request.headers['Via']['branch']+"\r\n"
-    regRequest += "From: "+request.headers['From']['raw']+";tag="+request.headers['From']['tag']+"\r\n"
-    regRequest += "To: "+request.headers['To']['raw']+";tag="+tag+"\r\n"
-    regRequest += "Call-ID: "+request.headers['Call-ID']+"\r\n"
-    regRequest += "CSeq: "+request.headers['CSeq']['check']+" "+request.headers['CSeq']['method']+"\r\n"
-    regRequest += "Contact: "+request.headers['Contact']+"\r\n" #TODO: Add Supported
-    regRequest += "User-Agent: pyVoIP """+pyVoIP.__version__+"\r\n"
-    regRequest += "Allow: "+(", ".join(pyVoIP.SIPCompatibleMethods))+"\r\n"
-    regRequest += "Content-Length: 0\r\n\r\n"
-    
-    self.tagLibrary[request.headers['Call-ID']] = tag
-    
-    return regRequest
-  
-  def genAnswer(self, request, sess_id, ms, sendtype):
-    #Generate body first for content length
-    body = "v=0\r\n"
-    body += "o=pyVoIP "+sess_id+" "+str(int(sess_id)+2)+" IN IP4 "+self.myIP+"\r\n" #TODO: Check IPv4/IPv6
-    body += "s=pyVoIP """+pyVoIP.__version__+"\r\n"
-    body += "c=IN IP4 "+self.myIP+"\r\n" #TODO: Check IPv4/IPv6
-    body += "t=0 0\r\n"
-    for x in ms: 
-      body += "m=audio "+str(x)+" RTP/AVP" #TODO: Check AVP mode from request
-      for m in ms[x]:
-        body += " "+str(m)
-    body += "\r\n" #m=audio <port> RTP/AVP <codecs>\r\n
-    for x in ms:
-      for m in ms[x]:
-        body += "a=rtpmap:"+str(m)+" "+str(ms[x][m])+"/"+str(ms[x][m].rate)+"\r\n"
-        if str(ms[x][m]) == "telephone-event":
-          body += "a=fmtp:"+str(m)+" 0-15\r\n"
-    body += "a=ptime:20\r\n"
-    body += "a=maxptime:150\r\n"
-    body += "a="+str(sendtype)+"\r\n"
-    
-    tag = self.tagLibrary[request.headers['Call-ID']]
-    
-    regRequest = "SIP/2.0 200 OK\r\n"
-    regRequest += "Via: SIP/2.0/UDP "+request.headers['Via']['address'][0]+":"+request.headers['Via']['address'][1]+";branch="+request.headers['Via']['branch']+"\r\n"
-    regRequest += "From: "+request.headers['From']['raw']+";tag="+request.headers['From']['tag']+"\r\n"
-    regRequest += "To: "+request.headers['To']['raw']+";tag="+tag+"\r\n"
-    regRequest += "Call-ID: "+request.headers['Call-ID']+"\r\n"
-    regRequest += "CSeq: "+request.headers['CSeq']['check']+" "+request.headers['CSeq']['method']+"\r\n"
-    regRequest += "Contact: <sip:"+self.username+"@"+self.myIP+":"+str(self.myPort)+">\r\n" #TODO: Add Supported
-    regRequest += "User-Agent: pyVoIP """+pyVoIP.__version__+"\r\n"
-    regRequest += "Allow: "+(", ".join(pyVoIP.SIPCompatibleMethods))+"\r\n"
-    regRequest += "Content-Type: application/sdp\r\n"
-    regRequest += "Content-Length: "+str(len(body))+"\r\n\r\n"
-    regRequest += body
-    
-    return regRequest
-    
-  def genInvite(self, number, sess_id, ms, sendtype, branch, call_id):
-    #Generate body first for content length
-    body = "v=0\r\n"
-    body += "o=pyVoIP "+sess_id+" "+str(int(sess_id)+2)+" IN IP4 "+self.myIP+"\r\n" #TODO: Check IPv4/IPv6
-    body += "s=pyVoIP """+pyVoIP.__version__+"\r\n"
-    body += "c=IN IP4 "+self.myIP+"\r\n" #TODO: Check IPv4/IPv6
-    body += "t=0 0\r\n"
-    for x in ms: 
-      body += "m=audio "+str(x)+" RTP/AVP" #TODO: Check AVP mode from request
-      for m in ms[x]:
-        body += " "+str(m)
-    body += "\r\n" #m=audio <port> RTP/AVP <codecs>\r\n
-    for x in ms:
-      for m in ms[x]:
-        body += "a=rtpmap:"+str(m)+" "+str(ms[x][m])+"/"+str(ms[x][m].rate)+"\r\n"
-        if str(ms[x][m]) == "telephone-event":
-          body += "a=fmtp:"+str(m)+" 0-15\r\n"
-    body += "a=ptime:20\r\n"
-    body += "a=maxptime:150\r\n"
-    body += "a="+str(sendtype)+"\r\n"
-    
-    tag = self.genTag()
-    self.tagLibrary[call_id] = tag
-    
-    invRequest = "INVITE sip:"+number+"@"+self.server+" SIP/2.0\r\n"
-    invRequest += "Via: SIP/2.0/UDP "+self.myIP+":"+str(self.myPort)+";branch="+branch+"\r\n"
-    invRequest += "Max-Forwards: 70\r\n"
-    invRequest += "Contact: <sip:"+self.username+"@"+self.myIP+":"+str(self.myPort)+">\r\n"
-    invRequest += "To: <sip:"+number+"@"+self.server+">\r\n"
-    invRequest += "From: <sip:"+self.username+"@"+self.myIP+">;tag="+tag+"\r\n"
-    invRequest += "Call-ID: "+call_id+"\r\n"
-    invRequest += "CSeq: "+str(self.inviteCounter.next())+" INVITE\r\n"
-    invRequest += "Allow: "+(", ".join(pyVoIP.SIPCompatibleMethods))+"\r\n"
-    invRequest += "Content-Type: application/sdp\r\n"
-    invRequest += "User-Agent: pyVoIP """+pyVoIP.__version__+"\r\n"
-    invRequest += "Content-Length: "+str(len(body))+"\r\n\r\n"
-    invRequest += body
-    
-    return invRequest
-    
-  def genBye(self, request):
-    tag = self.tagLibrary[request.headers['Call-ID']]
-    byeRequest = "BYE "+request.headers['Contact'].strip('<').strip('>')+" SIP/2.0\r\n"
-    byeRequest += "Via: SIP/2.0/UDP "+self.myIP+":"+str(self.myPort)+";branch="+request.headers['Via']['branch']+"\r\n"
-    if request.headers['From']['tag'] == tag:
-      byeRequest += "From: "+request.headers['From']['raw']+";tag="+tag+"\r\n"
-      byeRequest += "To: "+request.headers['To']['raw']+(";tag="+request.headers['To']['tag'] if request.headers['To']['tag'] != '' else '')+"\r\n"
-    else:
-      byeRequest += "To: "+request.headers['From']['raw']+";tag="+request.headers['From']['tag']+"\r\n"
-      byeRequest += "From: "+request.headers['To']['raw']+";tag="+tag+"\r\n"
-    byeRequest += "Call-ID: "+request.headers['Call-ID']+"\r\n"
-    byeRequest += "CSeq: "+str(int(request.headers['CSeq']['check'])+1)+" BYE\r\n"
-    byeRequest += "Contact: <sip:"+self.username+"@"+self.myIP+":"+str(self.myPort)+">\r\n"
-    byeRequest += "User-Agent: pyVoIP """+pyVoIP.__version__+"\r\n"
-    byeRequest += "Allow: "+(", ".join(pyVoIP.SIPCompatibleMethods))+"\r\n"
-    byeRequest += "Content-Length: 0\r\n\r\n"
-    
-    return byeRequest
-    
-  def genAck(self, request):
-    tag = self.tagLibrary[request.headers['Call-ID']]
-    ackMessage = "ACK "+request.headers['To']['raw'].strip('<').strip('>')+" SIP/2.0\r\n"
-    ackMessage += "Via: SIP/2.0/UDP "+self.myIP+":"+str(self.myPort)+";branch="+request.headers['Via']['branch']+"\r\n"
-    ackMessage += "Max-Forwards: 70\r\n"
-    ackMessage += "To: "+request.headers['To']['raw']+";tag="+request.headers['To']['tag']+"\r\n"
-    ackMessage += "From: "+request.headers['From']['raw']+";tag="+tag+"\r\n"
-    ackMessage += "Call-ID: "+request.headers['Call-ID']+"\r\n"
-    ackMessage += "CSeq: "+str(request.headers['CSeq']['check'])+" ACK\r\n"
-    ackMessage += "User-Agent: pyVoIP """+pyVoIP.__version__+"\r\n"
-    ackMessage += "Content-Length: 0\r\n\r\n"
-    
-    return ackMessage
-  
-  def invite(self, number, ms, sendtype):
-    branch = "z9hG4bK"+self.genCallID()[0:25]
-    call_id = self.genCallID()
-    sess_id = self.sessID.next()
-    invite = self.genInvite(number, str(sess_id), ms, sendtype, branch, call_id)
-    #print("Acquiring")
-    self.recvLock.acquire()
-    #print("Locked")
-    self.out.sendto(invite.encode('utf8'), (self.server, self.port))
-    debug('Invited')
-    response = SIPMessage(self.s.recv(8192))
-    
-    while (response.status != SIPStatus(401) and response.status != SIPStatus(100) and response.status != SIPStatus(180)) or response.headers['Call-ID'] != call_id:
-      if not self.NSD:
-        break
-      self.parseMessage(response)
-      response = SIPMessage(self.s.recv(8192))
-    
-    if response.status == SIPStatus(100) or response.status == SIPStatus(180):
-      return SIPMessage(invite.encode('utf8')), call_id, sess_id
-    debug("Received Response: "+response.summary())
-    ack = self.genAck(response)
-    self.out.sendto(ack.encode('utf8'), (self.server, self.port))
-    debug("Acknowledged")
-    authhash = self.genAuthorization(response)
-    nonce = response.authentication['nonce']
-    realm = response.authentication['realm']
-    auth = 'Authorization: Digest username="'+self.username
-    auth += '",realm="'+realm+'",nonce="'+nonce
-    auth += '",uri="sip:'+self.server
-    auth += ';transport=UDP",response="'+str(authhash, 'utf8')
-    auth += '",algorithm=MD5\r\n'
-    
-    invite = self.genInvite(number, str(sess_id), ms, sendtype, branch, call_id)
-    invite = invite.replace('\r\nContent-Length', '\r\n'+auth+'Content-Length')
-    
-    self.out.sendto(invite.encode('utf8'), (self.server, self.port))
-    
-    self.recvLock.release()
-    #print("Released")
-    return SIPMessage(invite.encode('utf8')), call_id, sess_id
-  
-  def bye(self, request):
-    message = self.genBye(request)
-    self.out.sendto(message.encode('utf8'), (self.server, self.port))
-
-  def deregister(self):
-    self.recvLock.acquire()
-    fake = SIPMessage(b'SIP/2.0 401 Unauthorized\r\nVia: SIP/2.0/UDP 192.168.0.64:5060;received=192.168.0.64\r\nFrom: <sip:5555555@127.0.0.1;transport=UDP>;tag=b4dbea69\r\nTo: <sip:5555555@127.0.0.1;transport=UDP>;tag=as6845844a\r\nCall-ID: '+self.genCallID().encode('utf8')+b'\r\nCSeq: 25273 REGISTER\r\nServer: Asterisk PBX 16.2.1~dfsg-1+deb10u1\r\nAllow: INVITE, ACK, CANCEL, OPTIONS, BYE, REFER, SUBSCRIBE, NOTIFY, INFO, PUBLISH, MESSAGE\r\nSupported: replaces, timer\r\nWWW-Authenticate: Digest algorithm=MD5, realm="asterisk", nonce="7140386d"\r\nContent-Length: 0\r\n\r\n')
-    
-    regRequest = self.genRegister(fake).replace('Expires: 300', 'Expires: 0')
-    
-    self.out.sendto(regRequest.encode('utf8'), (self.server, self.port))
-    
-    response = SIPMessage(self.s.recv(8192))
-    response = self.tryingTimeoutCheck(response)
-
-    
-    while response.status != SIPStatus(401):
-      if response.status == SIPStatus(500):
+            raise TimeoutError("Deregistering on SIP Server timed out")
+
+        response = SIPMessage(resp)
+        response = self.trying_timeout_check(response)
+
+        if response.status == SIPStatus(401):
+            # Unauthorized, likely due to being password protected.
+            regRequest = self.genRegister(response, deregister=True)
+            self.out.sendto(
+                regRequest.encode("utf8"), (self.server, self.port)
+            )
+            ready = select.select([self.s], [], [], self.register_timeout)
+            if ready[0]:
+                resp = self.s.recv(8192)
+                response = SIPMessage(resp)
+                if response.status == SIPStatus(401):
+                    # At this point, it's reasonable to assume that
+                    # this is caused by invalid credentials.
+                    debug("Unauthorized")
+                    raise InvalidAccountInfoError(
+                        "Invalid Username or "
+                        + "Password for SIP server "
+                        + f"{self.server}:"
+                        + f"{self.myPort}"
+                    )
+                elif response.status == SIPStatus(400):
+                    # Bad Request
+                    # TODO: implement
+                    # TODO: check if broken connection can be brought back
+                    # with new urn:uuid or reply with expire 0
+                    self._handle_bad_request()
+            else:
+                raise TimeoutError("Deregistering on SIP Server timed out")
+
+        if response.status == SIPStatus(500):
+            self.recvLock.release()
+            time.sleep(5)
+            return self.deregister()
+
+        if response.status == SIPStatus.OK:
+            self.recvLock.release()
+            return True
         self.recvLock.release()
-        time.sleep(5)
-        return self.deregister()
-      else:
-        self.parseMessage(response)
-      response = SIPMessage(self.s.recv(8192))
-    
-    regRequest = self.genRegister(response).replace('Expires: 300', 'Expires: 0')
-    
-    self.out.sendto(regRequest.encode('utf8'), (self.server, self.port))
-    
-    response = SIPMessage(self.s.recv(8192))
-    response = self.tryingTimeoutCheck(response)
-
-    if response.status==SIPStatus.OK:
-      return True
-    self.recvLock.release()
-    
-  def register(self):
-    self.recvLock.acquire()
-    fake = SIPMessage(b'SIP/2.0 401 Unauthorized\r\nVia: SIP/2.0/UDP 192.168.0.64:5060;received=192.168.0.64\r\nFrom: <sip:5555555@127.0.0.1;transport=UDP>;tag=b4dbea69\r\nTo: <sip:5555555@127.0.0.1;transport=UDP>;tag=as6845844a\r\nCall-ID: '+self.genCallID().encode('utf8')+b'\r\nCSeq: 25273 REGISTER\r\nServer: Asterisk PBX 16.2.1~dfsg-1+deb10u1\r\nAllow: INVITE, ACK, CANCEL, OPTIONS, BYE, REFER, SUBSCRIBE, NOTIFY, INFO, PUBLISH, MESSAGE\r\nSupported: replaces, timer\r\nWWW-Authenticate: Digest algorithm=MD5, realm="asterisk", nonce="7140386d"\r\nContent-Length: 0\r\n\r\n')
-    
-    regRequest = self.genRegister(fake)
-    
-    
-    self.out.sendto(regRequest.encode('utf8'), (self.server, self.port))
-    
-    
-    response = SIPMessage(self.s.recv(8192))
-    response = self.tryingTimeoutCheck(response)
-    
-
-    
-    if response.status != SIPStatus(401):
-      if response.status == SIPStatus(500):
+        return False
+
+    def register(self) -> bool:
+        self.recvLock.acquire()
+        firstRequest = self.genFirstRequest()
+        self.out.sendto(firstRequest.encode("utf8"), (self.server, self.port))
+
+        self.out.setblocking(False)
+
+        ready = select.select([self.out], [], [], self.register_timeout)
+        if ready[0]:
+            resp = self.s.recv(8192)
+        else:
+            raise TimeoutError("Registering on SIP Server timed out")
+
+        response = SIPMessage(resp)
+        response = self.trying_timeout_check(response)
+
+        if response.status == SIPStatus(400):
+            # Bad Request
+            # TODO: implement
+            # TODO: check if broken connection can be brought back
+            # with new urn:uuid or reply with expire 0
+            self._handle_bad_request()
+
+        if response.status == SIPStatus(401):
+            # Unauthorized, likely due to being password protected.
+            regRequest = self.genRegister(response)
+            self.out.sendto(
+                regRequest.encode("utf8"), (self.server, self.port)
+            )
+            ready = select.select([self.s], [], [], self.register_timeout)
+            if ready[0]:
+                resp = self.s.recv(8192)
+                response = SIPMessage(resp)
+                response = self.trying_timeout_check(response)
+                if response.status == SIPStatus(401):
+                    # At this point, it's reasonable to assume that
+                    # this is caused by invalid credentials.
+                    debug("Unauthorized")
+                    raise InvalidAccountInfoError(
+                        "Invalid Username or "
+                        + "Password for SIP server "
+                        + f"{self.server}:"
+                        + f"{self.myPort}"
+                    )
+                elif response.status == SIPStatus(400):
+                    # Bad Request
+                    # TODO: implement
+                    # TODO: check if broken connection can be brought back
+                    # with new urn:uuid or reply with expire 0
+                    self._handle_bad_request()
+            else:
+                raise TimeoutError("Registering on SIP Server timed out")
+
+        if response.status == SIPStatus(407):
+            # Proxy Authentication Required
+            # TODO: implement
+            debug("Proxy auth required")
+
+        # TODO: This must be done more reliable
+        if response.status not in [
+            SIPStatus(400),
+            SIPStatus(401),
+            SIPStatus(407),
+        ]:
+            # Unauthorized
+            if response.status == SIPStatus(500):
+                self.recvLock.release()
+                time.sleep(5)
+                return self.register()
+            else:
+                # TODO: determine if needed here
+                self.parseMessage(response)
+
+        debug(response.summary())
+        debug(response.raw)
+
         self.recvLock.release()
-        time.sleep(5)
-        return self.register()
-      else:
-        self.parseMessage(response)
-    
-    #debug(response.summary())
-    #debug(response.raw)
-    
-    regRequest = self.genRegister(response)
-    
-    self.out.sendto(regRequest.encode('utf8'), (self.server, self.port))
-    
-    response = SIPMessage(self.s.recv(8192))
-    self.recvLock.release()
-
-    response = self.tryingTimeoutCheck(response)
-
-    if response.status == SIPStatus.OK:
-      if self.NSD:
-        self.registerThread = Timer(295, self.register)
-        self.registerThread.name = "SIP Register"
-        self.registerThread.start()
-      return True
-    else:
-      raise InvalidAccountInfoError("Invalid Username or Password for SIP server "+self.server+':'+str(self.myPort))
-    return True
-
-  def tryingTimeoutCheck(self, response):
-    # Some servers need time to process the response. 
-    # when this happens, the first response you get from the server is
-    # SIPStatus.TRYING. This while loop tries checks every second for an updated
-    # response. It times out after 30 seconds.  
-    start_time = time.monotonic()
-    old_timeout = self.s.gettimeout()
-    while response.status == SIPStatus.TRYING:
-      self.s.settimeout(1)
-      try:
+        if response.status == SIPStatus.OK:
+            if self.NSD:
+                # self.subscribe(response)
+                self.registerThread = Timer(
+                    self.default_expires - 5, self.register
+                )
+                self.registerThread.name = (
+                    "SIP Register CSeq: " + f"{self.registerCounter.x}"
+                )
+                self.registerThread.start()
+            return True
+        else:
+            raise InvalidAccountInfoError(
+                "Invalid Username or Password for "
+                + f"SIP server {self.server}:"
+                + f"{self.myPort}"
+            )
+
+    def _handle_bad_request(self) -> None:
+        # Bad Request
+        # TODO: implement
+        # TODO: check if broken connection can be brought back
+        # with new urn:uuid or reply with expire 0
+        debug("Bad Request")
+
+    def subscribe(self, lastresponse: SIPMessage) -> None:
+        # TODO: check if needed and maybe implement fully
+        self.recvLock.acquire()
+
+        subRequest = self.genSubscribe(lastresponse)
+        self.out.sendto(subRequest.encode("utf8"), (self.server, self.port))
+
         response = SIPMessage(self.s.recv(8192))
-      except TimeoutError:
-        pass
-      self.s.settimeout(old_timeout)
-      if (time.monotonic() - start_time) >= 30:
-        raise ResponseTimeoutError("Timeout error for response, waited 30 seconds but SIPStatus is still TRYING")
-    return response
+
+        debug(f'Got response to subscribe: {str(response.heading, "utf8")}')
+
+        self.recvLock.release()
+
+    def trying_timeout_check(self, response: SIPMessage) -> SIPMessage:
+        """
+        Some servers need time to process the response.
+        When this happens, the first response you get from the server is
+        SIPStatus.TRYING. This while loop tries checks every second for an
+        updated response. It times out after 30 seconds.
+        """
+        start_time = time.monotonic()
+        while response.status == SIPStatus.TRYING:
+            if (time.monotonic() - start_time) >= self.register_timeout:
+                raise TimeoutError(
+                    f"Waited {self.register_timeout} seconds but server is "
+                    + "still TRYING"
+                )
+
+            ready = select.select([self.s], [], [], self.register_timeout)
+            if ready[0]:
+                resp = self.s.recv(8192)
+            response = SIPMessage(resp)
+        return response
diff --git a/pyVoIP/VoIP.py b/pyVoIP/VoIP.py
index 87ec073..fc8d22d 100644
--- a/pyVoIP/VoIP.py
+++ b/pyVoIP/VoIP.py
@@ -1,445 +1,765 @@
 from enum import Enum
 from pyVoIP import SIP, RTP
 from threading import Timer, Lock
-from typing import Optional
+from typing import Any, Callable, Dict, List, Optional
+import audioop
 import io
 import pyVoIP
 import random
-import socket
 import time
 import warnings
 
-__all__ = ['CallState', 'InvalidRangeError', 'InvalidStateError', 'NoPortsAvailableError', 'VoIPCall', 'VoIPPhone']
+
+__all__ = [
+    "CallState",
+    "InvalidRangeError",
+    "InvalidStateError",
+    "NoPortsAvailableError",
+    "VoIPCall",
+    "VoIPPhone",
+]
+
 debug = pyVoIP.debug
 
+
 class InvalidRangeError(Exception):
-  pass
+    pass
+
 
 class InvalidStateError(Exception):
-  pass
+    pass
+
 
 class NoPortsAvailableError(Exception):
-  pass
+    pass
+
 
 class CallState(Enum):
-  DIALING = "DIALING"
-  RINGING = "RINGING"
-  ANSWERED = "ANSWERED"
-  ENDED = "ENDED"
-
-'''
-For initiating a phone call, try sending the packet and the recieved OK packet will be sent to the VoIPCall request header.
-'''
-class VoIPCall():
-  def __init__(self, phone, callstate, request, session_id, myIP, ms = None):
-    self.state = callstate
-    self.phone = phone
-    self.sip = self.phone.sip
-    self.request = request
-    self.call_id = request.headers['Call-ID']
-    self.session_id = str(session_id)
-    self.myIP = myIP
-    self.rtpPortHigh = self.phone.rtpPortHigh
-    self.rtpPortLow = self.phone.rtpPortLow
-    
-    self.dtmfLock = Lock()
-    self.dtmf = io.StringIO()
-    
-    self.RTPClients = []
-    
-    self.connections = 0
-    self.audioPorts = 0
-    self.videoPorts = 0
-    
-    self.assignedPorts = {}
-    
-    
-    if callstate == CallState.RINGING:
-      audio = []
-      video = []
-      for x in self.request.body['c']:
-        self.connections += x['address_count']
-      for x in self.request.body['m']:
-        if x['type'] == "audio":
-          self.audioPorts += x['port_count']
-          audio.append(x)
-        elif x['type'] == "video":
-          self.videoPorts += x['port_count']
-          video.append(x)
+    DIALING = "DIALING"
+    RINGING = "RINGING"
+    ANSWERED = "ANSWERED"
+    ENDED = "ENDED"
+
+
+class PhoneStatus(Enum):
+    INACTIVE = "INACTIVE"
+    REGISTERING = "REGISTERING"
+    REGISTERED = "REGISTERED"
+    DEREGISTERING = "DEREGISTERING"
+    FAILED = "FAILED"
+
+
+class VoIPCall:
+    def __init__(
+        self,
+        phone: "VoIPPhone",
+        callstate: CallState,
+        request: SIP.SIPMessage,
+        session_id: int,
+        myIP: str,
+        ms: Optional[Dict[int, RTP.PayloadType]] = None,
+        sendmode="sendonly",
+    ):
+        self.state = callstate
+        self.phone = phone
+        self.sip = self.phone.sip
+        self.request = request
+        self.call_id = request.headers["Call-ID"]
+        self.session_id = str(session_id)
+        self.myIP = myIP
+        self.rtpPortHigh = self.phone.rtpPortHigh
+        self.rtpPortLow = self.phone.rtpPortLow
+        self.sendmode = sendmode
+
+        self.dtmfLock = Lock()
+        self.dtmf = io.StringIO()
+
+        self.RTPClients: List[RTP.RTPClient] = []
+
+        self.connections = 0
+        self.audioPorts = 0
+        self.videoPorts = 0
+
+        # Type checker being weird with this variable.
+        # Appears to be because this variable is used differently depending
+        # on whether we received or originated the call.
+        # Will need to refactor the code later to properly type this.
+        self.assignedPorts: Any = {}
+
+        if callstate == CallState.RINGING:
+            audio = []
+            video = []
+            for x in self.request.body["c"]:
+                self.connections += x["address_count"]
+            for x in self.request.body["m"]:
+                if x["type"] == "audio":
+                    self.audioPorts += x["port_count"]
+                    audio.append(x)
+                elif x["type"] == "video":
+                    self.videoPorts += x["port_count"]
+                    video.append(x)
+                else:
+                    warnings.warn(
+                        f"Unknown media description: {x['type']}", stacklevel=2
+                    )
+
+            # Ports Adjusted is used in case of multiple m tags.
+            if len(audio) > 0:
+                audioPortsAdj = self.audioPorts / len(audio)
+            else:
+                audioPortsAdj = 0
+            if len(video) > 0:
+                videoPortsAdj = self.videoPorts / len(video)
+            else:
+                videoPortsAdj = 0
+
+            if not (
+                (audioPortsAdj == self.connections or self.audioPorts == 0)
+                and (videoPortsAdj == self.connections or self.videoPorts == 0)
+            ):
+                # TODO: Throw error to PBX in this case
+                warnings.warn("Unable to assign ports for RTP.", stacklevel=2)
+                return
+
+            for i in request.body["m"]:
+                assoc = {}
+                e = False
+                for x in i["methods"]:
+                    try:
+                        p = RTP.PayloadType(int(x))
+                        assoc[int(x)] = p
+                    except ValueError:
+                        try:
+                            p = RTP.PayloadType(
+                                i["attributes"][x]["rtpmap"]["name"]
+                            )
+                            assoc[int(x)] = p
+                        except ValueError:
+                            # Sometimes rtpmap raise a KeyError because fmtp
+                            # is set instate
+                            pt = i["attributes"][x]["rtpmap"]["name"]
+                            warnings.warn(
+                                f"RTP Payload type {pt} not found.",
+                                stacklevel=20,
+                            )
+                            # Resets the warning filter so this warning will
+                            # come up again if it happens.  However, this
+                            # also resets all other warnings.
+                            warnings.simplefilter("default")
+                            p = RTP.PayloadType("UNKNOWN")
+                            assoc[int(x)] = p
+                        except KeyError:
+                            # fix issue 42
+                            # When rtpmap is not found, also set the found
+                            # element to UNKNOWN
+                            warnings.warn(
+                                f"RTP KeyError {x} not found.", stacklevel=20
+                            )
+                            p = RTP.PayloadType("UNKNOWN")
+                            assoc[int(x)] = p
+
+                if e:
+                    raise RTP.RTPParseError(
+                        f"RTP Payload type {pt} not found."
+                    )
+
+                # Make sure codecs are compatible.
+                codecs = {}
+                for m in assoc:
+                    if assoc[m] in pyVoIP.RTPCompatibleCodecs:
+                        codecs[m] = assoc[m]
+                # TODO: If no codecs are compatible then send error to PBX.
+
+                port = self.phone.request_port()
+                self.createRTPClients(
+                    codecs, self.myIP, port, request, i["port"]
+                )
+        elif callstate == CallState.DIALING:
+            if ms is None:
+                raise RuntimeError(
+                    "Media assignments are required when "
+                    + "initiating a call"
+                )
+            self.ms = ms
+            for m in self.ms:
+                self.port = m
+                self.assignedPorts[m] = self.ms[m]
+
+    def createRTPClients(
+        self,
+        codecs: Dict[int, RTP.PayloadType],
+        ip: str,
+        port: int,
+        request: SIP.SIPMessage,
+        baseport: int,
+    ) -> None:
+        warnings.warn(
+            "createRTPClients is deprecated due to PEP8 "
+            + "compliance. Use create_rtp_clients instead.",
+            DeprecationWarning,
+            stacklevel=2,
+        )
+        return self.create_rtp_clients(codecs, ip, port, request, baseport)
+
+    def create_rtp_clients(
+        self,
+        codecs: Dict[int, RTP.PayloadType],
+        ip: str,
+        port: int,
+        request: SIP.SIPMessage,
+        baseport: int,
+    ) -> None:
+        for ii in range(len(request.body["c"])):
+            # TODO: Check IPv4/IPv6
+            c = RTP.RTPClient(
+                codecs,
+                ip,
+                port,
+                request.body["c"][ii]["address"],
+                baseport + ii,
+                self.sendmode,
+                dtmf=self.dtmfCallback,
+            )
+            self.RTPClients.append(c)
+
+    def dtmfCallback(self, code: str) -> None:
+        warnings.warn(
+            "dtmfCallback is deprecated due to PEP8 compliance. "
+            + "Use dtmf_callback instead.",
+            DeprecationWarning,
+            stacklevel=2,
+        )
+        return self.dtmf_callback(code)
+
+    def __del__(self):
+        if hasattr(self, "phone"):
+            self.phone.release_ports(call=self)
+
+    def dtmf_callback(self, code: str) -> None:
+        self.dtmfLock.acquire()
+        bufferloc = self.dtmf.tell()
+        self.dtmf.seek(0, 2)
+        self.dtmf.write(code)
+        self.dtmf.seek(bufferloc, 0)
+        self.dtmfLock.release()
+
+    def getDTMF(self, length=1) -> str:
+        warnings.warn(
+            "getDTMF is deprecated due to PEP8 compliance. "
+            + "Use get_dtmf instead.",
+            DeprecationWarning,
+            stacklevel=2,
+        )
+        return self.get_dtmf(length)
+
+    def get_dtmf(self, length=1) -> str:
+        self.dtmfLock.acquire()
+        packet = self.dtmf.read(length)
+        self.dtmfLock.release()
+        return packet
+
+    def genMs(self) -> Dict[int, Dict[int, RTP.PayloadType]]:
+        warnings.warn(
+            "genMs is deprecated due to PEP8 compliance. "
+            + "Use gen_ms instead.",
+            DeprecationWarning,
+            stacklevel=2,
+        )
+        return self.gen_ms()
+
+    def gen_ms(self) -> Dict[int, Dict[int, RTP.PayloadType]]:
+        """
+        Generate m SDP attribute for answering originally and
+        for re-negotiations.
+        """
+        # TODO: this seems "dangerous" if for some reason sip server handles 2
+        # and more bindings it will cause duplicate RTP-Clients to spawn.
+        m = {}
+        for x in self.RTPClients:
+            x.start()
+            m[x.inPort] = x.assoc
+
+        return m
+
+    def renegotiate(self, request: SIP.SIPMessage) -> None:
+        m = self.genMs()
+        message = self.sip.genAnswer(
+            request, self.session_id, m, self.sendmode
+        )
+        self.sip.out.sendto(
+            message.encode("utf8"), (self.phone.server, self.phone.port)
+        )
+        for i in request.body["m"]:
+            for ii, client in zip(
+                range(len(request.body["c"])), self.RTPClients
+            ):
+                client.outIP = request.body["c"][ii]["address"]
+                client.outPort = i["port"] + ii  # TODO: Check IPv4/IPv6
+
+    def answer(self) -> None:
+        if self.state != CallState.RINGING:
+            raise InvalidStateError("Call is not ringing")
+        m = self.genMs()
+        message = self.sip.genAnswer(
+            self.request, self.session_id, m, self.sendmode
+        )
+        self.sip.out.sendto(
+            message.encode("utf8"), (self.phone.server, self.phone.port)
+        )
+        self.state = CallState.ANSWERED
+
+    def answered(self, request: SIP.SIPMessage) -> None:
+        if self.state != CallState.DIALING:
+            return
+
+        for i in request.body["m"]:
+            assoc = {}
+            e = False
+            for x in i["methods"]:
+                try:
+                    p = RTP.PayloadType(int(x))
+                    assoc[int(x)] = p
+                except ValueError:
+                    try:
+                        p = RTP.PayloadType(
+                            i["attributes"][x]["rtpmap"]["name"]
+                        )
+                        assoc[int(x)] = p
+                    except ValueError:
+                        e = True
+
+            if e:
+                raise RTP.RTPParseError(f"RTP Payload type {p} not found.")
+
+            self.createRTPClients(
+                assoc, self.myIP, self.port, request, i["port"]
+            )
+
+        for x in self.RTPClients:
+            x.start()
+        self.request.headers["Contact"] = request.headers["Contact"]
+        self.request.headers["To"]["tag"] = request.headers["To"]["tag"]
+        self.state = CallState.ANSWERED
+
+    def notFound(self, request: SIP.SIPMessage) -> None:
+        warnings.warn(
+            "notFound is deprecated due to PEP8 compliance. "
+            + "Use not_found instead.",
+            DeprecationWarning,
+            stacklevel=2,
+        )
+        return self.not_found(request)
+
+    def not_found(self, request: SIP.SIPMessage) -> None:
+        if self.state != CallState.DIALING:
+            debug(
+                "TODO: 500 Error, received a not found response for a "
+                + f"call not in the dailing state.  Call: {self.call_id}, "
+                + f"Call State: {self.state}"
+            )
+            return
+
+        for x in self.RTPClients:
+            x.stop()
+        self.state = CallState.ENDED
+        del self.phone.calls[self.request.headers["Call-ID"]]
+        debug("Call not found and terminated")
+        warnings.warn(
+            f"The number '{request.headers['To']['number']}' "
+            + "was not found.  Did you call the wrong number?  "
+            + "CallState set to CallState.ENDED.",
+            stacklevel=20,
+        )
+        # Resets the warning filter so this warning will
+        # come up again if it happens.  However, this
+        # also resets all other warnings.
+        warnings.simplefilter("default")
+
+    def unavailable(self, request: SIP.SIPMessage) -> None:
+        if self.state != CallState.DIALING:
+            debug(
+                "TODO: 500 Error, received an unavailable response for a "
+                + f"call not in the dailing state.  Call: {self.call_id}, "
+                + f"Call State: {self.state}"
+            )
+            return
+
+        for x in self.RTPClients:
+            x.stop()
+        self.state = CallState.ENDED
+        del self.phone.calls[self.request.headers["Call-ID"]]
+        debug("Call unavailable and terminated")
+        warnings.warn(
+            f"The number '{request.headers['To']['number']}' "
+            + "was unavailable.  CallState set to CallState.ENDED.",
+            stacklevel=20,
+        )
+        # Resets the warning filter so this warning will
+        # come up again if it happens.  However, this
+        # also resets all other warnings.
+        warnings.simplefilter("default")
+
+    def deny(self) -> None:
+        if self.state != CallState.RINGING:
+            raise InvalidStateError("Call is not ringing")
+        message = self.sip.genBusy(self.request)
+        self.sip.out.sendto(
+            message.encode("utf8"), (self.phone.server, self.phone.port)
+        )
+        for x in self.RTPClients:
+            x.stop()
+        self.state = CallState.ENDED
+        del self.phone.calls[self.request.headers["Call-ID"]]
+
+    def hangup(self) -> None:
+        if self.state != CallState.ANSWERED:
+            raise InvalidStateError("Call is not answered")
+        for x in self.RTPClients:
+            x.stop()
+        self.sip.bye(self.request)
+        self.state = CallState.ENDED
+        if self.request.headers["Call-ID"] in self.phone.calls:
+            del self.phone.calls[self.request.headers["Call-ID"]]
+
+    def bye(self) -> None:
+        if self.state == CallState.ANSWERED:
+            for x in self.RTPClients:
+                x.stop()
+            self.state = CallState.ENDED
+        if self.request.headers["Call-ID"] in self.phone.calls:
+            del self.phone.calls[self.request.headers["Call-ID"]]
+
+    def writeAudio(self, data: bytes) -> None:
+        warnings.warn(
+            "writeAudio is deprecated due to PEP8 compliance. "
+            + "Use write_audio instead.",
+            DeprecationWarning,
+            stacklevel=2,
+        )
+        return self.write_audio(data)
+
+    def write_audio(self, data: bytes) -> None:
+        for x in self.RTPClients:
+            x.write(data)
+
+    def readAudio(self, length=160, blocking=True) -> bytes:
+        warnings.warn(
+            "readAudio is deprecated due to PEP8 compliance. "
+            + "Use read_audio instead.",
+            DeprecationWarning,
+            stacklevel=2,
+        )
+        return self.read_audio(length, blocking)
+
+    def read_audio(self, length=160, blocking=True) -> bytes:
+        if len(self.RTPClients) == 1:
+            return self.RTPClients[0].read(length, blocking)
+        data = []
+        for x in self.RTPClients:
+            data.append(x.read(length))
+        # Mix audio from different sources before returning
+        nd = audioop.add(data.pop(0), data.pop(0), 1)
+        for d in data:
+            nd = audioop.add(nd, d, 1)
+        return nd
+
+
+class VoIPPhone:
+    def __init__(
+        self,
+        server: str,
+        port: int,
+        username: str,
+        password: str,
+        myIP="0.0.0.0",
+        callCallback: Optional[Callable[["VoIPCall"], None]] = None,
+        sipPort=5060,
+        rtpPortLow=10000,
+        rtpPortHigh=20000,
+    ):
+        if rtpPortLow > rtpPortHigh:
+            raise InvalidRangeError("'rtpPortHigh' must be >= 'rtpPortLow'")
+
+        self.rtpPortLow = rtpPortLow
+        self.rtpPortHigh = rtpPortHigh
+        self.NSD = False
+
+        self.portsLock = Lock()
+        self.assignedPorts: List[int] = []
+        self.session_ids: List[int] = []
+
+        self.server = server
+        self.port = port
+        self.myIP = myIP
+        self.username = username
+        self.password = password
+        self.callCallback = callCallback
+        self._status = PhoneStatus.INACTIVE
+
+        # "recvonly", "sendrecv", "sendonly", "inactive"
+        self.sendmode = "sendrecv"
+        self.recvmode = "sendrecv"
+
+        self.calls: Dict[str, VoIPCall] = {}
+        self.threads: List[Timer] = []
+        # Allows you to find call ID based off thread.
+        self.threadLookup: Dict[Timer, str] = {}
+        self.sip = SIP.SIPClient(
+            server,
+            port,
+            username,
+            password,
+            myIP=self.myIP,
+            myPort=sipPort,
+            callCallback=self.callback,
+        )
+
+    def callback(self, request: SIP.SIPMessage) -> None:
+        # debug("Callback: "+request.summary())
+        if request.type == pyVoIP.SIP.SIPMessageType.MESSAGE:
+            # debug("This is a message")
+            if request.method == "INVITE":
+                self._callback_MSG_Invite(request)
+            elif request.method == "BYE":
+                self._callback_MSG_Bye(request)
         else:
-          warnings.warn("Unknown media description: "+x['type'], stacklevel=2)
-      
-      #Ports Adjusted is used in case of multiple m=audio or m=video tags.
-      if len(audio) > 0:
-        audioPortsAdj = self.audioPorts/len(audio)
-      else:
-        audioPortsAdj = 0
-      if len(video) > 0:
-        videoPortsAdj = self.videoPorts/len(video)
-      else:
-        videoPortsAdj = 0
-      
-      if not ((audioPortsAdj == self.connections or self.audioPorts == 0) and (videoPortsAdj == self.connections or self.videoPorts == 0)):
-        warnings.warn("Unable to assign ports for RTP.", stacklevel=2) #TODO: Throw error to PBX in this case
-        return
-      
-      for i in request.body['m']:
-        assoc = {}
-        e = False
-        for x in i['methods']:
-          try:
-            p = RTP.PayloadType(int(x))
-            assoc[int(x)] = p
-          except ValueError:
-            try:
-              p = RTP.PayloadType(i['attributes'][x]['rtpmap']['name'])
-              assoc[int(x)] = p
-            except ValueError:
-              #e = True
-              pt = i['attributes'][x]['rtpmap']['name']
-              warnings.warn(f"RTP Payload type {pt} not found.", stacklevel=20)
-              warnings.simplefilter("default") #Resets the warning filter so this warning will come up again if it happens.  However, this also resets all other warnings as well.
-              p = RTP.PayloadType("UNKOWN")
-              assoc[int(x)] = p
-        
-        if e:
-          raise RTP.RTPParseError("RTP Payload type {} not found.".format(str(pt)))
-        
-        #Make sure codecs are compatible. 
-        codecs = {}
-        for m in assoc:
-          if assoc[m] in pyVoIP.RTPCompatibleCodecs:
-            codecs[m] = assoc[m]
-        #TODO: If no codecs are compatible then send error to PBX.
-        
-        port = self.phone.request_port()
-        for ii in range(len(request.body['c'])):
-          self.RTPClients.append(RTP.RTPClient(codecs, self.myIP, port, request.body['c'][ii]['address'], i['port']+ii, request.body['a']['transmit_type'], dtmf=self.dtmfCallback)) #TODO: Check IPv4/IPv6
-    elif callstate == CallState.DIALING:
-      self.ms = ms
-      for m in self.ms:
-        self.port = m
-        self.assignedPorts[m] = self.ms[m]
-
-  def __del__(self):
-    if hasattr(self, 'phone'):
-      self.phone.release_ports(call=self)
-
-  def dtmfCallback(self, code):
-    self.dtmfLock.acquire()
-    bufferloc = self.dtmf.tell()
-    self.dtmf.seek(0, 2)
-    self.dtmf.write(code)
-    self.dtmf.seek(bufferloc, 0)
-    self.dtmfLock.release()
-
-  def getDTMF(self, length=1):
-    self.dtmfLock.acquire()
-    packet = self.dtmf.read(length)
-    self.dtmfLock.release()
-    return packet
-
-  def genMs(self): #For answering originally and for re-negotiations
-    m = {}
-    for x in self.RTPClients:
-      x.start()
-      m[x.inPort] = x.assoc
-    
-    return m
-
-  def renegotiate(self, request):
-    m = self.genMs()
-    message = self.sip.genAnswer(request, self.session_id, m, request.body['a']['transmit_type'])
-    self.sip.out.sendto(message.encode('utf8'), (self.phone.server, self.phone.port))
-    for i in request.body['m']:
-      for ii, client in zip(range(len(request.body['c'])), self.RTPClients):
-        client.outIP = request.body['c'][ii]['address']
-        client.outPort = i['port']+ii #TODO: Check IPv4/IPv6
-
-  def answer(self):
-    if self.state != CallState.RINGING:
-      raise InvalidStateError("Call is not ringing")
-    m = self.genMs()
-    message = self.sip.genAnswer(self.request, self.session_id, m, self.request.body['a']['transmit_type'])
-    self.sip.out.sendto(message.encode('utf8'), (self.phone.server, self.phone.port))
-    self.state = CallState.ANSWERED
-    
-  def answered(self, request):
-    if self.state != CallState.DIALING:
-      return
-    
-    for i in request.body['m']:
-      assoc = {}
-      e = False
-      for x in i['methods']:
-        try:
-          p = RTP.PayloadType(int(x))
-          assoc[int(x)] = p
-        except ValueError:
-          try:
-            p = RTP.PayloadType(i['attributes'][x]['rtpmap']['name'])
-            assoc[int(x)] = p
-          except ValueError:
-            e = True
-      
-      if e:
-        raise RTP.ParseError("RTP Payload type {} not found.".format(str(pt)))
-      
-      
-      for ii in range(len(request.body['c'])):
-        self.RTPClients.append(RTP.RTPClient(assoc, self.myIP, self.port, request.body['c'][ii]['address'], i['port']+ii, request.body['a']['transmit_type'], dtmf=self.dtmfCallback)) #TODO: Check IPv4/IPv6
-    
-    for x in self.RTPClients:
-      x.start()
-    self.request.headers['Contact'] = request.headers['Contact']
-    self.request.headers['To']['tag'] = request.headers['To']['tag']
-    self.state = CallState.ANSWERED
-  
-  def notFound(self, request):
-    if self.state != CallState.DIALING:
-      debug(f"TODO: 500 Error, received a not found response for a call not in the dailing state.  Call: {self.call_id}, Call State: {self.state}")
-      return
-    
-    for x in self.RTPClients:
-      x.stop()
-    self.state = CallState.ENDED
-    del self.phone.calls[self.request.headers['Call-ID']]
-    debug("Call not found and terminated")
-    warnings.warn(f"The number '{request.headers['To']['number']}' was not found.  Did you call the wrong number? CallState set to CallState.ENDED.", stacklevel=20)
-    warnings.simplefilter("default") #Resets the warning filter so this warning will come up again if it happens.  However, this also resets all other warnings as well.
-  
-  def unavailable(self, request):
-    if self.state != CallState.DIALING:
-      debug(f"TODO: 500 Error, received an unavailable response for a call not in the dailing state.  Call: {self.call_id}, Call State: {self.state}")
-      return
-    
-    for x in self.RTPClients:
-      x.stop()
-    self.state = CallState.ENDED
-    del self.phone.calls[self.request.headers['Call-ID']]
-    debug("Call unavailable and terminated")
-    warnings.warn(f"The number '{request.headers['To']['number']}' was unavailable.  CallState set to CallState.ENDED.", stacklevel=20)
-    warnings.simplefilter("default") #Resets the warning filter so this warning will come up again if it happens.  However, this also resets all other warnings as well.
-  
-  def deny(self):
-    if self.state != CallState.RINGING:
-      raise InvalidStateError("Call is not ringing")
-    message = self.sip.genBusy(self.request)
-    self.sip.out.sendto(message.encode('utf8'), (self.phone.server, self.phone.port))
-    for x in self.RTPClients:
-      x.stop()
-    self.state = CallState.ENDED
-    del self.phone.calls[self.request.headers['Call-ID']]
-  
-  def hangup(self):
-    if self.state != CallState.ANSWERED:
-      raise InvalidStateError("Call is not answered")
-    for x in self.RTPClients:
-      x.stop()
-    self.sip.bye(self.request)
-    self.state = CallState.ENDED
-    del self.phone.calls[self.request.headers['Call-ID']]
-    
-  def bye(self):
-    if self.state == CallState.ANSWERED:
-      for x in self.RTPClients:
-        x.stop()
-      self.state = CallState.ENDED
-    del self.phone.calls[self.request.headers['Call-ID']]
-    
-  def writeAudio(self, data):
-    for x in self.RTPClients:
-      x.write(data)
-      
-  def readAudio(self, length=160, blocking=True):
-    if len(self.RTPClients) == 1:
-      return self.RTPClients[0].read(length, blocking)
-    data = []
-    for x in self.RTPClients:
-      data.append(x.read(length))
-    nd = audioop.add(data.pop(0), data.pop(0), 1) #Mix audio from different sources before returning
-    for d in data:
-      nd = audioop.add(nd, d, 1)
-    return nd
-      
-
-class VoIPPhone():
-  def __init__(self, server, port, username, password, callCallback=None, myIP=None, sipPort=5060, rtpPortLow=10000, rtpPortHigh=20000):
-    if rtpPortLow > rtpPortHigh:
-      raise InvalidRangeError("'rtpPortHigh' must be >= 'rtpPortLow'")
-      
-    self.rtpPortLow = rtpPortLow
-    self.rtpPortHigh = rtpPortHigh
-    self.NSD = False
-    
-    self.portsLock = Lock()
-    self.assignedPorts = []
-    self.session_ids = []
-    
-    self.server = server
-    self.port = port
-    self.hostname = socket.gethostname()
-    self.myIP = socket.gethostbyname(self.hostname)
-    if myIP!=None:
-      self.myIP = myIP
-    self.username = username
-    self.password = password
-    self.callCallback = callCallback
-    
-    self.calls = {}    
-    self.threads = []
-    self.threadLookup = {}  # Allows you to find call ID based of thread.
-    self.sip = SIP.SIPClient(server, port, username, password, myIP=self.myIP, myPort=sipPort, callCallback=self.callback)
-    
-  def callback(self, request):
-    call_id = request.headers['Call-ID']
-    #debug("Callback: "+request.summary())
-    if request.type == pyVoIP.SIP.SIPMessageType.MESSAGE:
-      #debug("This is a message")
-      if request.method == "INVITE":
+            if request.status == SIP.SIPStatus.OK:
+                self._callback_RESP_OK(request)
+            elif request.status == SIP.SIPStatus.NOT_FOUND:
+                self._callback_RESP_NotFound(request)
+            elif request.status == SIP.SIPStatus.SERVICE_UNAVAILABLE:
+                self._callback_RESP_Unavailable(request)
+
+    def getStatus(self) -> PhoneStatus:
+        warnings.warn(
+            "getStatus is deprecated due to PEP8 compliance. "
+            + "Use get_status instead.",
+            DeprecationWarning,
+            stacklevel=2,
+        )
+        return self.get_status()
+
+    def get_status(self) -> PhoneStatus:
+        return self._status
+
+    def _callback_MSG_Invite(self, request: SIP.SIPMessage) -> None:
+        call_id = request.headers["Call-ID"]
         if call_id in self.calls:
-          debug("Re-negotiation detected!")
-          self.calls[call_id].renegotiate(request)
-          #message = self.sip.genAnswer(request, self.calls[call_id].session_id, self.calls[call_id].genMs(), request.body['a']['transmit_type'])
-          #self.sip.out.sendto(message.encode('utf8'), (self.server, self.port))
-          return #Raise Error
-        if self.callCallback == None:
-          message = self.sip.genBusy(request)
-          self.sip.out.sendto(message.encode('utf8'), (self.server, self.port))
-        else:
-          debug("New call!")
-          sess_id = None
-          while sess_id == None:
-            proposed = random.randint(1, 100000)
-            if not proposed in self.session_ids:
-              self.session_ids.append(proposed)
-              sess_id = proposed
-          message = self.sip.genRinging(request)
-          self.sip.out.sendto(message.encode('utf8'), (self.server, self.port))
-          self.calls[call_id] = VoIPCall(self, CallState.RINGING, request, sess_id, self.myIP)
-          try:
-            t = Timer(1, self.callCallback, [self.calls[call_id]])
-            t.name = "Phone Call: "+call_id
-            t.start()
-            self.threads.append(t)
-            self.threadLookup[t] = call_id
-          except Exception:
+            debug("Re-negotiation detected!")
+            # TODO: this seems "dangerous" if for some reason sip server
+            # handles 2 and more bindings it will cause duplicate RTP-Clients
+            # to spawn.
+
+            # CallState.Ringing seems important here to prevent multiple
+            # answering and RTP-Client spawning. Find out when renegotiation
+            # is relevant.
+            if self.calls[call_id].state != CallState.RINGING:
+                self.calls[call_id].renegotiate(request)
+            return  # Raise Error
+        if self.callCallback is None:
             message = self.sip.genBusy(request)
-            self.sip.out.sendto(message.encode('utf8'), (self.server, self.port))
-            raise
-      elif request.method == "BYE":
-        if not call_id in self.calls:
-          return
+            self.sip.out.sendto(
+                message.encode("utf8"), (self.server, self.port)
+            )
+        else:
+            debug("New call!")
+            sess_id = None
+            while sess_id is None:
+                proposed = random.randint(1, 100000)
+                if proposed not in self.session_ids:
+                    self.session_ids.append(proposed)
+                    sess_id = proposed
+            message = self.sip.genRinging(request)
+            self.sip.out.sendto(
+                message.encode("utf8"), (self.server, self.port)
+            )
+            self._create_Call(request, sess_id)
+            try:
+                t = Timer(1, self.callCallback, [self.calls[call_id]])
+                t.name = f"Phone Call: {call_id}"
+                t.start()
+                self.threads.append(t)
+                self.threadLookup[t] = call_id
+            except Exception:
+                message = self.sip.genBusy(request)
+                self.sip.out.sendto(
+                    message.encode("utf8"), (self.server, self.port)
+                )
+                raise
+
+    def _callback_MSG_Bye(self, request: SIP.SIPMessage) -> None:
+        debug("BYE recieved")
+        call_id = request.headers["Call-ID"]
+        if call_id not in self.calls:
+            return
         self.calls[call_id].bye()
-    else:
-      if request.status == SIP.SIPStatus.OK:
+
+    def _callback_RESP_OK(self, request: SIP.SIPMessage) -> None:
         debug("OK recieved")
-        if not call_id in self.calls:
-          debug("Unknown call")
-          return
+        call_id = request.headers["Call-ID"]
+        if call_id not in self.calls:
+            debug("Unknown/No call")
+            return
+        # TODO: Somehow never is reached. Find out if you have a network
+        # issue here or your invite is wrong.
         self.calls[call_id].answered(request)
         debug("Answered")
         ack = self.sip.genAck(request)
-        self.sip.out.sendto(ack.encode('utf8'), (self.server, self.port))
-      elif request.status == SIP.SIPStatus.NOT_FOUND:
+        self.sip.out.sendto(ack.encode("utf8"), (self.server, self.port))
+
+    def _callback_RESP_NotFound(self, request: SIP.SIPMessage) -> None:
         debug("Not Found recieved, invalid number called?")
-        if not call_id in self.calls:
-          debug("Unkown call")
-          debug("TODO: Add 481 here as server is probably waiting for an ACK")
+        call_id = request.headers["Call-ID"]
+        if call_id not in self.calls:
+            debug("Unknown/No call")
+            debug(
+                "TODO: Add 481 here as server is probably waiting for "
+                + "an ACK"
+            )
         self.calls[call_id].notFound(request)
         debug("Terminating Call")
         ack = self.sip.genAck(request)
-        self.sip.out.sendto(ack.encode('utf8'), (self.server, self.port))
-      elif request.status == SIP.SIPStatus.SERVICE_UNAVAILABLE:
+        self.sip.out.sendto(ack.encode("utf8"), (self.server, self.port))
+
+    def _callback_RESP_Unavailable(self, request: SIP.SIPMessage) -> None:
         debug("Service Unavailable recieved")
-        if not call_id in self.calls:
-          debug("Unkown call")
-          debug("TODO: Add 481 here as server is probably waiting for an ACK")
+        call_id = request.headers["Call-ID"]
+        if call_id not in self.calls:
+            debug("Unkown call")
+            debug(
+                "TODO: Add 481 here as server is probably waiting for "
+                + "an ACK"
+            )
         self.calls[call_id].unavailable(request)
         debug("Terminating Call")
         ack = self.sip.genAck(request)
-        self.sip.out.sendto(ack.encode('utf8'), (self.server, self.port))
-
-  def request_port(self, blocking=True) -> int:
-    ports_available = [port for port in range(self.rtpPortLow,
-                       self.rtpPortHigh + 1) if port not in self.assignedPorts]
-    if len(ports_available) == 0:
-      # If no ports are available attempt to cleanup any missed calls.
-      self.release_ports()
-      ports_available = [port for port in range(self.rtpPortLow,
-                         self.rtpPortHigh + 1) if (port not in
-                                                   self.assignedPorts)]
-
-    while self.NSD and blocking and len(ports_available) == 0:
-      ports_available = [port for port in range(self.rtpPortLow,
-                         self.rtpPortHigh + 1) if (port not in
-                                                   self.assignedPorts)]
-      time.sleep(.5)
-      self.release_ports()
-
-    if len(ports_available) == 0:
-        raise NoPortsAvailableError("No ports were available to be assigned")
-
-    selection = random.choice(ports_available)
-    self.assignedPorts.append(selection)
-
-    return selection
-
-  def release_ports(self, call: Optional[VoIPCall] = None) -> None:
-    self.portsLock.acquire()
-    self._cleanup_dead_calls()
-    try:
-      if isinstance(call, VoIPCall):
-        ports = list(call.assignedPorts.keys())
-      else:
-        dnr_ports = []
-        for call_id in self.calls:
-          dnr_ports += list(self.calls[call_id].assignedPorts.keys())
-        ports = []
-        for port in self.assignedPorts:
-            if port not in dnr_ports:
-                ports.append(port)
-
-      for port in ports:
-        index = self.assignedPorts.index(port)
-        self.assignedPorts.pop(index)
-    finally:
-      self.portsLock.release()
-
-  def _cleanup_dead_calls(self) -> None:
-    to_delete =[]
-    for thread in self.threads:
-      if not thread.is_alive():
-        call_id = self.threadLookup[thread]
-        del self.calls[call_id]
-        del self.threadLookup[thread]
-        to_delete.append(thread)
-    for thread in to_delete:
-      index = self.threads.index(thread)
-      self.threads.pop(thread)
-
-  def start(self):
-    try:
-      self.sip.start()
-      self.NSD = True
-    except BaseException:
-      self.sip.stop()
-      self.NSD = False
-      raise
-    
-  def stop(self):
-    for x in self.calls.copy():
-      try:
-        self.calls[x].hangup()
-      except InvalidStateError:
-        pass
-    self.sip.stop()
-    
-  def call(self, number):
-    port = self.request_port()
-    medias = {}
-    medias[port] = {0: pyVoIP.RTP.PayloadType.PCMU, 101: pyVoIP.RTP.PayloadType.EVENT}
-    request, call_id, sess_id = self.sip.invite(number, medias, pyVoIP.RTP.TransmitType.SENDRECV)
-    self.calls[call_id] = VoIPCall(self, CallState.DIALING, request, sess_id, self.myIP, ms = medias)
-    
-    return self.calls[call_id]
+        self.sip.out.sendto(ack.encode("utf8"), (self.server, self.port))
+
+    def _create_Call(self, request: SIP.SIPMessage, sess_id: int) -> None:
+        """
+        Create VoIP call object. Should be separated to enable better
+        subclassing.
+        """
+        call_id = request.headers["Call-ID"]
+        self.calls[call_id] = VoIPCall(
+            self,
+            CallState.RINGING,
+            request,
+            sess_id,
+            self.myIP,
+            sendmode=self.recvmode,
+        )
+
+    def start(self) -> None:
+        self._status = PhoneStatus.REGISTERING
+        try:
+            self.sip.start()
+            self._status = PhoneStatus.REGISTERED
+            self.NSD = True
+        except Exception:
+            self._status = PhoneStatus.FAILED
+            self.sip.stop()
+            self.NSD = False
+            raise
+
+    def stop(self) -> None:
+        self._status = PhoneStatus.DEREGISTERING
+        for x in self.calls.copy():
+            try:
+                self.calls[x].hangup()
+            except InvalidStateError:
+                pass
+        self.sip.stop()
+        self._status = PhoneStatus.INACTIVE
+
+    def call(self, number: str) -> VoIPCall:
+        port = self.request_port()
+        medias = {}
+        medias[port] = {0: RTP.PayloadType.PCMU, 101: RTP.PayloadType.EVENT}
+        request, call_id, sess_id = self.sip.invite(
+            number, medias, RTP.TransmitType.SENDRECV
+        )
+        self.calls[call_id] = VoIPCall(
+            self,
+            CallState.DIALING,
+            request,
+            sess_id,
+            self.myIP,
+            ms=medias,
+            sendmode=self.sendmode,
+        )
+
+        return self.calls[call_id]
+
+    def request_port(self, blocking=True) -> int:
+        ports_available = [
+            port
+            for port in range(self.rtpPortLow, self.rtpPortHigh + 1)
+            if port not in self.assignedPorts
+        ]
+        if len(ports_available) == 0:
+            # If no ports are available attempt to cleanup any missed calls.
+            self.release_ports()
+            ports_available = [
+                port
+                for port in range(self.rtpPortLow, self.rtpPortHigh + 1)
+                if (port not in self.assignedPorts)
+            ]
+
+        while self.NSD and blocking and len(ports_available) == 0:
+            ports_available = [
+                port
+                for port in range(self.rtpPortLow, self.rtpPortHigh + 1)
+                if (port not in self.assignedPorts)
+            ]
+            time.sleep(0.5)
+            self.release_ports()
+
+            if len(ports_available) == 0:
+                raise NoPortsAvailableError(
+                    "No ports were available to be assigned"
+                )
+
+        selection = random.choice(ports_available)
+        self.assignedPorts.append(selection)
+
+        return selection
+
+    def release_ports(self, call: Optional[VoIPCall] = None) -> None:
+        self.portsLock.acquire()
+        self._cleanup_dead_calls()
+        try:
+            if isinstance(call, VoIPCall):
+                ports = list(call.assignedPorts.keys())
+            else:
+                dnr_ports = []
+                for call_id in self.calls:
+                    dnr_ports += list(self.calls[call_id].assignedPorts.keys())
+                ports = []
+                for port in self.assignedPorts:
+                    if port not in dnr_ports:
+                        ports.append(port)
+
+            for port in ports:
+                self.assignedPorts.remove(port)
+        finally:
+            self.portsLock.release()
+
+    def _cleanup_dead_calls(self) -> None:
+        to_delete = []
+        for thread in self.threads:
+            if not thread.is_alive():
+                call_id = self.threadLookup[thread]
+                try:
+                    del self.calls[call_id]
+                except KeyError:
+                    debug("Unable to delete from calls dictionary!")
+                    debug(f"call_id={call_id} calls={self.calls}")
+                try:
+                    del self.threadLookup[thread]
+                except KeyError:
+                    debug("Unable to delete from threadLookup dictionary!")
+                    debug(f"thread={thread} threadLookup={self.threadLookup}")
+                to_delete.append(thread)
+        for thread in to_delete:
+            self.threads.remove(thread)
diff --git a/pyVoIP/__init__.py b/pyVoIP/__init__.py
index 9d9d93c..b884cdc 100644
--- a/pyVoIP/__init__.py
+++ b/pyVoIP/__init__.py
@@ -1,23 +1,31 @@
+__all__ = ["SIP", "RTP", "VoIP"]
 
-__all__ = ['SIP', 'RTP', 'VoIP']
-
-version_info = (1, 5, 6)
+version_info = (1, 6, 0)
 
 __version__ = ".".join([str(x) for x in version_info])
 
 DEBUG = False
 
+"""
+The higher this variable is, the more often RTP packets are sent.
+This should only ever need to be 0.0. However, when testing on Windows,
+there has sometimes been jittering, setting this to 0.75 fixed this in testing.
+"""
+TRANSMIT_DELAY_REDUCTION = 0.0
+
+
 def debug(s, e=None):
-  if DEBUG:
-    print(s)
-  elif e is not None:
-    print(e)
+    if DEBUG:
+        print(s)
+    elif e is not None:
+        print(e)
 
-from pyVoIP.RTP import PayloadType
 
-SIPCompatibleMethods = ['INVITE', 'ACK', 'BYE']
-SIPCompatibleVersions = ['SIP/2.0']
+# noqa because import will fail if debug is not defined
+from pyVoIP.RTP import PayloadType  # noqa: E402
+
+SIPCompatibleMethods = ["INVITE", "ACK", "BYE", "CANCEL"]
+SIPCompatibleVersions = ["SIP/2.0"]
 
 RTPCompatibleVersions = [2]
 RTPCompatibleCodecs = [PayloadType.PCMU, PayloadType.PCMA, PayloadType.EVENT]
-
diff --git a/pyVoIP/py.typed b/pyVoIP/py.typed
new file mode 100644
index 0000000..e69de29
diff --git a/pyproject.toml b/pyproject.toml
index 236c211..29991d0 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,6 +1,9 @@
-[build-system]
-requires = [
-    "setuptools>=42",
-    "wheel"
-]
-build-backend = "setuptools.build_meta"
+[build-system]
+requires = [
+    "setuptools>=42",
+    "wheel"
+]
+build-backend = "setuptools.build_meta"
+[tool.black]
+line-length = 79
+target-version = ['py37']
diff --git a/setup.py b/setup.py
index 1393f9a..08c377b 100644
--- a/setup.py
+++ b/setup.py
@@ -3,11 +3,11 @@
 
 
 with open("README.md", "r", encoding='utf-8') as f:
-  long_description = f.read()
+    long_description = f.read()
 
 setup(
     name='pyVoIP',
-    version='1.5.6',
+    version='1.6.0-dev',
     description='PyVoIP is a pure python VoIP/SIP/RTP library.',
     long_description=long_description,
     long_description_content_type="text/markdown",
@@ -31,5 +31,6 @@
         "Topic :: Communications :: Telephony"
     ],
     packages=find_packages(),
+    package_data={'pyVoIP': ['py.typed']},
     python_requires=">=3.6"
 )
diff --git a/tox.ini b/tox.ini
new file mode 100644
index 0000000..7feb0f4
--- /dev/null
+++ b/tox.ini
@@ -0,0 +1,6 @@
+[flake8]
+ignore = 
+    # Default Ignore's removed:
+    # E203 whitespace before ':'
+    # W503 line break before binary operator
+    E203,W503


Test patch:
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
new file mode 100644
index 0000000..210d08b
--- /dev/null
+++ b/.github/workflows/test.yml
@@ -0,0 +1,32 @@
+# This workflow will install Python dependencies, run tests and lint
+# For more information see: https://help.github.com/actions/language-and-framework-guides/using-python-with-github-actions
+
+name: Test
+
+on:
+  push:
+    branches: [v1.6.0-dev]
+  pull_request:
+
+jobs:
+  build:
+    runs-on: ubuntu-latest
+    strategy:
+      matrix:
+        python-version:
+          - "3.7"
+
+    steps:
+      - uses: actions/checkout@v3.0.2
+        with:
+          fetch-depth: 2
+      - name: Set up Python ${{ matrix.python-version }}
+        uses: actions/setup-python@v4.0.0
+        with:
+          python-version: ${{ matrix.python-version }}
+      - name: Install dependencies
+        run: |
+          python -m pip install --upgrade pip
+          pip install -e . -r requirements-test.txt
+      - name: Black
+        run: black --check pyVoIP
diff --git a/requirements-test.txt b/requirements-test.txt
new file mode 100644
index 0000000..0a098f1
--- /dev/null
+++ b/requirements-test.txt
@@ -0,0 +1,1 @@
+black==22.8.0