Repository: eavanvalkenburg/pysiaalarm (language: python)

Issue:
line could not be parsed as a SIAEvent
Hi Eduard, 
I just updated to the 5.0. beta 1.
Now I get this error when HA receives the message:

2021-04-11 22:40:56 DEBUG (MainThread) [pysiaalarm.base_server] Incoming line: 9618004D"SIA-DCS"7960L0#123456[#123456|Nri1CL0^VX FRONTE ^]_22:40:48,04-11-2021
2021-04-11 22:40:56 WARNING (MainThread) [pysiaalarm.base_server] Last line could not be parsed as a SIAEvent or OHEvent, line was: 9618004D"SIA-DCS"7960L0#123456[#123456|Nri1CL0^VX FRONTE ^]_22:40:48,04-11-2021

Thanks for your help

Stefano


line could not be parsed as a SIAEvent or OHEvent
Hi,

I just updated to the 3.0.0b7 and i have now a problem with this message from Ajax alarm

BF940059"SIA-DCS"0004L0#AAA[#AAA|Nri1/PA501][X006E07.7777777][Y49N06.6666666]_20:45:46,04-22-2021

Thanks for your help
Phil
typo mistake for xdata identifier
in xdata.json
```
 "Y": {
        "identifier": "A",
        "name": "Latitude",
        "description": "Location of event, latitude.",
        "length": 12,
        "characters": "ASCII"
    },
```

must be (I think)

```
 "Y": {
        "identifier": "Y",
        "name": "Latitude",
        "description": "Location of event, latitude.",
        "length": 12,
        "characters": "ASCII"
    },
```
identifier is wrong


Discussion / hints:

Fix patch:
diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile
new file mode 100644
index 0000000..1c8a55f
--- /dev/null
+++ b/.devcontainer/Dockerfile
@@ -0,0 +1,22 @@
+# See here for image contents: https://github.com/microsoft/vscode-dev-containers/tree/v0.163.0/containers/python-3/.devcontainer/base.Dockerfile
+
+# [Choice] Python version: 3, 3.9, 3.8, 3.7, 3.6
+ARG VARIANT="3"
+FROM mcr.microsoft.com/vscode/devcontainers/python:0-${VARIANT}
+
+# [Option] Install Node.js
+ARG INSTALL_NODE="true"
+ARG NODE_VERSION="lts/*"
+RUN if [ "${INSTALL_NODE}" = "true" ]; then su vscode -c "umask 0002 && . /usr/local/share/nvm/nvm.sh && nvm install ${NODE_VERSION} 2>&1"; fi
+
+# [Optional] If your pip requirements rarely change, uncomment this section to add them to the image.
+# COPY requirements.txt /tmp/pip-tmp/
+# RUN pip3 --disable-pip-version-check --no-cache-dir install -r /tmp/pip-tmp/requirements.txt \
+#    && rm -rf /tmp/pip-tmp
+
+# [Optional] Uncomment this section to install additional OS packages.
+# RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \
+#     && apt-get -y install --no-install-recommends <your-package-list-here>
+
+# [Optional] Uncomment this line to install global node packages.
+# RUN su vscode -c "source /usr/local/share/nvm/nvm.sh && npm install -g <your-package-here>" 2>&1
\ No newline at end of file
diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json
new file mode 100644
index 0000000..93b2d95
--- /dev/null
+++ b/.devcontainer/devcontainer.json
@@ -0,0 +1,46 @@
+// For format details, see https://aka.ms/devcontainer.json. For config options, see the README at:
+// https://github.com/microsoft/vscode-dev-containers/tree/v0.163.0/containers/python-3
+{
+	"name": "Python 3",
+	"build": {
+		"dockerfile": "Dockerfile",
+		"context": "..",
+		"args": {
+			// Update 'VARIANT' to pick a Python version: 3, 3.6, 3.7, 3.8, 3.9
+			"VARIANT": "3.8",
+			// Options
+			"INSTALL_NODE": "false",
+			"NODE_VERSION": "lts/*"
+		}
+	},
+	"appPort": 8126,
+	// Set *default* container specific settings.json values on container create.
+	"settings": {
+		"terminal.integrated.shell.linux": "/bin/bash",
+		"python.pythonPath": "/usr/local/bin/python",
+		"python.linting.enabled": true,
+		"python.linting.pylintEnabled": true,
+		"python.formatting.provider": "black",
+		"python.formatting.autopep8Path": "/usr/local/py-utils/bin/autopep8",
+		"python.formatting.blackPath": "/usr/local/py-utils/bin/black",
+		"python.formatting.yapfPath": "/usr/local/py-utils/bin/yapf",
+		"python.linting.banditPath": "/usr/local/py-utils/bin/bandit",
+		"python.linting.flake8Path": "/usr/local/py-utils/bin/flake8",
+		"python.linting.mypyPath": "/usr/local/py-utils/bin/mypy",
+		"python.linting.pycodestylePath": "/usr/local/py-utils/bin/pycodestyle",
+		"python.linting.pydocstylePath": "/usr/local/py-utils/bin/pydocstyle",
+		"python.linting.pylintPath": "/usr/local/py-utils/bin/pylint",
+		"remote.autoForwardPorts": false
+	},
+	// Add the IDs of extensions you want installed when the container is created.
+	"extensions": [
+		"ms-python.python",
+		"ms-python.vscode-pylance"
+	],
+	// Use 'forwardPorts' to make a list of ports inside the container available locally.
+	// "forwardPorts": [ 8126 ],
+	// Use 'postCreateCommand' to run commands after the container is created.
+	"postCreateCommand": "sudo pip3 install -U setuptools setuptools_scm wheel tox; sudo pip3 install -e .[test]",
+	// Comment out connect as root instead. More info: https://aka.ms/vscode-remote/containers/non-root.
+	"remoteUser": "vscode"
+}
\ No newline at end of file
diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml
index e91c046..64782e7 100644
--- a/.github/workflows/main.yml
+++ b/.github/workflows/main.yml
@@ -1,22 +1,32 @@
-name: CI
+name: Test
 
 # Controls when the action will run. Triggers the workflow on push or pull request
 # events but only for the master branch
 on:
   push:
+    branches:
+      - master
+      - dev
+    tags-ignore:
+      - v*
   pull_request:
 
 jobs:
-  setup_test:
+  TOX:
+    name: "Run setup and mypy."
     runs-on: ubuntu-latest
-
+    strategy:
+      matrix:
+        python-version: [3.8, 3.9]
     steps:
-    - uses: actions/checkout@v2
-    - name: Set up Python
-      uses: actions/setup-python@v2
-      with:
-        python-version: 3.8
-    - name: Run setup of PySIAAlarm
-      run: python setup.py develop
-    - name: Test with pytest
-      run: python setup.py test
+      - uses: actions/checkout@v2
+      - name: Set up Python ${{ matrix.python-version }}
+        uses: actions/setup-python@v2
+        with:
+          python-version: ${{ matrix.python-version }}
+      - name: Run installs.
+        run: pip install -e .[test]
+      - name: Run Tox
+        run: tox
+      - name: Run Mypy
+        run: mypy
diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml
index b8461c5..4a88b26 100644
--- a/.github/workflows/release.yaml
+++ b/.github/workflows/release.yaml
@@ -1,5 +1,4 @@
-
-name: Build
+name: Build and Release
 
 on:
   release:
@@ -10,20 +9,20 @@ jobs:
     runs-on: ubuntu-latest
 
     steps:
-    - uses: actions/checkout@v2
-    - name: Set up Python
-      uses: actions/setup-python@v2
-      with:
-        python-version: 3.8
-    - name: Install dependencies
-      run: |
-        python -m pip install --upgrade pip
-        pip install twine wheel
-        python setup.py develop
-    - name: Build and publish
-      env: # Or as an environment variable
-        TWINE_USERNAME: ${{ secrets.pypi_username }}
-        TWINE_PASSWORD: ${{ secrets.pypi_password }}
-      run: |
-        python setup.py bdist_wheel
-        twine upload dist/* --non-interactive
\ No newline at end of file
+      - uses: actions/checkout@v2
+      - name: Set up Python
+        uses: actions/setup-python@v2
+        with:
+          python-version: 3.8
+      - name: Install dependencies
+        run: |
+          python -m pip install --upgrade pip
+          pip install twine wheel tox
+          pip install -e .
+      - name: Build and publish
+        env: # Or as an environment variable
+          TWINE_USERNAME: ${{ secrets.pypi_username }}
+          TWINE_PASSWORD: ${{ secrets.pypi_password }}
+        run: |
+          tox -e build
+          tox -e publish -- --repository pypi
diff --git a/.gitignore b/.gitignore
index c47b922..632717e 100644
--- a/.gitignore
+++ b/.gitignore
@@ -20,6 +20,7 @@ __pycache__/*
 .pydevproject
 .settings
 .idea
+.vscode
 tags
 
 # Package files
@@ -31,11 +32,11 @@ tags
 # Unittest and coverage
 htmlcov/*
 .coverage
+.coverage.*
 .tox
-junit.xml
+junit*.xml
 coverage.xml
 .pytest_cache/
-.hypothesis
 
 # Build and docs folder/files
 build/*
@@ -49,6 +50,6 @@ MANIFEST
 
 # Per-project virtualenvs
 .venv*/
+.conda*/
 
-# local config files
-local_config.json
+tests/*.json
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index d6f0fd1..754d384 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -1,6 +1,6 @@
 repos:
   - repo: https://github.com/psf/black
-    rev: 19.3b0
+    rev: 21.4b0
     hooks:
     -   id: black
   - repo: https://github.com/timothycrosley/isort
diff --git a/.readthedocs.yml b/.readthedocs.yml
new file mode 100644
index 0000000..1133df0
--- /dev/null
+++ b/.readthedocs.yml
@@ -0,0 +1,22 @@
+# Read the Docs configuration file
+# See https://docs.readthedocs.io/en/stable/config-file/v2.html for details
+
+# Required
+version: 2
+
+# Build documentation in the docs/ directory with Sphinx
+sphinx:
+  configuration: docs/conf.py
+
+# Build documentation with MkDocs
+#mkdocs:
+#  configuration: mkdocs.yml
+
+# Optionally build your docs in additional formats such as PDF
+formats:
+  - pdf
+
+python:
+  version: 3.8
+  install:
+    - requirements: docs/requirements.txt
diff --git a/.vscode/launch.json b/.vscode/launch.json
index 22c31e7..ba7a5c1 100644
--- a/.vscode/launch.json
+++ b/.vscode/launch.json
@@ -10,7 +10,7 @@
             "request": "test",
             "console": "integratedTerminal",
             "justMyCode": false,
-            "logToFile": true
+            "env": {"PYTEST_ADDOPTS": "--no-cov"}
         },
         {
             "name": "Python: AIO",
@@ -18,7 +18,7 @@
             "request": "launch",
             "program": "run_aio.py",
             "console": "integratedTerminal",
-            "cwd": "${workspaceFolder}\\tests"
+            "cwd": "${workspaceFolder}//tests"
         },
         {
             "name": "Python: Thread",
@@ -26,7 +26,7 @@
             "request": "launch",
             "program": "run.py",
             "console": "integratedTerminal",
-            "cwd": "${workspaceFolder}\\tests"
+            "cwd": "${workspaceFolder}//tests"
         },
         {
             "name": "Python: Client AIO",
@@ -34,7 +34,7 @@
             "request": "launch",
             "program": "test_client_aio.py",
             "console": "integratedTerminal",
-            "cwd": "${workspaceFolder}\\tests"
+            "cwd": "${workspaceFolder}//tests"
         }
     ]
 }
\ No newline at end of file
diff --git a/.vscode/settings.json b/.vscode/settings.json
index 2fd24ec..b9131c0 100644
--- a/.vscode/settings.json
+++ b/.vscode/settings.json
@@ -6,7 +6,7 @@
     "python.testing.unittestEnabled": false,
     "python.testing.nosetestsEnabled": false,
     "python.testing.pytestEnabled": true,
-    "python.pythonPath": "C:\\Anaconda3\\python.exe",
+    "python.pythonPath": "/usr/local/bin/python",
     "python.linting.flake8Enabled": true,
     "python.linting.enabled": true,
     "python.linting.pydocstyleEnabled": true,
diff --git a/CHANGELOG.rst b/CHANGELOG.rst
index 171d861..226e6f5 100644
--- a/CHANGELOG.rst
+++ b/CHANGELOG.rst
@@ -2,19 +2,9 @@
 Changelog
 =========
 
-Version 1.0
-===========
-
-- V1!
-- Cleaned up a lot of code, made the errors and behaviour more consistent with official SIA DC-09 spec.
-- Added error_count on SIAServer to show the number of events being sent that were not handled and passed to func.
-
-Version 0.2
-===========
-
-- Revised version that handles multiple accounts being listened on on the same server, also optimized some code.
-
 Version 0.1
 ===========
 
-- Initial version of package for listening to SIA enabled alarm systems.
+- Feature A added
+- FIX: nasty bug #1729 fixed
+- add your changes here!
diff --git a/LICENSE.txt b/LICENSE.txt
index 8f741a4..785ea00 100644
--- a/LICENSE.txt
+++ b/LICENSE.txt
@@ -1,6 +1,6 @@
 The MIT License (MIT)
 
-Copyright (c) 2020 E.A. van Valkenburg
+Copyright (c) 2021 E.A. van Valkenburg
 
 Permission is hereby granted, free of charge, to any person obtaining a copy
 of this software and associated documentation files (the "Software"), to deal
diff --git a/README.rst b/README.rst
index 96487f7..a372c05 100644
--- a/README.rst
+++ b/README.rst
@@ -3,41 +3,19 @@ pysiaalarm
 ==========
 
 
-Python package for creating a client that talks with SIA-based alarm systems.  Currently tested using a Ajax Systems alarm system. If you have other systems please reach out.
+Python package for creating a client that talks with SIA-based alarm systems.
 
 
 Description
 ===========
 
-This package was created to talk with alarm systems using the SIA protocal, it was tested using a Ajax system, but should support all defined SIA codes. 
-It creates a new thread with a TCP Server or a asyncio coroutine running bound to a host and port, the alarm system acts a client that sends messages to that server and the server acknowledges the messages and call the supplied function.
+A longer description of your project goes here...
 
 
-Config 
-==========
-
-For threaded:
-
-.. code-block:: python
-
-    from pysiaalarm import SIAClient, SIAAccount
-
-For asyncio:
-
-.. code-block:: python
-
-    from pysiaalarm.aio import SIAClient, SIAAccount
-
-
-The SIAClient takes these arguments:
-
-- host: if there is a specific host to talk to, usually has '' for localhost.
-- port: the TCP port your alarm system communicates with.
-- accounts: list of type SIAAccount that are to be allowed to send messages to this server
-- function: a function that will be called for every event that it handles, takes only a SIAEvent as parameter and does not pass back anything.
+.. _pyscaffold-notes:
 
-SIAAccount takes these arguments:
+Note
+====
 
-- account_id: the account id as 3-16 ASCII hex characters.
-- [optional] key: encryption key specified in your alarm system 16, 24, or 32 ASCII characters
-- [optional] allowed_timeband: encrypted messages have a timestamp and those are checked against this timeband, by default the timestamp is allowed between -40 and +20 seconds comparing the timestamp in the message and the current timestamp of the system running the server.
+This project has been set up using PyScaffold 4.0. For details and usage
+information on PyScaffold see https://pyscaffold.org/.
diff --git a/docs/Makefile b/docs/Makefile
index 96c4245..fa2cc35 100644
--- a/docs/Makefile
+++ b/docs/Makefile
@@ -1,193 +1,29 @@
 # Makefile for Sphinx documentation
 #
 
-# You can set these variables from the command line.
-SPHINXOPTS    =
-SPHINXBUILD   = sphinx-build
-PAPER         =
-BUILDDIR      = ../build/sphinx/
+# You can set these variables from the command line, and also
+# from the environment for the first two.
+SPHINXOPTS    ?=
+SPHINXBUILD   ?= sphinx-build
+SOURCEDIR     = .
+BUILDDIR      = _build
 AUTODOCDIR    = api
-AUTODOCBUILD  = sphinx-apidoc
-PROJECT       = pysiaalarm
-MODULEDIR     = ../src/pysiaalarm
 
 # User-friendly check for sphinx-build
 ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $?), 1)
-$(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don't have Sphinx installed, grab it from http://sphinx-doc.org/)
+$(error "The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don't have Sphinx installed, grab it from http://sphinx-doc.org/")
 endif
 
-# Internal variables.
-PAPEROPT_a4     = -D latex_paper_size=a4
-PAPEROPT_letter = -D latex_paper_size=letter
-ALLSPHINXOPTS   = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .
-# the i18n builder cannot share the environment and doctrees with the others
-I18NSPHINXOPTS  = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .
-
-.PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext doc-requirements
+.PHONY: help clean Makefile
 
+# Put it first so that "make" without argument is like "make help".
 help:
-	@echo "Please use \`make <target>' where <target> is one of"
-	@echo "  html       to make standalone HTML files"
-	@echo "  dirhtml    to make HTML files named index.html in directories"
-	@echo "  singlehtml to make a single large HTML file"
-	@echo "  pickle     to make pickle files"
-	@echo "  json       to make JSON files"
-	@echo "  htmlhelp   to make HTML files and a HTML help project"
-	@echo "  qthelp     to make HTML files and a qthelp project"
-	@echo "  devhelp    to make HTML files and a Devhelp project"
-	@echo "  epub       to make an epub"
-	@echo "  latex      to make LaTeX files, you can set PAPER=a4 or PAPER=letter"
-	@echo "  latexpdf   to make LaTeX files and run them through pdflatex"
-	@echo "  latexpdfja to make LaTeX files and run them through platex/dvipdfmx"
-	@echo "  text       to make text files"
-	@echo "  man        to make manual pages"
-	@echo "  texinfo    to make Texinfo files"
-	@echo "  info       to make Texinfo files and run them through makeinfo"
-	@echo "  gettext    to make PO message catalogs"
-	@echo "  changes    to make an overview of all changed/added/deprecated items"
-	@echo "  xml        to make Docutils-native XML files"
-	@echo "  pseudoxml  to make pseudoxml-XML files for display purposes"
-	@echo "  linkcheck  to check all external links for integrity"
-	@echo "  doctest    to run all doctests embedded in the documentation (if enabled)"
+	@$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
 
 clean:
 	rm -rf $(BUILDDIR)/* $(AUTODOCDIR)
 
-$(AUTODOCDIR): $(MODULEDIR)
-	mkdir -p $@
-	$(AUTODOCBUILD) -f -o $@ $^
-
-doc-requirements: $(AUTODOCDIR)
-
-html: doc-requirements
-	$(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html
-	@echo
-	@echo "Build finished. The HTML pages are in $(BUILDDIR)/html."
-
-dirhtml: doc-requirements
-	$(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml
-	@echo
-	@echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml."
-
-singlehtml: doc-requirements
-	$(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml
-	@echo
-	@echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml."
-
-pickle: doc-requirements
-	$(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle
-	@echo
-	@echo "Build finished; now you can process the pickle files."
-
-json: doc-requirements
-	$(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json
-	@echo
-	@echo "Build finished; now you can process the JSON files."
-
-htmlhelp: doc-requirements
-	$(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp
-	@echo
-	@echo "Build finished; now you can run HTML Help Workshop with the" \
-	      ".hhp project file in $(BUILDDIR)/htmlhelp."
-
-qthelp: doc-requirements
-	$(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp
-	@echo
-	@echo "Build finished; now you can run "qcollectiongenerator" with the" \
-	      ".qhcp project file in $(BUILDDIR)/qthelp, like this:"
-	@echo "# qcollectiongenerator $(BUILDDIR)/qthelp/$(PROJECT).qhcp"
-	@echo "To view the help file:"
-	@echo "# assistant -collectionFile $(BUILDDIR)/qthelp/$(PROJECT).qhc"
-
-devhelp: doc-requirements
-	$(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp
-	@echo
-	@echo "Build finished."
-	@echo "To view the help file:"
-	@echo "# mkdir -p $HOME/.local/share/devhelp/$(PROJECT)"
-	@echo "# ln -s $(BUILDDIR)/devhelp $HOME/.local/share/devhelp/$(PROJEC)"
-	@echo "# devhelp"
-
-epub: doc-requirements
-	$(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub
-	@echo
-	@echo "Build finished. The epub file is in $(BUILDDIR)/epub."
-
-patch-latex:
-	find _build/latex -iname "*.tex" | xargs -- \
-		sed -i'' 's~includegraphics{~includegraphics\[keepaspectratio,max size={\\textwidth}{\\textheight}\]{~g'
-
-latex: doc-requirements
-	$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
-	$(MAKE) patch-latex
-	@echo
-	@echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex."
-	@echo "Run \`make' in that directory to run these through (pdf)latex" \
-	      "(use \`make latexpdf' here to do that automatically)."
-
-latexpdf: doc-requirements
-	$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
-	$(MAKE) patch-latex
-	@echo "Running LaTeX files through pdflatex..."
-	$(MAKE) -C $(BUILDDIR)/latex all-pdf
-	@echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex."
-
-latexpdfja: doc-requirements
-	$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
-	@echo "Running LaTeX files through platex and dvipdfmx..."
-	$(MAKE) -C $(BUILDDIR)/latex all-pdf-ja
-	@echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex."
-
-text: doc-requirements
-	$(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text
-	@echo
-	@echo "Build finished. The text files are in $(BUILDDIR)/text."
-
-man: doc-requirements
-	$(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man
-	@echo
-	@echo "Build finished. The manual pages are in $(BUILDDIR)/man."
-
-texinfo: doc-requirements
-	$(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo
-	@echo
-	@echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo."
-	@echo "Run \`make' in that directory to run these through makeinfo" \
-	      "(use \`make info' here to do that automatically)."
-
-info: doc-requirements
-	$(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo
-	@echo "Running Texinfo files through makeinfo..."
-	make -C $(BUILDDIR)/texinfo info
-	@echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo."
-
-gettext: doc-requirements
-	$(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale
-	@echo
-	@echo "Build finished. The message catalogs are in $(BUILDDIR)/locale."
-
-changes: doc-requirements
-	$(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes
-	@echo
-	@echo "The overview file is in $(BUILDDIR)/changes."
-
-linkcheck: doc-requirements
-	$(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck
-	@echo
-	@echo "Link check complete; look for any errors in the above output " \
-	      "or in $(BUILDDIR)/linkcheck/output.txt."
-
-doctest: doc-requirements
-	$(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest
-	@echo "Testing of doctests in the sources finished, look at the " \
-	      "results in $(BUILDDIR)/doctest/output.txt."
-
-xml: doc-requirements
-	$(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml
-	@echo
-	@echo "Build finished. The XML files are in $(BUILDDIR)/xml."
-
-pseudoxml: doc-requirements
-	$(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml
-	@echo
-	@echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml."
+# Catch-all target: route all unknown targets to Sphinx using the new
+# "make mode" option.  $(O) is meant as a shortcut for $(SPHINXOPTS).
+%: Makefile
+	@$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
diff --git a/docs/SIA Codes, OS Malevich 2.8_2.9.xlsx b/docs/SIA Codes, OS Malevich 2.8_2.9.xlsx
new file mode 100644
index 0000000..3c9aa4a
Binary files /dev/null and b/docs/SIA Codes, OS Malevich 2.8_2.9.xlsx differ
diff --git a/docs/conf.py b/docs/conf.py
index c27e657..d0a16e3 100644
--- a/docs/conf.py
+++ b/docs/conf.py
@@ -1,16 +1,18 @@
-# -*- coding: utf-8 -*-
-#
 # This file is execfile()d with the current directory set to its containing dir.
 #
-# Note that not all possible configuration values are present in this
-# autogenerated file.
+# This file only contains a selection of the most common options. For a full
+# list see the documentation:
+# https://www.sphinx-doc.org/en/master/usage/configuration.html
 #
 # All configuration values have a default; values that are commented out
 # serve to show the default.
-import inspect
+
 import os
-import shutil
 import sys
+import inspect
+import shutil
+
+# -- Path setup --------------------------------------------------------------
 
 __location__ = os.path.join(
     os.getcwd(), os.path.dirname(inspect.getfile(inspect.currentframe()))
@@ -21,7 +23,7 @@
 # documentation root, use os.path.abspath to make it absolute, like shown here.
 sys.path.insert(0, os.path.join(__location__, "../src"))
 
-# -- Run sphinx-apidoc ------------------------------------------------------
+# -- Run sphinx-apidoc -------------------------------------------------------
 # This hack is necessary since RTD does not issue `sphinx-apidoc` before running
 # `sphinx-build -b html . _build/html`. See Issue:
 # https://github.com/rtfd/readthedocs.org/issues/1139
@@ -43,20 +45,22 @@
 
 try:
     import sphinx
-    from pkg_resources import parse_version
 
-    cmd_line_template = "sphinx-apidoc -f -o {outputdir} {moduledir}"
+    cmd_line_template = (
+        "sphinx-apidoc --implicit-namespaces -f -o {outputdir} {moduledir}"
+    )
     cmd_line = cmd_line_template.format(outputdir=output_dir, moduledir=module_dir)
 
     args = cmd_line.split(" ")
-    if parse_version(sphinx.__version__) >= parse_version("1.7"):
+    if tuple(sphinx.__version__.split(".")) >= ("1", "7"):
+        # This is a rudimentary parse_version to avoid external dependencies
         args = args[1:]
 
     apidoc.main(args)
 except Exception as e:
     print("Running `sphinx-apidoc` failed!\n{}".format(e))
 
-# -- General configuration -----------------------------------------------------
+# -- General configuration ---------------------------------------------------
 
 # If your documentation needs a minimal Sphinx version, state it here.
 # needs_sphinx = '1.0'
@@ -89,8 +93,8 @@
 master_doc = "index"
 
 # General information about the project.
-project = u"pysiaalarm"
-copyright = u"2020, E.A. van Valkenburg"
+project = "pysiaalarm"
+copyright = "2021, E.A. van Valkenburg"
 
 # The version info for the project you're documenting, acts as replacement for
 # |version| and |release|, also used in various other places throughout the
@@ -113,7 +117,7 @@
 
 # List of patterns, relative to source directory, that match files and
 # directories to ignore when looking for source files.
-exclude_patterns = ["_build"]
+exclude_patterns = ["_build", "Thumbs.db", ".DS_Store", ".venv"]
 
 # The reST default role (used for this markup: `text`) to use for all documents.
 # default_role = None
@@ -139,7 +143,7 @@
 # keep_warnings = False
 
 
-# -- Options for HTML output ---------------------------------------------------
+# -- Options for HTML output -------------------------------------------------
 
 # The theme to use for HTML and HTML Help pages.  See the documentation for
 # a list of builtin themes.
@@ -148,7 +152,10 @@
 # Theme options are theme-specific and customize the look and feel of a theme
 # further.  For a list of options available for each theme, see the
 # documentation.
-html_theme_options = {"sidebar_width": "300px", "page_width": "1200px"}
+html_theme_options = {
+    "sidebar_width": "300px",
+    "page_width": "1200px"
+}
 
 # Add any paths that contain custom themes here, relative to this directory.
 # html_theme_path = []
@@ -224,27 +231,21 @@
 htmlhelp_basename = "pysiaalarm-doc"
 
 
-# -- Options for LaTeX output --------------------------------------------------
+# -- Options for LaTeX output ------------------------------------------------
 
 latex_elements = {
-    # The paper size ('letterpaper' or 'a4paper').
-    # 'papersize': 'letterpaper',
-    # The font size ('10pt', '11pt' or '12pt').
-    # 'pointsize': '10pt',
+    # The paper size ("letterpaper" or "a4paper").
+    # "papersize": "letterpaper",
+    # The font size ("10pt", "11pt" or "12pt").
+    # "pointsize": "10pt",
     # Additional stuff for the LaTeX preamble.
-    # 'preamble': '',
+    # "preamble": "",
 }
 
 # Grouping the document tree into LaTeX files. List of tuples
 # (source start file, target name, title, author, documentclass [howto/manual]).
 latex_documents = [
-    (
-        "index",
-        "user_guide.tex",
-        u"pysiaalarm Documentation",
-        u"E.A. van Valkenburg",
-        "manual",
-    )
+    ("index", "user_guide.tex", "pysiaalarm Documentation", "E.A. van Valkenburg", "manual")
 ]
 
 # The name of an image file (relative to this directory) to place at the top of
@@ -267,14 +268,15 @@
 # If false, no module index is generated.
 # latex_domain_indices = True
 
-# -- External mapping ------------------------------------------------------------
+# -- External mapping --------------------------------------------------------
 python_version = ".".join(map(str, sys.version_info[0:2]))
 intersphinx_mapping = {
     "sphinx": ("http://www.sphinx-doc.org/en/stable", None),
     "python": ("https://docs.python.org/" + python_version, None),
     "matplotlib": ("https://matplotlib.org", None),
     "numpy": ("https://docs.scipy.org/doc/numpy", None),
-    "sklearn": ("http://scikit-learn.org/stable", None),
-    "pandas": ("http://pandas.pydata.org/pandas-docs/stable", None),
+    "sklearn": ("https://scikit-learn.org/stable", None),
+    "pandas": ("https://pandas.pydata.org/pandas-docs/stable", None),
     "scipy": ("https://docs.scipy.org/doc/scipy/reference", None),
+    "pyscaffold": ("https://pyscaffold.org/en/stable", None),
 }
diff --git a/docs/index.rst b/docs/index.rst
index 9d38595..67c899a 100644
--- a/docs/index.rst
+++ b/docs/index.rst
@@ -1,6 +1,6 @@
-=====
+==========
 pysiaalarm
-=====
+==========
 
 This is the documentation of **pysiaalarm**.
 
@@ -29,6 +29,7 @@ Contents
 .. toctree::
    :maxdepth: 2
 
+   Overview <readme>
    License <license>
    Authors <authors>
    Changelog <changelog>
diff --git a/docs/readme.rst b/docs/readme.rst
new file mode 100644
index 0000000..81995ef
--- /dev/null
+++ b/docs/readme.rst
@@ -0,0 +1,2 @@
+.. _readme:
+.. include:: ../README.rst
diff --git a/docs/requirements.txt b/docs/requirements.txt
new file mode 100644
index 0000000..2ddf98a
--- /dev/null
+++ b/docs/requirements.txt
@@ -0,0 +1,5 @@
+# Requirements file for ReadTheDocs, check .readthedocs.yml.
+# To build the module reference correctly, make sure every external package
+# under `install_requires` in `setup.cfg` is also listed here!
+sphinx>=3.2.1
+# sphinx_rtd_theme
diff --git a/pyproject.toml b/pyproject.toml
new file mode 100644
index 0000000..2c63dbb
--- /dev/null
+++ b/pyproject.toml
@@ -0,0 +1,8 @@
+[build-system]
+# AVOID CHANGING REQUIRES: IT WILL BE UPDATED BY PYSCAFFOLD!
+requires = ["setuptools>=46.1.0", "setuptools_scm[toml]>=5", "wheel"]
+build-backend = "setuptools.build_meta"
+
+[tool.setuptools_scm]
+# See configuration details in https://github.com/pypa/setuptools_scm
+version_scheme = "no-guess-dev"
diff --git a/setup.cfg b/setup.cfg
index f358307..e4f13a4 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -6,14 +6,18 @@
 name = pysiaalarm
 description = Python package for creating a client that talks with SIA-based alarm systems.
 author = E.A. van Valkenburg
-author-email = github@vanvalkenburg.eu
-license = mit
-long-description = file: README.rst
-long-description-content-type = text/x-rst; charset=UTF-8
-url = https://github.com/eavanvalkenburg/pysiaalarm
-project-urls =
+author_email = github@vanvalkenburg.eu
+license = MIT
+long_description = file: README.rst
+long_description_content_type = text/x-rst; charset=UTF-8
+# Add here related links, for example:
+project_urls =
     Documentation = https://github.com/eavanvalkenburg/pysiaalarm
+    Source = https://github.com/eavanvalkenburg/pysiaalarm
+    
+# Change if running only on Windows, Mac or Linux (comma-separated)
 platforms = any
+
 # Add here all kinds of additional classifiers as defined under
 # https://pypi.python.org/pypi?%3Aaction=list_classifiers
 classifiers =
@@ -21,109 +25,84 @@ classifiers =
     Programming Language :: Python
     Programming Language :: Python :: 3
 
+
 [options]
 zip_safe = False
-packages = find:
+packages = find_namespace:
 include_package_data = True
 package_dir =
     =src
-# DON'T CHANGE THE FOLLOWING LINE! IT WILL BE UPDATED BY PYSCAFFOLD!
-setup_requires = pyscaffold>=3.2a0,<3.3a0
-# Add here dependencies of your project (semicolon/line-separated), e.g.
-# install_requires = numpy; scipy
+
+# Require a min/specific Python version (comma-separated conditions)
+python_requires = >=3.8
+
+# Add here dependencies of your project (line-separated), e.g. requests>=2.2,<3.0.
+# Version specifiers like >=2.2,<3.0 avoid problems due to API changes in
+# new major versions. This works if the required packages follow Semantic Versioning.
+# For more information, check out https://semver.org/.
 install_requires =
+    importlib-metadata; python_version<"3.8"
     pycryptodome>=3.9.7
-
-# The usage of test_requires is discouraged, see `Dependency Management` docs
-# tests_require = pytest; pytest-cov
-# Require a specific Python version, e.g. Python 2.7 or >= 3.4
-# python_requires = >=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*
+    dataclasses_json==0.5.3
 
 [options.packages.find]
 where = src
 exclude =
     tests
+    docs
 
 [options.extras_require]
-testing =
+# Add here additional requirements for extra features, to install with:
+# `pip install pysiaalarm[PDF]` like:
+# PDF = ReportLab; RXP
+
+# Add here test requirements (semicolon/line-separated)
+test =
+    setuptools
+    setuptools_scm 
+    wheel
     pytest
     pytest-cov
-    mock
-    hypothesis
+    pytest-asyncio
+    pytest-cases
+    tox
+    mypy
 
 [options.entry_points]
-# Add here console scripts like:
-# console_scripts =
-#     script_name = pysiaalarm.module:function
-# For example:
-# console_scripts =
-#     fibonacci = pysiaalarm.skeleton:run
-# And any other entry points, for example:
-# pyscaffold.cli =
-#     awesome = pyscaffoldext.awesome.extension:AwesomeExtension
-
-[test]
-# py.test options when running `python setup.py test`
-# addopts = --verbose
-extras = True
-
-[tool:pytest]
-# Options for py.test:
-# Specify command line options as you would do when invoking py.test directly.
-# e.g. --cov-report html (or xml) for html/xml output or --junitxml junit.xml
-# in order to write a coverage file that can be read by Jenkins.
-addopts =
-    --cov pysiaalarm --cov-report term-missing
-    # --verbose
-    # -vv
-    -s
-norecursedirs =
-    dist
-    build
-    .tox
-testpaths = tests
-python_classes = testSIA
-log_cli = true
-log_cli_level = INFO
-junit_family=xunit1
 
-[aliases]
-dists = bdist_wheel
 
 [bdist_wheel]
 # Use this option if your package is pure-python
 universal = 1
 
-[build_sphinx]
-source_dir = docs
-build_dir = build/sphinx
-
 [devpi:upload]
 # Options for the devpi: PyPI server and packaging tool
 # VCS export must be deactivated since we are using setuptools-scm
-no-vcs = 1
+no_vcs = 1
 formats = bdist_wheel
 
 [flake8]
 # Some sane defaults for the code style checker flake8
+max_line_length = 88
+extend_ignore = E203, W503
+# ^  Black-compatible
+#    E203 and W503 have edge cases handled by black
 exclude =
     .tox
     build
     dist
     .eggs
     docs/conf.py
-    tests/*
-ignore = E203, E266, E501, W503, F403, F401
-max-line-length = 89
-max-complexity = 18
-select = B,C,E,F,W,T4,B9
 
-[isort]
-profile=black
-src_paths=src,tests
+[mypy]
+files = src/
+python_version = 3.8
+disallow_untyped_defs = True
+ignore_missing_imports = True
 
 [pyscaffold]
 # PyScaffold's parameters when the project was created.
 # This will be used when updating. Do not change!
-version = 3.2.3
+version = 4.0.1
 package = pysiaalarm
+extensions =
diff --git a/setup.py b/setup.py
index 761961b..6d04ccb 100644
--- a/setup.py
+++ b/setup.py
@@ -1,23 +1,22 @@
-# -*- coding: utf-8 -*-
 """
     Setup file for pysiaalarm.
     Use setup.cfg to configure your project.
 
-    This file was generated with PyScaffold 3.2.3.
+    This file was generated with PyScaffold 4.0.
     PyScaffold helps you to put up the scaffold of your new Python project.
     Learn more under: https://pyscaffold.org/
 """
-import sys
 
-from pkg_resources import VersionConflict, require
 from setuptools import setup
 
-try:
-    require("setuptools>=38.3")
-except VersionConflict:
-    print("Error: version of setuptools is too old (<38.3)!")
-    sys.exit(1)
-
-
 if __name__ == "__main__":
-    setup(use_pyscaffold=True)
+    try:
+        setup(use_scm_version={"version_scheme": "no-guess-dev"})
+    except:  # noqa
+        print(
+            "\n\nAn error occurred while building the project, "
+            "please ensure you have the most updated version of setuptools, "
+            "setuptools_scm and wheel with:\n"
+            "   pip install -U setuptools setuptools_scm wheel\n\n"
+        )
+        raise
diff --git a/src/pysiaalarm/__init__.py b/src/pysiaalarm/__init__.py
index a8f7bba..ba8475a 100644
--- a/src/pysiaalarm/__init__.py
+++ b/src/pysiaalarm/__init__.py
@@ -1,25 +1,36 @@
 # -*- coding: utf-8 -*-
+"""Init for pysiaalarm."""
 __author__ = "E.A. van Valkenburg"
 __copyright__ = "E.A. van Valkenburg"
 __license__ = "mit"
 
-from pkg_resources import DistributionNotFound, get_distribution
+import sys
+
+if sys.version_info[:2] >= (3, 8):
+    # TODO: Import directly (no need for conditional) when `python_requires = >= 3.8`
+    from importlib.metadata import PackageNotFoundError, version  # pragma: no cover
+else:
+    from importlib_metadata import (  # pylint: disable=F0401; pragma: no cover
+        PackageNotFoundError,
+        version,
+    )
 
 try:
+    # Change here if project is renamed and does not equal the package name
     dist_name = __name__
-    __version__ = get_distribution(dist_name).version
-except DistributionNotFound:
+    __version__ = version(dist_name)
+except PackageNotFoundError:  # pragma: no cover
     __version__ = "unknown"
 finally:
-    del get_distribution, DistributionNotFound
+    del version, PackageNotFoundError
 
-from .sia_account import SIAAccount, SIAResponseType
-from .sia_client import SIAClient
-from .sia_errors import (
+from .account import SIAAccount
+from .sync.client import SIAClient
+from .errors import (
     InvalidAccountFormatError,
     InvalidAccountLengthError,
     InvalidKeyFormatError,
     InvalidKeyLengthError,
 )
-from .sia_event import SIAEvent
-from .sia_server import SIAServer
+from .event import SIAEvent, OHEvent
+from .utils import CommunicationsProtocol
diff --git a/src/pysiaalarm/account.py b/src/pysiaalarm/account.py
new file mode 100644
index 0000000..9c5247e
--- /dev/null
+++ b/src/pysiaalarm/account.py
@@ -0,0 +1,70 @@
+"""Class for SIA Accounts."""
+import logging
+from dataclasses import dataclass, field
+from dataclasses_json import dataclass_json, config, Exclude
+from typing import Optional, Tuple
+
+from .errors import (
+    InvalidAccountFormatError,
+    InvalidAccountLengthError,
+    InvalidKeyFormatError,
+    InvalidKeyLengthError,
+)
+
+_LOGGER = logging.getLogger(__name__)
+
+
+@dataclass_json
+@dataclass
+class SIAAccount:
+    """Class for SIA Accounts."""
+
+    account_id: str
+    key: Optional[str] = None
+    allowed_timeband: Tuple[int, int] = (40, 20)
+    key_b: Optional[bytes] = field(
+        repr=False, default=None, metadata=config(exclude=Exclude.ALWAYS)  # type: ignore
+    )
+
+    def __post_init__(self) -> None:
+        """Rewrite the key as bytes."""
+        self.key_b = self.key.encode("utf-8") if self.key else None
+
+    @property
+    def encrypted(self) -> bool:
+        """Return true when encrypted."""
+        return True if self.key_b else False
+
+    @classmethod
+    def validate_account(cls, account_id: str = None, key: str = None) -> None:
+        """Validate a accounts information, can be used before creating as well. Can be used to check the individual arguments as well.
+
+        Keyword Arguments:
+            account_id {str} -- The account id specified by the alarm system, should be 3-16 characters hexadecimal. (default: {None})
+            key {str} -- The encryption key specified by the alarm system, should be 16,24 or 32 characters hexadecimal. (default: {None})
+
+        Raises:
+            InvalidKeyFormatError: If the key is not a valid hexadecimal string.
+            InvalidKeyLengthError: If the key does not have 16, 24 or 32 characters.
+            InvalidAccountFormatError: If the account id is not a valid hexadecimal string.
+            InvalidAccountLengthError: If the account id does not have between 3 and 16 characters.
+
+        """
+        if account_id is not None:  # pragma: no cover
+            try:
+                int(account_id, 16)
+            except ValueError:
+                raise InvalidAccountFormatError
+            try:
+                assert 3 <= len(account_id) <= 16
+            except AssertionError:
+                raise InvalidAccountLengthError
+        if key is not None:  # pragma: no cover
+            try:
+                int(key, 16)
+            except ValueError:
+                raise InvalidKeyFormatError
+            try:
+                assert len(key) in (16, 24, 32)
+            except AssertionError:
+                raise InvalidKeyLengthError
diff --git a/src/pysiaalarm/aio/__init__.py b/src/pysiaalarm/aio/__init__.py
index be64975..5cf6748 100644
--- a/src/pysiaalarm/aio/__init__.py
+++ b/src/pysiaalarm/aio/__init__.py
@@ -1,16 +1,16 @@
 # -*- coding: utf-8 -*-
+"""Init for aio."""
 from .. import (
+    CommunicationsProtocol,
     InvalidAccountFormatError,
     InvalidAccountLengthError,
     InvalidKeyFormatError,
     InvalidKeyLengthError,
     SIAAccount,
     SIAEvent,
-    SIAResponseType,
     __author__,
     __copyright__,
     __license__,
     __version__,
 )
-from .sia_client import SIAClient
-from .sia_server import SIAServer
+from .client import SIAClient
diff --git a/src/pysiaalarm/aio/client.py b/src/pysiaalarm/aio/client.py
new file mode 100644
index 0000000..98b9e8c
--- /dev/null
+++ b/src/pysiaalarm/aio/client.py
@@ -0,0 +1,95 @@
+"""This is a the main class for the SIA Client."""
+from __future__ import annotations
+
+import asyncio
+import logging
+from types import TracebackType
+from typing import Any, Callable, Dict, List, Optional, Type
+
+from .. import __author__, __copyright__, __license__, __version__
+from ..account import SIAAccount
+from ..base_client import BaseSIAClient
+from ..event import SIAEvent
+from ..utils import CommunicationsProtocol
+from .server import SIAServer
+
+_LOGGER = logging.getLogger(__name__)
+
+
+class SIAClient(BaseSIAClient):
+    """Class for Async SIA Client."""
+
+    def __init__(
+        self,
+        host: str,
+        port: int,
+        accounts: List[SIAAccount],
+        function: Callable[[SIAEvent], None],
+        protocol: CommunicationsProtocol = CommunicationsProtocol.TCP,
+    ):
+        """Create the asynchronous SIA Client object.
+
+        Arguments:
+            host {str} -- Host to run the server on, usually would be ""
+            port {int} -- The port the server listens to.
+            accounts {List[SIAAccount]} -- List of SIA Accounts to add.
+            function {Callable[[SIAEvent], None]} -- The function that gets called for each event, can be a asyncio coroutine, otherwise the function gets wrapped to be non-blocking.
+            protocol {CommunicationsProtocol Enum} -- CommunicationsProtocol to use, TCP or UDP.
+
+        """
+        if not asyncio.iscoroutinefunction(function):
+            raise TypeError("Function should be a coroutine, create with async def.")
+        BaseSIAClient.__init__(self, host, port, accounts, function, protocol)
+        self.task: Any = None
+        self.transport: Any = None
+        self.dgprotocol: Any = None
+        self.sia_server: Any = None
+        if self.protocol == CommunicationsProtocol.TCP:
+            self.sia_server = SIAServer(self._accounts, self._func, self._counts)
+
+    async def __aenter__(self, **kwargs: Dict[str, Any]) -> SIAClient:
+        """Start with as context manager."""
+        await self.start(**kwargs)
+        return self
+
+    async def __aexit__(
+        self,
+        exc_type: Optional[Type[BaseException]],
+        exc_val: Optional[BaseException],
+        traceback: Optional[TracebackType],
+    ) -> Optional[bool]:
+        """End as context manager."""
+        await self.stop()
+        return True
+
+    async def start(self, **kwargs: Any) -> None:
+        """Start the asynchronous SIA server.
+
+        The rest of the arguments are passed directly to asyncio.start_server().
+
+        """
+        _LOGGER.debug("Starting SIA.")
+        if self.protocol == CommunicationsProtocol.TCP:
+            self.coro = asyncio.start_server(
+                self.sia_server.handle_line, self._host, self._port, **kwargs
+            )
+            self.task = asyncio.create_task(self.coro)
+            return
+        loop = asyncio.get_running_loop()
+        self.transport, self.dgprotocol = await loop.create_datagram_endpoint(
+            lambda: SIAServer(self._accounts, self._func, self._counts),
+            local_addr=(self._host, self._port),
+            **kwargs,
+        )
+        return
+
+    async def stop(self) -> None:
+        """Stop the asynchronous SIA server."""
+        _LOGGER.debug("Stopping SIA.")
+        if self.transport is not None:
+            self.transport.close()
+            return
+        if self.sia_server is not None:
+            self.sia_server.shutdown_flag = True
+        if self.task is not None:  # pragma: no cover
+            await asyncio.gather(self.task)
diff --git a/src/pysiaalarm/aio/server.py b/src/pysiaalarm/aio/server.py
new file mode 100644
index 0000000..3b95550
--- /dev/null
+++ b/src/pysiaalarm/aio/server.py
@@ -0,0 +1,108 @@
+"""This is the class for the actual TCP handler override of the handle method."""
+import asyncio
+import logging
+from typing import Any, Callable, Dict, Optional, Tuple, Union
+
+from .. import __author__, __copyright__, __license__, __version__
+from ..account import SIAAccount
+from ..base_server import BaseSIAServer
+from ..const import EMPTY_BYTES
+from ..event import NAKEvent, OHEvent, SIAEvent
+from ..utils import Counter, ResponseType
+
+_LOGGER = logging.getLogger(__name__)
+
+
+class SIAServer(BaseSIAServer, asyncio.DatagramProtocol):
+    """Class for SIA TCP Server Async."""
+
+    def __init__(
+        self,
+        accounts: Dict[str, SIAAccount],
+        func: Callable[[SIAEvent], None],
+        counts: Counter,
+    ):
+        """Create a SIA Server.
+
+        Arguments:
+            server_address {tuple(string, int)} -- the address the server should listen on.
+            accounts {Dict[str, SIAAccount]} -- accounts as dict with account_id as key, SIAAccount object as value.
+            func {Callable[[SIAEvent], None]} -- Function called for each valid SIA event, that can be matched to a account.
+            counts {Counter} -- counter kept by client to give insights in how many errorous events were discarded of each type.
+
+        """
+        BaseSIAServer.__init__(self, accounts, func, counts)
+
+    async def _respond(
+        self,
+        event: Union[SIAEvent, OHEvent, NAKEvent],
+        writer: asyncio.StreamWriter = None,
+        addr: Optional[Tuple[str, int]] = None,
+    ) -> None:
+        """Respond to the message using the right approach."""
+        try:
+            if writer:
+                writer.write(event.create_response())
+                await writer.drain()
+                return
+            if (
+                self.transport
+                and addr
+                and isinstance(self.transport, asyncio.DatagramTransport)
+            ):  # pragma: no cover
+                self.transport.sendto(event.create_response(), addr)
+                return
+        except Exception as exp:  # pragma: no cover
+            _LOGGER.error(
+                "Exception caught while responding with: %s, exception: %s",
+                event.create_response(),
+                exp,
+            )
+
+    async def _handle_data(
+        self,
+        data: bytes,
+        writer: asyncio.StreamWriter = None,
+        addr: Optional[Tuple[str, int]] = None,
+    ) -> None:
+        """Handle data universally for both TCP and UDP."""
+        line = str.strip(data.decode("ascii", errors="ignore"))
+        if not line:  # pragma: no cover
+            return
+        event = self.parse_and_check_event(line)  # type: ignore
+        await self._respond(event, writer=writer, addr=addr)
+        if event and isinstance(event, SIAEvent) and event.response == ResponseType.ACK:
+            await self.async_func_wrap(event)  # type: ignore
+
+    async def handle_line(
+        self, reader: asyncio.StreamReader, writer: asyncio.StreamWriter
+    ) -> None:
+        """Handle line for SIA Events. This supports TCP connections.
+
+        Arguments:
+            reader {asyncio.StreamReader} -- StreamReader with new data.
+            writer {asyncio.StreamWriter} -- StreamWriter to respond.
+
+        """
+        while True and not self.shutdown_flag:  # pragma: no cover  # type: ignore
+            try:
+                data = await reader.read(1000)
+            except ConnectionResetError:
+                break
+            if data == EMPTY_BYTES or reader.at_eof():
+                break
+            await self._handle_data(data, writer=writer)
+
+        writer.close()
+
+    def connection_made(self, transport: asyncio.BaseTransport) -> None:
+        """Connect callback for datagrams."""
+        self.transport = transport
+
+    def datagram_received(self, data: bytes, addr: Tuple[str, int]) -> None:
+        """Receive and process datagrams. This support UDP connections."""
+        asyncio.create_task(self._handle_data(data, addr=addr))
+
+    def connection_lost(self, _: Any) -> None:
+        """Close and reset transport when connection lost."""
+        self.transport.close()
diff --git a/src/pysiaalarm/aio/sia_client.py b/src/pysiaalarm/aio/sia_client.py
deleted file mode 100644
index b3a5cb3..0000000
--- a/src/pysiaalarm/aio/sia_client.py
+++ /dev/null
@@ -1,65 +0,0 @@
-"""This is a the main class for the SIA Client."""
-import asyncio
-import logging
-from typing import Callable, Coroutine, List, Union
-
-from .. import __author__, __copyright__, __license__, __version__
-from ..base_sia_client import BaseSIAClient
-from ..sia_account import SIAAccount
-from ..sia_event import SIAEvent
-from .sia_server import SIAServer
-
-_LOGGER = logging.getLogger(__name__)
-
-
-class SIAClient(BaseSIAClient):
-    """Class for Async SIA Client."""
-
-    def __init__(
-        self,
-        host: str,
-        port: int,
-        accounts: List[SIAAccount],
-        function: Callable[[SIAEvent], None],
-    ):
-        """Create the asynchronous SIA Client object.
-
-        Arguments:
-            host {str} -- Host to run the server on, usually would be ""
-            port {int} -- The port the server listens to.
-            accounts {List[SIAAccount]} -- List of SIA Accounts to add.
-            function {Callable[[SIAEvent], None]} -- The function that gets called for each event, can be a asyncio coroutine, otherwise the function gets wrapped to be non-blocking.
-
-        """
-        if not asyncio.iscoroutinefunction(function):
-            function = asyncio.coroutine(function)
-        BaseSIAClient.__init__(self, host, port, accounts, function)
-        self.sia_server = SIAServer(self._accounts, self._func, self._counts)
-
-    async def __aenter__(self):
-        """Start with as context manager."""
-        self.start()
-        return self
-
-    async def __aexit__(self, type, value, tb):
-        """End as context manager."""
-        await self.stop()
-
-    def start(self, **kwargs):
-        """Start the asynchronous SIA server.
-
-        The rest of the arguments are passed directly to asyncio.start_server().
-
-        """
-        _LOGGER.debug("Starting SIA.")
-        loop = asyncio.get_event_loop()
-        self.coro = asyncio.start_server(
-            self.sia_server.handle_line, self._host, self._port, loop=loop, **kwargs
-        )
-        self.task = loop.create_task(self.coro)
-
-    async def stop(self):
-        """Stop the asynchronous SIA server."""
-        _LOGGER.debug("Stopping SIA.")
-        self.sia_server.shutdown_flag = True
-        await self.task
diff --git a/src/pysiaalarm/aio/sia_server.py b/src/pysiaalarm/aio/sia_server.py
deleted file mode 100644
index 1c02cdb..0000000
--- a/src/pysiaalarm/aio/sia_server.py
+++ /dev/null
@@ -1,79 +0,0 @@
-"""This is the class for the actual TCP handler override of the handle method."""
-import asyncio
-import logging
-from typing import Callable, Coroutine, Dict, Union
-
-from .. import __author__, __copyright__, __license__, __version__
-from ..base_sia_server import BaseSIAServer
-from ..sia_account import SIAAccount, SIAResponseType
-from ..sia_event import SIAEvent
-
-_LOGGER = logging.getLogger(__name__)
-empty_bytes = b""
-
-
-class SIAServer(BaseSIAServer):
-    """Class for SIA Server Async."""
-
-    def __init__(
-        self,
-        accounts: Dict[str, SIAAccount],
-        func: Callable[[SIAEvent], None],
-        counts: Dict,
-    ):
-        """Create a SIA Server.
-
-        Arguments:
-            server_address {tuple(string, int)} -- the address the server should listen on.
-            accounts Dict[str, SIAAccount] -- accounts as dict with account_id as key, SIAAccount object as value.
-            func Callable[[SIAEvent], None] -- Function called for each valid SIA event, that can be matched to a account.
-            error_count Dict -- counter kept by client to give insights in how many errorous events were discarded of each type.
-
-        """
-        BaseSIAServer.__init__(self, accounts, func, counts)
-
-    async def handle_line(self, reader, writer):
-        """Handle for SIA Events.
-
-        Arguments:
-            reader {asyncio.StreamReader} -- StreamReader with new data.
-            writer {asyncio.StreamWriter} -- StreamWriter to respond.
-
-        """
-        while True and not self.shutdown_flag:
-            try:
-                data = await reader.read(1000)
-            except ConnectionResetError:
-                break
-            if data == empty_bytes or reader.at_eof():
-                break
-            line = str.strip(data.decode("ascii"))
-            if not line:
-                return
-            _LOGGER.debug("Incoming line: %s", line)
-            self.counts["events"] = self.counts["events"] + 1
-            event, account, response = self.parse_and_check_event(line)
-            try:
-                writer.write(account.create_response(event, response))
-                await writer.drain()
-            except Exception as exp:
-                _LOGGER.warning(
-                    "Exception caught while responding to event: %s, exception: %s",
-                    event,
-                    exp,
-                )
-
-            if not (event and response == SIAResponseType.ACK):
-                continue
-            self.counts["valid_events"] = self.counts["valid_events"] + 1
-            try:
-                await self.func(event)
-            except Exception as exp:
-                _LOGGER.warning(
-                    "Last event: %s, gave error in user function: %s.", event, exp
-                )
-                self.counts["errors"]["user_code"] = (
-                    self.counts["errors"]["user_code"] + 1
-                )
-
-        writer.close()
diff --git a/src/pysiaalarm/base_sia_client.py b/src/pysiaalarm/base_client.py
similarity index 66%
rename from src/pysiaalarm/base_sia_client.py
rename to src/pysiaalarm/base_client.py
index 51bff49..1823de1 100644
--- a/src/pysiaalarm/base_sia_client.py
+++ b/src/pysiaalarm/base_client.py
@@ -1,9 +1,10 @@
 """This is a the main class for the SIA Client."""
 from abc import ABC, abstractmethod
-from typing import Callable, Coroutine, List, Union
+from typing import Any, Callable, Coroutine, List, Union
 
-from .sia_account import SIAAccount
-from .sia_event import SIAEvent
+from .account import SIAAccount
+from .event import SIAEvent
+from .utils import CommunicationsProtocol, Counter
 
 
 class BaseSIAClient(ABC):
@@ -15,6 +16,7 @@ def __init__(
         port: int,
         accounts: List[SIAAccount],
         function: Callable[[SIAEvent], None],
+        protocol: CommunicationsProtocol = CommunicationsProtocol.TCP,
     ):
         """Create the SIA Client object.
 
@@ -23,25 +25,17 @@ def __init__(
             port {int} -- The port the server listens to.
             accounts {List[SIAAccount]} -- List of SIA Accounts to add.
             function {Callable[[SIAEvent], None]} -- The function that gets called for each event.
+            protocol {CommunicationsProtocol Enum} -- CommunicationsProtocol to use, TCP or UDP.
 
         """
-        self.sia_server = None
+        self.sia_server: Any = None
         self._host = host
         self._port = port
+        self.protocol = protocol
         self.accounts = accounts
         self._func = function
-        self._counts = {
-            "events": 0,
-            "valid_events": 0,
-            "errors": {
-                "crc": 0,
-                "timestamp": 0,
-                "account": 0,
-                "code": 0,
-                "format": 0,
-                "user_code": 0,
-            },
-        }
+
+        self._counts = Counter()
 
     @property
     def accounts(self) -> List[SIAAccount]:
@@ -54,7 +48,7 @@ def accounts(self) -> List[SIAAccount]:
         return list(self._accounts.values())
 
     @accounts.setter
-    def accounts(self, new_accounts: List[SIAAccount]):
+    def accounts(self, new_accounts: List[SIAAccount]) -> None:
         """Set the accounts to monitor.
 
         Args:
@@ -66,14 +60,16 @@ def accounts(self, new_accounts: List[SIAAccount]):
             self.sia_server.accounts = self._accounts
 
     @property
-    def counts(self):
-        """Return the counts dict."""
+    def counts(self) -> Counter:
+        """Return the counts object."""
         return self._counts
 
     @abstractmethod
-    def start(self):
+    def start(self, **kwargs: Any) -> Union[None, Coroutine[Any, Any, None]]:
         """Abstract method for start."""
+        pass  # pragma: no cover
 
     @abstractmethod
-    def stop(self):
+    def stop(self) -> Union[None, Coroutine[Any, Any, None]]:
         """Abstract method for stop."""
+        pass  # pragma: no cover
diff --git a/src/pysiaalarm/base_server.py b/src/pysiaalarm/base_server.py
new file mode 100644
index 0000000..b2c0c7c
--- /dev/null
+++ b/src/pysiaalarm/base_server.py
@@ -0,0 +1,129 @@
+"""This is the base class with the handling logic for both sia_servers."""
+import asyncio
+import logging
+from abc import ABC
+from typing import Awaitable, Callable, Dict, Union
+
+from .account import SIAAccount
+from .const import (
+    COUNTER_ACCOUNT,
+    COUNTER_CODE,
+    COUNTER_CRC,
+    COUNTER_EVENTS,
+    COUNTER_FORMAT,
+    COUNTER_TIMESTAMP,
+    COUNTER_USER_CODE,
+)
+from .errors import EventFormatError
+from .event import NAKEvent, OHEvent, SIAEvent
+from .utils import Counter
+
+_LOGGER = logging.getLogger(__name__)
+
+
+class BaseSIAServer(ABC):
+    """Base class for SIA Server."""
+
+    def __init__(
+        self,
+        accounts: Dict[str, SIAAccount],
+        func: Union[Callable[[SIAEvent], Awaitable[None]], Callable[[SIAEvent], None]],
+        counts: Counter,
+    ):
+        """Create a SIA Server.
+
+        Arguments:
+            accounts Dict[str, SIAAccount] -- accounts as dict with account_id as key, SIAAccount object as value.
+            func Callable[[SIAEvent], None] -- Function called for each valid SIA event, that can be matched to a account.
+            counts Counter -- counter kept by client to give insights in how many errorous events were discarded of each type.
+        """
+        self.accounts = accounts
+        self.func = func
+        self.counts = counts
+        self.shutdown_flag = False
+
+    def parse_and_check_event(self, line: str) -> Union[SIAEvent, OHEvent, NAKEvent]:
+        """Parse and check the line and create the event, check the account and define the response.
+
+        Args:
+            line (str): Line to parse
+
+        Returns:
+            SIAEvent: The SIAEvent type of the parsed line.
+            ResponseType: The response to send to the alarm.
+
+        """
+        self.log_and_count(COUNTER_EVENTS, line=line)
+        try:
+            event = SIAEvent.from_line(line, self.accounts)
+        except EventFormatError:
+            self.log_and_count(COUNTER_FORMAT, line)
+            return NAKEvent()
+
+        if isinstance(event, OHEvent):
+            return event  # pragma: no cover
+        if not event.valid_message:
+            self.log_and_count(COUNTER_CRC, event=event)
+        elif not event.sia_account:
+            self.log_and_count(COUNTER_ACCOUNT, event=event)
+        elif event.code_not_found:
+            self.log_and_count(COUNTER_CODE, event=event)
+        elif not event.valid_timestamp:
+            self.log_and_count(COUNTER_TIMESTAMP, event=event)
+        return event
+
+    async def async_func_wrap(self, event: SIAEvent) -> None:
+        """Wrap the user function in a try."""
+        self.counts.increment_valid_events()
+        try:
+            await self.func(event)  # type: ignore
+        except Exception as exp:
+            self.log_and_count(COUNTER_USER_CODE, event=event, exception=exp)
+
+    def func_wrap(self, event: SIAEvent) -> None:
+        """Wrap the user function in a try."""
+        self.counts.increment_valid_events()
+        try:
+            self.func(event)
+        except Exception as exp:
+            self.log_and_count(COUNTER_USER_CODE, event=event, exception=exp)
+
+    def log_and_count(
+        self,
+        counter: str,
+        line: str = None,
+        event: SIAEvent = None,
+        exception: Exception = None,
+    ) -> None:
+        """Log the appropriate line and increment the right counter."""
+        if counter == COUNTER_FORMAT:
+            _LOGGER.warning(
+                "Last line could not be parsed as a SIAEvent or OHEvent, line was: %s",
+                line,
+            )
+        if counter == COUNTER_USER_CODE and event and exception:
+            _LOGGER.warning(
+                "Last event: %s, gave error in user function: %s.", event, exception
+            )
+        if counter == COUNTER_CRC and event:
+            _LOGGER.warning(
+                "CRC mismatch, ignoring message. Sent CRC: %s, Calculated CRC: %s. Line was %s",
+                event.msg_crc,
+                event.calc_crc,
+                event.full_message,
+            )
+        if counter == COUNTER_ACCOUNT and event:
+            _LOGGER.warning(
+                "Unknown or non-existing account (%s) was used by the event: %s",
+                event.account,
+                event,
+            )
+        if counter == COUNTER_CODE and event:
+            _LOGGER.warning(
+                "Code not found, replying with DUH to account: %s", event.account
+            )
+        if counter == COUNTER_TIMESTAMP and event:
+            _LOGGER.warning("Event timestamp is no longer valid: %s", event.timestamp)
+        if counter == COUNTER_EVENTS and line:
+            _LOGGER.debug("Incoming line: %s", line)
+        self.counts.increment(counter)
diff --git a/src/pysiaalarm/base_sia_server.py b/src/pysiaalarm/base_sia_server.py
deleted file mode 100644
index 7154940..0000000
--- a/src/pysiaalarm/base_sia_server.py
+++ /dev/null
@@ -1,103 +0,0 @@
-"""This is the base class with the handling logic for both sia_servers."""
-import logging
-from abc import ABC
-from typing import Callable, Dict, Tuple
-
-from .sia_account import SIAAccount, SIAResponseType
-from .sia_errors import EventFormatError
-from .sia_event import SIAEvent
-
-_LOGGER = logging.getLogger(__name__)
-
-
-class BaseSIAServer(ABC):
-    """Base class for SIA Server."""
-
-    def __init__(
-        self,
-        accounts: Dict[str, SIAAccount],
-        func: Callable[[SIAEvent], None],
-        counts: Dict,
-    ):
-        """Create a SIA Server.
-
-        Arguments:
-            accounts Dict[str, SIAAccount] -- accounts as dict with account_id as key, SIAAccount object as value.
-            func Callable[[SIAEvent], None] -- Function called for each valid SIA event, that can be matched to a account.
-            counts Dict -- counter kept by client to give insights in how many errorous events were discarded of each type.
-
-        """
-        self.accounts = accounts
-        self.func = func
-        self.counts = counts
-        self.shutdown_flag = False
-
-    def parse_and_check_event(
-        self, line: str
-    ) -> Tuple[SIAEvent, SIAAccount, SIAResponseType]:
-        """Parse and check the line and create the event, check the account and define the response.
-
-        Args:
-            line (str): Line to parse
-
-        Returns:
-            SIAEvent: The SIAEvent type of the parsed line.
-            SIAAccount: The SIAAccount the line came from.
-            SIAResponseType: The response to send to the alarm.
-
-        """
-        try:
-            event = SIAEvent(line)
-        except EventFormatError:
-            self.counts["errors"]["format"] = self.counts["errors"]["format"] + 1
-            _LOGGER.warning(
-                "Last line could not be parsed as a SIAEvent, line was: %s", line
-            )
-            return None, SIAAccount(""), SIAResponseType.NAK
-
-        if not event.valid_message:
-            self.counts["errors"]["crc"] = self.counts["errors"]["crc"] + 1
-            _LOGGER.warning(
-                "CRC mismatch, ignoring message. Sent CRC: %s, Calculated CRC: %s. Line was %s",
-                event.msg_crc,
-                event.calc_crc,
-                line,
-            )
-            return event, SIAAccount(event.account), None
-
-        account = self.accounts.get(event.account)
-        if not account:
-            self.counts["errors"]["account"] = self.counts["errors"]["account"] + 1
-            _LOGGER.warning(
-                "Unknown or non-existing account (%s) was used by the event: %s",
-                event.account,
-                event,
-            )
-            return event, SIAAccount(event.account), SIAResponseType.NAK
-
-        try:
-            event = account.decrypt(event)
-        except EventFormatError:
-            self.counts["errors"]["format"] = self.counts["errors"]["format"] + 1
-            _LOGGER.warning(
-                "Decrypting last line: %s could not be parsed as a SIAEvent, content: %s",
-                line,
-                event.content,
-            )
-            return None, SIAAccount(""), SIAResponseType.NAK
-
-        _LOGGER.debug("Parsed event: %s.", event)
-
-        if event.code_not_found and event.message_type == "SIA-DCS":
-            self.counts["errors"]["code"] = self.counts["errors"]["code"] + 1
-            _LOGGER.warning(
-                "Code not found, replying with DUH to account: %s", event.account
-            )
-            return event, account, SIAResponseType.DUH
-
-        if not event.valid_timestamp(account.allowed_timeband):
-            self.counts["errors"]["timestamp"] = self.counts["errors"]["timestamp"] + 1
-            _LOGGER.warning("Event timestamp is no longer valid: %s", event.timestamp)
-            return event, account, SIAResponseType.NAK
-
-        return event, account, SIAResponseType.ACK
diff --git a/src/pysiaalarm/const.py b/src/pysiaalarm/const.py
new file mode 100644
index 0000000..1bf48ed
--- /dev/null
+++ b/src/pysiaalarm/const.py
@@ -0,0 +1,14 @@
+"""Constants for SIA."""
+COUNTER_EVENTS = "events"
+COUNTER_VALID = "valid_events"
+COUNTER_FORMAT = "format"
+COUNTER_CRC = "crc"
+COUNTER_CODE = "code"
+COUNTER_TIMESTAMP = "timestamp"
+COUNTER_ACCOUNT = "account"
+COUNTER_USER_CODE = "user_code"
+
+IV = bytes.fromhex("00000000000000000000000000000000")
+EMPTY_BYTES = b""
+
+RSP_XDATA = ["K"]
diff --git a/src/pysiaalarm/data/__init__.py b/src/pysiaalarm/data/__init__.py
new file mode 100644
index 0000000..478abcd
--- /dev/null
+++ b/src/pysiaalarm/data/__init__.py
@@ -0,0 +1,1 @@
+"""Data helpers and files."""
\ No newline at end of file
diff --git a/src/pysiaalarm/data/adm_mapping.json b/src/pysiaalarm/data/adm_mapping.json
new file mode 100644
index 0000000..ba914b2
--- /dev/null
+++ b/src/pysiaalarm/data/adm_mapping.json
@@ -0,0 +1,18 @@
+{
+    "100": {"1": "MA", "3": "MH"},
+    "101": {"1": "PA", "3": "PH"},
+    "110": {"1": "FA", "3": "FH"},
+    "111": {"1": "FA", "3": "FH"},
+    "112": {"1": "FA", "3": "FH"},
+    "113": {"1": "WA", "3": "WH"},
+    "114": {"1": "KA", "3": "KH"},
+    "117": {"1": "FA", "3": "FH"},
+    "120": {"1": "PA", "3": "PH"},
+    "121": {"1": "HA", "3": "HH"},
+    "122": {"1": "HA", "3": "HH"},
+    "124": {"1": "HA", "3": "HH"},
+    "125": {"1": "HA", "3": "HH"},
+    "130": {"1": "BA", "3": "BH"},
+    "131": {"1": "NL", "3": "OR"},
+    "602": {"1": "RP", "3": "RP"}
+}
diff --git a/src/pysiaalarm/data/data.py b/src/pysiaalarm/data/data.py
new file mode 100644
index 0000000..ae74cb4
--- /dev/null
+++ b/src/pysiaalarm/data/data.py
@@ -0,0 +1,58 @@
+"""Data related utils for pysiaalarm."""
+import json
+from dataclasses import dataclass
+from dataclasses_json import dataclass_json
+from typing import Dict, Optional
+
+import pkg_resources
+
+FILE_SIA_CODES = "sia_codes.json"
+FILE_XDATA = "xdata.json"
+FILE_ADM_MAPPING = "adm_mapping.json"
+
+
+@dataclass_json
+@dataclass
+class SIACode:
+    """Class for SIACodes."""
+
+    code: str
+    type: str
+    description: str
+    concerns: str
+
+
+@dataclass_json
+@dataclass
+class SIAXData:
+    """Class for Xdata."""
+
+    identifier: str
+    name: str
+    description: str
+    length: int
+    characters: str
+    value: Optional[str] = None
+
+
+def _load_data(file: str) -> dict:
+    """Load the one of the data json files."""
+    stream = pkg_resources.resource_stream(__name__, file)
+    return json.load(stream)
+
+
+def _load_sia_codes() -> Dict[str, SIACode]:
+    """Alias for loading sia codes file."""
+    data = _load_data(FILE_SIA_CODES)
+    return {key: SIACode(**value) for (key, value) in data.items()}
+
+
+def _load_xdata() -> Dict[str, SIAXData]:
+    """Alias for loading xdata file."""
+    data = _load_data(FILE_XDATA)
+    return {key: SIAXData(**value) for (key, value) in data.items()}
+
+
+def _load_adm_mapping() -> Dict[str, Dict[str, str]]:
+    """Alias for loading adm mapping file."""
+    return _load_data(FILE_ADM_MAPPING)
diff --git a/src/pysiaalarm/sia_const.py b/src/pysiaalarm/data/sia_codes.json
similarity index 80%
rename from src/pysiaalarm/sia_const.py
rename to src/pysiaalarm/data/sia_codes.json
index be4507f..a1f0f81 100644
--- a/src/pysiaalarm/sia_const.py
+++ b/src/pysiaalarm/data/sia_codes.json
@@ -1,1924 +1,1928 @@
-"""Constants for SIA."""
-
-ALL_CODES = {
+{
     "AA": {
         "code": "AA",
         "type": "Alarm – Panel Substitution",
         "description": "An attempt to substitute an alternate alarm panel for a secure panel has been made",
-        "concerns": "Condition number",
+        "concerns": "Condition number"
     },
     "AB": {
         "code": "AB",
         "type": "Abort",
         "description": "An event message was not sent due to User action",
-        "concerns": "Zone or point",
+        "concerns": "Zone or point"
     },
     "AF": {
         "code": "AF",
         "type": "Arming Event",
         "description": "Scenario",
-        "concerns": "Zone or point",
+        "concerns": "Zone or point"
     },
     "AN": {
         "code": "AN",
         "type": "Analog Restoral",
         "description": "An analog fire sensor has been restored to normal operation",
-        "concerns": "Zone or point",
+        "concerns": "Zone or point"
     },
     "AR": {
         "code": "AR",
         "type": "AC Restoral",
         "description": "AC power has been restored",
-        "concerns": "Unused",
+        "concerns": "Unused"
     },
     "AS": {
         "code": "AS",
         "type": "Analog Service",
         "description": "An analog fire sensor needs to be cleaned or calibrated",
-        "concerns": "Zone or point",
+        "concerns": "Zone or point"
     },
     "AT": {
         "code": "AT",
         "type": "AC Trouble",
         "description": "AC power has been failed",
-        "concerns": "Unused",
+        "concerns": "Unused"
     },
     "BA": {
         "code": "BA",
         "type": "Burglary Alarm",
         "description": "Burglary zone has been violated while armed",
-        "concerns": "Zone or point",
+        "concerns": "Zone or point"
     },
     "BB": {
         "code": "BB",
         "type": "Burglary Bypass",
         "description": "Burglary zone has been bypassed",
-        "concerns": "Zone or point",
+        "concerns": "Zone or point"
     },
     "BC": {
         "code": "BC",
         "type": "Burglary Cancel",
         "description": "Alarm has been cancelled by authorized user",
-        "concerns": "User number",
+        "concerns": "User number"
     },
     "BD": {
         "code": "BD",
         "type": "Swinger Trouble",
         "description": "A non-fire zone has been violated after a Swinger Shutdown on the zone",
-        "concerns": "Zone or point",
+        "concerns": "Zone or point"
     },
     "BE": {
         "code": "BE",
         "type": "Swinger Trouble Restore",
         "description": "A non-fire zone restores to normal from a Swinger Trouble state",
-        "concerns": "Zone or point",
+        "concerns": "Zone or point"
     },
     "BG": {
         "code": "BG",
         "type": "Unverified Event - Burglary",
         "description": "A point assigned to a Cross Point group has gone into alarm but the Cross Point remained normal",
-        "concerns": "Zone or point",
+        "concerns": "Zone or point"
     },
     "BH": {
         "code": "BH",
         "type": "Burglary Alarm Restore",
         "description": "Alarm condition eliminated",
-        "concerns": "Zone or point",
+        "concerns": "Zone or point"
     },
     "BJ": {
         "code": "BJ",
         "type": "Burglary Trouble Restore",
         "description": "Trouble condition eliminated",
-        "concerns": "Zone or point",
+        "concerns": "Zone or point"
     },
     "BM": {
         "code": "BM",
         "type": "Burglary Alarm - Cross Point",
         "description": "Burglary alarm w/cross point also in alarm - alarm verified",
-        "concerns": "Zone or point",
+        "concerns": "Zone or point"
     },
     "BR": {
         "code": "BR",
         "type": "Burglary Restoral",
         "description": "Alarm/trouble condition has been eliminated",
-        "concerns": "Zone or point",
+        "concerns": "Zone or point"
     },
     "BS": {
         "code": "BS",
         "type": "Burglary Supervisory",
         "description": "Unsafe intrusion detection system condition",
-        "concerns": "Zone or point",
+        "concerns": "Zone or point"
     },
     "BT": {
         "code": "BT",
         "type": "Burglary Trouble",
         "description": "Burglary zone disabled by fault",
-        "concerns": "Zone or point",
+        "concerns": "Zone or point"
     },
     "BU": {
         "code": "BU",
         "type": "Burglary Unbypass",
         "description": "Zone bypass has been removed",
-        "concerns": "Zone or point",
+        "concerns": "Zone or point"
     },
     "BV": {
         "code": "BV",
         "type": "Burglary Verified",
         "description": "A burglary alarm has occurred and been verified within programmed conditions. (zone or point not sent)",
-        "concerns": "Area number",
+        "concerns": "Area number"
     },
     "BX": {
         "code": "BX",
         "type": "Burglary Test",
         "description": "Burglary zone activated during testing",
-        "concerns": "Zone or point",
+        "concerns": "Zone or point"
     },
     "BZ": {
         "code": "BZ",
         "type": "Missing Supervision",
         "description": "A non-fire Supervisory point has gone missing",
-        "concerns": "Zone or point",
+        "concerns": "Zone or point"
     },
     "CA": {
         "code": "CA",
         "type": "Automatic Closing",
         "description": "System armed automatically",
-        "concerns": "Area number",
+        "concerns": "Area number"
     },
     "CB": {
         "code": "CB",
         "type": "Arming Event",
         "description": "Group has been disarmed automatically",
-        "concerns": "Area number",
+        "concerns": "Area number"
     },
     "CC": {
         "code": "CC",
         "type": "Arming Event",
         "description": "Unsuccessful arming attempt",
-        "concerns": "Area number",
+        "concerns": "Area number"
     },
     "CD": {
         "code": "CD",
         "type": "Closing Delinquent",
         "description": "The system has not been armed for a programmed amount of time",
-        "concerns": "Area number",
+        "concerns": "Area number"
     },
     "CE": {
         "code": "CE",
         "type": "Closing Extend",
         "description": "Extend closing time",
-        "concerns": "User number",
+        "concerns": "User number"
     },
     "CF": {
         "code": "CF",
         "type": "Forced Closing",
         "description": "System armed, some zones not ready",
-        "concerns": "User number",
+        "concerns": "User number"
     },
     "CG": {
         "code": "CG",
         "type": "Close Area",
         "description": "System has been partially armed",
-        "concerns": "Area number",
+        "concerns": "Area number"
     },
     "CI": {
         "code": "CI",
         "type": "Fail to Close",
         "description": "An area has not been armed at the end of the closing window",
-        "concerns": "Area number",
+        "concerns": "Area number"
     },
     "CJ": {
         "code": "CJ",
         "type": "Late Close",
         "description": "An area was armed after the closing window",
-        "concerns": "User number",
+        "concerns": "User number"
     },
     "CK": {
         "code": "CK",
         "type": "Early Close",
         "description": "An area was armed before the closing window",
-        "concerns": "User number",
+        "concerns": "User number"
     },
     "CL": {
         "code": "CL",
         "type": "Closing Report",
         "description": "System armed, normal",
-        "concerns": "User number",
+        "concerns": "User number"
     },
     "CM": {
         "code": "CM",
         "type": "Missing Alarm - Recent Closing",
         "description": "A point has gone missing within 2 minutes of closing",
-        "concerns": "Zone or point",
+        "concerns": "Zone or point"
     },
     "CO": {
         "code": "CO",
         "type": "Command Sent",
         "description": "A command has been sent to an expansion/peripheral device",
-        "concerns": "Condition number",
+        "concerns": "Condition number"
     },
     "CP": {
         "code": "CP",
         "type": "Automatic Closing",
         "description": "System armed automatically",
-        "concerns": "User number",
+        "concerns": "User number"
     },
     "CQ": {
         "code": "CQ",
         "type": "Remote Closing",
         "description": "The system was armed from a remote location",
-        "concerns": "User number",
+        "concerns": "User number"
     },
     "CR": {
         "code": "CR",
         "type": "Recent Closing",
         "description": "An alarm occurred within five minutes after the system was closed",
-        "concerns": "User number",
+        "concerns": "User number"
     },
     "CS": {
         "code": "CS",
         "type": "Closing Keyswitch",
         "description": "Account has been armed by keyswitch",
-        "concerns": "Zone or point",
+        "concerns": "Zone or point"
     },
     "CT": {
         "code": "CT",
         "type": "Late to Open",
         "description": "System was not disarmed on time",
-        "concerns": "Area number",
+        "concerns": "Area number"
     },
     "CW": {
         "code": "CW",
         "type": "Was Force Armed",
         "description": "Header for a force armed session, forced point msgs may follow",
-        "concerns": "Area number",
+        "concerns": "Area number"
     },
     "CX": {
         "code": "CX",
         "type": "Custom Function Executed",
         "description": "The panel has executed a preprogrammed set of instructions",
-        "concerns": "Custom Function number",
+        "concerns": "Custom Function number"
     },
     "CZ": {
         "code": "CZ",
         "type": "Point Closing",
         "description": "A point, as opposed to a whole area or account, has closed",
-        "concerns": "Zone or point",
+        "concerns": "Zone or point"
     },
     "DA": {
         "code": "DA",
         "type": "Card Assigned",
         "description": "An access ID has been added to the controller",
-        "concerns": "User number",
+        "concerns": "User number"
     },
     "DB": {
         "code": "DB",
         "type": "Card Deleted",
         "description": "An access ID has been deleted from the controller",
-        "concerns": "User number",
+        "concerns": "User number"
     },
     "DC": {
         "code": "DC",
         "type": "Access Closed",
         "description": "Access to all users prohibited",
-        "concerns": "Door number",
+        "concerns": "Door number"
     },
     "DD": {
         "code": "DD",
         "type": "Access Denied",
         "description": "Access denied, unknown code",
-        "concerns": "Door number",
+        "concerns": "Door number"
     },
     "DE": {
         "code": "DE",
         "type": "Request to Enter",
         "description": "An access point was opened via a Request to Enter device",
-        "concerns": "Door number",
+        "concerns": "Door number"
     },
     "DF": {
         "code": "DF",
         "type": "Door Forced",
         "description": "Door opened without access request",
-        "concerns": "Door number",
+        "concerns": "Door number"
     },
     "DG": {
         "code": "DG",
         "type": "Access Granted",
         "description": "Door access granted",
-        "concerns": "Door number",
+        "concerns": "Door number"
     },
     "DH": {
         "code": "DH",
         "type": "Door Left Open - Restoral",
         "description": "An access point in a Door Left Open state has restored",
-        "concerns": "Door number",
+        "concerns": "Door number"
     },
     "DI": {
         "code": "DI",
         "type": "Access Denied – Passback",
         "description": "Access denied because credential has not exited area before attempting to re-enter same area",
-        "concerns": "Door number",
+        "concerns": "Door number"
     },
     "DJ": {
         "code": "DJ",
         "type": "Door Forced - Trouble",
         "description": "An access point has been forced open in an unarmed area",
-        "concerns": "Door number",
+        "concerns": "Door number"
     },
     "DK": {
         "code": "DK",
         "type": "Access Lockout",
         "description": "Access denied, known code",
-        "concerns": "Door number",
+        "concerns": "Door number"
     },
     "DL": {
         "code": "DL",
         "type": "Door Left Open - Alarm",
         "description": "An open access point when open time expired in an armed area",
-        "concerns": "Door number",
+        "concerns": "Door number"
     },
     "DM": {
         "code": "DM",
         "type": "Door Left Open - Trouble",
         "description": "An open access point when open time expired in an unarmed area",
-        "concerns": "Door number",
+        "concerns": "Door number"
     },
     "DN": {
         "code": "DN",
         "type": "Door Left Open (non-alarm, non-trouble)",
         "description": "An access point was open when the door cycle time expired",
-        "concerns": "Door number",
+        "concerns": "Door number"
     },
     "DO": {
         "code": "DO",
         "type": "Access Open",
         "description": "Access to authorized users allowed",
-        "concerns": "Door number",
+        "concerns": "Door number"
     },
     "DP": {
         "code": "DP",
         "type": "Access Denied - Unauthorized Time",
         "description": "An access request was denied because the request is occurring outside the user’s authorized time window(s)",
-        "concerns": "Door number",
+        "concerns": "Door number"
     },
     "DQ": {
         "code": "DQ",
         "type": "Access Denied - Unauthorized Arming State",
         "description": "An access request was denied because the user was not authorized in this area when the area was armed",
-        "concerns": "Door number",
+        "concerns": "Door number"
     },
     "DR": {
         "code": "DR",
         "type": "Door Restoral",
         "description": "Access alarm/trouble condition eliminated",
-        "concerns": "Door number",
+        "concerns": "Door number"
     },
     "DS": {
         "code": "DS",
         "type": "Door Station",
         "description": "Identifies door for next report",
-        "concerns": "Door number",
+        "concerns": "Door number"
     },
     "DT": {
         "code": "DT",
         "type": "Access Trouble",
         "description": "Access system trouble",
-        "concerns": "Unused",
+        "concerns": "Unused"
     },
     "DU": {
         "code": "DU",
         "type": "Dealer ID",
         "description": "Dealer ID number",
-        "concerns": "Dealer ID",
+        "concerns": "Dealer ID"
     },
     "DV": {
         "code": "DV",
         "type": "Access Denied - Unauthorized Entry Level",
         "description": "An access request was denied because the user is not authorized in this area",
-        "concerns": "Door number",
+        "concerns": "Door number"
     },
     "DW": {
         "code": "DW",
         "type": "Access Denied - Interlock",
         "description": "An access request was denied because the doors associated Interlock point is open",
-        "concerns": "Door number",
+        "concerns": "Door number"
     },
     "DX": {
         "code": "DX",
         "type": "Request to Exit",
         "description": "An access point was opened via a Request to Exit device",
-        "concerns": "Door number",
+        "concerns": "Door number"
     },
     "DY": {
         "code": "DY",
         "type": "Door Locked",
         "description": "The door’s lock has been engaged",
-        "concerns": "Door number",
+        "concerns": "Door number"
     },
     "DZ": {
         "code": "DZ",
         "type": "Access Denied - Door Secured",
         "description": "An access request was denied because the door has been placed in an Access Closed state",
-        "concerns": "Door number",
+        "concerns": "Door number"
     },
     "EA": {
         "code": "EA",
         "type": "Exit Alarm",
         "description": "An exit zone remained violated at the end of the exit delay period",
-        "concerns": "Zone or point",
+        "concerns": "Zone or point"
     },
     "EE": {
         "code": "EE",
         "type": "Exit Error",
         "description": "An exit zone remained violated at the end of the exit delay period",
-        "concerns": "User number",
+        "concerns": "User number"
     },
     "EJ": {
         "code": "EJ",
         "type": "Expansion Tamper Restore",
         "description": "Expansion device tamper restoral",
-        "concerns": "Expansion device number",
+        "concerns": "Expansion device number"
     },
     "EM": {
         "code": "EM",
         "type": "Expansion Device Missing",
         "description": "Expansion device missing",
-        "concerns": "Expansion device number",
+        "concerns": "Expansion device number"
     },
     "EN": {
         "code": "EN",
         "type": "Expansion Missing Restore",
         "description": "Expansion device communications re-established",
-        "concerns": "Expansion device number",
+        "concerns": "Expansion device number"
     },
     "ER": {
         "code": "ER",
         "type": "Expansion Restoral",
         "description": "Expansion device trouble eliminated",
-        "concerns": "Expander number",
+        "concerns": "Expander number"
     },
     "ES": {
         "code": "ES",
         "type": "Expansion Device Tamper",
         "description": "Expansion device enclosure tamper",
-        "concerns": "Expansion device number",
+        "concerns": "Expansion device number"
     },
     "ET": {
         "code": "ET",
         "type": "Expansion Trouble",
         "description": "Expansion device trouble",
-        "concerns": "Expander number",
+        "concerns": "Expander number"
     },
     "EX": {
         "code": "EX",
         "type": "External Device Condition",
         "description": "A specific reportable condition is detected on an external device",
-        "concerns": "Device number",
+        "concerns": "Device number"
     },
     "EZ": {
         "code": "EZ",
         "type": "Missing Alarm - Exit Error",
         "description": "A point remained missing at the end of the exit delay period",
-        "concerns": "Point number",
+        "concerns": "Point number"
     },
     "FA": {
         "code": "FA",
         "type": "Fire Alarm",
         "description": "Fire condition detected",
-        "concerns": "Zone or point",
+        "concerns": "Zone or point"
     },
     "FB": {
         "code": "FB",
         "type": "Fire Bypass",
         "description": "Zone has been bypassed",
-        "concerns": "Zone or point",
+        "concerns": "Zone or point"
     },
     "FC": {
         "code": "FC",
         "type": "Fire Cancel",
         "description": "A Fire Alarm has been cancelled by an authorized person",
-        "concerns": "Zone or point",
+        "concerns": "Zone or point"
     },
     "FG": {
         "code": "FG",
         "type": "Unverified Event – Fire",
         "description": "A point assigned to a Cross Point group has gone into alarm but the Cross Point remained normal",
-        "concerns": "Zone or point",
+        "concerns": "Zone or point"
     },
     "FH": {
         "code": "FH",
         "type": "Fire Alarm Restore",
         "description": "Alarm condition eliminated",
-        "concerns": "Zone or point",
+        "concerns": "Zone or point"
     },
     "FI": {
         "code": "FI",
         "type": "Fire Test Begin",
         "description": "The transmitter area's fire test has begun",
-        "concerns": "Area number",
+        "concerns": "Area number"
     },
     "FJ": {
         "code": "FJ",
         "type": "Fire Trouble Restore",
         "description": "Trouble condition eliminated",
-        "concerns": "Zone or point",
+        "concerns": "Zone or point"
     },
     "FK": {
         "code": "FK",
         "type": "Fire Test End",
         "description": "The transmitter area's fire test has ended",
-        "concerns": "Area number",
+        "concerns": "Area number"
     },
     "FL": {
         "code": "FL",
         "type": "Fire Alarm Silenced",
         "description": "The fire panel’s sounder was silenced by command",
-        "concerns": "Zone or point",
+        "concerns": "Zone or point"
     },
     "FM": {
         "code": "FM",
         "type": "Fire Alarm - Cross Point",
         "description": "Fire Alarm with Cross Point also in alarm verifying the Fire Alarm",
-        "concerns": "Point number",
+        "concerns": "Point number"
     },
     "FQ": {
         "code": "FQ",
         "type": "Fire Supervisory Trouble Restore",
         "description": "A fire supervisory zone that was in trouble condition has now restored to normal",
-        "concerns": "Zone or point",
+        "concerns": "Zone or point"
     },
     "FR": {
         "code": "FR",
         "type": "Fire Restoral",
         "description": "Alarm/trouble condition has been eliminated",
-        "concerns": "Zone or point",
+        "concerns": "Zone or point"
     },
     "FS": {
         "code": "FS",
         "type": "Fire Supervisory",
         "description": "Unsafe fire detection system condition",
-        "concerns": "Zone or point",
+        "concerns": "Zone or point"
     },
     "FT": {
         "code": "FT",
         "type": "Fire Trouble",
         "description": "Zone disabled by fault",
-        "concerns": "Zone or point",
+        "concerns": "Zone or point"
     },
     "FU": {
         "code": "FU",
         "type": "Fire Unbypass",
         "description": "Bypass has been removed",
-        "concerns": "Zone or point",
+        "concerns": "Zone or point"
     },
     "FV": {
         "code": "FV",
         "type": "Fire Supervision Restore",
         "description": "A fire supervision zone that was in alarm has restored to normal",
-        "concerns": "Zone or point",
+        "concerns": "Zone or point"
     },
     "FW": {
         "code": "FW",
         "type": "Fire Supervisory Trouble",
         "description": "A fire supervisory zone is now in a trouble condition",
-        "concerns": "Zone or point",
+        "concerns": "Zone or point"
     },
     "FX": {
         "code": "FX",
         "type": "Fire Test",
         "description": "Fire zone activated during test",
-        "concerns": "Zone or point",
+        "concerns": "Zone or point"
     },
     "FY": {
         "code": "FY",
         "type": "Missing Fire Trouble",
         "description": "A fire point is now logically missing",
-        "concerns": "Zone or point",
+        "concerns": "Zone or point"
     },
     "FZ": {
         "code": "FZ",
         "type": "Missing Fire Supervision",
         "description": "A Fire Supervisory point has gone missing",
-        "concerns": "Zone or point",
+        "concerns": "Zone or point"
     },
     "GA": {
         "code": "GA",
         "type": "Gas Alarm",
         "description": "Gas alarm condition detected",
-        "concerns": "Zone or point",
+        "concerns": "Zone or point"
     },
     "GB": {
         "code": "GB",
         "type": "Gas Bypass",
         "description": "Zone has been bypassed",
-        "concerns": "Zone or point",
+        "concerns": "Zone or point"
     },
     "GH": {
         "code": "GH",
         "type": "Gas Alarm Restore",
         "description": "Alarm condition eliminated",
-        "concerns": "Zone or point",
+        "concerns": "Zone or point"
     },
     "GJ": {
         "code": "GJ",
         "type": "Gas Trouble Restore",
         "description": "Trouble condition eliminated",
-        "concerns": "Zone or point",
+        "concerns": "Zone or point"
     },
     "GR": {
         "code": "GR",
         "type": "Gas Restoral",
         "description": "Alarm/trouble condition has been eliminated",
-        "concerns": "Zone or point",
+        "concerns": "Zone or point"
     },
     "GS": {
         "code": "GS",
         "type": "Gas Supervisory",
         "description": "Unsafe gas detection system condition",
-        "concerns": "Zone or point",
+        "concerns": "Zone or point"
     },
     "GT": {
         "code": "GT",
         "type": "Gas Trouble",
         "description": "Zone disabled by fault",
-        "concerns": "Zone or point",
+        "concerns": "Zone or point"
     },
     "GU": {
         "code": "GU",
         "type": "Gas Unbypass",
         "description": "Bypass has been removed",
-        "concerns": "Zone or point",
+        "concerns": "Zone or point"
     },
     "GX": {
         "code": "GX",
         "type": "Gas Test",
         "description": "Zone activated during test",
-        "concerns": "Zone or point",
+        "concerns": "Zone or point"
     },
     "HA": {
         "code": "HA",
         "type": "Holdup Alarm",
         "description": "Silent alarm, user under duress",
-        "concerns": "Zone or point",
+        "concerns": "Zone or point"
     },
     "HB": {
         "code": "HB",
         "type": "Holdup Bypass",
         "description": "Zone has been bypassed",
-        "concerns": "Zone or point",
+        "concerns": "Zone or point"
     },
     "HH": {
         "code": "HH",
         "type": "Holdup Alarm Restore",
         "description": "Alarm condition eliminated",
-        "concerns": "Zone or point",
+        "concerns": "Zone or point"
     },
     "HJ": {
         "code": "HJ",
         "type": "Holdup Trouble Restore",
         "description": "Trouble condition eliminated",
-        "concerns": "Zone or point",
+        "concerns": "Zone or point"
     },
     "HR": {
         "code": "HR",
         "type": "Holdup Restoral",
         "description": "Alarm/trouble condition has been eliminated",
-        "concerns": "Zone or point",
+        "concerns": "Zone or point"
     },
     "HS": {
         "code": "HS",
         "type": "Holdup Supervisory",
         "description": "Unsafe holdup system condition",
-        "concerns": "Zone or point",
+        "concerns": "Zone or point"
     },
     "HT": {
         "code": "HT",
         "type": "Holdup Trouble",
         "description": "Zone disabled by fault",
-        "concerns": "Zone or point",
+        "concerns": "Zone or point"
     },
     "HU": {
         "code": "HU",
         "type": "Holdup Unbypass",
         "description": "Bypass has been removed",
-        "concerns": "Zone or point",
+        "concerns": "Zone or point"
     },
     "IA": {
         "code": "IA",
         "type": "Equipment Failure Condition",
         "description": "A specific, reportable condition is detected on a device",
-        "concerns": "Point number",
+        "concerns": "Point number"
     },
     "IR": {
         "code": "IR",
         "type": "Equipment Fail - Restoral",
         "description": "The equipment condition has been restored to normal",
-        "concerns": "Point number",
+        "concerns": "Point number"
     },
     "JA": {
         "code": "JA",
         "type": "User code Tamper",
         "description": "Too many unsuccessful attempts have been made to enter a user ID",
-        "concerns": "Area number",
+        "concerns": "Area number"
+    },
+    "JC": {
+        "code": "JC",
+        "type": "User code tamper canceled",
+        "description": "Break password restore",
+        "concerns": "Area number"
     },
     "JD": {
         "code": "JD",
         "type": "Date Changed",
         "description": "The date was changed in the transmitter/receiver",
-        "concerns": "User number",
+        "concerns": "User number"
     },
     "JH": {
         "code": "JH",
         "type": "Holiday Changed",
         "description": "The transmitter's holiday schedule has been changed",
-        "concerns": "User number",
+        "concerns": "User number"
     },
     "JK": {
         "code": "JK",
         "type": "Latchkey Alert",
         "description": "A designated user passcode has not been entered during a scheduled time window",
-        "concerns": "User number",
+        "concerns": "User number"
     },
     "JL": {
         "code": "JL",
         "type": "Log Threshold",
         "description": "The transmitter's log memory has reached its threshold level",
-        "concerns": "Unused",
+        "concerns": "Unused"
     },
     "JO": {
         "code": "JO",
         "type": "Log Overflow",
         "description": "The transmitter's log memory has overflowed",
-        "concerns": "Unused",
+        "concerns": "Unused"
     },
     "JP": {
         "code": "JP",
         "type": "User On Premises",
         "description": "A designated user passcode has been used to gain access to the premises.",
-        "concerns": "User number",
+        "concerns": "User number"
     },
     "JR": {
         "code": "JR",
         "type": "Schedule Executed",
         "description": "An automatic scheduled event was executed",
-        "concerns": "Area number",
+        "concerns": "Area number"
     },
     "JS": {
         "code": "JS",
         "type": "Schedule Changed",
         "description": "An automatic schedule was changed",
-        "concerns": "User number",
+        "concerns": "User number"
     },
     "JT": {
         "code": "JT",
         "type": "Time Changed",
         "description": "The time was changed in the transmitter/receiver",
-        "concerns": "User number",
+        "concerns": "User number"
     },
     "JV": {
         "code": "JV",
         "type": "User code Changed",
         "description": "A user's code has been changed",
-        "concerns": "User number",
+        "concerns": "User number"
     },
     "JX": {
         "code": "JX",
         "type": "User code Deleted",
         "description": "A user's code has been removed",
-        "concerns": "User number",
+        "concerns": "User number"
     },
     "JY": {
         "code": "JY",
         "type": "User code Added",
         "description": "A user’s code has been added",
-        "concerns": "User number",
+        "concerns": "User number"
     },
     "JZ": {
         "code": "JZ",
         "type": "User Level Set",
         "description": "A user’s authority level has been set",
-        "concerns": "User number",
+        "concerns": "User number"
     },
     "KA": {
         "code": "KA",
         "type": "Heat Alarm",
         "description": "High temperature detected on premise",
-        "concerns": "Zone or point",
+        "concerns": "Zone or point"
     },
     "KB": {
         "code": "KB",
         "type": "Heat Bypass",
         "description": "Zone has been bypassed",
-        "concerns": "Zone or point",
+        "concerns": "Zone or point"
     },
     "KH": {
         "code": "KH",
         "type": "Heat Alarm Restore",
         "description": "Alarm condition eliminated",
-        "concerns": "Zone or point",
+        "concerns": "Zone or point"
     },
     "KJ": {
         "code": "KJ",
         "type": "Heat Trouble Restore",
         "description": "Trouble condition eliminated",
-        "concerns": "Zone or point",
+        "concerns": "Zone or point"
     },
     "KR": {
         "code": "KR",
         "type": "Heat Restoral",
         "description": "Alarm/trouble condition has been eliminated",
-        "concerns": "Zone or point",
+        "concerns": "Zone or point"
     },
     "KS": {
         "code": "KS",
         "type": "Heat Supervisory",
         "description": "Unsafe heat detection system condition",
-        "concerns": "Zone or point",
+        "concerns": "Zone or point"
     },
     "KT": {
         "code": "KT",
         "type": "Heat Trouble",
         "description": "Zone disabled by fault",
-        "concerns": "Zone or point",
+        "concerns": "Zone or point"
     },
     "KU": {
         "code": "KU",
         "type": "Heat Unbypass",
         "description": "Bypass has been removed",
-        "concerns": "Zone or point",
+        "concerns": "Zone or point"
     },
     "LB": {
         "code": "LB",
         "type": "Local Program",
         "description": "Begin local programming",
-        "concerns": "Unused",
+        "concerns": "Unused"
     },
     "LD": {
         "code": "LD",
         "type": "Local Program Denied",
         "description": "Access code incorrect",
-        "concerns": "Unused",
+        "concerns": "Unused"
     },
     "LE": {
         "code": "LE",
         "type": "Listen-in Ended",
         "description": "The listen-in session has been terminated",
-        "concerns": "Unused",
+        "concerns": "Unused"
     },
     "LF": {
         "code": "LF",
         "type": "Listen-in Begin",
         "description": "The listen-in session with the RECEIVER has begun",
-        "concerns": "Unused",
+        "concerns": "Unused"
     },
     "LR": {
         "code": "LR",
         "type": "Phone Line Restoral",
         "description": "Phone line restored to service",
-        "concerns": "Line number",
+        "concerns": "Line number"
     },
     "LS": {
         "code": "LS",
         "type": "Local Program Success",
         "description": "Local programming successful",
-        "concerns": "Unused",
+        "concerns": "Unused"
     },
     "LT": {
         "code": "LT",
         "type": "Phone Line Trouble",
         "description": "Phone line trouble report",
-        "concerns": "Line number",
+        "concerns": "Line number"
     },
     "LU": {
         "code": "LU",
         "type": "Local Program Fail",
         "description": "Local programming unsuccessful",
-        "concerns": "Unused",
+        "concerns": "Unused"
     },
     "LX": {
         "code": "LX",
         "type": "Local Programming Ended",
         "description": "A local programming session has been terminated",
-        "concerns": "Unused",
+        "concerns": "Unused"
     },
     "MA": {
         "code": "MA",
         "type": "Medical Alarm",
         "description": "Emergency assistance request",
-        "concerns": "Zone or point",
+        "concerns": "Zone or point"
     },
     "MB": {
         "code": "MB",
         "type": "Medical Bypass",
         "description": "Zone has been bypassed",
-        "concerns": "Zone or point",
+        "concerns": "Zone or point"
     },
     "MH": {
         "code": "MH",
         "type": "Medical Alarm Restore",
         "description": "Alarm condition eliminated",
-        "concerns": "Zone or point",
+        "concerns": "Zone or point"
     },
     "MI": {
         "code": "MI",
         "type": "Message",
         "description": "A canned message is being sent",
-        "concerns": "Message number",
+        "concerns": "Message number"
     },
     "MJ": {
         "code": "MJ",
         "type": "Medical Trouble Restore",
         "description": "Trouble condition eliminated",
-        "concerns": "Zone or point",
+        "concerns": "Zone or point"
     },
     "MR": {
         "code": "MR",
         "type": "Medical Restoral",
         "description": "Alarm/trouble condition has been eliminated",
-        "concerns": "Zone or point",
+        "concerns": "Zone or point"
     },
     "MS": {
         "code": "MS",
         "type": "Medical Supervisory",
         "description": "Unsafe system condition exists",
-        "concerns": "Zone or point",
+        "concerns": "Zone or point"
     },
     "MT": {
         "code": "MT",
         "type": "Medical Trouble",
         "description": "Zone disabled by fault",
-        "concerns": "Zone or point",
+        "concerns": "Zone or point"
     },
     "MU": {
         "code": "MU",
         "type": "Medical Unbypass",
         "description": "Bypass has been removed",
-        "concerns": "Zone or point",
+        "concerns": "Zone or point"
     },
     "NA": {
         "code": "NA",
         "type": "No Activity",
         "description": "There has been no zone activity for a programmed amount of time",
-        "concerns": "Zone number",
+        "concerns": "Zone number"
     },
     "NB": {
         "code": "NB",
         "type": "Arming Event",
         "description": "Scenario",
-        "concerns": "Area number",
+        "concerns": "Area number"
     },
     "NC": {
         "code": "NC",
         "type": "Network Condition",
         "description": "A communications network has a specific reportable condition",
-        "concerns": "Network number",
+        "concerns": "Network number"
     },
     "ND": {
         "code": "ND",
         "type": "Arming Event",
         "description": "Night mode deactivated - duress",
-        "concerns": "Area number",
+        "concerns": "Area number"
     },
     "NE": {
         "code": "NE",
         "type": "Arming Event",
         "description": "Unsuccessful Night mode activation attempt",
-        "concerns": "Area number",
+        "concerns": "Area number"
     },
     "NF": {
         "code": "NF",
         "type": "Forced Perimeter Arm",
         "description": "Some zones/points not ready",
-        "concerns": "Area number",
+        "concerns": "Area number"
     },
     "NL": {
         "code": "NL",
         "type": "Perimeter Armed",
         "description": "An area has been perimeter armed",
-        "concerns": "Area number",
+        "concerns": "Area number"
     },
     "NM": {
         "code": "NM",
         "type": "Perimeter Armed, User Defined",
         "description": "A user defined area has been perimeter armed",
-        "concerns": "Area number",
+        "concerns": "Area number"
     },
     "NO": {
         "code": "NO",
         "type": "Arming Event",
         "description": "Night mode deactivated automatically",
-        "concerns": "Area number",
+        "concerns": "Area number"
     },
     "NP": {
         "code": "NP",
         "type": "Arming Event",
         "description": "Night mode deactivated using device",
-        "concerns": "Zone or point",
+        "concerns": "Zone or point"
     },
     "NR": {
         "code": "NR",
         "type": "Network Restoral",
         "description": "A communications network has returned to normal operation",
-        "concerns": "Network number",
+        "concerns": "Network number"
     },
     "NS": {
         "code": "NS",
         "type": "Activity Resumed",
         "description": "A zone has detected activity after an alert",
-        "concerns": "Zone number",
+        "concerns": "Zone number"
     },
     "NT": {
         "code": "NT",
         "type": "Network Failure",
         "description": "A communications network has failed",
-        "concerns": "Network number",
+        "concerns": "Network number"
     },
     "OA": {
         "code": "OA",
         "type": "Automatic Opening",
         "description": "System has disarmed automatically",
-        "concerns": "Area number",
+        "concerns": "Area number"
     },
     "OB": {
         "code": "OB",
         "type": "Arming Event",
         "description": "Group has been disarmed automatically",
-        "concerns": "Area number",
+        "concerns": "Area number"
     },
     "OC": {
         "code": "OC",
         "type": "Cancel Report",
         "description": "Untyped zone cancel",
-        "concerns": "User number",
+        "concerns": "User number"
     },
     "OG": {
         "code": "OG",
         "type": "Open Area",
         "description": "System has been partially disarmed",
-        "concerns": "Area number",
+        "concerns": "Area number"
     },
     "OH": {
         "code": "OH",
         "type": "Early to Open from Alarm",
         "description": "An area in alarm was disarmed before the opening window",
-        "concerns": "User number",
+        "concerns": "User number"
     },
     "OI": {
         "code": "OI",
         "type": "Fail to Open",
         "description": "An area has not been armed at the end of the opening window",
-        "concerns": "Area number",
+        "concerns": "Area number"
     },
     "OJ": {
         "code": "OJ",
         "type": "Late Open",
         "description": "An area was disarmed after the opening window",
-        "concerns": "User number",
+        "concerns": "User number"
     },
     "OK": {
         "code": "OK",
         "type": "Early Open",
         "description": "An area was disarmed before the opening window",
-        "concerns": "User number",
+        "concerns": "User number"
     },
     "OL": {
         "code": "OL",
         "type": "Late to Open from Alarm",
         "description": "An area in alarm was disarmed after the opening window",
-        "concerns": "User number",
+        "concerns": "User number"
     },
     "OP": {
         "code": "OP",
         "type": "Opening Report",
         "description": "Account was disarmed",
-        "concerns": "User number",
+        "concerns": "User number"
     },
     "OQ": {
         "code": "OQ",
         "type": "Remote Opening",
         "description": "The system was disarmed from a remote location",
-        "concerns": "User number",
+        "concerns": "User number"
     },
     "OR": {
         "code": "OR",
         "type": "Disarm From Alarm",
         "description": "Account in alarm was reset/disarmed",
-        "concerns": "User number",
+        "concerns": "User number"
     },
     "OS": {
         "code": "OS",
         "type": "Opening Keyswitch",
         "description": "Account has been disarmed by keyswitch",
-        "concerns": "Zone or point",
+        "concerns": "Zone or point"
     },
     "OT": {
         "code": "OT",
         "type": "Late To Close",
         "description": "System was not armed on time",
-        "concerns": "User number",
+        "concerns": "User number"
     },
     "OU": {
         "code": "OU",
         "type": "Output State – Trouble",
         "description": "An output on a peripheral device or NAC is not functioning",
-        "concerns": "Output number",
+        "concerns": "Output number"
     },
     "OV": {
         "code": "OV",
         "type": "Output State – Restore",
         "description": "An output on a peripheral device or NAC is back to normal operation",
-        "concerns": "Output number",
+        "concerns": "Output number"
     },
     "OZ": {
         "code": "OZ",
         "type": "Point Opening",
         "description": "A point, rather than a full area or account, disarmed",
-        "concerns": "Zone or point",
+        "concerns": "Zone or point"
     },
     "PA": {
         "code": "PA",
         "type": "Panic Alarm",
         "description": "Emergency assistance request, manually activated",
-        "concerns": "Zone or point",
+        "concerns": "Zone or point"
     },
     "PB": {
         "code": "PB",
         "type": "Panic Bypass",
         "description": "Panic zone has been bypassed",
-        "concerns": "Zone or point",
+        "concerns": "Zone or point"
     },
     "PF": {
         "code": "PF",
         "type": "Malfunction",
         "description": "MotionCam lost connection with hub over photos communication channel",
-        "concerns": "Zone or point",
+        "concerns": "Zone or point"
     },
     "PH": {
         "code": "PH",
         "type": "Panic Alarm Restore",
         "description": "Alarm condition eliminated",
-        "concerns": "Zone or point",
+        "concerns": "Zone or point"
     },
     "PJ": {
         "code": "PJ",
         "type": "Panic Trouble Restore",
         "description": "Trouble condition eliminated",
-        "concerns": "Zone or point",
+        "concerns": "Zone or point"
     },
     "PO": {
         "code": "PO",
         "type": "Malfunction restore",
         "description": "Connection restored, device in room",
-        "concerns": "Unused",
+        "concerns": "Unused"
     },
     "PR": {
         "code": "PR",
         "type": "Panic Restoral",
         "description": "Alarm/trouble condition has been eliminated",
-        "concerns": "Zone or point",
+        "concerns": "Zone or point"
     },
     "PS": {
         "code": "PS",
         "type": "Panic Supervisory",
         "description": "Unsafe system condition exists",
-        "concerns": "Zone or point",
+        "concerns": "Zone or point"
     },
     "PT": {
         "code": "PT",
         "type": "Panic Trouble",
         "description": "Zone disabled by fault",
-        "concerns": "Zone or point",
+        "concerns": "Zone or point"
     },
     "PU": {
         "code": "PU",
         "type": "Panic Unbypass",
         "description": "Panic zone bypass has been removed",
-        "concerns": "Zone or point",
+        "concerns": "Zone or point"
     },
     "QA": {
         "code": "QA",
         "type": "Emergency Alarm",
         "description": "Emergency assistance request",
-        "concerns": "Zone or point",
+        "concerns": "Zone or point"
     },
     "QB": {
         "code": "QB",
         "type": "Emergency Bypass",
         "description": "Zone has been bypassed",
-        "concerns": "Zone or point",
+        "concerns": "Zone or point"
     },
     "QH": {
         "code": "QH",
         "type": "Emergency Alarm Restore",
         "description": "Alarm condition has been eliminated",
-        "concerns": "Zone or point",
+        "concerns": "Zone or point"
     },
     "QJ": {
         "code": "QJ",
         "type": "Emergency Trouble Restore",
         "description": "Trouble condition has been eliminated",
-        "concerns": "Zone or point",
+        "concerns": "Zone or point"
     },
     "QR": {
         "code": "QR",
         "type": "Emergency Restoral",
         "description": "Alarm/trouble condition has been eliminated",
-        "concerns": "Zone or point",
+        "concerns": "Zone or point"
     },
     "QS": {
         "code": "QS",
         "type": "Emergency Supervisory",
         "description": "Unsafe system condition exists",
-        "concerns": "Zone or point",
+        "concerns": "Zone or point"
     },
     "QT": {
         "code": "QT",
         "type": "Emergency Trouble",
         "description": "Zone disabled by fault",
-        "concerns": "Zone or point",
+        "concerns": "Zone or point"
     },
     "QU": {
         "code": "QU",
         "type": "Emergency Unbypass",
         "description": "Bypass has been removed",
-        "concerns": "Zone or point",
+        "concerns": "Zone or point"
     },
     "RA": {
         "code": "RA",
         "type": "Remote Programmer Call Failed",
         "description": "Transmitter failed to communicate with the remote programmer",
-        "concerns": "Unused",
+        "concerns": "Unused"
     },
     "RB": {
         "code": "RB",
         "type": "Remote Program Begin",
         "description": "Remote programming session initiated",
-        "concerns": "Unused",
+        "concerns": "Unused"
     },
     "RC": {
         "code": "RC",
         "type": "Relay Close",
         "description": "A relay has energized",
-        "concerns": "Relay number",
+        "concerns": "Relay number"
     },
     "RD": {
         "code": "RD",
         "type": "Remote Program Denied",
         "description": "Access passcode incorrect",
-        "concerns": "Unused",
+        "concerns": "Unused"
     },
     "RN": {
         "code": "RN",
         "type": "Remote Reset",
         "description": "A TRANSMITTER was reset via a remote programmer",
-        "concerns": "Unused",
+        "concerns": "Unused"
     },
     "RO": {
         "code": "RO",
         "type": "Relay Open",
         "description": "A relay has de-energized",
-        "concerns": "Relay number",
+        "concerns": "Relay number"
     },
     "RP": {
         "code": "RP",
         "type": "Automatic Test",
         "description": "Automatic communication test report",
-        "concerns": "Unused",
+        "concerns": "Unused"
     },
     "RR": {
         "code": "RR",
         "type": "Power Up",
         "description": "System lost power, is now restored",
-        "concerns": "Unused",
+        "concerns": "Unused"
     },
     "RS": {
         "code": "RS",
         "type": "Remote Program Success",
         "description": "Remote programming successful",
-        "concerns": "Unused",
+        "concerns": "Unused"
     },
     "RT": {
         "code": "RT",
         "type": "Data Lost",
         "description": "Dialer data lost, transmission error",
-        "concerns": "Line number",
+        "concerns": "Line number"
     },
     "RU": {
         "code": "RU",
         "type": "Remote Program Fail",
         "description": "Remote programming unsuccessful",
-        "concerns": "Unused",
+        "concerns": "Unused"
     },
     "RX": {
         "code": "RX",
         "type": "Manual Test",
         "description": "Manual communication test report",
-        "concerns": "User number",
+        "concerns": "User number"
     },
     "RY": {
         "code": "RY",
         "type": "Test Off Normal",
         "description": "Test signal(s) indicates abnormal condition(s) exist",
-        "concerns": "Zone or point",
+        "concerns": "Zone or point"
     },
     "SA": {
         "code": "SA",
         "type": "Sprinkler Alarm",
         "description": "Sprinkler flow condition exists",
-        "concerns": "Zone or point",
+        "concerns": "Zone or point"
     },
     "SB": {
         "code": "SB",
         "type": "Sprinkler Bypass",
         "description": "Sprinkler zone has been bypassed",
-        "concerns": "Zone or point",
+        "concerns": "Zone or point"
     },
     "SC": {
         "code": "SC",
         "type": "Change of State",
         "description": "An expansion/peripheral device is reporting a new condition or state change",
-        "concerns": "Condition number",
+        "concerns": "Condition number"
     },
     "SH": {
         "code": "SH",
         "type": "Sprinkler Alarm Restore",
         "description": "Alarm condition eliminated",
-        "concerns": "Zone or point",
+        "concerns": "Zone or point"
     },
     "SJ": {
         "code": "SJ",
         "type": "Sprinkler Trouble Restore",
         "description": "Trouble condition eliminated",
-        "concerns": "Zone or point",
+        "concerns": "Zone or point"
     },
     "SM": {
         "code": "SM",
         "type": "Malfunction",
         "description": "LeakProtect Malfunction",
-        "concerns": "Zone or point",
+        "concerns": "Zone or point"
     },
     "SR": {
         "code": "SR",
         "type": "Sprinkler Restoral",
         "description": "Alarm/trouble condition has been eliminated",
-        "concerns": "Zone or point",
+        "concerns": "Zone or point"
     },
     "SS": {
         "code": "SS",
         "type": "Sprinkler Supervisory",
         "description": "Unsafe sprinkler system condition",
-        "concerns": "Zone or point",
+        "concerns": "Zone or point"
     },
     "ST": {
         "code": "ST",
         "type": "Sprinkler Trouble",
         "description": "Zone disabled by fault",
-        "concerns": "Zone or point",
+        "concerns": "Zone or point"
     },
     "SU": {
         "code": "SU",
         "type": "Sprinkler Unbypass",
         "description": "Sprinkler zone bypass has been removed",
-        "concerns": "Zone or point",
+        "concerns": "Zone or point"
     },
     "TA": {
         "code": "TA",
         "type": "Tamper Alarm",
         "description": "Alarm equipment enclosure opened",
-        "concerns": "Zone or point",
+        "concerns": "Zone or point"
     },
     "TB": {
         "code": "TB",
         "type": "Tamper Bypass",
         "description": "Tamper detection has been bypassed",
-        "concerns": "Zone or point",
+        "concerns": "Zone or point"
     },
     "TC": {
         "code": "TC",
         "type": "All Points Tested",
         "description": "All point tested",
-        "concerns": "Unused",
+        "concerns": "Unused"
     },
     "TE": {
         "code": "TE",
         "type": "Test End",
         "description": "Communicator restored to operation",
-        "concerns": "Unused",
+        "concerns": "Unused"
     },
     "TH": {
         "code": "TH",
         "type": "Tamper Alarm Restore",
         "description": "An Expansion Device’s tamper switch restores to normal from an Alarm state",
-        "concerns": "Unused",
+        "concerns": "Unused"
     },
     "TJ": {
         "code": "TJ",
         "type": "Tamper Trouble Restore",
         "description": "An Expansion Device’s tamper switch restores to normal from a Trouble state",
-        "concerns": "Unused",
+        "concerns": "Unused"
     },
     "TP": {
         "code": "TP",
         "type": "Walk Test Point",
         "description": "This point was tested during a Walk Test",
-        "concerns": "Point number",
+        "concerns": "Point number"
     },
     "TR": {
         "code": "TR",
         "type": "Tamper Restoral",
         "description": "Alarm equipment enclosure has been closed",
-        "concerns": "Zone or point",
+        "concerns": "Zone or point"
     },
     "TS": {
         "code": "TS",
         "type": "Test Start",
         "description": "Communicator taken out of operation",
-        "concerns": "Unused",
+        "concerns": "Unused"
     },
     "TT": {
         "code": "TT",
         "type": "Tamper Trouble",
         "description": "Equipment enclosure opened in disarmed state",
-        "concerns": "Zone or point",
+        "concerns": "Zone or point"
     },
     "TU": {
         "code": "TU",
         "type": "Tamper Unbypass",
         "description": "Tamper detection bypass has been removed",
-        "concerns": "Zone or point",
+        "concerns": "Zone or point"
     },
     "TW": {
         "code": "TW",
         "type": "Area Watch Start",
         "description": "Area watch feature has been activated",
-        "concerns": "Unused",
+        "concerns": "Unused"
     },
     "TX": {
         "code": "TX",
         "type": "Test Report",
         "description": "An unspecified (manual or automatic) communicator test",
-        "concerns": "Unused",
+        "concerns": "Unused"
     },
     "TZ": {
         "code": "TZ",
         "type": "Area Watch End",
         "description": "Area watch feature has been deactivated",
-        "concerns": "Unused",
+        "concerns": "Unused"
     },
     "UA": {
         "code": "UA",
         "type": "Untyped Zone Alarm",
         "description": "Alarm condition from zone of unknown type",
-        "concerns": "Zone or point",
+        "concerns": "Zone or point"
     },
     "UB": {
         "code": "UB",
         "type": "Untyped Zone Bypass",
         "description": "Zone of unknown type has been bypassed",
-        "concerns": "Zone or point",
+        "concerns": "Zone or point"
     },
     "UG": {
         "code": "UG",
         "type": "Unverified Event – Untyped",
         "description": "A point assigned to a Cross Point group has gone into alarm but the Cross Point remained normal",
-        "concerns": "Zone or point",
+        "concerns": "Zone or point"
     },
     "UH": {
         "code": "UH",
         "type": "Untyped Alarm Restore",
         "description": "Alarm condition eliminated",
-        "concerns": "Zone or point",
+        "concerns": "Zone or point"
     },
     "UJ": {
         "code": "UJ",
         "type": "Untyped Trouble Restore",
         "description": "Trouble condition eliminated",
-        "concerns": "Zone or point",
+        "concerns": "Zone or point"
     },
     "UR": {
         "code": "UR",
         "type": "Untyped Zone Restoral",
         "description": "Alarm/trouble condition eliminated from zone of unknown type",
-        "concerns": "Zone or point",
+        "concerns": "Zone or point"
     },
     "US": {
         "code": "US",
         "type": "Untyped Zone Supervisory",
         "description": "Unsafe condition from zone of unknown type",
-        "concerns": "Zone or point",
+        "concerns": "Zone or point"
     },
     "UT": {
         "code": "UT",
         "type": "Untyped Zone Trouble",
         "description": "Trouble condition from zone of unknown type",
-        "concerns": "Zone or point",
+        "concerns": "Zone or point"
     },
     "UU": {
         "code": "UU",
         "type": "Untyped Zone Unbypass",
         "description": "Bypass on zone of unknown type has been removed",
-        "concerns": "Zone or point",
+        "concerns": "Zone or point"
     },
     "UX": {
         "code": "UX",
         "type": "Undefined",
         "description": "An undefined alarm condition has occurred",
-        "concerns": "Unused",
+        "concerns": "Unused"
     },
     "UY": {
         "code": "UY",
         "type": "Untyped Missing Trouble",
         "description": "A point or device which was not armed is now logically missing",
-        "concerns": "Zone or point",
+        "concerns": "Zone or point"
     },
     "UZ": {
         "code": "UZ",
         "type": "Untyped Missing Alarm",
         "description": "A point or device which was armed is now logically missing",
-        "concerns": "Zone or point",
+        "concerns": "Zone or point"
     },
     "VI": {
         "code": "VI",
         "type": "Printer Paper In",
         "description": "TRANSMITTER or RECEIVER paper in",
-        "concerns": "Printer number",
+        "concerns": "Printer number"
     },
     "VO": {
         "code": "VO",
         "type": "Printer Paper Out",
         "description": "TRANSMITTER or RECEIVER paper out",
-        "concerns": "Printer number",
+        "concerns": "Printer number"
     },
     "VR": {
         "code": "VR",
         "type": "Printer Restore",
         "description": "TRANSMITTER or RECEIVER trouble restored",
-        "concerns": "Printer number",
+        "concerns": "Printer number"
     },
     "VT": {
         "code": "VT",
         "type": "Printer Trouble",
         "description": "TRANSMITTER or RECEIVER trouble",
-        "concerns": "Printer number",
+        "concerns": "Printer number"
     },
     "VX": {
         "code": "VX",
         "type": "Printer Test",
         "description": "TRANSMITTER or RECEIVER test",
-        "concerns": "Printer number",
+        "concerns": "Printer number"
     },
     "VY": {
         "code": "VY",
         "type": "Printer Online",
         "description": "RECEIVER’S printer is now online",
-        "concerns": "Unused",
+        "concerns": "Unused"
     },
     "VZ": {
         "code": "VZ",
         "type": "Printer Offline",
         "description": "RECEIVER’S printer is now offline",
-        "concerns": "Unused",
+        "concerns": "Unused"
     },
     "WA": {
         "code": "WA",
         "type": "Water Alarm",
         "description": "Water detected at protected premises",
-        "concerns": "Zone or point",
+        "concerns": "Zone or point"
     },
     "WB": {
         "code": "WB",
         "type": "Water Bypass",
         "description": "Water detection has been bypassed",
-        "concerns": "Zone or point",
+        "concerns": "Zone or point"
     },
     "WH": {
         "code": "WH",
         "type": "Water Alarm Restore",
         "description": "Water alarm condition eliminated",
-        "concerns": "Zone or point",
+        "concerns": "Zone or point"
     },
     "WJ": {
         "code": "WJ",
         "type": "Water Trouble Restore",
         "description": "Water trouble condition eliminated",
-        "concerns": "Zone or point",
+        "concerns": "Zone or point"
     },
     "WR": {
         "code": "WR",
         "type": "Water Restoral",
         "description": "Water alarm/trouble condition has been eliminated",
-        "concerns": "Zone or point",
+        "concerns": "Zone or point"
     },
     "WS": {
         "code": "WS",
         "type": "Water Supervisory",
         "description": "Water unsafe water detection system condition",
-        "concerns": "Zone or point",
+        "concerns": "Zone or point"
     },
     "WT": {
         "code": "WT",
         "type": "Water Trouble",
         "description": "Water zone disabled by fault",
-        "concerns": "Zone or point",
+        "concerns": "Zone or point"
     },
     "WU": {
         "code": "WU",
         "type": "Water Unbypass",
         "description": "Water detection bypass has been removed",
-        "concerns": "Zone or point",
+        "concerns": "Zone or point"
     },
     "XA": {
         "code": "XA",
         "type": "Extra Account Report",
         "description": "CS RECEIVER has received an event from a non-existent account",
-        "concerns": "Unused",
+        "concerns": "Unused"
     },
     "XC": {
         "code": "XC",
         "type": "Malfunction restore",
         "description": "Connection restored, device in room",
-        "concerns": "Unused",
+        "concerns": "Unused"
     },
     "XE": {
         "code": "XE",
         "type": "Extra Point",
         "description": "Panel has sensed an extra point not specified for this site",
-        "concerns": "Point number",
+        "concerns": "Point number"
     },
     "XF": {
         "code": "XF",
         "type": "Extra RF Point",
         "description": "Panel has sensed an extra RF point not specified for this site",
-        "concerns": "Point number",
+        "concerns": "Point number"
     },
     "XH": {
         "code": "XH",
         "type": "RF Interference Restoral",
         "description": "A radio device is no longer detecting RF Interference",
-        "concerns": "Receiver number",
+        "concerns": "Receiver number"
     },
     "XI": {
         "code": "XI",
         "type": "Sensor Reset",
         "description": "A user has reset a sensor",
-        "concerns": "Zone or point",
+        "concerns": "Zone or point"
     },
     "XJ": {
         "code": "XJ",
         "type": "RF Receiver Tamper Restoral",
         "description": "A Tamper condition at a premises RF Receiver has been restored",
-        "concerns": "Receiver number",
+        "concerns": "Receiver number"
     },
     "XL": {
         "code": "XL",
         "type": "Low Received Signal Strength/Connection Lost",
         "description": "The RF signal strength of a reported event is below minimum level",
-        "concerns": "Receiver number",
+        "concerns": "Receiver number"
     },
     "XM": {
         "code": "XM",
         "type": "Missing Alarm - Cross Point",
         "description": "Missing Alarm verified by Cross Point in Alarm (or missing)",
-        "concerns": "Zone or point",
+        "concerns": "Zone or point"
     },
     "XQ": {
         "code": "XQ",
         "type": "RF Interference",
         "description": "A radio device is detecting RF Interference",
-        "concerns": "Receiver number",
+        "concerns": "Receiver number"
     },
     "XR": {
         "code": "XR",
         "type": "Transmitter Battery Restoral",
         "description": "Low battery has been corrected",
-        "concerns": "Zone or point",
+        "concerns": "Zone or point"
     },
     "XS": {
         "code": "XS",
         "type": "RF Receiver Tamper",
         "description": "A Tamper condition at a premises receiver is detected",
-        "concerns": "Receiver number",
+        "concerns": "Receiver number"
     },
     "XT": {
         "code": "XT",
         "type": "Transmitter Battery Trouble",
         "description": "Low battery in wireless transmitter",
-        "concerns": "Zone or point",
+        "concerns": "Zone or point"
     },
     "XW": {
         "code": "XW",
         "type": "Forced Point",
         "description": "A point was forced out of the system at arm time",
-        "concerns": "Zone or point",
+        "concerns": "Zone or point"
     },
     "XX": {
         "code": "XX",
         "type": "Fail to Test",
         "description": "A specific test from a panel was not received",
-        "concerns": "Unused",
+        "concerns": "Unused"
     },
     "YA": {
         "code": "YA",
         "type": "Bell Fault",
         "description": "A trouble condition has been detected on a Local Bell, Siren, or Annunciator",
-        "concerns": "Unused",
+        "concerns": "Unused"
     },
     "YB": {
         "code": "YB",
         "type": "Busy Seconds",
         "description": "Percent of time receiver's line card is on-line",
-        "concerns": "Line card number",
+        "concerns": "Line card number"
     },
     "YC": {
         "code": "YC",
         "type": "Communications Fail",
         "description": "RECEIVER and TRANSMITTER",
-        "concerns": "Unused",
+        "concerns": "Unused"
     },
     "YD": {
         "code": "YD",
         "type": "Receiver Line Card Trouble",
         "description": "A line card identified by the passed address is in trouble",
-        "concerns": "Line card number",
+        "concerns": "Line card number"
     },
     "YE": {
         "code": "YE",
         "type": "Receiver Line Card Restored",
         "description": "A line card identified by the passed address is restored",
-        "concerns": "Line card number",
+        "concerns": "Line card number"
     },
     "YF": {
         "code": "YF",
         "type": "Parameter Checksum Fail",
         "description": "System data corrupted",
-        "concerns": "Unused",
+        "concerns": "Unused"
     },
     "YG": {
         "code": "YG",
         "type": "Parameter Changed",
         "description": "A TRANSMITTER’S parameters have been changed",
-        "concerns": "Unused",
+        "concerns": "Unused"
     },
     "YH": {
         "code": "YH",
         "type": "Bell Restored",
         "description": "A trouble condition has been restored on a Local Bell, Siren, or Annunciator",
-        "concerns": "Unused",
+        "concerns": "Unused"
     },
     "YI": {
         "code": "YI",
         "type": "Overcurrent Trouble",
         "description": "An Expansion Device has detected an overcurrent condition",
-        "concerns": "Unused",
+        "concerns": "Unused"
     },
     "YJ": {
         "code": "YJ",
         "type": "Overcurrent Restore",
         "description": "An Expansion Device has restored from an overcurrent condition",
-        "concerns": "Unused",
+        "concerns": "Unused"
     },
     "YK": {
         "code": "YK",
         "type": "Communications Restoral",
         "description": "TRANSMITTER has resumed communication with a RECEIVER",
-        "concerns": "Unused",
+        "concerns": "Unused"
     },
     "YM": {
         "code": "YM",
         "type": "System Battery Missing",
         "description": "TRANSMITTER/RECEIVER battery is missing",
-        "concerns": "Unused",
+        "concerns": "Unused"
     },
     "YN": {
         "code": "YN",
         "type": "Invalid Report",
         "description": "TRANSMITTER has sent a packet with invalid data",
-        "concerns": "Unused",
+        "concerns": "Unused"
     },
     "YO": {
         "code": "YO",
         "type": "Unknown Message",
         "description": "An unknown message was received from automation or the printer",
-        "concerns": "Unused",
+        "concerns": "Unused"
     },
     "YP": {
         "code": "YP",
         "type": "Power Supply Trouble",
         "description": "TRANSMITTER/RECEIVER has a problem with the power supply",
-        "concerns": "Unused",
+        "concerns": "Unused"
     },
     "YQ": {
         "code": "YQ",
         "type": "Power Supply Restored",
         "description": "TRANSMITTER’S/RECEIVER’S power supply has been restored",
-        "concerns": "Unused",
+        "concerns": "Unused"
     },
     "YR": {
         "code": "YR",
         "type": "System Battery Restoral",
         "description": "Low battery has been corrected",
-        "concerns": "Unused",
+        "concerns": "Unused"
     },
     "YS": {
         "code": "YS",
         "type": "Communications Trouble",
         "description": "RECEIVER and TRANSMITTER",
-        "concerns": "Unused",
+        "concerns": "Unused"
     },
     "YT": {
         "code": "YT",
         "type": "System Battery Trouble",
         "description": "Low battery in control/communicator",
-        "concerns": "Unused",
+        "concerns": "Unused"
     },
     "YU": {
         "code": "YU",
         "type": "Diagnostic Error",
         "description": "An expansion/peripheral device is reporting a diagnostic error",
-        "concerns": "Condition number",
+        "concerns": "Condition number"
     },
     "YW": {
         "code": "YW",
         "type": "Watchdog Reset",
         "description": "The TRANSMITTER created an internal reset",
-        "concerns": "Unused",
+        "concerns": "Unused"
     },
     "YX": {
         "code": "YX",
         "type": "Service Required",
         "description": "A TRANSMITTER/RECEIVER needs service",
-        "concerns": "Unused",
+        "concerns": "Unused"
     },
     "YY": {
         "code": "YY",
         "type": "Status Report",
         "description": "This is a header for an account status report transmission",
-        "concerns": "Unused",
+        "concerns": "Unused"
     },
     "YZ": {
         "code": "YZ",
         "type": "Service Completed",
         "description": "Required TRANSMITTER / RECEIVER service completed",
-        "concerns": "Mfr defined",
+        "concerns": "Mfr defined"
     },
     "ZA": {
         "code": "ZA",
         "type": "Freeze Alarm",
         "description": "Low temperature detected at premises",
-        "concerns": "Zone or point",
+        "concerns": "Zone or point"
     },
     "ZB": {
         "code": "ZB",
         "type": "Freeze Bypass",
         "description": "Low temperature detection has been bypassed",
-        "concerns": "Zone or point",
+        "concerns": "Zone or point"
     },
     "ZH": {
         "code": "ZH",
         "type": "Freeze Alarm Restore",
         "description": "Alarm condition eliminated",
-        "concerns": "Zone or point",
+        "concerns": "Zone or point"
     },
     "ZJ": {
         "code": "ZJ",
         "type": "Freeze Trouble Restore",
         "description": "Trouble condition eliminated",
-        "concerns": "Zone or point",
+        "concerns": "Zone or point"
     },
     "ZR": {
         "code": "ZR",
         "type": "Freeze Restoral",
         "description": "Alarm/trouble condition has been eliminated",
-        "concerns": "Zone or point",
+        "concerns": "Zone or point"
     },
     "ZS": {
         "code": "ZS",
         "type": "Freeze Supervisory",
         "description": "Unsafe freeze detection system condition",
-        "concerns": "Zone or point",
+        "concerns": "Zone or point"
     },
     "ZT": {
         "code": "ZT",
         "type": "Freeze Trouble",
         "description": "Zone disabled by fault",
-        "concerns": "Zone or point",
+        "concerns": "Zone or point"
     },
     "ZU": {
         "code": "ZU",
         "type": "Freeze Unbypass",
         "description": "Low temperature detection bypass removed",
-        "concerns": "Zone or point",
+        "concerns": "Zone or point"
     },
     "ZY": {
         "code": "ZY",
         "type": "Common Event",
         "description": "Device was switched on",
-        "concerns": "Hub",
+        "concerns": "Hub"
     },
     "ZZ": {
         "code": "ZZ",
         "type": "Common Event",
-        "description": "turned off",
-        "concerns": "Hub",
-    },
-}
+        "description": "Device was switched off",
+        "concerns": "Hub"
+    }
+}
\ No newline at end of file
diff --git a/src/pysiaalarm/data/xdata.json b/src/pysiaalarm/data/xdata.json
new file mode 100644
index 0000000..e6451ea
--- /dev/null
+++ b/src/pysiaalarm/data/xdata.json
@@ -0,0 +1,128 @@
+{
+    "A": {
+        "identifier": "A",
+        "name": "Authentication Hash",
+        "description": "A hash of the message that allows the message to be authenticated.",
+        "length": 12,
+        "characters": "ASCII"
+    },
+    "C": {
+        "identifier": "C",
+        "name": "Supervision Category",
+        "description": "An identifier for the number of communication paths and link supervision category",
+        "length": 64,
+        "characters": "ASCII"
+    },
+    "H": {
+        "identifier": "H",
+        "name": "Time of Occurence",
+        "description": "Time that event occurred (may be different than message time stamp)",
+        "length": 21,
+        "characters": "ASCII"
+    },
+    "I": {
+        "identifier": "I",
+        "name": "Alarm Text",
+        "description": "Alarm text which may be a description of the event or a comment regarding the event.",
+        "length": 256,
+        "characters": "Win1252"
+    },
+    "J": {
+        "identifier": "J",
+        "name": "Network Path",
+        "description": "Manufacturer specific identifier for the path that was used for the communication",
+        "length": 1,
+        "characters": "ASCII"
+    },
+    "K": {
+        "identifier": "K",
+        "name": "Encryption Key",
+        "description": "Key exchange request from CSR to PE (up to 256 bits)",
+        "length": 64,
+        "characters": "ASCII"
+    },
+    "L": {
+        "identifier": "L",
+        "name": "Location",
+        "description": "Location of event on site",
+        "length": 256,
+        "characters": "Win1252"
+    },
+    "M": {
+        "identifier": "M",
+        "name": "MAC Address",
+        "description": "MAC address of the PE.",
+        "length": 12,
+        "characters": "ASCII"
+    },
+    "N": {
+        "identifier": "N",
+        "name": "Network Address",
+        "description": "Hardware network address associated with the communication on path used.",
+        "length": 128,
+        "characters": "ASCII"
+    },
+    "P": {
+        "identifier": "P",
+        "name": "Authentication Hash",
+        "description": "contains a message used to support programming or other interactive operations with the receiver",
+        "length": 256,
+        "characters": "Win1252"
+    },
+    "O": {
+        "identifier": "O",
+        "name": "Building Name",
+        "description": "Building name.",
+        "length": 256,
+        "characters": "Win1252"
+    },
+    "R": {
+        "identifier": "R",
+        "name": "Room",
+        "description": "Room of the event.",
+        "length": 256,
+        "characters": "Win1252"
+    },
+    "S": {
+        "identifier": "S",
+        "name": "Site name",
+        "description": "Site name describing the premises.",
+        "length": 256,
+        "characters": "Win1252"
+    },
+    "T": {
+        "identifier": "T",
+        "name": "Alarm Trigger",
+        "description": "Trigger for the event.",
+        "length": 1,
+        "characters": "ASCII"
+    },
+    "V": {
+        "identifier": "V",
+        "name": "Verification",
+        "description": "information about audio or video information that may be associated with the event report.",
+        "length": 256,
+        "characters": "Win1252"
+    },
+    "X": {
+        "identifier": "X",
+        "name": "Longitude",
+        "description": "Location of event, longitude.",
+        "length": 12,
+        "characters": "ASCII"
+    },
+    "Y": {
+        "identifier": "Y",
+        "name": "Latitude",
+        "description": "Location of event, latitude.",
+        "length": 12,
+        "characters": "ASCII"
+    },
+    "Z": {
+        "identifier": "Z",
+        "name": "Altitude",
+        "description": "Location of event, altitude.",
+        "length": 12,
+        "characters": "ASCII"
+    }
+}
\ No newline at end of file
diff --git a/src/pysiaalarm/sia_errors.py b/src/pysiaalarm/errors.py
similarity index 85%
rename from src/pysiaalarm/sia_errors.py
rename to src/pysiaalarm/errors.py
index 67ae66a..b5a1611 100644
--- a/src/pysiaalarm/sia_errors.py
+++ b/src/pysiaalarm/errors.py
@@ -29,3 +29,9 @@ class EventFormatError(Exception):
     """Error for when a event is incorrectly formatted."""
 
     pass
+
+
+class EventCRCError(Exception):
+    """Error for when a event hass a mismatched CRC."""
+
+    pass
diff --git a/src/pysiaalarm/event.py b/src/pysiaalarm/event.py
new file mode 100644
index 0000000..8acbbc4
--- /dev/null
+++ b/src/pysiaalarm/event.py
@@ -0,0 +1,500 @@
+"""This is a class for SIA Events."""
+from __future__ import annotations
+
+import logging
+from abc import ABC, abstractmethod, abstractproperty
+from dataclasses import dataclass, field
+from dataclasses_json import dataclass_json, config, Exclude
+from datetime import datetime, timedelta, timezone
+from typing import Dict, Optional, Union, List
+
+from Crypto.Cipher import AES
+from Crypto.Cipher._mode_cbc import CbcMode
+
+from .account import SIAAccount
+from .const import IV, RSP_XDATA
+from .errors import EventFormatError
+from .utils import (
+    MAIN_MATCHER,
+    OH_MATCHER,
+    MessageTypes,
+    ResponseType,
+    SIACode,
+    SIAXData,
+    _get_matcher,
+    _load_adm_mapping,
+    _load_sia_codes,
+    _load_xdata,
+)
+
+_LOGGER = logging.getLogger(__name__)
+
+
+@dataclass_json()
+@dataclass  # type: ignore
+class BaseEvent(ABC):
+    """Base class for Events."""
+
+    # From Main Matcher
+    full_message: Optional[str] = None
+    msg_crc: Optional[str] = None
+    length: Optional[str] = None
+    encrypted: Optional[bool] = None
+    message_type: Optional[Union[MessageTypes, str]] = None
+    receiver: Optional[str] = None
+    line: Optional[str] = None
+    account: Optional[str] = None
+    sequence: Optional[str] = None
+
+    # Content to be parsed
+    content: Optional[str] = None
+    encrypted_content: Optional[str] = None
+
+    # From (Encrypted) Content
+    ti: Optional[str] = None
+    id: Optional[str] = None
+    ri: Optional[str] = None
+    code: Optional[str] = None
+    message: Optional[str] = None
+    x_data: Optional[str] = None
+    timestamp: Optional[datetime] = None
+
+    # From ADM-CID
+    event_qualifier: Optional[str] = None
+    event_type: Optional[str] = None
+    partition: Optional[str] = None
+
+    # Parsed fields
+    calc_crc: Optional[str] = None
+    extended_data: Optional[List[SIAXData]] = None
+    sia_account: Optional[SIAAccount] = field(
+        metadata=config(exclude=Exclude.ALWAYS), default=None  # type: ignore
+    )
+    sia_code: Optional[SIACode] = None
+
+    # Parse flags
+    _content_parsed: bool = False
+    _encrypted_content_decrypted: bool = False
+    _adm_parsed: bool = False
+    _sia_added: bool = False
+    _xdata_parsed: bool = False
+
+    @property
+    def valid_message(self) -> bool:
+        """Return True for OH and NAK Events."""
+        return True  # pragma: no cover
+
+    @property
+    def code_not_found(self) -> bool:
+        """Return True if there is no Code."""
+        return True if self.sia_code is None else False
+
+    @abstractproperty
+    def response(self) -> Optional[ResponseType]:
+        """Abstract method."""
+        pass  # pragma: no cover
+
+    @property
+    def valid_timestamp(self) -> bool:
+        """Return True for OH and NAK Events."""
+        return True  # pragma: no cover
+
+    @abstractmethod
+    def create_response(self) -> bytes:
+        """Abstract method."""
+        pass  # pragma: no cover
+
+    def set_sia_code(self) -> None:
+        """Return the SIA Code object, based on the code field."""
+        if self.code:  # pragma: no cover
+            self.sia_code = _load_sia_codes().get(self.code)  # pylint: disable=E1101
+            self._sia_added = True
+
+    def _get_crypter(self) -> Optional[CbcMode]:
+        """Give back a encrypter/decrypter."""
+        if not self.sia_account:
+            return None  # pragma: no cover
+        if not self.sia_account.key_b:
+            return None  # pragma: no cover
+        cypher = AES.new(self.sia_account.key_b, AES.MODE_CBC, IV)
+        if isinstance(cypher, CbcMode):
+            return cypher
+        return None  # pragma: no cover
+
+    @classmethod
+    def from_line(
+        cls, incoming: str, accounts: Optional[Dict[str, SIAAccount]] = None
+    ) -> SIAEvent:
+        """Create a Event from a line.
+
+        Arguments:
+            incoming {str} -- The line to be parsed.
+            accounts {List[SIAAccount]} -- accounts to check against, optional
+
+        Raises:
+            EventFormatError: If the event is not formatted according to SIA DC09 or ADM-CID.
+
+        """
+        line_match = MAIN_MATCHER.match(incoming)
+        if not line_match:
+            oh_event = OH_MATCHER.match(incoming)
+            if oh_event:
+                fields = oh_event.groupdict()
+                return OHEvent(
+                    full_message=incoming,
+                    msg_crc="",
+                    message_type=MessageTypes.OH,
+                    length=str(len(incoming)),
+                    encrypted=False,
+                    receiver=fields["receiver"],
+                    line=fields["line"],
+                    account=fields["account"],
+                    id=fields["id"],
+                )
+            raise EventFormatError(
+                "No matches found, event was not a SIA or ADM Spec event, line was: %s",
+                incoming,
+            )
+        main_content = line_match.groupdict()
+
+        encrypted = True if main_content["encrypted_flag"] else False
+        acc = main_content["account"]
+        sia_account = None
+        if accounts and acc:
+            sia_account = accounts.get(acc, None)
+
+        return SIAEvent(
+            full_message=incoming[8:],
+            msg_crc=main_content["crc"],
+            length=main_content["length"],
+            encrypted=encrypted,
+            message_type=main_content["message_type"],
+            sequence=main_content["sequence"],
+            receiver=main_content["receiver"],
+            line=main_content["line"],
+            account=acc,
+            content=main_content["rest"] if not encrypted else None,
+            encrypted_content=main_content["rest"] if encrypted else None,
+            sia_account=sia_account,
+        )
+
+    @staticmethod
+    def _get_timestamp() -> str:
+        """Create a timestamp in the right format."""
+        return datetime.utcnow().strftime("_%H:%M:%S,%m-%d-%Y")
+
+    @staticmethod
+    def _crc_calc(msg: Optional[str]) -> Optional[str]:
+        """Calculate the CRC of the msg."""
+        if msg is None:  # pragma: no cover
+            return None
+        crc = 0
+        for letter in str.encode(msg):
+            temp = letter
+            for _ in range(0, 8):
+                temp ^= crc & 1
+                crc >>= 1
+                if (temp & 1) != 0:
+                    crc ^= 0xA001
+                temp >>= 1
+        return ("%x" % crc).upper().zfill(4)
+
+
+@dataclass_json
+@dataclass
+class SIAEvent(BaseEvent):
+    """Class for SIAEvents."""
+
+    def __post_init__(self) -> None:
+        """Run post init logic."""
+        # Convert the message type to the enum
+        if isinstance(self.message_type, str):  # pragma: no cover
+            self.message_type = MessageTypes(self.message_type)
+
+        # Calculate the CRC of the message.
+        if not self.calc_crc:
+            self.calc_crc = self._crc_calc(self.full_message)
+        # If there is encrypted content and a key, decrypt
+        if (
+            self.sia_account
+            and self.sia_account.encrypted
+            and self.encrypted_content
+            and not self._encrypted_content_decrypted
+        ):
+            self.decrypt_content()
+        # If there is content (either after decrypting or directly) parse
+        if self.content and not self._content_parsed:
+            self.parse_content()
+        # If it is a ADM-CID message, map the qualifier and type to a code.
+        if (
+            self.message_type == MessageTypes.ADMCID
+            and self.event_qualifier is not None
+            and self.event_type is not None
+            and not self._adm_parsed
+        ):
+            self.parse_adm()
+        # If there is a code, map it to the full SIA Code spec.
+        if self.code and not self._sia_added:
+            self.set_sia_code()
+        # If there is x_data, parse it.
+        if self.x_data and not self._xdata_parsed:
+            self.parse_extended_data()  # pragma: no cover
+
+    @property
+    def response(self) -> Optional[ResponseType]:
+        """Get the responsetype."""
+        if not self.valid_message:
+            return None
+        if self.code_not_found:
+            return ResponseType.DUH
+        if not self.sia_account:
+            return ResponseType.NAK  # pragma: no cover
+        if not self.valid_timestamp:
+            return ResponseType.NAK
+        if self.extended_data is not None:  # pragma: no cover
+            if [x for x in self.extended_data if x.identifier in RSP_XDATA]:
+                return ResponseType.RSP
+        return ResponseType.ACK
+
+    @property
+    def sia_string(self) -> str:  # pragma: no cover
+        """Create a string with the SIA codes and some other fields."""
+        if self.sia_code:
+            return f"Code: {self.code}, Type: {self.sia_code.type}, \
+            Description: {self.sia_code.description}"
+        return f"Code: {self.code}"
+
+    @property
+    def valid_length(self) -> bool:
+        """Check if the length of the message is the same in the message and supplied. Will not throw an error if not correct."""
+        if self.length is None or self.full_message is None:  # pragma: no cover
+            return True
+        return int(self.length) == int(
+            str(len(self.full_message)), 16
+        )  # pragma: no cover
+
+    @property
+    def valid_message(self) -> bool:
+        """Check the validity of the message by comparing the sent CRC with the calculated CRC."""
+        return self.msg_crc == self.calc_crc
+
+    @property
+    def valid_timestamp(self) -> bool:
+        """Check if the timestamp is within bounds."""
+        if not self.sia_account:  # pragma: no cover
+            return True
+        if self.sia_account.allowed_timeband is None:  # pragma: no cover
+            return True
+        if self.timestamp:
+            current_time = datetime.now(timezone.utc)
+            current_min = current_time - timedelta(
+                seconds=self.sia_account.allowed_timeband[0]
+            )
+            current_plus = current_time + timedelta(
+                seconds=self.sia_account.allowed_timeband[1]
+            )
+            return current_min <= self.timestamp <= current_plus
+        return True  # pragma: no cover
+
+    def create_response(self) -> bytes:
+        """Create a response message, based on account, event and response type.
+
+        Returns:
+            bytes -- Response to send back to sender.
+
+        """
+        response_type = self.response
+        x_data = None
+        if response_type is None:
+            return b"\n\r"
+        if not self.sia_account:
+            return f'"{response_type.value}"'.encode("ascii")
+        if (
+            self.extended_data
+            and [x for x in self.extended_data if x.identifier == "K"] is not None
+            and self.sia_account.key is not None
+        ):
+            x_data = f"[K{self.sia_account.key}]"
+        if not self.encrypted or response_type == ResponseType.DUH:
+            res = f'"{response_type.value}"{self.sequence}R{self.receiver}L{self.line}#{self.account}[]{x_data if x_data else ""}'
+        else:
+            encrypted_content = self.encrypt_content(
+                f']{x_data if x_data else ""}{self._get_timestamp()}'
+            )
+            res = f'"*{response_type.value}"{self.sequence}R{self.receiver}L{self.line}#{self.account}[{encrypted_content}'
+        header = ("%04x" % len(res)).upper()
+        return f"\n{self._crc_calc(res)}{header}{res}\r".encode("ascii")
+
+    def decrypt_content(self) -> None:
+        """Decrypt the content, if the account is encrypted, otherwise pass back the event."""
+        if not self.encrypted_content:  # pragma: no cover
+            return None
+        decr = self._get_crypter()
+        if not decr:
+            return None  # pragma: no cover
+        self.content = decr.decrypt(bytes.fromhex(self.encrypted_content)).decode(
+            "ascii", "ignore"
+        )
+        self._encrypted_content_decrypted = True
+
+    def encrypt_content(self, message: str) -> Optional[str]:
+        """Encrypt a string.
+
+        Arguments:
+            message {str} -- String to encrypt.
+
+        Returns:
+            str -- Encrypted string, if encrypted account.
+
+        """
+        encr = self._get_crypter()
+        if not encr:
+            return None  # pragma: no cover
+        fill_size = len(message) + 16 - len(message) % 16
+        return encr.encrypt(message.zfill(fill_size).encode("ascii")).hex().upper()
+
+    def parse_adm(self) -> None:
+        """Parse the event qualifier and type for ADM messages."""
+        if self.event_qualifier and self.event_type:  # pragma: no cover
+            sub_map = _load_adm_mapping().get(self.event_type, None)
+            if sub_map:
+                self.code = sub_map.get(self.event_qualifier, None)
+        self._adm_parsed = True
+
+    def parse_content(self) -> None:
+        """Set the internal content field and also parse the content and store the right things."""
+        if self.message_type is None or self.encrypted is None:  # pragma: no cover
+            return
+        matcher = _get_matcher(self.message_type, self.encrypted)
+        matches = matcher.match(self.content)
+        if not matches:
+            raise EventFormatError(
+                "Parse content: no matches found in %s, using matcher: %s",
+                self.content,
+                matcher,
+            )
+        content = matches.groupdict()
+        _LOGGER.debug("Content matches: %s", content)
+        # Parse specific fields per message type
+        if self.message_type == MessageTypes.ADMCID:
+            self.partition = content["partition"]
+            self.event_qualifier = content["event_qualifier"]
+            self.event_type = content["event_type"]
+        else:
+            self.code = content["code"]
+            self.ti = content["ti"]
+            self.id = content["id"]
+            self.message = content["message"]
+
+        # Parse generic fields
+        if not self.account:
+            self.account = content["account"]
+        self.ri = content["ri"]
+        self.x_data = content["xdata"]
+        if content["timestamp"]:
+            try:
+                ts = datetime.strptime(content["timestamp"], "%H:%M:%S,%m-%d-%Y")
+                self.timestamp = ts.replace(tzinfo=timezone.utc)
+            except ValueError:
+                _LOGGER.warning(
+                    "Timestamp could not be parsed as a timestamp: %s",
+                    content["timestamp"],
+                )
+        if self.message_type == MessageTypes.NULL and self.code_not_found:
+            self.code = "RP"
+            self.ri = "0"
+        self._content_parsed = True
+
+    def parse_extended_data(self) -> None:
+        """Set extended data."""
+        if self.x_data is None:  # pragma: no cover
+            return
+        x_data_list = self.x_data.split("][")
+        self.extended_data = []
+        for xd in x_data_list:  # pragma: no cover
+            xdata = _load_xdata().get(xd[0], None)
+            if xdata:
+                xdata.value = xd[1:]
+                self.extended_data.append(xdata)
+        self._xdata_parsed = True
+
+    def sia_account_from_message(self) -> Optional[SIAAccount]:  # pragma: no cover
+        """Return the SIA Account, if there is not account added, create one based on the account in the message."""
+        if self.account is not None:
+            return SIAAccount(self.account)
+        return None
+
+    def __str__(self) -> str:
+        """Return the event as a string."""
+        return f"\
+Content: {self.content}, \
+Zone (ri): {self.ri}, \
+Code: {self.code}, \
+Message: {self.message if self.message else ''}, \
+Account: {self.account}, \
+Receiver: {self.receiver}, \
+Line: {self.line}, \
+Timestamp: {self.timestamp}, \
+Length: {self.length}, \
+Sequence: {self.sequence}, \
+CRC: {self.msg_crc}, \
+Calc CRC: {self.calc_crc}, \
+Encrypted Content: {self.encrypted_content}, \
+Full Message: {self.full_message}."
+
+
+@dataclass_json
+@dataclass
+class OHEvent(SIAEvent):
+    """Class for OH events."""
+
+    code: str = "RP"
+
+    def __post_init__(self) -> None:
+        """If there is a code, map it to the full SIA Code spec."""
+        if isinstance(self.message_type, str):  # pragma: no cover
+            self.message_type = MessageTypes(self.message_type)
+
+        if not self._sia_added:  # pragma: no cover
+            self.set_sia_code()
+
+    @property
+    def response(self) -> ResponseType:
+        """Return ACK for OH Events."""
+        return ResponseType.ACK  # pragma: no cover
+
+    def create_response(self) -> bytes:
+        """Create a response message, based on account, event and response type.
+
+        Returns:
+            str -- Response to send back to sender.
+
+        """
+        return '"ACK"'.encode("ascii")  # pragma: no cover
+
+
+@dataclass_json
+@dataclass
+class NAKEvent(BaseEvent):
+    """Class for NAK Events."""
+
+    def __post_init__(self) -> None:
+        """Run post init work."""
+        if isinstance(self.message_type, str):  # pragma: no cover
+            self.message_type = MessageTypes(self.message_type)
+
+    @property
+    def response(self) -> ResponseType:
+        """Return NAK for NAK Events."""
+        return ResponseType.NAK
+
+    def create_response(self) -> bytes:
+        """Create a response message, based on account, event and response type.
+
+        Returns:
+            str -- Response to send back to sender.
+
+        """
+        res = f'"NAK"0000L0R0A0[]{self._get_timestamp()}'
+        header = ("%04x" % len(res)).upper()
+        return f"\n{self._crc_calc(res)}{header}{res}\r".encode("ascii")
diff --git a/src/pysiaalarm/sia_account.py b/src/pysiaalarm/sia_account.py
deleted file mode 100644
index 337d237..0000000
--- a/src/pysiaalarm/sia_account.py
+++ /dev/null
@@ -1,206 +0,0 @@
-"""Class for SIA Accounts."""
-import logging
-from datetime import datetime
-from enum import Enum
-from typing import Tuple
-
-from Crypto import Random
-from Crypto.Cipher import AES
-
-from . import __author__, __copyright__, __license__, __version__
-from .sia_errors import (
-    InvalidAccountFormatError,
-    InvalidAccountLengthError,
-    InvalidKeyFormatError,
-    InvalidKeyLengthError,
-)
-from .sia_event import SIAEvent
-
-_LOGGER = logging.getLogger(__name__)
-
-IV = bytes.fromhex("00000000000000000000000000000000")
-
-
-class SIAResponseType(Enum):
-    """Class with response types for events."""
-
-    ACK = 1
-    DUH = 2
-    NAK = 3
-
-
-def _create_padded_message(message: str) -> str:
-    """Create padded message, used for encrypted responses."""
-    fill_size = len(message) + 16 - len(message) % 16
-    return message.zfill(fill_size)
-
-
-def _get_timestamp() -> str:
-    """Create a timestamp in the right format."""
-    return datetime.utcnow().strftime("_%H:%M:%S,%m-%d-%Y")
-
-
-class SIAAccount:
-    """Class for SIA Accounts."""
-
-    def __init__(
-        self,
-        account_id: str,
-        key: str = None,
-        allowed_timeband: Tuple[int, int] = (40, 20),
-    ):
-        """Create a SIA Account.
-
-        Arguments:
-            account_id {str} -- The account id specified by the alarm system, should be 3-16 characters hexadecimal.
-
-        Keyword Arguments:
-            key {str} -- The encryption key specified by the alarm system, should be 16,24 or 32 characters hexadecimal. (default: {None})
-            allowed_timeband {Tuple[int, int]} -- Seconds before and after current time that a message is valid in, unencrypted messages do not have to have a timestamp.
-
-        """
-        SIAAccount.validate_account(account_id, key)
-        self.account_id = account_id
-        self.key = key.encode("utf-8") if key else key
-        self.allowed_timeband = allowed_timeband
-        self.encrypted = True if key else False
-        # if self.encrypted:
-        #     self.decrypter = AES.new(self.key, AES.MODE_CBC, IV)
-        #     self.encrypter = AES.new(self.key, AES.MODE_CBC, IV)
-
-    def _get_crypter(self):
-        """Give back a encrypter/decrypter."""
-        return AES.new(self.key, AES.MODE_CBC, IV)
-
-    def encrypt(self, message: str) -> str:
-        """Encrypt a string, usually used for endings.
-
-        Arguments:
-            message {str} -- String to encrypt.
-
-        Returns:
-            str -- Encrypted string, if encrypted account.
-
-        """
-        if self.encrypted and message:
-            encr = self._get_crypter()
-            # _LOGGER.debug(
-            #     "Encrypt with New AES: %s",
-            #     encr.encrypt(_create_padded_message(message).encode("ascii"))
-            #     .hex()
-            #     .upper(),
-            # )
-            # _LOGGER.debug(
-            #     "Encrypt with Reused AES: %s",
-            #     self.encrypter.encrypt(_create_padded_message(message).encode("ascii"))
-            #     .hex()
-            #     .upper(),
-            # )
-            return (
-                encr.encrypt(_create_padded_message(message).encode("ascii"))
-                .hex()
-                .upper()
-            )
-        else:
-            return message
-
-    def decrypt(self, event: SIAEvent) -> SIAEvent:
-        """Decrypt the event, if the account is encrypted, otherwise pass back the event.
-
-        Arguments:
-            event {SIAEvent} -- Event to be decrypted.
-
-        Returns:
-            SIAEvent -- Event decrypted, with parsed fields.
-
-        """
-        if self.encrypted and event.encrypted_content:
-            decr = self._get_crypter()
-            # _LOGGER.debug(
-            #     "Decrypt with New AES: %s",
-            #     decr.decrypt(bytes.fromhex(event.encrypted_content)).decode(
-            #         "ascii", "ignore"
-            #     ),
-            # )
-            # _LOGGER.debug(
-            #     "Decrypt with Reused AES: %s",
-            #     self.decrypter.decrypt(bytes.fromhex(event.encrypted_content)).decode(
-            #         "ascii", "ignore"
-            #     ),
-            # )
-            event.content = decr.decrypt(bytes.fromhex(event.encrypted_content)).decode(
-                "ascii", "ignore"
-            )
-        return event
-
-    def create_response(self, event: SIAEvent, response_type: SIAResponseType) -> str:
-        """Create a response message, based on account, event and response type.
-
-        Arguments:
-            event {SIAEvent} -- Event to respond to.
-            response_type {SIAResponseType} -- Response type.
-
-        Returns:
-            str -- Response to send back to sender.
-
-        """
-        if response_type == SIAResponseType.ACK and event:
-            if self.encrypted:
-                res = f'"*ACK"{event.sequence}L0#{event.account}[{self.encrypt("]"+_get_timestamp())}'
-            else:
-                res = f'"ACK"{event.sequence}L0#{event.account}[]'
-        elif response_type == SIAResponseType.DUH and event:
-            res = f'"DUH"{event.sequence}L0#{event.account}[]'
-        elif response_type == SIAResponseType.NAK:
-            res = f'"NAK"0000L0R0A0[]{_get_timestamp()}'
-        elif not response_type:
-            return b"\n\r"
-        else:
-            _LOGGER.warning(
-                "Could not find the right response message for response type: %s and optional event: %s",
-                response_type,
-                event,
-            )
-            return b"\n\r"
-
-        header = ("%04x" % len(res)).upper()
-        return f"\n{SIAEvent.crc_calc(res)}{header}{res}\r".encode("ascii")
-
-    @classmethod
-    def validate_account(cls, account_id: str = None, key: str = None):
-        """Validate a accounts information, can be used before creating as well. Can be used to check the individual arguments as well.
-
-        Keyword Arguments:
-            account_id {str} -- The account id specified by the alarm system, should be 3-16 characters hexadecimal. (default: {None})
-            key {str} -- The encryption key specified by the alarm system, should be 16,24 or 32 characters hexadecimal. (default: {None})
-
-        Raises:
-            InvalidKeyFormatError: If the key is not a valid hexadecimal string.
-            InvalidKeyLengthError: If the key does not have 16, 24 or 32 characters.
-            InvalidAccountFormatError: If the account id is not a valid hexadecimal string.
-            InvalidAccountLengthError: If the account id does not have between 3 and 16 characters.
-
-        Returns:
-            bool, error -- Returns True if all was good, an error is raised otherwise.
-
-        """
-        if key:
-            try:
-                int(key, 16)
-            except ValueError:
-                raise InvalidKeyFormatError
-            try:
-                assert len(key) in (16, 24, 32)
-            except AssertionError:
-                raise InvalidKeyLengthError
-        if account_id:
-            try:
-                int(account_id, 16)
-            except ValueError:
-                raise InvalidAccountFormatError
-            try:
-                assert 3 <= len(account_id) <= 16
-            except AssertionError:
-                raise InvalidAccountLengthError
-
-        return True
diff --git a/src/pysiaalarm/sia_client.py b/src/pysiaalarm/sia_client.py
deleted file mode 100644
index f5e735f..0000000
--- a/src/pysiaalarm/sia_client.py
+++ /dev/null
@@ -1,71 +0,0 @@
-# -*- coding: utf-8 -*-
-"""This is a the main class for the SIA Client."""
-import asyncio
-import logging
-import socket
-from threading import Thread
-from typing import Callable, Coroutine, List, Union
-
-from . import __author__, __copyright__, __license__, __version__
-from .base_sia_client import BaseSIAClient
-from .sia_account import SIAAccount
-from .sia_event import SIAEvent
-from .sia_server import SIAServer
-
-_LOGGER = logging.getLogger(__name__)
-
-
-class SIAClient(Thread, BaseSIAClient):
-    """Class for Sync SIA Client."""
-
-    def __init__(
-        self,
-        host: str,
-        port: int,
-        accounts: List[SIAAccount],
-        function: Callable[[SIAEvent], None],
-    ):
-        """Create the threaded SIA Client object.
-
-        Arguments:
-            host {str} -- Host to run the server on, usually would be ""
-            port {int} -- The port the server listens to.
-            accounts {List[SIAAccount]} -- List of SIA Accounts to add.
-            function {Callable[[SIAEvent], None]} -- The function that gets called for each event.
-
-        """
-        if asyncio.iscoroutinefunction(function):
-            raise TypeError(
-                "Asyncio coroutines as the function are not supported, please use the aio version of the SIAClient for that."
-            )
-        Thread.__init__(self)
-        BaseSIAClient.__init__(self, host, port, accounts, function)
-        self.sia_server = SIAServer(
-            (self._host, self._port), self._accounts, self._func, self._counts
-        )
-
-    def __enter__(self):
-        """Start with as context manager."""
-        self.start()
-        return self
-
-    def __exit__(self, type, value, tb):
-        """End as context manager."""
-        self.stop()
-
-    def start(self):
-        """Start the SIA TCP Handler thread."""
-        _LOGGER.debug("Starting SIA.")
-        self.server_thread = Thread(
-            target=self.sia_server.serve_forever, name="SIAServerThread"
-        )
-        self.server_thread.daemon = True
-        self.server_thread.start()
-
-    def stop(self):
-        """Stop the SIA TCP Handler thread."""
-        _LOGGER.debug("Stopping SIA.")
-        self.sia_server.shutdown_flag = True
-        self.sia_server.shutdown()
-        self.sia_server.server_close()
-        self.server_thread.join()
diff --git a/src/pysiaalarm/sia_event.py b/src/pysiaalarm/sia_event.py
deleted file mode 100644
index e91eb1e..0000000
--- a/src/pysiaalarm/sia_event.py
+++ /dev/null
@@ -1,217 +0,0 @@
-# -*- coding: utf-8 -*-
-"""This is a class for SIA Events."""
-import logging
-import re
-from datetime import datetime, timedelta
-
-from . import __author__, __copyright__, __license__, __version__
-from .sia_const import ALL_CODES
-from .sia_errors import EventFormatError
-
-_LOGGER = logging.getLogger(__name__)
-
-main_regex = r"""
-(?P<crc>[A-F0-9]{4})
-(?P<length>[A-F0-9]{4})\"
-(?P<encrypted_flag>[*])?
-(?P<message_type>SIA-DCS|NULL)\"
-(?P<sequence>[0-9]{4})
-(?P<receiver>R[A-F0-9]{1,6})?
-(?P<prefix>L[A-F0-9]{1,6})
-[#]?(?P<account>[A-F0-9]{3,16})?
-[\[]
-(?P<rest>.*)
-"""
-MAIN_MATCHER = re.compile(main_regex, re.X)
-
-content_regex = r"""
-[#]?(?P<account>[A-F0-9]{3,16})?
-[|]?
-(?:Nri)?
-(?P<zone>\d*)?
-\/?
-(?P<code>[a-zA-z]{2})?
-(?P<message>.*)
-[\]]
-[_]?
-(?P<timestamp>[0-9:,-]*)?
-"""
-CONTENT_MATCHER = re.compile(content_regex, re.X)
-
-encr_content_regex = r"""
-(?:[^\|\[\]]*)
-[|]?
-[#]?(?P<account>[A-F0-9]{3,16})?
-[|]?
-(?:.*Nri)?
-(?P<zone>\d*)?
-\/?
-(?P<code>[a-zA-z]{2})?
-(?P<message>.*)
-[\]][_]?
-(?P<timestamp>[0-9:,-]*)?
-"""
-ENCR_CONTENT_MATCHER = re.compile(encr_content_regex, re.X)
-
-
-class SIAEvent:
-    """Class for SIAEvents."""
-
-    def __init__(self, line: str):
-        """Create a SIA Event from a line.
-
-        Arguments:
-            line {str} -- The line to be parsed.
-
-        Raises:
-            EventFormatError: If the event is not formatted according to SIA DC09.
-
-        """
-        line_match = MAIN_MATCHER.match(line)
-        if not line_match:
-            raise EventFormatError(
-                "No matches found, event was not a SIA Spec event, line was: %s", line
-            )
-        main_content = line_match.groupdict()
-
-        self.type = None
-        self.description = None
-        self.concerns = None
-        self.timestamp = None
-        self._code = None
-        self.zone = None
-        self.type = None
-        self.description = None
-        self.concerns = None
-        self.code_not_found = True
-        self._content = None
-        self.message = None
-        self.msg_crc = main_content["crc"]
-        self.length = main_content["length"]
-        self.encrypted = True if main_content["encrypted_flag"] else False
-        self.message_type = main_content["message_type"]
-        self.sequence = main_content["sequence"]
-        self.receiver = main_content["receiver"]
-        self.prefix = main_content["prefix"]
-        self.account = main_content["account"]
-        if self.encrypted:
-            self.encrypted_content = main_content["rest"]
-        else:
-            self.encrypted_content = None
-            self.content = main_content["rest"]
-        self.full_message = line[8:]
-        self.calc_crc = SIAEvent.crc_calc(self.full_message)
-
-    @property
-    def content(self):
-        """Return the content field."""
-        return self._content
-
-    @content.setter
-    def content(self, new):
-        """Set the content and parse it."""
-        self._content = new
-        if self.encrypted:
-            matches = ENCR_CONTENT_MATCHER.match(self._content)
-        else:
-            matches = CONTENT_MATCHER.match(self._content)
-        if not matches:
-            raise EventFormatError(
-                "Parse content: no matches found in %s", self._content
-            )
-        content = matches.groupdict()
-        if not self.account:
-            self.account = content["account"]
-        self.zone = content["zone"]
-        self.code = content["code"]
-        self.message = content["message"]
-        self.timestamp = (
-            datetime.strptime(content["timestamp"], "%H:%M:%S,%m-%d-%Y")
-            if content["timestamp"]
-            else None
-        )
-        if self.message_type == "NULL" and not self.code:
-            self.code = "RP"
-            self.zone = 0
-
-    @property
-    def code(self):
-        """Return the code field."""
-        return self._code
-
-    @code.setter
-    def code(self, new):
-        """Set self.code and get the related fields for a code."""
-        self._code = new
-        full = ALL_CODES.get(self._code, None)
-        if full:
-            self.type = full.get("type")
-            self.description = full.get("description")
-            self.concerns = full.get("concerns")
-            self.code_not_found = False
-        else:
-            self.code_not_found = True
-
-    def valid_timestamp(self, allowed_timeband) -> bool:
-        """Check if the timestamp is within bounds."""
-        if not allowed_timeband[0]:
-            return True
-        if self.timestamp:
-            current_time = datetime.utcnow()
-            current_min = current_time - timedelta(seconds=allowed_timeband[0])
-            current_plus = current_time + timedelta(seconds=allowed_timeband[1])
-            return current_min <= self.timestamp <= current_plus
-        else:
-            return True
-
-    @classmethod
-    def crc_calc(cls, msg: str) -> str:
-        """Calculate the CRC of the events."""
-        crc = 0
-        for letter in str.encode(msg):
-            temp = letter
-            for _ in range(0, 8):
-                temp ^= crc & 1
-                crc >>= 1
-                if (temp & 1) != 0:
-                    crc ^= 0xA001
-                temp >>= 1
-        return ("%x" % crc).upper().zfill(4)
-
-    @property
-    def valid_message(self) -> bool:
-        """Check the validity of the message by comparing the sent CRC with the calculated CRC."""
-        return self.msg_crc == self.calc_crc
-
-    @property
-    def valid_length(self) -> bool:
-        """Check if the length of the message is the same in the message and supplied. Will not throw an error if not correct."""
-        return int(self.length) == int(str(len(self.full_message)), 16)
-
-    @property
-    def sia_string(self) -> str:
-        """Create a string with the SIA codes and some other fields."""
-        return f"Code: {self.code}, Type: {self.type}, \
-            Description: {self.description}"
-
-    def __str__(self):
-        """Return a string of a event."""
-        return f"\
-Content: {self.content}, \
-Zone: {self.zone}, \
-Code: {self.code}, \
-Message: {self.message if self.message else ''}, \
-Concerns: {self.concerns}, \
-Type: {self.type}, \
-Description: {self.description}, \
-Account: {self.account}, \
-Receiver: {self.receiver}, \
-Prefix: {self.prefix}, \
-Timestamp: {self.timestamp}, \
-Length: {self.length}, \
-Sequence: {self.sequence}, \
-CRC: {self.msg_crc}, \
-Calc CRC: {self.calc_crc}, \
-Message type: {self.type}, \
-Encrypted Content: {self.encrypted_content}, \
-Full Message: {self.full_message}."
diff --git a/src/pysiaalarm/sia_server.py b/src/pysiaalarm/sia_server.py
deleted file mode 100644
index 957610f..0000000
--- a/src/pysiaalarm/sia_server.py
+++ /dev/null
@@ -1,91 +0,0 @@
-"""This is the class for the actual TCP handler override of the handle method."""
-import logging
-from socketserver import BaseRequestHandler, ThreadingTCPServer
-from typing import Callable, Dict, Tuple
-
-from . import __author__, __copyright__, __license__, __version__
-from .base_sia_server import BaseSIAServer
-from .sia_account import SIAAccount
-from .sia_account import SIAResponseType as resp
-from .sia_event import SIAEvent
-
-_LOGGER = logging.getLogger(__name__)
-
-
-class SIAServer(ThreadingTCPServer, BaseSIAServer):
-    """Class for a threaded SIA Server."""
-
-    daemon_threads = True
-    allow_reuse_address = True
-
-    def __init__(
-        self,
-        server_address: Tuple[str, int],
-        accounts: Dict[str, SIAAccount],
-        func: Callable[[SIAEvent], None],
-        counts: Dict,
-    ):
-        """Create a SIA Server.
-
-        Arguments:
-            server_address Tuple[string, int] -- the address the server should listen on.
-            accounts Dict[str, SIAAccount] -- accounts as dict with account_id as key, SIAAccount object as value.
-            func Callable[[SIAEvent], None] -- Function called for each valid SIA event, that can be matched to a account.
-            counts Dict -- counter kept by client to give insights in how many errorous events were discarded of each type.
-
-        """
-        ThreadingTCPServer.__init__(self, server_address, SIATCPHandler)
-        BaseSIAServer.__init__(self, accounts, func, counts)
-
-
-class SIATCPHandler(BaseRequestHandler):
-    """Class for TCP Handling."""
-
-    _received_data = "".encode()
-
-    def handle(self):
-        """Overwritten method for the RequestHandler."""
-        while True and not self.server.shutdown_flag:
-            raw = self.request.recv(1024)
-            if not raw:
-                break
-            raw = bytearray(raw)
-            while len(raw) > 0:  # True and not self.server.shutdown_flag:
-                splitter = raw.find(b"\r")
-                if splitter == -1:
-                    line = raw
-                    raw = ""
-                    # break
-                else:
-                    line = raw[1:splitter]
-                    raw = raw[splitter + 1 :]
-                decoded_line = line.decode("ascii")
-                _LOGGER.debug("Incoming line: %s", decoded_line)
-                self.server.counts["events"] = self.server.counts["events"] + 1
-                event, account, response = self.server.parse_and_check_event(
-                    decoded_line
-                )
-                try:
-                    self.request.sendall(account.create_response(event, response))
-                except Exception as exp:
-                    _LOGGER.warning(
-                        "Exception caught while responding to event: %s, exception: %s",
-                        event,
-                        exp,
-                    )
-                # check for event and if the response is acknowledge, which means the event is valid.
-                if event and response == resp.ACK:
-                    self.server.counts["valid_events"] = (
-                        self.server.counts["valid_events"] + 1
-                    )
-                    try:
-                        self.server.func(event)
-                    except Exception as exp:
-                        _LOGGER.warning(
-                            "Last event: %s, gave error in user function: %s.",
-                            event,
-                            exp,
-                        )
-                        self.server.counts["errors"]["user_code"] = (
-                            self.server.counts["errors"]["user_code"] + 1
-                        )
diff --git a/src/pysiaalarm/sync/__init__.py b/src/pysiaalarm/sync/__init__.py
new file mode 100644
index 0000000..7f3c4c6
--- /dev/null
+++ b/src/pysiaalarm/sync/__init__.py
@@ -0,0 +1,1 @@
+"""Init file for sync folder."""
\ No newline at end of file
diff --git a/src/pysiaalarm/sync/client.py b/src/pysiaalarm/sync/client.py
new file mode 100644
index 0000000..f6b2e92
--- /dev/null
+++ b/src/pysiaalarm/sync/client.py
@@ -0,0 +1,96 @@
+# -*- coding: utf-8 -*-
+"""This is a the main class for the SIA Client."""
+from __future__ import annotations
+
+import asyncio
+import logging
+from threading import Thread
+from types import TracebackType
+from typing import Any, Callable, List, Optional, Type
+
+from ..account import SIAAccount
+from ..base_client import BaseSIAClient
+from ..event import SIAEvent
+from ..utils import CommunicationsProtocol
+from .server import SIATCPServer, SIAUDPServer
+
+_LOGGER = logging.getLogger(__name__)
+
+
+class SIAClient(Thread, BaseSIAClient):
+    """Class for Sync SIA Client."""
+
+    def __init__(
+        self,
+        host: str,
+        port: int,
+        accounts: List[SIAAccount],
+        function: Callable[[SIAEvent], None],
+        protocol: CommunicationsProtocol = CommunicationsProtocol.TCP,
+    ):
+        """Create the threaded SIA Client object.
+
+        Arguments:
+            host {str} -- Host to run the server on, usually would be ""
+            port {int} -- The port the server listens to.
+            accounts {List[SIAAccount]} -- List of SIA Accounts to add.
+            function {Callable[[SIAEvent], None]} -- The function that gets called for each event.
+            protocol {CommunicationsProtocol Enum} -- CommunicationsProtocol to use, TCP or UDP.
+
+        """
+        if asyncio.iscoroutinefunction(function):
+            raise TypeError(
+                "Asyncio coroutines as the function are not supported, please use the aio version of the SIAClient for that."
+            )
+        Thread.__init__(self)
+        BaseSIAClient.__init__(self, host, port, accounts, function, protocol)
+        self.set_tcp_server() if self.protocol == CommunicationsProtocol.TCP else self.set_udp_server()
+
+    def set_tcp_server(self) -> None:
+        """Set the sia server to a TCP server."""
+        self.sia_server = SIATCPServer(  # type: ignore
+            (self._host, self._port), self._accounts, self._func, self._counts  # type: ignore
+        )
+
+    def set_udp_server(self) -> None:
+        """Set the sia server to a UDP server."""
+        self.sia_server = SIAUDPServer(  # type: ignore
+            (self._host, self._port), self._accounts, self._func, self._counts  # type: ignore
+        )
+
+    def __enter__(self) -> SIAClient:
+        """Start with as context manager."""
+        self.start()
+        return self
+
+    def __exit__(
+        self,
+        exc_type: Optional[Type[BaseException]],
+        exc_value: Optional[BaseException],
+        traceback: Optional[TracebackType],
+    ) -> Optional[bool]:
+        """End as context manager."""
+        self.stop()
+        return True
+
+    def start(self, **kwargs: Any) -> None:
+        """Start the SIA Handler thread."""
+        _LOGGER.debug("Starting SIA.")
+        if self.sia_server is not None:  # pragma: no cover
+            self.server_thread = Thread(
+                target=self.sia_server.serve_forever,
+                name="SIAServerThread",
+                kwargs=kwargs,
+            )
+            self.server_thread.daemon = True
+            self.server_thread.start()
+
+    def stop(self) -> None:
+        """Stop the SIA TCP Handler thread."""
+        _LOGGER.debug("Stopping SIA.")
+        if self.sia_server is not None:  # pragma: no cover
+            self.sia_server.shutdown_flag = True
+            self.sia_server.shutdown()
+            self.sia_server.server_close()
+        if self.server_thread is not None:  # pragma: no cover
+            self.server_thread.join()
diff --git a/src/pysiaalarm/sync/handler.py b/src/pysiaalarm/sync/handler.py
new file mode 100644
index 0000000..6d05fd2
--- /dev/null
+++ b/src/pysiaalarm/sync/handler.py
@@ -0,0 +1,86 @@
+"""This is the class for the actual TCP handler override of the handle method."""
+
+import logging
+from socketserver import BaseRequestHandler
+
+from ..event import SIAEvent
+from ..utils import ResponseType
+
+_LOGGER = logging.getLogger(__name__)
+
+
+class BaseSIAHandler(BaseRequestHandler):
+    """Base case for Request handling."""
+
+    _received_data = "".encode()
+
+    def handle_raw_line(self, raw: bytes) -> None:
+        """Handle the line."""
+        while len(raw) > 0:
+            splitter = raw.find(b"\r")
+            if splitter == -1:
+                line = raw
+                raw = b""
+            else:  # pragma: no cover
+                line = raw[1:splitter]
+                raw = raw[splitter + 1 :]
+            decoded_line = line.decode("ascii", errors="ignore")
+            event = self.server.parse_and_check_event(decoded_line)  # type: ignore
+            self.respond(event)
+            if event and event.response == ResponseType.ACK:
+                self.server.func_wrap(event)  # type: ignore
+
+    def respond(self, event: SIAEvent) -> None:
+        """Abstract method for responding."""
+        pass  # pragma: no cover
+
+
+class SIATCPHandler(BaseSIAHandler):
+    """Class for TCP Handling."""
+
+    def handle(self) -> None:
+        """Overwritten method for the RequestHandler."""
+        while True and not self.server.shutdown_flag:  # type: ignore # pragma: no cover
+            raw = self.request.recv(1024)
+            if not raw:
+                break
+            raw = bytearray(raw)
+            self.handle_raw_line(raw)
+
+    def respond(self, event: SIAEvent) -> None:
+        """Respond to the event."""
+        try:
+            self.request.sendall(event.create_response())
+        except Exception as exp:  # pragma: no cover
+            _LOGGER.error(
+                "Exception caught while responding to event: %s, exception: %s",
+                event.create_response(),
+                exp,
+            )
+
+
+class SIAUDPHandler(BaseSIAHandler):
+    """Class for UDP Handling."""
+
+    def handle(self) -> None:
+        """Overwritten method for the RequestHandler."""
+        if not self.server.shutdown_flag:  # type: ignore # pragma: no cover
+            raw = self.request[0]
+            # socket = self.request[1]
+            if raw:
+                raw = bytearray(raw)
+                self.handle_raw_line(raw)
+
+    def respond(self, event: SIAEvent) -> None:
+        """Respond to the event."""
+        try:
+            self.request[1].sendto(
+                event.create_response(),
+                self.client_address,
+            )
+        except Exception as exp:  # pragma: no cover
+            _LOGGER.error(
+                "Exception caught while responding to event: %s, exception: %s",
+                event.create_response(),
+                exp,
+            )
diff --git a/src/pysiaalarm/sync/server.py b/src/pysiaalarm/sync/server.py
new file mode 100644
index 0000000..5db7c87
--- /dev/null
+++ b/src/pysiaalarm/sync/server.py
@@ -0,0 +1,65 @@
+"""This is the class for the actual TCP handler override of the handle method."""
+
+import logging
+from socketserver import ThreadingTCPServer, ThreadingUDPServer
+from typing import Callable, Dict, Tuple
+
+from ..account import SIAAccount
+from ..base_server import BaseSIAServer
+from ..event import SIAEvent
+from ..utils import Counter
+from .handler import SIATCPHandler, SIAUDPHandler
+
+_LOGGER = logging.getLogger(__name__)
+
+
+class SIATCPServer(ThreadingTCPServer, BaseSIAServer):
+    """Class for a threaded SIA TCP Server."""
+
+    daemon_threads = True
+    allow_reuse_address = True
+
+    def __init__(
+        self,
+        server_address: Tuple[str, int],
+        accounts: Dict[str, SIAAccount],
+        func: Callable[[SIAEvent], None],
+        counts: Counter,
+    ):
+        """Create a SIA TCP Server.
+
+        Arguments:
+            server_address Tuple[string, int] -- the address the server should listen on.
+            accounts Dict[str, SIAAccount] -- accounts as dict with account_id as key, SIAAccount object as value.
+            func Callable[[SIAEvent], None] -- Function called for each valid SIA event, that can be matched to a account.
+            counts Counter -- counter kept by client to give insights in how many errorous events were discarded of each type.
+
+        """
+        ThreadingTCPServer.__init__(self, server_address, SIATCPHandler)
+        BaseSIAServer.__init__(self, accounts, func, counts)
+
+
+class SIAUDPServer(ThreadingUDPServer, BaseSIAServer):
+    """Class for a threaded SIA UDP Server."""
+
+    daemon_threads = True
+    allow_reuse_address = True
+
+    def __init__(
+        self,
+        server_address: Tuple[str, int],
+        accounts: Dict[str, SIAAccount],
+        func: Callable[[SIAEvent], None],
+        counts: Counter,
+    ):
+        """Create a SIA UDP Server.
+
+        Arguments:
+            server_address Tuple[string, int] -- the address the server should listen on.
+            accounts Dict[str, SIAAccount] -- accounts as dict with account_id as key, SIAAccount object as value.
+            func Callable[[SIAEvent], None] -- Function called for each valid SIA event, that can be matched to a account.
+            counts Counter -- counter kept by client to give insights in how many errorous events were discarded of each type.
+
+        """
+        ThreadingUDPServer.__init__(self, server_address, SIAUDPHandler)
+        BaseSIAServer.__init__(self, accounts, func, counts)
diff --git a/src/pysiaalarm/utils/__init__.py b/src/pysiaalarm/utils/__init__.py
new file mode 100644
index 0000000..d5f7f70
--- /dev/null
+++ b/src/pysiaalarm/utils/__init__.py
@@ -0,0 +1,12 @@
+"""Init of pysiaalarm utils."""
+
+from ..data.data import (
+    SIACode,
+    SIAXData,
+    _load_adm_mapping,
+    _load_sia_codes,
+    _load_xdata,
+)
+from .counter import Counter
+from .enums import CommunicationsProtocol, MessageTypes, ResponseType
+from .regexes import MAIN_MATCHER, OH_MATCHER, _get_matcher
diff --git a/src/pysiaalarm/utils/counter.py b/src/pysiaalarm/utils/counter.py
new file mode 100644
index 0000000..82ec233
--- /dev/null
+++ b/src/pysiaalarm/utils/counter.py
@@ -0,0 +1,106 @@
+"""Counter helper class."""
+from dataclasses import dataclass
+from typing import Optional
+
+from ..const import (
+    COUNTER_ACCOUNT,
+    COUNTER_CODE,
+    COUNTER_CRC,
+    COUNTER_EVENTS,
+    COUNTER_FORMAT,
+    COUNTER_TIMESTAMP,
+    COUNTER_USER_CODE,
+    COUNTER_VALID,
+)
+
+
+@dataclass
+class Counter:
+    """Class for the counter."""
+
+    error_account: int = 0
+    error_code: int = 0
+    error_crc: int = 0
+    error_format: int = 0
+    error_timestamp: int = 0
+    error_user_function: int = 0
+    events: int = 0
+    valid_events: int = 0
+
+    def increment_error_account(self) -> None:
+        """Increment the error_account count."""
+        self.error_account += 1
+
+    def increment_error_code(self) -> None:
+        """Increment the error_code count."""
+        self.error_code += 1
+
+    def increment_error_crc(self) -> None:
+        """Increment the error_crc count."""
+        self.error_crc += 1
+
+    def increment_error_format(self) -> None:
+        """Increment the error_format count."""
+        self.error_format += 1
+
+    def increment_error_timestamp(self) -> None:
+        """Increment the error_timestamp count."""
+        self.error_timestamp += 1
+
+    def increment_error_user_function(self) -> None:
+        """Increment the error_user_function count."""
+        self.error_user_function += 1
+
+    def increment_events(self) -> None:
+        """Increment the events count."""
+        self.events += 1
+
+    def increment_valid_events(self) -> None:
+        """Increment the valid_events count."""
+        self.valid_events += 1
+
+    def get(self, item: str) -> Optional[int]:
+        """Get the right counter."""
+        if item == COUNTER_ACCOUNT:
+            return self.error_account
+        if item == COUNTER_CODE:
+            return self.error_code
+        if item == COUNTER_CRC:
+            return self.error_crc
+        if item == COUNTER_FORMAT:
+            return self.error_format
+        if item == COUNTER_TIMESTAMP:
+            return self.error_timestamp
+        if item == COUNTER_USER_CODE:
+            return self.error_user_function
+        if item == COUNTER_VALID:
+            return self.valid_events
+        if item == COUNTER_EVENTS:
+            return self.events
+        return None  # pragma: no cover
+
+    def increment(self, item: str) -> None:  # pragma: no cover
+        """Increment the right counter."""
+        if item == COUNTER_ACCOUNT:
+            self.increment_error_account()
+            return
+        if item == COUNTER_CODE:
+            self.increment_error_code()
+            return
+        if item == COUNTER_CRC:
+            self.increment_error_crc()
+            return
+        if item == COUNTER_FORMAT:
+            self.increment_error_format()
+            return
+        if item == COUNTER_TIMESTAMP:
+            self.increment_error_timestamp()
+            return
+        if item == COUNTER_USER_CODE:
+            self.increment_error_user_function()
+            return
+        if item == COUNTER_VALID:
+            self.increment_valid_events()
+            return
+        if item == COUNTER_EVENTS:
+            self.increment_events()
diff --git a/src/pysiaalarm/utils/enums.py b/src/pysiaalarm/utils/enums.py
new file mode 100644
index 0000000..eb26069
--- /dev/null
+++ b/src/pysiaalarm/utils/enums.py
@@ -0,0 +1,39 @@
+"""Enum classes for Pysiaalarm."""
+from enum import Enum, auto
+from typing import Any
+
+
+class AutoName(Enum):
+    """Auto name the enums."""
+
+    @staticmethod
+    def _generate_next_value_(
+        name: str, start: Any, count: Any, last_values: Any
+    ) -> str:
+        """Return name as value for Enum."""
+        return name
+
+
+class CommunicationsProtocol(AutoName):
+    """CommunicationsProtocol enumerator for SIA."""
+
+    TCP = auto()
+    UDP = auto()
+
+
+class ResponseType(AutoName):
+    """Class with response types for events."""
+
+    ACK = auto()
+    DUH = auto()
+    NAK = auto()
+    RSP = auto()
+
+
+class MessageTypes(Enum):
+    """Message type enumerator for SIA."""
+
+    SIADCS = "SIA-DCS"
+    ADMCID = "ADM-CID"
+    NULL = "NULL"
+    OH = "OH"
diff --git a/src/pysiaalarm/utils/regexes.py b/src/pysiaalarm/utils/regexes.py
new file mode 100644
index 0000000..1452140
--- /dev/null
+++ b/src/pysiaalarm/utils/regexes.py
@@ -0,0 +1,76 @@
+"""Helper script with all Regexes."""
+import re
+from typing import Union
+
+from .enums import MessageTypes
+
+# sample OH: SR0001L0001    006969XX    [ID00000000]
+oh_regex = r"""
+^S
+(?:R)(?P<receiver>(?<=R)(?:\d{4}))
+(?:L)(?P<line>(?<=L)(?:\d{4}))
+\s+(?P<account>\w{8})\s+
+\[(?P<id>\w+)\]$
+"""
+OH_MATCHER = re.compile(oh_regex, re.X)
+
+main_regex = r"""
+(?P<crc>[A-Fa-f0-9]{4})
+(?P<length>[A-Fa-f0-9]{4})\"
+(?P<encrypted_flag>[*])?
+(?P<message_type>SIA-DCS|ADM-CID|NULL)\"
+(?P<sequence>[0-9]{4})
+(?P<receiver>R[A-Fa-f0-9]{1,6})?
+(?P<line>L[A-Fa-f0-9]{1,6})
+[#]?(?P<account>[A-Fa-f0-9]{3,16})?
+[\[]
+(?P<rest>.*)
+"""
+MAIN_MATCHER = re.compile(main_regex, re.X)
+
+sia_content_regex = r"""
+[#]?(?P<account>[A-Fa-f0-9]{3,16})?
+[|]?
+[N]?
+(?:ti)?(?:(?<=ti)(?P<ti>\d{2}:\d{2}))?\/?
+(?:id)?(?:(?<=id)(?P<id>\d*))?\/?
+(?:ri)?(?:(?<=ri)(?P<ri>\d*))?\/?
+(?P<code>[a-zA-Z]{2})?
+(?P<message>.[^\[\]]*)?
+[\]]
+(?:\[(?:(?<=\[)(?P<xdata>.*)(?=\]))\])?
+[_]?
+(?P<timestamp>[0-9:,-]*)?$
+"""
+SIA_CONTENT_MATCHER = re.compile(sia_content_regex, re.X)
+
+encr_sia_content_regex = r"""(?:[^\|\[\]]*)[|]?"""
+ENCR_SIA_CONTENT_MATCHER = re.compile(encr_sia_content_regex + sia_content_regex, re.X)
+
+adm_content_regex = r"""
+[#]?(?P<account>[A-F0-9]{3,16})?
+[|]?
+(?P<event_qualifier>\d{1})
+(?P<event_type>\d{3})
+\s
+(?P<partition>\d{2})
+\s
+(?P<ri>\d{3})
+[\]]
+(?:\[(?:(?<=\[)(?P<xdata>.[^\[\]]*)(?=\]))\])?
+[_]?
+(?P<timestamp>[0-9:,-]*)?$
+"""
+ADM_CONTENT_MATCHER = re.compile(adm_content_regex, re.X)
+
+encr_adm_content_regex = r"""(?:[^\|\[\]]*)[|]?"""
+ENCR_ADM_CONTENT_MATCHER = re.compile(encr_adm_content_regex + adm_content_regex, re.X)
+
+
+def _get_matcher(
+    message_type: Union[MessageTypes, str], encrypted: bool = False
+) -> re.Pattern:
+    """Extract the content using the different regexes."""
+    if message_type == MessageTypes.ADMCID:
+        return ENCR_ADM_CONTENT_MATCHER if encrypted else ADM_CONTENT_MATCHER
+    return ENCR_SIA_CONTENT_MATCHER if encrypted else SIA_CONTENT_MATCHER
diff --git a/tox.ini b/tox.ini
new file mode 100644
index 0000000..bc38e72
--- /dev/null
+++ b/tox.ini
@@ -0,0 +1,68 @@
+# Tox configuration file
+# Read more under https://tox.readthedocs.org/
+# THIS SCRIPT IS SUPPOSED TO BE AN EXAMPLE. MODIFY IT ACCORDING TO YOUR NEEDS!
+
+[tox]
+minversion = 3.15
+envlist = default
+
+
+[testenv]
+description = invoke pytest to run automated tests
+isolated_build = False
+setenv =
+    TOXINIDIR = {toxinidir}
+passenv =
+    HOME
+extras =
+    test
+commands =
+    pytest
+
+
+[testenv:{clean,build}]
+description =
+    Build (or clean) the package in isolation according to instructions in:
+    https://setuptools.readthedocs.io/en/latest/build_meta.html#how-to-use-it
+    https://github.com/pypa/pep517/issues/91
+    https://github.com/pypa/build
+# NOTE: build is still experimental, please refer to the links for updates/issues
+skip_install = True
+changedir = {toxinidir}
+deps =
+    build: build
+commands =
+    clean: python -c 'from shutil import rmtree; rmtree("build", True); rmtree("dist", True)'
+    build: python -m build .
+# By default `build` produces wheels, you can also explicitly use the flags `--sdist` and `--wheel`
+
+
+[testenv:{docs,doctests}]
+description = invoke sphinx-build to build the docs/run doctests
+setenv =
+    DOCSDIR = {toxinidir}/docs
+    BUILDDIR = {toxinidir}/docs/_build
+    docs: BUILD = html
+    doctests: BUILD = doctest
+deps =
+    -r {toxinidir}/docs/requirements.txt
+    # ^  requirements.txt shared with Read The Docs
+commands =
+    sphinx-build -b {env:BUILD} -d "{env:BUILDDIR}/doctrees" "{env:DOCSDIR}" "{env:BUILDDIR}/{env:BUILD}" {posargs}
+
+
+[testenv:publish]
+description =
+    Publish the package you have been developing to a package index server.
+    By default, it uses testpypi. If you really want to publish your package
+    to be publicly accessible in PyPI, use the `-- --repository pypi` option.
+skip_install = True
+changedir = {toxinidir}
+passenv =
+    TWINE_USERNAME
+    TWINE_PASSWORD
+    TWINE_REPOSITORY
+deps = twine
+commands =
+    python -m twine check dist/*
+    python -m twine upload {posargs:--repository testpypi} dist/*


Test patch:
diff --git a/pytest.ini b/pytest.ini
new file mode 100644
index 0000000..4db3cd7
--- /dev/null
+++ b/pytest.ini
@@ -0,0 +1,22 @@
+[pytest]
+# Specify command line options as you would do when invoking pytest directly.
+# e.g. --cov-report html (or xml) for html/xml output or --junitxml junit.xml
+# in order to write a coverage file that can be read by Jenkins.
+# CAUTION: --cov flags may prohibit setting breakpoints while debugging.
+#          Comment those flags to avoid this py.test issue.
+addopts =
+    --cov pysiaalarm --cov-report term-missing
+    -vvv
+norecursedirs =
+    dist
+    build
+    .tox
+testpaths = tests
+python_classes = testSIA
+# Use pytest markers to select/deselect specific tests
+markers =
+    sync
+    aio
+    both
+#     slow: mark tests as slow (deselect with '-m "not slow"')
+#     system: mark end-to-end system tests
\ No newline at end of file
diff --git a/tests/__init__.py b/tests/__init__.py
index e69de29..fb6157d 100644
--- a/tests/__init__.py
+++ b/tests/__init__.py
@@ -0,0 +1,1 @@
+"""Tests for SIA Alarm package."""
diff --git a/tests/create_line.py b/tests/create_line.py
deleted file mode 100644
index a19c9ec..0000000
--- a/tests/create_line.py
+++ /dev/null
@@ -1,67 +0,0 @@
-import binascii
-import json
-import logging
-import random
-import socket
-import sys
-import time
-from binascii import hexlify
-from datetime import datetime, timedelta
-
-from Crypto import Random
-from Crypto.Cipher import AES
-from Crypto.Util.Padding import pad
-
-from pysiaalarm import SIAAccount, SIAEvent
-from pysiaalarm.sia_account import _create_padded_message
-from pysiaalarm.sia_const import ALL_CODES
-
-BASIC_CONTENT = f"|Nri<zone>/<code>000]<timestamp>"
-BASIC_LINE = f'SIA-DCS"<seq>L0#<account>[<content>'
-NULL_LINE = f'NULL"<seq>L0#<account>[<content>'
-
-_LOGGER = logging.getLogger(__name__)
-
-
-def create_line(
-    key,
-    account,
-    sia_account,
-    code,
-    type="SIA-DCS",
-    generate=True,
-    timestamp=None,
-    alter_crc=False,
-):
-    # acc =
-    if generate:
-        seq = str(random.randint(1000, 9999))
-        timestamp = (
-            timestamp
-            if timestamp
-            else (datetime.utcnow()).strftime("_%H:%M:%S,%m-%d-%Y")
-        )
-    else:
-        seq = "7654"
-        timestamp = "_16:04:02,07-09-2020"
-    if type == "SIA-DCS":
-        content = (
-            BASIC_CONTENT.replace("<zone>", "0" if code == "RP" else "1")
-            .replace("<code>", code)
-            .replace("<timestamp>", timestamp)
-        )
-        base_line = BASIC_LINE
-    else:
-        content = f"]{timestamp}"
-        if key:
-            content = _create_padded_message(content)
-        base_line = NULL_LINE
-    _LOGGER.debug("Content to be sent: %s", content)
-    content = sia_account.encrypt(content)
-    line = f'"{"*" if key else ""}{base_line.replace("<account>", account).replace("<content>", content).replace("<seq>", seq)}'
-
-    crc = SIAEvent.crc_calc(line)
-    if alter_crc:
-        crc = ("%04x" % random.randrange(16 ** 4)).upper()
-    leng = str(int(str(len(line)), 16)).zfill(4)
-    return rf"{crc}{leng}{line}"
diff --git a/tests/run.py b/tests/run.py
index 4ddfff9..c6df91c 100644
--- a/tests/run.py
+++ b/tests/run.py
@@ -17,7 +17,7 @@ def func(event: SIAEvent):
     config = json.load(f)
 print("Config: ", config)
 account = [SIAAccount(config["account_id"], config["key"])]
-sleep_time = 120
+sleep_time = 1200
 with SIAClient(config["host"], config["port"], account, function=func) as client:
     time.sleep(sleep_time)
     counts = client.counts
diff --git a/tests/run_re.py b/tests/run_re.py
index e469ef0..2d16ac0 100644
--- a/tests/run_re.py
+++ b/tests/run_re.py
@@ -3,60 +3,79 @@
 
 logging.basicConfig(level=logging.DEBUG)
 from pysiaalarm import SIAAccount, SIAEvent
+from pysiaalarm.utils.regexes import (
+    MAIN_MATCHER,
+    ENCR_ADM_CONTENT_MATCHER,
+    ENCR_SIA_CONTENT_MATCHER,
+    SIA_CONTENT_MATCHER,
+    ADM_CONTENT_MATCHER,
+    OH_MATCHER,
+)
 
-# matcher = re.compile(regex, re.X)
-# print(matcher.pattern)
-main_regex = r"""
-(?P<crc>[A-F0-9]{4})
-(?P<length>[A-F0-9]{4})\"
-(?P<encrypted_flag>[*])?
-(?P<message_type>SIA-DCS|NULL)\"
-(?P<sequence>[0-9]{4})
-(?P<receiver>R[A-F0-9]{1,6})?
-(?P<prefix>L[A-F0-9]{1,6})
-[#]?(?P<account>[A-F0-9]{3,16})?
-[\[]
-(?P<rest>.*)
-"""
-MAIN_MATCHER = re.compile(main_regex, re.X)
+# # matcher = re.compile(regex, re.X)
+# # print(matcher.pattern)
+# main_regex = r"""
+# (?P<crc>[A-F0-9]{4})
+# (?P<length>[A-F0-9]{4})\"
+# (?P<encrypted_flag>[*])?
+# (?P<message_type>SIA-DCS|NULL)\"
+# (?P<sequence>[0-9]{4})
+# (?P<receiver>R[A-F0-9]{1,6})?
+# (?P<prefix>L[A-F0-9]{1,6})
+# [#]?(?P<account>[A-F0-9]{3,16})?
+# [\[]
+# (?P<rest>.*)
+# """
+# MAIN_MATCHER = re.compile(main_regex, re.X)
 
-content_regex = r"""
-[#]?(?P<account>[A-F0-9]{3,16})?
-[|]?
-(?:Nri)?
-(?P<zone>\d*)?
-\/?
-(?P<code>[a-zA-z]{2})?
-(?P<message>.*)
-[\]]
-[_]?
-(?P<timestamp>[0-9:,-]*)?
-"""
-CONTENT_MATCHER = re.compile(content_regex, re.X)
+# # “ti”hh:mm/ time (e.g. ti10:23/).
+# # “id”nnn/ user number, if applicable; otherwise not sent (e.g.
+# # “ri”nn/ partition no. (e.g. ri12/ or ri3).
+# content_regex = r"""
+# [#]?(?P<account>[A-F0-9]{3,16})?
+# [|]?
+# [N]?
+# (?:ti)?(?:(?<=ti)(?P<ti>\d{2}:\d{2}))?\/?
+# (?:id)?(?:(?<=id)(?P<id>\d*))?\/?
+# (?:ri)?(?:(?<=ri)(?P<ri>\d*))?\/?
+# (?P<code>[a-zA-z]{2})?
+# (?P<message>\w*)
+# [\]]
+# (?:\[(?:(?<=\[)(?P<extendeddata>\w*)(?=\]))\])?
+# [_]?
+# (?P<timestamp>[0-9:,-]*)?
+# """
+# CONTENT_MATCHER = re.compile(content_regex, re.X)
 
-encr_content_regex = r"""
-(?:[^\|\[\]]*)
-[|]?
-[#]?(?P<account>[A-F0-9]{3,16})?
-[|]?
-(?:.*Nri)?
-(?P<zone>\d*)?
-\/?
-(?P<code>[a-zA-z]{2})?
-(?P<message>.*)
-[\]][_]?
-(?P<timestamp>[0-9:,-]*)?
-"""
-ENCR_CONTENT_MATCHER = re.compile(encr_content_regex, re.X)
+# encr_content_regex = r"""(?:[^\|\[\]]*)[|]?"""
+# ENCR_CONTENT_MATCHER = re.compile(encr_content_regex + content_regex, re.X)
 
-lines = [
-    # r'2E680078"SIA-DCS"6002L0#AAA[|Nri1/CL501]_14:12:04,09-25-2019',
-    # r'76D80055"*NULL"0000R0L0#AAAB[B4BC8B40D0E6D959D6BEA78E88CC0B2155741A3C44FBB96D476A3E557CAD64D9',
-    # r'01010051"SIA-DCS"4738R0001L0001[#006969|Nri04/OP001*’NM]DDA2FBB313169D87|#006969',
-    # r'C4160279"SIA-DCS"5268L0#AAA[.Rr\x1d PaG\'5"�\n\x03��|Nri1/WA000]_08:40:47,07-08-2020',
-    # r'68370130"*NULL"6327L0#AAA[9F719719F36B05547CD730D4615FE0A4B5BB7A27A9F500741A07C3F7328FC7A1',
-    # r'43580023"SIA-DCS"0084L0#AAA[#AAA|Nri1/XC12]'
-    r'85DF0078"*SIA-DCS"4480L0#EA1984[83F153789366885D5F83DD5A8D19F691DE6602D5D71342E244C040C5D10D89040444068312750F38DF7E63AD3DE8AD5A'
+# heartbeat_regex = r"""
+# ^S
+# (?:R)(?:(?<=R)(?P<receiver>\d{4}))
+# (?:L)(?:(?<=L)(?P<prefix>\d{4}))
+# \s+\w{8}\s+
+# \[(?P<id>\w+)\]$
+# """
+
+# HEARTBEAT_MATCHER = re.compile(heartbeat_regex, re.X)
+
+sia_lines = [
+    r'2E680078"SIA-DCS"6002L0#AAA[|Nri1/CL501]_14:12:04,09-25-2019',
+    r'76D80055"*NULL"0000R0L0#AAAB[B4BC8B40D0E6D959D6BEA78E88CC0B2155741A3C44FBB96D476A3E557CAD64D9',
+    r'01010051"SIA-DCS"4738R0001L0001[#006969|Nri04/OP001*’NM]DDA2FBB313169D87|#006969',
+    r'C4160279"SIA-DCS"5268L0#AAA[.Rr\x1d PaG\'5"�\n\x03��|Nri1/WA000]_08:40:47,07-08-2020',
+    r'68370130"*NULL"6327L0#AAA[9F719719F36B05547CD730D4615FE0A4B5BB7A27A9F500741A07C3F7328FC7A1',
+    r'43580023"SIA-DCS"0084L0#AAA[#AAA|Nri1/XC12]'
+    r'85DF0078"*SIA-DCS"4480L0#EA1984[83F153789366885D5F83DD5A8D19F691DE6602D5D71342E244C040C5D10D89040444068312750F38DF7E63AD3DE8AD5A',
+    r'1908002B"SIA-DCS"0000L0#3080[#3080|Nti19:44/id1/ri4/RP]',
+    r'43580023"*SIA-DCS"0084L0#AAA[P$WwA!#|#EA1984|Nri1/NL4]_08:38:13,01-07-2021',
+    r'2E680078"SIA-DCS"6002L0#AAA[|Nri1/CL501][A38475345]_14:12:04,09-25-2019',
+    r"SR0001L0001    006969XX    [ID00000000]",
+]
+
+adm_lines = [
+    r'87CD0037"ADM-CID"9876R579BDFL789ABC#12345A[#12345A|1110 00 129]_14:12:04,09-25-201'
 ]
 
 encr_content = [
@@ -65,36 +84,68 @@
     r"T,o |#EA1984|Nri0/RP0000]_20:44:12,08-11-2020",
     r"_ |#EA1984|Nri0/RP0000]_22:27:02,08-11-2020",
     r"&* |#EA1984|Nri0/RP0000]_05:19:49,08-12-2020",
+    r"P$WwA!#|#EA1984|Nri1/NL4]_08:38:13,01-07-2021",
 ]
+unen_content = [
+    r"#006969|Nri04/OP001NM]",
+]
+
+# for content in unen_content:
+#     content_match = SIA_CONTENT_MATCHER.match(content)
+#     print("unen_content_match groups", content_match.groupdict())
 
 # for content in encr_content:
-#     en_content_match = CONTENT_MATCHER.match(content)
+#     en_content_match = ENCR_SIA_CONTENT_MATCHER.match(content)
 #     print("en_content_match groups", en_content_match.groupdict())
 
-for line in lines:
-    # print("Line ", line)
-    # # print(SIAEvent(line))
-    # prefix = prefix_matcher.match(line)
-    # # re.match(prefix_regex, line, re.X)
-    # print("Prefix groups", prefix.groupdict())
-    # if prefix:
-    #     if prefix.group("encrypted_flag"):
-    #         # if re.search(r"(\*SIA-DCS|\*NULL)", line):
-    #         encrypted_content = prefix.group("rest")
-    #         print(encrypted_content)
-    #         # print(
-    #         #     "Groups ", re.match(regex_encrypted, prefix.group("rest"), re.X).groupdict()
-    #         # )
-    #     else:
-    #         print(
-    #             "Groups ",
-    #             re.match(content_regex, prefix.group("rest"), re.VERBOSE).groupdict(),
-    #         )
-    #         # print("Groups ", matcher.match(line).groupdict())
-    acc = SIAAccount("EA1984", "3BD7E66AA9E2F190")
-    ev = SIAEvent(line)
-    print(ev)
-    print(ev.encrypted)
-    # print(ev.valid_message)
-    print(acc.decrypt(ev))
-    # print(acc.decrypt(ev))
+# for line in sia_lines:
+#     print("Line ", line)
+#     # print(SIAEvent(line))
+#     prefix = MAIN_MATCHER.match(line)
+#     # re.match(prefix_regex, line, re.X)
+#     if prefix:
+#         print("Groups", prefix.groupdict())
+#         if prefix.group("encrypted_flag"):
+#             # if re.search(r"(\*SIA-DCS|\*NULL)", line):
+#             encrypted_content = prefix.group("rest")
+#             print(encrypted_content)
+#             content = ENCR_SIA_CONTENT_MATCHER.match(encrypted_content)
+#             if content:
+#                 print("Groups ", content.groupdict())
+#             # print(
+#             #     "Groups ", re.match(regex_encrypted, prefix.group("rest"), re.X).groupdict()
+#             # )
+#         else:
+#             content = SIA_CONTENT_MATCHER.match(prefix.group("rest"))
+#             if content:
+#                 print("Groups ", content.groupdict())
+#     else:
+#         heartbeat = OH_MATCHER.match(line)
+#         if heartbeat:
+#             print("Groups ", heartbeat.groupdict())
+
+for line in adm_lines:
+    print("Line ", line)
+    # print(SIAEvent(line))
+    prefix = MAIN_MATCHER.match(line)
+    # re.match(prefix_regex, line, re.X)
+    if prefix:
+        print("Groups", prefix.groupdict())
+        if prefix.group("encrypted_flag"):
+            # if re.search(r"(\*SIA-DCS|\*NULL)", line):
+            encrypted_content = prefix.group("rest")
+            print(encrypted_content)
+            content = ENCR_ADM_CONTENT_MATCHER.match(encrypted_content)
+            if content:
+                print("Groups ", content.groupdict())
+            # print(
+            #     "Groups ", re.match(regex_encrypted, prefix.group("rest"), re.X).groupdict()
+            # )
+        else:
+            content = ADM_CONTENT_MATCHER.match(prefix.group("rest"))
+            if content:
+                print("Groups ", content.groupdict())
+    else:
+        heartbeat = OH_MATCHER.match(line)
+        if heartbeat:
+            print("Groups ", heartbeat.groupdict())
diff --git a/tests/test_alarm.py b/tests/test_alarm.py
new file mode 100644
index 0000000..4123c71
--- /dev/null
+++ b/tests/test_alarm.py
@@ -0,0 +1,58 @@
+#!/usr/bin/python
+"""Run a test client."""
+import json
+import logging
+import socket
+import sys
+import time
+
+from pysiaalarm import CommunicationsProtocol
+from .test_utils import create_line_from_test_case, create_random_line
+
+logging.basicConfig(level=logging.DEBUG)
+_LOGGER = logging.getLogger(__name__)
+
+
+def send_messages(config, test_case=None, time_between=5):
+    """Create the socket client and start sending messages every 5 seconds, until stopped, or the server disappears."""
+    _LOGGER.info("Test client config: %s", config)
+    host = config["host"]
+    port = config["port"]
+    protocol = (
+        socket.SOCK_STREAM
+        if config["protocol"] == CommunicationsProtocol.TCP
+        else socket.SOCK_DGRAM
+    )
+
+    with socket.socket(socket.AF_INET, protocol) as sock:
+        try:
+            sock.connect((host, port))  # connect to the server
+        except ConnectionRefusedError:
+            _LOGGER.error("Connection refused in test_alarm.py.")
+            return
+
+        if test_case:
+            message = create_line_from_test_case(config, test_case)
+            _LOGGER.debug(f"Sending to server: {message}")
+            sock.sendall(message.encode())  # send message
+            data = sock.recv(1024).decode()  # receive response
+            return
+        while True:
+            message = create_random_line(config)
+            _LOGGER.debug(f"Sending to server: {message}")
+            sock.sendall(message.encode())  # send message
+            data = sock.recv(1024).decode()  # receive response
+            _LOGGER.debug(f"Received from server: {data}")  # show in terminal
+            time.sleep(time_between)
+
+
+if __name__ == "__main__":
+    """Run main with a config."""
+    _LOGGER.info(sys.argv)
+    try:  # sys.argv.index(1)
+        file = sys.argv[1]
+    except:
+        file = "tests//local_config.json"
+    with open(file, "r") as f:
+        config = json.load(f)
+    send_messages(config)
diff --git a/tests/test_alarm_aio.py b/tests/test_alarm_aio.py
new file mode 100644
index 0000000..25a2154
--- /dev/null
+++ b/tests/test_alarm_aio.py
@@ -0,0 +1,53 @@
+#!/usr/bin/python
+"""Run a test client."""
+import asyncio
+import json
+import logging
+import sys
+
+from .test_utils import create_line_from_test_case, create_random_line
+
+
+logging.basicConfig(level=logging.DEBUG)
+_LOGGER = logging.getLogger(__name__)
+
+
+async def async_send_messages(config, test_case, time_between):
+    """Send message async."""
+    host = config["host"]
+    port = config["port"]
+    _LOGGER.debug("Opening connection")
+    reader, writer = await asyncio.open_connection(host, port)
+    if test_case:
+        _LOGGER.debug("Number of cases: %s", len(test_case))
+        for tc in test_case:
+            message = create_line_from_test_case(config, tc)
+            _LOGGER.debug("Message: %s", message)
+            writer.write(message.encode())
+            data = await reader.read(100)
+            _LOGGER.debug(f"Received from server: {data.decode()}")
+            await asyncio.sleep(time_between)
+        writer.close()
+        return
+    try:
+        while True:
+            message = create_random_line(config)
+            _LOGGER.debug("Message: %s", message)
+            writer.write(message.encode())
+            data = await reader.read(100)
+            _LOGGER.debug(f"Received from server: {data.decode()}")
+            await asyncio.sleep(time_between)
+    finally:
+        writer.close()
+
+
+if __name__ == "__main__":
+    """Run main with a config."""
+    _LOGGER.info(sys.argv)
+    try:  # sys.argv.index(1)
+        file = sys.argv[1]
+    except:
+        file = "tests//local_config.json"
+    with open(file, "r") as f:
+        config = json.load(f)
+    asyncio.get_event_loop().run_until_complete(async_send_messages(config, None, 3))
diff --git a/tests/test_client.py b/tests/test_client.py
deleted file mode 100644
index caadcd3..0000000
--- a/tests/test_client.py
+++ /dev/null
@@ -1,216 +0,0 @@
-#!/usr/bin/python
-"""Run a test client."""
-import json
-import logging
-import random
-import socket
-import sys
-import time
-from binascii import hexlify, unhexlify
-from datetime import datetime, timedelta
-
-from Crypto import Random
-from Crypto.Cipher import AES
-
-from pysiaalarm import SIAAccount, SIAEvent
-from pysiaalarm.sia_const import ALL_CODES
-
-from .create_line import create_line
-
-BASIC_CONTENT = f"|Nri<zone>/<code>000]<timestamp>"
-BASIC_LINE = f'SIA-DCS"<seq>L0#<account>[<content>'
-
-
-def create_test_items(key, content):
-    """Create encrypted content."""
-    encrypter = AES.new(
-        key.encode("utf-8"), AES.MODE_CBC, unhexlify("00000000000000000000000000000000")
-    )
-
-    extra = len(content) % 16
-    unencrypted = (16 - extra) * "0" + content
-    return (
-        hexlify(encrypter.encrypt(unencrypted.encode("utf-8")))
-        .decode(encoding="utf-8")
-        .upper()
-    )
-
-
-def get_timestamp(timed) -> str:
-    """Create a timestamp in the right format."""
-    return (datetime.utcnow() - timed).strftime("_%H:%M:%S,%m-%d-%Y")
-
-
-def create_test_line(key, account, code, timestamp, alter_crc=False):
-    """Create a test line, with encrytion if key is supplied."""
-    seq = str(random.randint(1000, 9999))
-    content = (
-        BASIC_CONTENT.replace("<zone>", "0" if code == "RP" else "1")
-        .replace("<code>", code)
-        .replace("<timestamp>", timestamp)
-    )
-    if key:
-        content = create_test_items(key, content)
-    line = f'"{"*" if key else ""}{BASIC_LINE.replace("<account>", account).replace("<content>", content).replace("<seq>", seq)}'
-    crc = SIAEvent.crc_calc(line)
-    leng = int(str(len(line)), 16)
-
-    pad = (4 - len(str(leng))) * "0"
-
-    length = pad + str(leng)
-    if alter_crc:
-        crc = ("%04x" % random.randrange(16 ** 4)).upper()
-    return f"\n{crc}{length}{line}\r"
-
-
-CODES = [
-    # "BA",
-    # "BR",
-    # "CA",
-    # "CF",
-    # "CG",
-    # "CL",
-    # "CP",
-    # "CQ",
-    "GA",
-    "GH",
-    # "NL",
-    # "OA",
-    # "OG",
-    # "OP",
-    # "OQ",
-    # "OR",
-    "RP",
-    # "TA",
-    "WA",
-    "WH",
-    # "YG",
-]
-
-
-def random_code(test_case=None):
-    """Choose a random code."""
-    codes = [code for code in CODES]
-    return random.choice(codes)
-
-
-def random_alter_crc(test_case=None):
-    """Choose a random bool for alter_crc."""
-    if test_case:
-        if test_case.get("crc"):
-            return True
-        else:
-            return False
-    else:
-        return random.random() < 0.1
-
-
-def non_existing_code(code, test_case=None):
-    """Randomly choose a non-existant code or keep code."""
-    if test_case:
-        if test_case.get("code"):
-            return "ZW"
-        else:
-            return code
-    else:
-        return "ZW" if random.random() < 0.1 else code
-
-
-def different_account(account, test_case=None):
-    """Randomly choose a non-existant account or keep account."""
-    if test_case:
-        if test_case.get("account"):
-            return "FFFFFFFFF"
-        else:
-            return account
-    else:
-        return "FFFFFFFFF" if random.random() < 0.1 else account
-
-
-def timestamp_offset(test_case=None):
-    """Create timestamp offset for testing."""
-    if test_case:
-        if test_case.get("time"):
-            return 100
-        else:
-            return 0
-    else:
-        return random.randint(0, 1)
-
-
-def client_program(
-    config,
-    time_between=5,
-    test_case=None,  # [{"code": False, "crc": False, "account": False}]
-):
-    """Create the socket client and start sending messages every 5 seconds, until stopped, or the server disappears."""
-    logging.info("Test client config: %s", config)
-    host = config["host"]  # as both code is running on same pc
-    port = config["port"]  # socket server port number
-
-    client_socket = socket.socket()  # instantiate
-    client_socket.connect((host, port))  # connect to the server
-    index = 0
-    cases = len(test_case) if test_case else None
-    logging.debug("Number of cases: %s", cases)
-    stop = False
-    while True and not stop:
-        logging.debug("Index: %s", index)
-        if cases:
-            tc = test_case[index]
-        else:
-            tc = None
-        alter_crc = random_alter_crc(tc)
-        code = non_existing_code(random_code(), tc)
-        account = different_account(config["account_id"], tc)
-        timed = timedelta(seconds=timestamp_offset(tc))
-        timestamp = get_timestamp(timed)
-        acc = SIAAccount(account, config["key"])
-        message = create_line(
-            config["key"],
-            account,
-            acc,
-            code,
-            generate=True,
-            timestamp=timestamp,
-            alter_crc=alter_crc,
-        )
-        # message = create_test_line(config["key"], account, code, timestamp, alter_crc)
-        print(
-            f"Message with account: {account}, code: {code}, altered crc: {alter_crc}, timedelta: {timed}"
-        )
-        print(f"Sending to server: {message}")
-        client_socket.send(message.encode())  # send message
-        data = client_socket.recv(1024).decode()  # receive response
-        print(f"Received from server: {data}")  # show in terminal
-        if cases:
-            if index < cases - 1:
-                index += 1
-            else:
-                stop = True
-        # else:
-        # if alter_crc:
-        #     assert len(str.strip(data)) == 0
-        # elif account == "FFFFFFFFF":
-        #     assert data.find("NAK") > 0
-        # elif timed.seconds >= 40:
-        #     assert data.find("NAK") > 0
-        # elif code == "ZZ":
-        #     assert data.find("DUH") > 0
-        # else:
-        #     assert data.find("ACK") > 0
-        time.sleep(time_between)
-
-    client_socket.close()  # close the connection
-
-
-if __name__ == "__main__":
-    """Run main with a config."""
-    logging.info(sys.argv)
-    if sys.argv[1]:
-        file = sys.argv[1]
-    else:
-        file = "unencrypted_config.json"
-    with open(file, "r") as f:
-        config = json.load(f)
-    client_program(config)
diff --git a/tests/test_client_aio.py b/tests/test_client_aio.py
deleted file mode 100644
index a70ddac..0000000
--- a/tests/test_client_aio.py
+++ /dev/null
@@ -1,218 +0,0 @@
-#!/usr/bin/python
-"""Run a test client."""
-import asyncio
-import json
-import logging
-import random
-import sys
-import time
-from binascii import hexlify, unhexlify
-from datetime import datetime, timedelta
-
-from Crypto import Random
-from Crypto.Cipher import AES
-
-from pysiaalarm.sia_const import ALL_CODES
-from pysiaalarm.sia_event import SIAEvent
-
-BASIC_CONTENT = f"|Nri<zone>/<code>000]<timestamp>"
-BASIC_LINE = f'SIA-DCS"<seq>L0#<account>[<content>'
-
-
-def create_test_items(key, content):
-    """Create encrypted content."""
-    encrypter = AES.new(
-        key.encode("utf-8"), AES.MODE_CBC, unhexlify("00000000000000000000000000000000")
-    )
-
-    extra = len(content) % 16
-    unencrypted = (16 - extra) * "0" + content
-    return (
-        hexlify(encrypter.encrypt(unencrypted.encode("utf-8")))
-        .decode(encoding="utf-8")
-        .upper()
-    )
-
-
-def get_timestamp(timed) -> str:
-    """Create a timestamp in the right format."""
-    return (datetime.utcnow() - timed).strftime("_%H:%M:%S,%m-%d-%Y")
-
-
-def create_test_line(key, account, code, timestamp, alter_crc=False):
-    """Create a test line, with encrytion if key is supplied."""
-    seq = str(random.randint(1000, 9999))
-    content = (
-        BASIC_CONTENT.replace("<zone>", "0" if code == "RP" else "1")
-        .replace("<code>", code)
-        .replace("<timestamp>", timestamp)
-    )
-    if key:
-        content = create_test_items(key, content)
-    line = f'"{"*" if key else ""}{BASIC_LINE.replace("<account>", account).replace("<content>", content).replace("<seq>", seq)}'
-    crc = SIAEvent.crc_calc(line)
-    leng = int(str(len(line)), 16)
-    pad = (4 - len(str(leng))) * "0"
-    length = pad + str(leng)
-    if alter_crc:
-        crc = ("%04x" % random.randrange(16 ** 4)).upper()
-    return f"\n{crc}{length}{line}\r"
-
-
-CODES = [
-    "BA",
-    "BR",
-    "CA",
-    "CF",
-    "CG",
-    "CL",
-    "CP",
-    "CQ",
-    "GA",
-    "GH",
-    "NL",
-    "OA",
-    "OG",
-    "OP",
-    "OQ",
-    "OR",
-    "RP",
-    "RP",
-    "RP",
-    "RP",
-    "RP",
-    "RP",
-    "RP",
-    "RP",
-    "RP",
-    "RP",
-    "RP",
-    "RP",
-    "TA",
-    "WA",
-    "WH",
-    "YG",
-]
-
-
-def random_code(test_case=None):
-    """Choose a random code."""
-    codes = [code for code in CODES]
-    return random.choice(codes)
-
-
-def random_alter_crc(test_case=None):
-    """Choose a random bool for alter_crc."""
-    if test_case:
-        if test_case.get("crc"):
-            return True
-        else:
-            return False
-    else:
-        return random.random() < 0.1
-
-
-def non_existing_code(code, test_case=None):
-    """Randomly choose a non-existant code or keep code."""
-    if test_case:
-        if test_case.get("code"):
-            return "ZZ"
-        else:
-            return code
-    else:
-        return "ZZ" if random.random() < 0.1 else code
-
-
-def different_account(account, test_case=None):
-    """Randomly choose a non-existant account or keep account."""
-    if test_case:
-        if test_case.get("account"):
-            return "FFFFFFFFF"
-        else:
-            return account
-    else:
-        return "FFFFFFFFF" if random.random() < 0.1 else account
-
-
-def timestamp_offset(test_case=None):
-    """Create timestamp offset for testing."""
-    if test_case:
-        if test_case.get("time"):
-            return 100
-        else:
-            return 0
-    else:
-        return random.randint(0, 60)
-
-
-@asyncio.coroutine
-def tcp_client(message, host, port, loop):
-    print(f"Trying to send: {message}")
-
-    reader, writer = yield from asyncio.open_connection(host, port, loop=loop)
-    writer.write(message.encode())
-
-    data = yield from reader.read(100)
-    print(f"Received from server: {data.decode()}")
-
-    writer.close()
-
-
-def client_program(
-    config,
-    time_between=3,
-    test_case=None,  # [{"code": False, "crc": False, "account": False}]
-):
-    """Create the socket client and start sending messages every 5 seconds, until stopped, or the server disappears."""
-    logging.info("Test client config: %s", config)
-    host = config["host"]  # as both code is running on same pc
-    port = config["port"]  # socket server port number
-
-    loop = asyncio.get_event_loop()
-    task = loop.create_task(
-        async_send_messages(host, port, loop, test_case, time_between)
-    )
-    loop.run_until_complete(task)
-    loop.close()
-
-
-async def async_send_messages(host, port, loop, test_case, time_between):
-    stop = False
-    cases = len(test_case) if test_case else None
-    logging.debug("Number of cases: %s", cases)
-    index = 0
-    while True and not stop:
-        logging.debug("Index: %s", index)
-        if cases:
-            tc = test_case[index]
-        else:
-            tc = None
-        alter_crc = random_alter_crc(tc)
-        code = non_existing_code(random_code(), tc)
-        account = different_account(config["account_id"], tc)
-        timed = timedelta(seconds=timestamp_offset(tc))
-        timestamp = get_timestamp(timed)
-        message = create_test_line(config["key"], account, code, timestamp, alter_crc)
-        print(
-            f"Message with account: {account}, code: {code}, altered crc: {alter_crc}, timedelta: {timed}"
-        )
-        # loop.create_task(tcp_client(message, host, port, loop))
-        await tcp_client(message, host, port, loop)
-        if cases:
-            if index < cases - 1:
-                index += 1
-            else:
-                stop = True
-        await asyncio.sleep(time_between)
-
-
-if __name__ == "__main__":
-    """Run main with a config."""
-    logging.info(sys.argv)
-    # if sys.argv and sys.argv[1]:
-    #     file = sys.argv[1]
-    # else:
-    file = "local_config.json"
-    with open(file, "r") as f:
-        config = json.load(f)
-    client_program(config)
diff --git a/tests/test_sia_client.py b/tests/test_sia_client.py
deleted file mode 100644
index 610a8de..0000000
--- a/tests/test_sia_client.py
+++ /dev/null
@@ -1,425 +0,0 @@
-# -*- coding: utf-8 -*-
-"""Class for tests of pysiaalarm."""
-import asyncio
-import json
-import logging
-import random
-import re
-import socket
-import threading
-import time
-from typing import Type
-
-import pytest
-from hypothesis import example, given, settings
-from hypothesis import strategies as st
-from mock import MagicMock, PropertyMock, patch
-
-from pysiaalarm import (
-    InvalidAccountFormatError,
-    InvalidAccountLengthError,
-    InvalidKeyFormatError,
-    InvalidKeyLengthError,
-    SIAAccount,
-    SIAClient,
-    SIAEvent,
-    sia_event,
-)
-from pysiaalarm.aio import SIAClient as SIAClientA
-from pysiaalarm.sia_account import SIAResponseType, _create_padded_message
-from pysiaalarm.sia_errors import EventFormatError
-from tests.test_client import client_program
-from tests.test_utils import create_test_items
-
-from .create_line import create_line
-
-logging.basicConfig(level=logging.INFO)
-_LOGGER = logging.getLogger(__name__)
-
-KEY = "AAAAAAAAAAAAAAAA"
-ACCOUNT = "1111"
-HOST = "localhost"
-PORT = 7777
-
-
-def func(event: SIAEvent):
-    """Pass for testing."""
-    pass
-
-
-class testSIA(object):
-    """Class for pysiaalarm tests."""
-
-    @pytest.mark.parametrize(
-        "line, account, type, code, error",
-        [
-            (
-                r'60AB0078"*SIA-DCS"5994L0#AAA[5AB718E008C616BF16F6468033A11326B0F7546CAB230910BCA10E4DEBA42283C436E4F8EFF50931070DDE36D5BB5F0C',
-                "AAA",
-                None,
-                None,
-                Exception,
-            ),
-            (
-                r'E5D50078"SIA-DCS"6002L0#AAA[|Nri1/CL501]_14:12:04,09-25-2019',
-                "AAA",
-                "Closing Report",
-                "CL",
-                Exception,
-            ),
-            (
-                r'90820051"SIA-DCS"4738R0001L0001[#006969|Nri04/OP001*NM]DDA2FBB313169D87|#006969',
-                "006969",
-                "Opening Report",
-                "OP",
-                Exception,
-            ),
-            (
-                r'76D80055"*NULL"0000R0L0#AAAB[B4BC8B40D0E6D959D6BEA78E88CC0B2155741A3C44FBB96D476A3E557CAD64D9',
-                "AAAB",
-                None,
-                None,
-                Exception,
-            ),
-            (
-                r'C4160279"SIA-DCS"5268L0#AAA[Nri1/WA000]_08:40:47,07-08-2020',
-                "AAA",
-                "Water Alarm",
-                "WA",
-                Exception,
-            ),
-            (r"this is not a parsable event", None, None, None, EventFormatError),
-        ],
-    )
-    def test_event_parsing(self, line, account, type, code, error):
-        """Test event parsing methods."""
-        try:
-            event = SIAEvent(line)
-            print(event.sia_string)
-            assert event.code == code
-            assert event.type == type
-            assert event.account == account
-            print(event.valid_length)
-            print(event.valid_message)
-        except Exception as e:
-            if error == Exception:
-                _LOGGER.debug("Error thrown: %s", e)
-                assert False
-            if e == error:
-                assert True
-
-    @pytest.mark.parametrize(
-        "key, account, port, error",
-        [
-            ("ZZZZZZZZZZZZZZZZ", ACCOUNT, 7777, InvalidKeyFormatError),
-            ("158888888888888", ACCOUNT, 7777, InvalidKeyLengthError),
-            ("1688888888888888", ACCOUNT, 7777, None),
-            ("23888888888888888888888", ACCOUNT, 7777, InvalidKeyLengthError),
-            ("248888888888888888888888", ACCOUNT, 7777, None),
-            ("3188888888888888888888888888888", ACCOUNT, 7777, InvalidKeyLengthError),
-            ("32888888888888888888888888888888", ACCOUNT, 7777, None),
-            (KEY, "22", 7777, InvalidAccountLengthError),
-            (KEY, "ZZZ", 7777, InvalidAccountFormatError),
-        ],
-    )
-    def test_sia_key_account_errors(self, key, account, port, error):
-        """Test sia client behaviour."""
-        try:
-            SIAClient(
-                host="",
-                port=port,
-                accounts=[SIAAccount(account_id=account, key=key)],
-                function=func,
-            )
-            assert False if error else True
-        except Exception as exp:
-            assert isinstance(exp, error)
-
-    @pytest.mark.parametrize(
-        "config_file, port_add, fail_func",
-        [
-            ("tests\\unencrypted_config.json", 1, False),
-            ("tests\\encrypted_config.json", 2, False),
-            ("tests\\unencrypted_config.json", 3, True),
-            ("tests\\encrypted_config.json", 4, True),
-        ],
-    )
-    def test_client(self, config_file, port_add, fail_func):
-        """Test the client.
-
-        Arguments:
-            config_file {str} -- Filename of the config.
-
-        """
-        try:
-            with open(config_file, "r") as f:
-                config = json.load(f)
-        except:  # noqa: E722
-            config = {"host": HOST, "port": PORT, "account_id": ACCOUNT, "key": None}
-        config["port"] = config["port"] + port_add
-        events = []
-
-        if fail_func:
-
-            def func_append(event: SIAEvent):
-                raise ValueError("test error in user func")
-
-        else:
-
-            def func_append(event: SIAEvent):
-                events.append(event)
-
-        siac = SIAClient(
-            host="",
-            port=config["port"],
-            accounts=[SIAAccount(account_id=config["account_id"], key=config["key"])],
-            function=func_append,
-        )
-        siac.start()
-
-        tests = [
-            {"code": False, "crc": False, "account": False, "time": False},
-            {"code": True, "crc": False, "account": False, "time": False},
-            {"code": False, "crc": True, "account": False, "time": False},
-            {"code": False, "crc": False, "account": True, "time": False},
-            {"code": False, "crc": False, "account": False, "time": True},
-        ]
-
-        t = threading.Thread(
-            target=client_program, name="test_client", args=(config, 1, tests)
-        )
-        t.daemon = True
-        t.start()  # stops after the five events have been sent.
-
-        # run for 7 seconds
-        time.sleep(10)
-
-        siac.stop()
-        assert siac.counts == {
-            "events": 5,
-            "valid_events": 1,
-            "errors": {
-                "crc": 1,
-                "timestamp": 1,
-                "account": 1,
-                "code": 1,
-                "format": 0,
-                "user_code": 1 if fail_func else 0,
-            },
-        }
-        assert len(events) == 0 if fail_func else 1
-
-    @pytest.mark.parametrize(
-        "config_file, port_add, fail_func",
-        [
-            ("tests\\unencrypted_config.json", 5, False),
-            ("tests\\encrypted_config.json", 6, False),
-            ("tests\\unencrypted_config.json", 7, True),
-            ("tests\\encrypted_config.json", 8, True),
-        ],
-    )
-    async def test_async_client(self, config_file, port_add, fail_func):
-        """Test the client.
-
-        Arguments:
-            config_file {str} -- Filename of the config.
-
-        """
-        try:
-            with open(config_file, "r") as f:
-                config = json.load(f)
-        except:  # noqa: E722
-            config = {"host": HOST, "port": PORT, "account_id": ACCOUNT, "key": None}
-        config["port"] = config["port"] + port_add
-        events = []
-
-        if fail_func:
-
-            async def func_append(event: SIAEvent):
-                raise ValueError("test error in user func")
-
-        else:
-
-            async def func_append(event: SIAEvent):
-                events.append(event)
-
-        siac = SIAClientA(
-            host="",
-            port=config["port"],
-            accounts=[SIAAccount(account_id=config["account_id"], key=config["key"])],
-            function=func_append,
-        )
-        siac.start()
-
-        tests = [
-            {"code": False, "crc": False, "account": False, "time": False},
-            {"code": True, "crc": False, "account": False, "time": False},
-            {"code": False, "crc": True, "account": False, "time": False},
-            {"code": False, "crc": False, "account": True, "time": False},
-            {"code": False, "crc": False, "account": False, "time": True},
-        ]
-
-        t = threading.Thread(
-            target=client_program, name="test_client", args=(config, 1, tests)
-        )
-        t.daemon = True
-        t.start()  # stops after the five events have been sent.
-
-        # run for 7 seconds
-        await asyncio.sleep(10)
-
-        await siac.stop()
-        assert siac.counts == {
-            "events": 5,
-            "valid_events": 1,
-            "errors": {
-                "crc": 1,
-                "timestamp": 1,
-                "account": 1,
-                "code": 1,
-                "format": 0,
-                "user_code": 1 if fail_func else 0,
-            },
-        }
-        assert len(events) == 0 if fail_func else 1
-
-    @pytest.mark.parametrize(
-        ("key, account, code, type, alter_key, wrong_event"),
-        [
-            (None, "AAA", "RP", "SIA-DCS", False, False),
-            (None, "AAA", "WA", "SIA-DCS", False, False),
-            ("AAAAAAAAAAAAAAAA", "AAA", "RP", "SIA-DCS", False, False),
-            (None, "AAA", "RP", "NULL", False, False),
-            ("AAAAAAAAAAAAAAAA", "AAA", "RP", "NULL", False, False),
-            ("AAAAAAAAAAAAAAAA", "AAA", "RP", "NULL", True, False),
-            ("AAAAAAAAAAAAAAAA", "AAA", "RP", "NULL", False, True),
-        ],
-    )
-    def test_server(self, key, account, code, type, alter_key, wrong_event):
-        """Test the server parsing."""
-        config = {"host": HOST, "port": PORT, "account_id": account, "key": key}
-        events = []
-
-        def func_append(event: SIAEvent):
-            events.append(event)
-
-        siac = SIAClient(
-            host="",
-            port=config["port"],
-            accounts=[
-                SIAAccount(
-                    account_id=config["account_id"],
-                    key=config["key"],
-                    allowed_timeband=(None, None),
-                )
-            ],
-            function=func_append,
-        )
-
-        if alter_key:
-            key = key[:-1] + str(int(key[-1], 16) - 1)
-        sia_acc = SIAAccount(account, key)
-        line = create_line(key, account, sia_acc, code, type)
-        if wrong_event:
-            line = "This is not a SIA Event."
-        _LOGGER.info("Line sent to server: %s", line)
-        ev, acc, resp = siac.sia_server.parse_and_check_event(line)
-        if alter_key or wrong_event:
-            assert ev is None
-            assert resp == SIAResponseType.NAK
-        else:
-            assert resp == SIAResponseType.ACK
-            assert acc.account_id == account
-            assert ev.account == account
-            assert ev.code == code
-        line = create_line(key, account, sia_acc, code, type)
-        ev, acc, resp = siac.sia_server.parse_and_check_event(line)
-
-    def test_accounts(self):
-        """Test the account getting and setting."""
-        acc_list = [
-            SIAAccount(account_id=ACCOUNT, key=KEY, allowed_timeband=(None, None))
-        ]
-        acc_list2 = [
-            SIAAccount(account_id=ACCOUNT, key=KEY, allowed_timeband=(None, None)),
-            SIAAccount(account_id="1112", key=KEY, allowed_timeband=(None, None)),
-        ]
-        siac = SIAClient(
-            host="", port=PORT, accounts=acc_list, function=(lambda ev: print(ev))
-        )
-        assert siac.accounts == acc_list
-        siac.accounts = acc_list2
-        assert siac.accounts == acc_list2
-
-    @pytest.mark.parametrize(
-        ("async_func, async_client, error"),
-        [
-            (False, False, None),
-            (False, True, None),
-            (True, False, TypeError),
-            (True, True, None),
-        ],
-    )
-    def test_func(self, async_func, async_client, error):
-        """Test the function setting."""
-        acc_list = [
-            SIAAccount(account_id=ACCOUNT, key=KEY, allowed_timeband=(None, None))
-        ]
-        if async_func:
-
-            async def func(event):
-                pass
-
-        else:
-
-            def func(event):
-                pass
-
-        try:
-            if async_client:
-                SIAClientA("", PORT, acc_list, func)
-            else:
-                SIAClient("", PORT, acc_list, func)
-            assert error is None
-        except error:
-            assert error is not None
-        except Exception:
-            assert False
-
-    async def test_context(self):
-        """Test the context manager functions."""
-        acc_list = [
-            SIAAccount(account_id=ACCOUNT, key=KEY, allowed_timeband=(None, None))
-        ]
-        with SIAClient(HOST, PORT, acc_list, function=lambda ev: print(ev)) as cl:
-            assert cl.accounts == acc_list
-
-        async with SIAClientA(
-            HOST, PORT, acc_list, function=lambda ev: print(ev)
-        ) as cl:
-            assert cl.accounts == acc_list
-
-    content = st.text(
-        st.characters(min_codepoint=32, max_codepoint=126), min_size=4, max_size=15
-    ).filter(lambda x: bool(re.fullmatch(r"^(?![0]+).*(?<![0]{1,14})$", x)))
-    # ^(?=.*\d)(?=.*[B-Zb-z]{2,}).*$
-    key = st.from_regex(
-        regex=r"^[0-9A-E]{16}$|^[0-9A-E]{24}$|^[0-9A-E]{32}$", fullmatch=True
-    )
-
-    # @given(key, content)
-    # @example("AAAAAAAAAAAAAAAA", "|Nri1/GH000]_12:16:13,07-16-2020")
-    # @example("AAAAAAAAAAAAAAAA", "|Nri1/ZW000]_12:30:51,07-16-2020")
-    # def test_encrypt_decrypt(self, key, content):
-    #     """Test encryption and decryption."""
-    #     _LOGGER.debug("Key: %s", key)
-    #     acc = SIAAccount(ACCOUNT, key)
-    #     _LOGGER.debug("Unencrypted message: %s", content)
-    #     message = acc.encrypt(content)
-    #     with patch("pysiaalarm.SIAEvent") as event:
-    #         type(event).encrypted_content = PropertyMock(return_value=message)
-    #         _LOGGER.debug("Encrypted message event: %s", event.encrypted_content)
-    #         decrypted_event = acc.decrypt(event)
-    #         _LOGGER.debug("Decrypted message: %s", decrypted_event.content)
-    #         out_content = decrypted_event.content[-len(content) :]
-    #         assert out_content == content
diff --git a/tests/test_sia_package.py b/tests/test_sia_package.py
new file mode 100644
index 0000000..5447faf
--- /dev/null
+++ b/tests/test_sia_package.py
@@ -0,0 +1,406 @@
+# -*- coding: utf-8 -*-
+"""Class for tests of pysiaalarm."""
+import logging
+import threading
+import asyncio
+import pytest
+from dataclasses import asdict
+
+from pytest_cases import fixture_plus, parametrize_with_cases, fixture
+from unittest.mock import patch
+
+from pysiaalarm import (
+    SIAAccount,
+    SIAClient,
+    SIAEvent,
+)
+from pysiaalarm.aio import SIAClient as SIAClientA
+from pysiaalarm.event import NAKEvent, BaseEvent
+from pysiaalarm.const import COUNTER_USER_CODE, COUNTER_VALID, COUNTER_EVENTS
+
+from tests.test_alarm import send_messages
+from tests.test_utils import ACCOUNT, KEY, HOST, create_test_line
+from tests.test_sia_package_cases import *  # pylint: disable=W0614
+
+
+logging.basicConfig(level=logging.INFO)
+_LOGGER = logging.getLogger(__name__)
+
+
+@pytest.fixture
+def account():
+    """Return default account."""
+    return SIAAccount(ACCOUNT, KEY)
+
+
+@pytest.fixture
+def account_list(account):
+    """Return default account."""
+    return [account]
+
+
+@fixture(unpack_into="client, config, good, sync")
+@pytest.mark.asyncio
+@parametrize_with_cases("good", prefix="handler_")
+@parametrize_with_cases("key", prefix="encrypted_")
+@parametrize_with_cases("sync", prefix="sync_")
+@parametrize_with_cases("protocol", prefix="proto_")
+def create_client(
+    unused_tcp_port_factory,
+    event_handler,
+    async_event_handler,
+    bad_handler,
+    async_bad_handler,
+    key,
+    good,
+    protocol,
+    sync,
+):
+    """Fixture to create a SIA Client."""
+    config = {
+        "host": HOST,
+        "port": unused_tcp_port_factory(),
+        "account_id": ACCOUNT,
+        "key": key,
+        "protocol": protocol,
+    }
+
+    if sync:
+        client = SIAClient(
+            host=config["host"],
+            port=config["port"],
+            accounts=[SIAAccount(config["account_id"], config["key"])],
+            function=event_handler if good else bad_handler,
+            protocol=config["protocol"],
+        )
+    else:
+        client = SIAClientA(
+            host=config["host"],
+            port=config["port"],
+            accounts=[SIAAccount(config["account_id"], config["key"])],
+            function=async_event_handler if good else async_bad_handler,
+            protocol=config["protocol"],
+        )
+    return (client, config, good, sync)
+
+
+def get_func(type="sync"):
+    """Get a test func."""
+
+    async def async_func(event: SIAEvent):
+        """Pass for testing."""
+        pass
+
+    def func(event: SIAEvent):
+        """Pass for testing."""
+        pass
+
+    if type == "both":
+        return (func, async_func)
+    elif type == "aio":
+        return async_func
+    else:
+        return func
+
+
+@fixture_plus(
+    unpack_into="events, event_handler, async_event_handler, bad_handler, async_bad_handler"
+)
+def get_event_func():
+    """Return the handler function."""
+    events = []
+
+    async def async_event_handler(event: SIAEvent):
+        events.append(event)
+
+    def event_handler(event: SIAEvent):
+        events.append(event)
+
+    def bad_handler(_):
+        raise ValueError("test error in user func")
+
+    async def async_bad_handler(_):
+        raise ValueError("test error in user func")
+
+    return (events, event_handler, async_event_handler, bad_handler, async_bad_handler)
+
+
+class testSIA(object):
+    """Class for pysiaalarm tests."""
+
+    pytestmark = pytest.mark.asyncio
+
+    @parametrize_with_cases("cl, dic, clear_account", cases=ToFromDict)
+    def test_to_from_dict(self, cl, dic, clear_account):
+        """Test the to and from dict methods."""
+        _LOGGER.warning("Dict before: %s", dic)
+        instance = cl.from_dict(dic)
+        _LOGGER.warning("Class instance %s", instance)
+        if cl == SIAAccount:
+            assert instance.account_id == dic["account_id"]
+            assert instance.key == dic["key"]
+            dic2 = instance.to_dict()
+        else:
+            assert instance.account == dic["account"]
+            assert instance.code == dic["code"]
+            dic2 = instance.to_dict(encode_json=True)
+
+        _LOGGER.warning("Dict after: %s", dic2)
+        for key, value in dic.items():
+            if key != "sia_account":
+                assert dic2[key] == value
+            else:
+                if dic[key]:
+                    assert instance.sia_account is not None
+
+    @parametrize_with_cases(
+        "line, account_id, type, code, error_type, extended_data_flag",
+        cases=EventParsing,
+    )
+    def test_event_parsing(
+        self, line, account_id, type, code, error_type, extended_data_flag
+    ):
+        """Test event parsing methods."""
+        catch = error_type if error_type is not None else Exception
+        try:
+            if extended_data_flag:
+                event = SIAEvent.from_line(line, {account_id: SIAAccount(
+                            account_id, KEY, allowed_timeband=None
+                        )})
+            else:
+                event = SIAEvent.from_line(line)
+            assert event.code == code
+            if code:
+                assert event.sia_code.type == type
+            assert event.account == account_id
+            if extended_data_flag:
+                _LOGGER.warning("CRC: %s", event.calc_crc)
+                assert event.extended_data is not None
+                if event.extended_data[0].identifier == "K":
+                    _LOGGER.debug("RSP Response: %s", event.create_response())
+                    assert event.response == ResponseType.RSP
+                else:
+                    assert event.response == ResponseType.ACK
+        except AssertionError as e:
+            raise e
+        except catch as e:
+            if error_type is None:
+                _LOGGER.debug("Error thrown: %s", e)
+                assert False
+            if isinstance(e, error_type):
+                assert True
+
+    @parametrize_with_cases(
+        "line, account_id, type, code, error_type, extended_data_flag",
+        cases=EventParsing,
+    )
+    def test_event_serialization(
+        self, line, account_id, type, code, error_type, extended_data_flag
+    ):
+        """Test event parsing methods."""
+        if error_type is not None:
+            pytest.skip("Wrong messages will not serialize.")
+            return
+        event = BaseEvent.from_line(line)
+        event_type = event.__class__
+        event_dict = event.to_dict(encode_json=True)
+        event2 = event_type.from_dict(event_dict)
+        assert event == event2
+
+    @parametrize_with_cases("key, account_id, error_type", cases=AccountSetup)
+    def test_sia_account_setup(
+        self, unused_tcp_port_factory, key, account_id, error_type
+    ):
+        """Test sia client behaviour."""
+        try:
+            SIAAccount.validate_account(account_id, key)
+            SIAClient(
+                host="",
+                port=unused_tcp_port_factory(),
+                accounts=[SIAAccount(account_id=account_id, key=key)],
+                function=get_func("sync"),
+            )
+            assert True if error_type is None else False
+        except Exception as exp:
+            assert isinstance(exp, error_type)
+
+    @parametrize_with_cases("key, account_id, error_type", cases=AccountSetup)
+    def test_sia_account_validation(self, key, account_id, error_type):
+        """Test sia account validation."""
+        try:
+            SIAAccount.validate_account(account_id, key)
+            assert True if error_type is None else False
+        except Exception as exp:
+            assert isinstance(exp, error_type)
+
+    @parametrize_with_cases("fault", prefix="fault_")
+    async def test_clients(
+        self,
+        event_loop,
+        unused_tcp_port_factory,
+        events,
+        client,
+        config,
+        good,
+        sync,
+        fault,
+    ):
+        """Test the clients."""
+        if sync:
+            client.start()
+        else:
+            await client.start()
+        await asyncio.sleep(0.01)
+
+        t = threading.Thread(
+            target=send_messages,
+            name="send_messages",
+            args=(config, {fault: True}),
+        )
+        t.daemon = True
+        t.start()  # stops after the event has been sent.
+        await asyncio.sleep(0.1)
+
+        if sync:
+            client.stop()
+        else:
+            await client.stop()
+
+        _LOGGER.warning("Registered events: %s", client.counts)
+
+        if fault is not None:
+            assert client.counts.get(fault) == 1
+        else:
+            assert client.counts.get(COUNTER_VALID) == 1
+        if good and not fault:
+            assert len(events) == 1
+            assert client.counts.get(COUNTER_EVENTS) == 1
+        if not good and not fault:
+            assert client.counts.get(COUNTER_USER_CODE) == 1
+        if not good or fault:
+            assert len(events) == 0
+
+    @parametrize_with_cases("msg_type", prefix="msg_")
+    @parametrize_with_cases(
+        "code, alter_key, wrong_event, response", cases=ParseAndCheckEvent
+    )
+    @parametrize_with_cases("key", prefix="encrypted_")
+    def test_parse_and_check(
+        self,
+        unused_tcp_port_factory,
+        event_handler,
+        key,
+        code,
+        msg_type,
+        alter_key,
+        wrong_event,
+        response,
+    ):
+        """Test the server parsing."""
+        if msg_type in ("ADM-CID", "NULL") and code == "ZX":
+            pytest.skip(
+                "Unknown code is not usefull to test in ADM-CID and NULL messages."
+            )
+            return
+        if alter_key and key is None:
+            pytest.skip("Alter key is not usefull to test when not encrypted.")
+            return
+
+        siac = SIAClient(
+            HOST, unused_tcp_port_factory(), [SIAAccount(ACCOUNT, key)], event_handler
+        )
+        line = create_test_line(
+            account=ACCOUNT,
+            key=key,
+            code=code,
+            msg_type=msg_type,
+            wrong_event=wrong_event,
+            alter_key=alter_key,
+        )
+
+        _LOGGER.warning("Line to parse: %s", line)
+        event = siac.sia_server.parse_and_check_event(line)
+        _LOGGER.warning("Event received: %s", event)
+
+        assert event.response == response
+        if wrong_event or alter_key:
+            assert isinstance(event, NAKEvent)
+        else:
+            assert event.account == ACCOUNT
+            assert event.code == "RP" if msg_type == "NULL" else code
+
+    def test_accounts_get_set(self, account_list, unused_tcp_port):
+        """Test the account getting and setting."""
+        siac = SIAClient(
+            host="",
+            port=unused_tcp_port,
+            accounts=account_list,
+            function=(lambda ev: print(ev)),
+        )
+        assert siac.accounts == account_list
+        acc_list2 = [
+            SIAAccount(account_id=ACCOUNT, key=KEY, allowed_timeband=(None, None)),
+            SIAAccount(account_id="1112", key=KEY, allowed_timeband=(None, None)),
+        ]
+        siac.accounts = acc_list2
+        assert siac.accounts == acc_list2
+
+    @parametrize_with_cases("sync_func", prefix="sync_")
+    @parametrize_with_cases("sync_client", prefix="sync_")
+    def test_func_errors(
+        self,
+        account_list,
+        unused_tcp_port_factory,
+        sync_client,
+        sync_func,
+    ):
+        """Test the function setting."""
+        error_type = TypeError if sync_client != sync_func else None
+        try:
+            if sync_client:
+                SIAClient(
+                    HOST,
+                    unused_tcp_port_factory(),
+                    account_list,
+                    get_func("sync" if sync_func else "aio"),
+                )
+            else:
+                SIAClientA(
+                    HOST,
+                    unused_tcp_port_factory(),
+                    account_list,
+                    get_func("sync" if sync_func else "aio"),
+                )
+            assert error_type is None
+        except Exception as e:
+            if isinstance(e, error_type):
+                assert True
+            else:
+                assert False
+
+    @parametrize_with_cases("sync", prefix="sync_")
+    async def test_context(self, account_list, unused_tcp_port_factory, sync):
+        """Test the context manager functions."""
+        if sync:
+            with patch.object(SIAClient, "stop") as client:
+                with SIAClient(
+                    HOST,
+                    unused_tcp_port_factory(),
+                    account_list,
+                    function=get_func("sync"),
+                ) as cl:
+                    assert cl.accounts == account_list
+                client.assert_called_once()
+
+        else:
+            with patch.object(SIAClientA, "stop") as client:
+                with patch("asyncio.start_server"):
+                    # test Async
+                    async with SIAClientA(
+                        HOST,
+                        unused_tcp_port_factory(),
+                        account_list,
+                        function=get_func("aio"),
+                    ) as cl:
+                        assert cl.accounts == account_list
+                client.assert_awaited_once()
diff --git a/tests/test_sia_package_cases.py b/tests/test_sia_package_cases.py
new file mode 100644
index 0000000..9ec2461
--- /dev/null
+++ b/tests/test_sia_package_cases.py
@@ -0,0 +1,483 @@
+"""Test cases."""
+from pytest_cases import case
+
+from pysiaalarm import (
+    InvalidAccountFormatError,
+    InvalidAccountLengthError,
+    InvalidKeyFormatError,
+    InvalidKeyLengthError,
+    CommunicationsProtocol,
+    SIAAccount,
+    SIAEvent,
+)
+from pysiaalarm.errors import EventFormatError
+from pysiaalarm.utils import ResponseType
+from pysiaalarm.const import (
+    COUNTER_ACCOUNT,
+    COUNTER_CRC,
+    COUNTER_CODE,
+    COUNTER_FORMAT,
+    COUNTER_TIMESTAMP,
+)
+from tests.test_utils import ACCOUNT, KEY
+
+
+def encrypted_encrypted():
+    """Return a key for a encrypted test."""
+    return KEY
+
+
+def encrypted_unencrypted():
+    """Return None for a unencrypted test."""
+    return None
+
+
+def proto_tcp():
+    """Return TCP protocol."""
+    return CommunicationsProtocol.TCP
+
+
+def proto_udp():
+    """Return UDP protocol."""
+    return CommunicationsProtocol.UDP
+
+
+def sync_sync():
+    """Return True for running a sync/threaded test."""
+    return True
+
+
+def sync_async():
+    """Return False for running a async test."""
+    return False
+
+
+def handler_good():
+    """Return True for a good event handler."""
+    return True
+
+
+def handler_bad():
+    """Return False for a bad event handler."""
+    return False
+
+
+def msg_siadcs():
+    """Test class for Message type SIA-DCS."""
+    return "SIA-DCS"
+
+
+def msg_admcid():
+    """Test class for Message type ADM-CID."""
+    return "ADM-CID"
+
+
+def msg_null():
+    """Test class for Message type SIA-DCS."""
+    return "NULL"
+
+
+def fault_none():
+    """Test class for no fault in the message."""
+    return None
+
+
+def fault_code():
+    """Test class for a code fault in the message."""
+    return COUNTER_CODE
+
+
+def fault_crc():
+    """Test class for a crc fault in the message."""
+    return COUNTER_CRC
+
+
+def fault_format():
+    """Test class for a format fault in the message."""
+    return COUNTER_FORMAT
+
+
+def fault_account():
+    """Test class for a account fault in the message."""
+    return COUNTER_ACCOUNT
+
+
+def fault_timestamp():
+    """Test class for a timestamp fault in the message."""
+    return COUNTER_TIMESTAMP
+
+
+class EventParsing:
+    """Test cases for event parsing.
+
+    Emits these fields: "line, account_id, type, code, error, extended_data_flag"
+
+    """
+
+    def case_bug(self):
+        """Test case from #11."""
+        return (
+            r'9618004D"SIA-DCS"7960L0#123456[#123456|Nri1CL0^VX FRONTE ^]_22:40:48,04-11-2021',
+            "123456",
+            "Closing Report",
+            "CL",
+            None,
+            False,
+        )
+
+    def case_bug2(self):
+        """Test case from SIA #42."""
+        return (
+            r'5670004D"SIA-DCS"8070L0#123456[#123456|Nri0KR1^TAST. 001 ^]_13:49:12,04-13-2021',
+            "123456",
+            "Heat Restoral",
+            "KR",
+            None,
+            False,
+        )
+
+    def case_dc04(self):
+        """Test case DC04 format - NOT SUPPORTED so throws an error."""
+        return (
+            r'<x0A>CE110032"SIA-DCS"9876R579BDFL789ABC#12345A[#12345A|NFA129]_14:12:04,09-25-201',
+            "12345A",
+            None,
+            None,
+            EventFormatError,
+            False,
+        )
+
+    def case_adm_cid(self):
+        """Test case ADM-CID format."""
+        return (
+            r'87CD0037"ADM-CID"9876R579BDFL789ABC#12345A[#12345A|1110 00 129]_14:12:04,09-25-201',
+            "12345A",
+            "Fire Alarm",
+            "FA",
+            None,
+            False,
+        )
+
+    def case_xdata_M(self):
+        """Input a closing report event in SIA DC-09 with M xdata."""
+        return (
+            r'87570078"SIA-DCS"6002L0#AAA[|Nri1/CL501][M0026B9E4268B]_14:12:04,09-25-2019',
+            "AAA",
+            "Closing Report",
+            "CL",
+            None,
+            True,
+        )
+
+    def case_xdata_K(self):
+        """Input a closing report event in SIA DC-09 with K xdata."""
+        return (
+            rf'02310052"SIA-DCS"6002L0#{ACCOUNT}[|Nri1/RP000][KAAAAAAAAAAAAAAAA]_14:12:04,09-25-2019',
+            ACCOUNT,
+            "Automatic Test",
+            "RP",
+            None,
+            True,
+        )
+
+    def case_xdata_X_and_Y(self):
+        """Input a closing report event in SIA DC-09 with K xdata."""
+        return (
+            rf'5B090059"SIA-DCS"0004L0#{ACCOUNT}[#{ACCOUNT}|Nri1/PA501][X006E07.7777777][Y49N06.6666666]_20:45:46,04-22-2021',
+            ACCOUNT,
+            "Panic Alarm",
+            "PA",
+            None,
+            True,
+        )
+
+    # TODO: add tests for different SIA versions (DC-04, DC09, DC09X)
+    def case_encrypted(self):
+        """Input a encrypted line."""
+        return (
+            r'60AB0078"*SIA-DCS"5994L0#AAA[5AB718E008C616BF16F6468033A11326B0F7546CAB230910BCA10E4DEBA42283C436E4F8EFF50931070DDE36D5BB5F0C',
+            "AAA",
+            None,
+            None,
+            None,
+            False,
+        )
+
+    def case_oh(self):
+        """Input a OH event."""
+        return (
+            r"SR0001L0001    006969XX    [ID00000000]",
+            "006969XX",
+            "Automatic Test",
+            "RP",
+            None,
+            False,
+        )
+
+    def case_cl(self):
+        """Input a closing report event in SIA DC-09."""
+        return (
+            r'E5D50078"SIA-DCS"6002L0#AAA[|Nri1/CL501]_14:12:04,09-25-2019',
+            "AAA",
+            "Closing Report",
+            "CL",
+            None,
+            False,
+        )
+
+    def case_op(self):
+        """Input a opening report event."""
+        return (
+            r'90820051"SIA-DCS"4738R0001L0001[#006969|Nri04/OP001NM]',
+            "006969",
+            "Opening Report",
+            "OP",
+            None,
+            False,
+        )
+
+    def case_null(self):
+        """Input a encrypted null event."""
+        return (
+            r'76D80055"*NULL"0000R0L0#AAAB[B4BC8B40D0E6D959D6BEA78E88CC0B2155741A3C44FBB96D476A3E557CAD64D9',
+            "AAAB",
+            None,
+            None,
+            None,
+            False,
+        )
+
+    def case_wa(self):
+        """Input a water alarm event."""
+        return (
+            r'C4160279"SIA-DCS"5268L0#AAA[Nri1/WA000]_08:40:47,07-08-2020',
+            "AAA",
+            "Water Alarm",
+            "WA",
+            None,
+            False,
+        )
+
+    def case_eventformaterror(self):
+        """Input a event format error event."""
+        return (
+            r"this is not a parsable event",
+            None,
+            None,
+            None,
+            EventFormatError,
+            False,
+        )
+
+
+class AccountSetup:
+    """Test cases for key and account errors.
+
+    Emits these fields: "key, account_id, error"
+
+    """
+
+    def case_InvalidKeyFormat(self):
+        """Test invalid key format."""
+        return ("ZZZZZZZZZZZZZZZZ", ACCOUNT, InvalidKeyFormatError)
+
+    def case_InvalidKeyLength_15(self):
+        """Test invalid key length at 15."""
+        return ("158888888888888", ACCOUNT, InvalidKeyLengthError)
+
+    def case_correct_16(self):
+        """Test correct key at 16."""
+        return ("1688888888888888", ACCOUNT, None)
+
+    def case_InvalidKeyLength_23(self):
+        """Test invalid key length at 23."""
+        return (
+            "23888888888888888888888",
+            ACCOUNT,
+            InvalidKeyLengthError,
+        )
+
+    def case_correct_24(self):
+        """Test correct key at 24."""
+        return ("248888888888888888888888", ACCOUNT, None)
+
+    def case_InvalidKeyLength_31(self):
+        """Test invalid key length at 31."""
+        return (
+            "3188888888888888888888888888888",
+            ACCOUNT,
+            InvalidKeyLengthError,
+        )
+
+    def case_correct_32(self):
+        """Test correct at 32."""
+        return ("32888888888888888888888888888888", ACCOUNT, None)
+
+    def case_InvalidAccountLength(self):
+        """Test invalid account length at 2."""
+        return (KEY, "22", InvalidAccountLengthError)
+
+    def case_InvalidAccountFormat(self):
+        """Test invalid account format."""
+        return (KEY, "ZZZ", InvalidAccountFormatError)
+
+    def case_InvalidAccountLength_NoKey(self):
+        """Test invalid account length at 2."""
+        return (None, "22", InvalidAccountLengthError)
+
+    def case_InvalidAccountFormat_NoKey(self):
+        """Test invalid account format."""
+        return (None, "ZZZ", InvalidAccountFormatError)
+
+
+class ParseAndCheckEvent:
+    """Test cases for parse and check event function.
+
+    Emits these fields: "code, alter_key, wrong_event, response"
+
+    """
+
+    def case_rp(self):
+        """Test unencrypted parsing of RP event."""
+        return ("RP", False, False, ResponseType.ACK)
+
+    def case_wa(self):
+        """Test unencrypted parsing of WA event."""
+        return ("WA", False, False, ResponseType.ACK)
+
+    def case_altered_key(self):
+        """Test encrypted parsing of RP event.
+
+        Altered key means the event can be parsed as a SIA Event but the content cannot be decrypted.
+
+        """
+        return ("RP", True, False, ResponseType.NAK)
+
+    def case_wrong_event(self):
+        """Test encrypted parsing of RP event."""
+        return ("RP", False, True, ResponseType.NAK)
+
+    @case(tags="SIA-DCS")
+    def case_non_existent_code(self):
+        """Test parsing for non existing code."""
+        return ("ZX", False, False, ResponseType.DUH)
+
+
+class ToFromDict:
+    """Test the event serialization to and from dict.
+
+    Emits the class and a dict of that class.
+
+    """
+
+    def case_sia_event(self):
+        """Test event from dict."""
+        d_ev = {
+            "full_message": '"SIA-DCS"5268L0#AAA[Nri1/WA000]_08:40:47,07-08-2020',
+            "msg_crc": "C416",
+            "length": "0279",
+            "encrypted": False,
+            "message_type": "SIA-DCS",
+            "receiver": None,
+            "line": "L0",
+            "account": "AAA",
+            "sequence": "5268",
+            "content": "Nri1/WA000]_08:40:47,07-08-2020",
+            "encrypted_content": None,
+            "ti": None,
+            "id": None,
+            "ri": "1",
+            "code": "WA",
+            "message": "000",
+            "x_data": None,
+            "timestamp": 1594197647.0,
+            "event_qualifier": None,
+            "event_type": None,
+            "partition": None,
+            "calc_crc": "E9A4",
+            "extended_data": None,
+            "sia_account": None,
+            "sia_code": {
+                "code": "WA",
+                "type": "Water Alarm",
+                "description": "Water detected at protected premises",
+                "concerns": "Zone or point",
+            },
+        }
+        return SIAEvent, d_ev, False
+
+    def case_sia_event_with_account(self):
+        """Test event from dict."""
+        d_ev = {
+            "full_message": '"SIA-DCS"5268L0#AAA[Nri1/WA000]_08:40:47,07-08-2020',
+            "msg_crc": "C416",
+            "length": "0279",
+            "encrypted": False,
+            "message_type": "SIA-DCS",
+            "receiver": None,
+            "line": "L0",
+            "account": "AAA",
+            "sequence": "5268",
+            "content": "Nri1/WA000]_08:40:47,07-08-2020",
+            "encrypted_content": None,
+            "ti": None,
+            "id": None,
+            "ri": "1",
+            "code": "WA",
+            "message": "000",
+            "x_data": None,
+            "timestamp": 1594197647.0,
+            "event_qualifier": None,
+            "event_type": None,
+            "partition": None,
+            "calc_crc": "E9A4",
+            "extended_data": None,
+            "sia_account": {"account_id": ACCOUNT, "key": KEY},
+            "sia_code": {
+                "code": "WA",
+                "type": "Water Alarm",
+                "description": "Water detected at protected premises",
+                "concerns": "Zone or point",
+            },
+        }
+        return SIAEvent, d_ev, False
+
+    def case_sia_event_with_account_clear(self):
+        """Test event from dict."""
+        d_ev = {
+            "full_message": '"SIA-DCS"5268L0#AAA[Nri1/WA000]_08:40:47,07-08-2020',
+            "msg_crc": "C416",
+            "length": "0279",
+            "encrypted": False,
+            "message_type": "SIA-DCS",
+            "receiver": None,
+            "line": "L0",
+            "account": "AAA",
+            "sequence": "5268",
+            "content": "Nri1/WA000]_08:40:47,07-08-2020",
+            "encrypted_content": None,
+            "ti": None,
+            "id": None,
+            "ri": "1",
+            "code": "WA",
+            "message": "000",
+            "x_data": None,
+            "timestamp": 1594197647.0,
+            "event_qualifier": None,
+            "event_type": None,
+            "partition": None,
+            "calc_crc": "E9A4",
+            "extended_data": None,
+            "sia_account": {"account_id": ACCOUNT, "key": KEY},
+            "sia_code": {
+                "code": "WA",
+                "type": "Water Alarm",
+                "description": "Water detected at protected premises",
+                "concerns": "Zone or point",
+            },
+        }
+        return SIAEvent, d_ev, True
+
+    def case_sia_account(self):
+        """Test account to and from dict."""
+        return SIAAccount, {"account_id": ACCOUNT, "key": KEY}, False
diff --git a/tests/test_utils.py b/tests/test_utils.py
index 070db8d..2d6176f 100644
--- a/tests/test_utils.py
+++ b/tests/test_utils.py
@@ -1,16 +1,137 @@
 """Utils for testing pysiaalarm."""
-from binascii import hexlify, unhexlify
+import logging
+from binascii import hexlify
+from datetime import datetime, timedelta
 
+import random
 from Crypto import Random
 from Crypto.Cipher import AES
 
+from pysiaalarm.utils import (
+    _load_sia_codes,
+)
+from pysiaalarm.const import IV
 
-def create_test_items(key, content):
-    """Create encrypted content."""
-    encrypter = AES.new(
-        key.encode("utf-8"), AES.MODE_CBC, unhexlify("00000000000000000000000000000000")
+logging.basicConfig(level=logging.INFO)
+_LOGGER = logging.getLogger(__name__)
+
+BASIC_CONTENT = f"|Nri<zone>/<code>000]<timestamp>"
+BASE_LINE = f'<type>"<seq>L0#<account>[<content>'
+UNKNOWN_CODE = "ZX"
+
+KEY = "AAAAAAAAAAAAAAAA"
+ACCOUNT = "1111"
+HOST = "127.0.0.1"
+PORT = 7777
+
+ADM_REVERSE_MAP = {"RP": 1602, "WA": 1113}
+
+
+def _get_crypter(key: bytes):
+    """Give back a encrypter/decrypter."""
+    return AES.new(key, AES.MODE_CBC, IV)
+
+
+def crc_calc(msg: str) -> str:
+    """Calculate the CRC of the msg."""
+    crc = 0
+    for letter in str.encode(msg):
+        temp = letter
+        for _ in range(0, 8):
+            temp ^= crc & 1
+            crc >>= 1
+            if (temp & 1) != 0:
+                crc ^= 0xA001
+            temp >>= 1
+    return ("%x" % crc).upper().zfill(4)
+
+
+def _create_padded_message(message):
+    """Pad the message."""
+    fill_size = len(message) + 16 - len(message) % 16
+    return message.zfill(fill_size)
+
+
+def create_random_line(config):
+    """Create a full test line with some random values."""
+    return create_test_line(
+        account="FFFFFFFFF" if random.random() < 0.1 else config["account_id"],
+        key=config["key"],
+        code=UNKNOWN_CODE if random.random() < 0.1 else _get_random_code(),
+        seq=str(random.randint(1000, 9999)),
+        time_offset=timedelta(seconds=random.randint(0, 100)),
+        alter_crc=random.random() < 0.1,
     )
 
+
+def create_line_from_test_case(config, tc):
+    """Create a full test line."""
+    return create_test_line(
+        account="FFFFFFFFF" if tc.get("account", False) else config["account_id"],
+        key=config["key"],
+        code=UNKNOWN_CODE if tc.get("code", False) else _get_random_code(),
+        seq=str(random.randint(1000, 9999)),
+        time_offset=timedelta(seconds=100 if tc.get("timestamp", False) else 0),
+        alter_crc=tc.get("crc", False),
+        wrong_event=tc.get("format", False),
+    )
+
+
+def create_test_line(
+    account,
+    key,
+    code,
+    seq="7654",
+    time_offset=timedelta(0),
+    msg_type="SIA-DCS",
+    alter_crc=False,
+    alter_key=False,
+    wrong_event=False,
+    use_fixed_time=False,
+):
+    """Create a test line, with encrytion if key is supplied."""
+    if wrong_event:
+        return "This is not a SIA Event."
+    timestamp = _get_timestamp(time_offset)
+    if use_fixed_time:
+        timestamp = "_16:04:02,07-09-2020"
+    if key:
+        if alter_key:
+            _LOGGER.debug("Old key: %s", key)
+            key = key[:-1] + str(int(key[-1], 16) - 1)
+            _LOGGER.debug("New key: %s", key)
+    line = _construct_string(msg_type, seq, account, code, timestamp, key)
+    if alter_crc:
+        crc = ("%04x" % random.randrange(16 ** 4)).upper()
+    else:
+        crc = crc_calc(line)
+    leng = str(int(str(len(line)), 16)).zfill(4)
+    return rf"{crc}{leng}{line}"
+
+
+def _construct_string(msg_type, seq, account, code, timestamp, key=None):
+    """Construct the string based on the inputs."""
+    return f'"{"*" if key else ""}{msg_type}"{seq}L0#{account}[{_construct_content(msg_type, "0" if code == "RP" else "1", code, timestamp, key)}'
+
+
+def _construct_content(msg_type, zone, code, timestamp, key=None):
+    """Construct the content of the message."""
+    cont = f"]{timestamp}"
+    if msg_type == "SIA-DCS":
+        cont = f"|Nri{zone}/{code}000" + cont
+    if msg_type == "ADM-CID":
+        # z = f"{int(zone):03d}"
+        cont = f"|{ADM_REVERSE_MAP.get(code)} 00 {int(zone):03d}" + cont
+    if key:
+        return _encrypt_content(key, cont)
+    return cont
+
+
+def _encrypt_content(key, content):
+    """Create encrypted content."""
+    if not isinstance(key, bytes):
+        key = key.encode("utf-8")
+    encrypter = _get_crypter(key)
     extra = len(content) % 16
     unencrypted = (16 - extra) * "0" + content
     return (
@@ -18,3 +139,14 @@ def create_test_items(key, content):
         .decode(encoding="utf-8")
         .upper()
     )
+
+
+def _get_timestamp(timed: timedelta) -> str:
+    """Create a timestamp in the right format."""
+    return (datetime.utcnow() - timed).strftime("_%H:%M:%S,%m-%d-%Y")
+
+
+def _get_random_code() -> str:
+    """Get a random code from all codes."""
+    codes = [code for code in _load_sia_codes()]
+    return random.choice(codes)
diff --git a/tests/unencrypted_config.json b/tests/unencrypted_config.json
index cbffe16..7dff213 100644
--- a/tests/unencrypted_config.json
+++ b/tests/unencrypted_config.json
@@ -1,5 +1,5 @@
 {
-    "host": "localhost",
+    "host": "",
     "port": 8125,
     "account_id": "AAA",
     "key": ""