Repository: d-Rickyy-b/pyBrematic (language: python)

Issue:
Check if it's needed to pass the gateway instance to a device implementation
At the moment we are using the information about the gateway inside the device class to calculate the proper gateway header etc. From a software design PoV (abstraction) this is bad, because the device should not care about what gateway is being used to control it.

https://github.com/d-Rickyy-b/pyBrematic/blob/fd7e45e8eb9ec83cf948562800b09e1569e5f6b9/pyBrematic/devices/brennenstuhl/RCS1000N.py#L72-L77

Now since I am looking closely at the PowerSwitch code and seeing that they have implemented it the same way, I am very curious if it would work the same by extracting this code into the gateway class.

While checking the PowerSwitch code I also noticed, that several header fields are set depending on the device itself in combination with a certain gateway. Now I am not sure if those variables are exchangeable and changing them wouldn't mess up everything, so I am very cautious about this issue.

Example:
```
$ grep -r "sSpeedConnAir = \""
bat/RC3500_A_IP44_DE.java:    private String sSpeedConnAir = "14";
bat/RC_AAA1000_A_IP44_Outdoor.java:    private String sSpeedConnAir = "14";
brennenstuhl/RCS1000NComfort.java:    private String sSpeedConnAir = "14";
brennenstuhl/RCS1044NComfort.java:    private String sSpeedConnAir = "14";
elro/AB440D_200W.java:    private String sSpeedConnAir = "14";
elro/AB440D_300W.java:    private String sSpeedConnAir = "14";
elro/AB440ID.java:    private String sSpeedConnAir = "14";
elro/AB440L.java:    private String sSpeedConnAir = "14";
elro/AB440S.java:    private String sSpeedConnAir = "14";
elro/AB440WD.java:    private String sSpeedConnAir = "14";
intertechno/CMR1000.java:    private String sSpeedConnAir = "140";
intertechno/IT1500.java:    private String sSpeedConnAir = "140";
intertechno/ITR1500.java:    private String sSpeedConnAir = "140";
mumbi/m_FS300.java:    private String sSpeedConnAir = "14";
pollin_electronic/Set2605.java:    private String sSpeedConnAir = "14";
rev/Ritter.java:    private String sSpeedConnAir = "14";
rev/Telecontrol.java:    private String sSpeedConnAir = "16";
universal/HX2262_Comp.java:    private String sSpeedConnAir = "140";
vivanco/FSS31000W.java:    private String sSpeedConnAir = "14";
```

And
```
$ grep -r "headITGW = \""
bat/RC3500_A_IP44_DE.java:    private String headITGW = "0,0,10,11200,350,26,0,";
bat/RC_AAA1000_A_IP44_Outdoor.java:    private String headITGW = "0,0,10,11200,350,26,0,";
brennenstuhl/RCS1000NComfort.java:    private String headITGW = "0,0,10,11200,350,26,0,";
brennenstuhl/RCS1044NComfort.java:    private String headITGW = "0,0,10,11200,350,26,0,";
elro/AB440D_200W.java:    private String headITGW = "0,0,10,11200,350,26,0,";
elro/AB440D_300W.java:    private String headITGW = "0,0,10,11200,350,26,0,";
elro/AB440ID.java:    private String headITGW = "0,0,10,11200,350,26,0,";
elro/AB440L.java:    private String headITGW = "0,0,10,11200,350,26,0,";
elro/AB440S.java:    private String headITGW = "0,0,10,11200,350,26,0,";
elro/AB440WD.java:    private String headITGW = "0,0,10,11200,350,26,0,";
intertechno/CMR1000.java:    private String headITGW = "0,0,6,11125,89,26,0,";
intertechno/IT1500.java:    private String headITGW = "0,0,6,11125,89,26,0,";
intertechno/ITR1500.java:    private String headITGW = "0,0,6,11125,89,26,0,";
mumbi/m_FS300.java:    private String headITGW = "0,0,10,11200,350,26,0,";
pollin_electronic/Set2605.java:    private String headITGW = "0,0,10,11200,350,26,0,";
rev/Ritter.java:    private String headITGW = "0,0,10,11200,350,26,0,";
rev/Telecontrol.java:    private String headITGW = "0,0,10,11200,350,26,0,";
universal/HX2262_Comp.java:    private String headITGW = "0,0,6,11125,89,26,0,";
vivanco/FSS31000W.java:    private String headITGW = "0,0,6,11200,350,26,0,";
```


Discussion / hints:

Fix patch:
diff --git a/.github/workflows/python-pypi-publish.yml b/.github/workflows/python-pypi-publish.yml
index d4ba985..cec8a13 100644
--- a/.github/workflows/python-pypi-publish.yml
+++ b/.github/workflows/python-pypi-publish.yml
@@ -21,7 +21,17 @@ jobs:
     - name: Install dependencies
       run: |
         python -m pip install --upgrade pip
-        pip install setuptools wheel twine
+        pip install setuptools wheel twine flake8 pytest
+        if [ -f requirements.txt ]; then pip install -r requirements.txt; fi
+    - name: Lint with flake8
+      run: |
+        # stop the build if there are Python syntax errors or undefined names
+        flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics
+        # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide
+        flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics
+    - name: Test with pytest
+      run: |
+        pytest
     - name: Build and publish
       env:
         TWINE_USERNAME: ${{ secrets.PYPI_USERNAME }}
diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
index 0000000..db4d465
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,50 @@
+# Changelog
+
+All notable changes to this project will be documented in this file.
+
+The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres
+to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
+
+## [Unreleased]
+
+## [1.2.0] - 2021-01-05
+### Added
+
+- Implement CMR300, CMR500 and ITR300 devices ([1a9082a](https://github.com/d-Rickyy-b/pyBrematic/commit/1a9082a67e5c025c2dd7e0b8d2fc47108df204c6), [740ecf1](https://github.com/d-Rickyy-b/pyBrematic/commit/82ff04c5f69b778ea27f8f105f54d9fae43b03ce), [82ff04c](https://github.com/d-Rickyy-b/pyBrematic/commit/82ff04c5f69b778ea27f8f105f54d9fae43b03ce))
+- Add methods to generate system and unit code for intertechno devices ([380ba42](https://github.com/d-Rickyy-b/pyBrematic/commit/380ba42d3d811c1aee74b93c83e74c88402b27c8))
+- Implement ConnAir gateway ([40ac9e9](https://github.com/d-Rickyy-b/pyBrematic/commit/40ac9e90e998c55a74a4f7ad2688bda97ba92d66))
+
+## [1.1.0] - 2020-12-27
+
+### Added
+
+- Implement support for ITR3500 ([3fc05b1](https://github.com/d-Rickyy-b/pyBrematic/commit/3fc05b1c9683c72dd1291dbb353f30bed3e040dd))
+- Support for local storage needed for autopair ([ca29013](https://github.com/d-Rickyy-b/pyBrematic/commit/ca29013edd693855628231d05f0ef9c6aac8f4c8))
+- Implement Intertechno autopair devices such as ITL500 ([0fd8123](https://github.com/d-Rickyy-b/pyBrematic/commit/0fd812356d573c7cd332c75071c3f53e5e8f59e1))
+- Add Action enum to easily choose the action that should be executed ([c983641](https://github.com/d-Rickyy-b/pyBrematic/commit/c98364196b69230c6a8dd48c9d805ebfdf66f97e))
+
+### CI
+
+- Implement GitHub Actions workflow for building and deploying packages on each release (PS: might still be a bit buggy at the moment)
+
+## [1.0.0] - 2018-07-07
+
+### Added
+
+- Implemented Intertechno gateway ([cb13362](https://github.com/d-Rickyy-b/pyBrematic/commit/cb1336296e82e0dbb34c0abb5f9fe4a7f2bf3c4f))
+- Implement CMR1000 and PAR1500 ([40427d9](https://github.com/d-Rickyy-b/pyBrematic/commit/40427d9d70abb94832a62715ab885103f1a8b20a))
+- Implement AB440SA ([7d61f49](https://github.com/d-Rickyy-b/pyBrematic/commit/7d61f49214938118486d3c20d52b489d2585e148))
+
+### Fixed
+
+- Fix get_signal function for RCR1000N device ([90001d7](https://github.com/d-Rickyy-b/pyBrematic/commit/90001d7bd62c15cca066343b8b649ea1f465daa7))
+
+## [0.0.1] - 2018-05-10
+
+Initial release with a single gateway and outlet.
+
+[unreleased]: https://github.com/d-Rickyy-b/pyBrematic/compare/v1.2.0...HEAD
+[1.2.0]: https://github.com/d-Rickyy-b/pyBrematic/compare/v1.1.0...v1.2.0
+[1.1.0]: https://github.com/d-Rickyy-b/pyBrematic/compare/v1.0.0...v1.1.0
+[1.0.0]: https://github.com/d-Rickyy-b/pyBrematic/compare/v0.0.1...v1.0.0
+[0.0.1]: https://github.com/d-Rickyy-b/pyBrematic/tree/v0.0.1
diff --git a/README.md b/README.md
index 1eb004c..48df7ca 100644
--- a/README.md
+++ b/README.md
@@ -1,23 +1,60 @@
+# pyBrematic
+
 [![Build Status](https://travis-ci.com/d-Rickyy-b/pyBrematic.svg?branch=master)](https://travis-ci.com/d-Rickyy-b/pyBrematic)
 [![PyPI version](https://badge.fury.io/py/pyBrematic.svg)](https://pypi.org/project/pyBrematic)
 [![Coverage Status](https://coveralls.io/repos/github/d-Rickyy-b/pyBrematic/badge.svg?branch=master)](https://coveralls.io/github/d-Rickyy-b/pyBrematic?branch=master)
 
-# pyBrematic
-This project offers a home for controlling your remote power outlets (and potentially other stuff) with the Python programming language. With the help of the community we might get other devices working aswell.
+The topic "smart home" or "home automation" in particular has become increasingly popular throughout the last few years. While many manufacturers are relying
+on cloud infrastructure, there are some that produce local-only devices, using the 433 MHz ISM band.
+
+The python module "pyBrematic" enables you to control and automate your 433 MHz remote power outlets (and other switches/dimmers) with the Python programming
+language. All you need for this is a supported 433 MHz network gateway, such as the `Intertechno ITGW-433`, the `Brematic GWY 433` (or `CONNAIR 433`, which is
+basically the same as the Brematic one).
+
+With the help of the community we might get other devices working as well.
 
 ### Installation
-You can simply install the module via [pip](https://de.wikipedia.org/wiki/Pip_(Python)) like this:
+
+This module is available on pypi and hence can be downloaded via pip like this:
 
 `pip install pyBrematic`
 
-If you have multiple versions of Python installed, make sure to use the Python 3 package manager:
+And if you are having issues with installing the package, try to use the `--user` switch,
+to [install it to your home directory](https://stackoverflow.com/questions/42988977/what-is-the-purpose-pip-install-user).  
+PyBrematic has no external dependencies. Only Python versions >= 3.5 are supported.
+
+### Example usage
 
-`pip3 install pyBrematic`
+To check out how to use the module, go to the [example file](https://github.com/d-Rickyy-b/pyBrematic/blob/master/pyBrematic/example/example.py). There you'll
+find an example configuration of how to use the module.
 
-And if you are having issues with installing the package, try to use the `--user` switch, to [install it to your home directory](https://stackoverflow.com/questions/42988977/what-is-the-purpose-pip-install-user).
+```python
+from pyBrematic import Action
+from pyBrematic.devices.brennenstuhl import RCS1000N
+from pyBrematic.devices.intertechno import ITR3500, calc_system_and_unit_code
+from pyBrematic.gateways import BrennenstuhlGateway
 
-### Example usage
-To check out how to use the module, go to the [example file](https://github.com/d-Rickyy-b/pyBrematic/blob/master/pyBrematic/example/example.py) where I wrote a little example script, to show how to use the module.
+# Set your system and unit codes
+system_code = "11110"  # DIPs 1-4 are in the 'up' position, 5 is 'down'
+unit_code = "10000"  # DIP A is in the 'up' position, B-E are 'down'
+
+# Create a new device with the specified codes
+desk_lamp = RCS1000N(system_code, unit_code)
+
+# For Intertechno devices you can also use the following methods to get the code
+# via master/slave (letter/number) notation. Allowed values are "A-P" and "1-16".
+led_system_code, led_unit_code = calc_system_and_unit_code("A3")
+led_strip = ITR3500(led_system_code, led_unit_code)
+
+# Create a new gateway located at the specified IP
+gw = BrennenstuhlGateway("192.168.178.9")
+
+# Send the request and pass it the device and the action (on/off)
+gw.send_request(desk_lamp, Action.ON)
+gw.send_request(desk_lamp, Action.OFF)
+```
 
 ### Important notice
-Since all data packets are sent to the gateways via [UDP](https://en.wikipedia.org/wiki/User_Datagram_Protocol), it cannot be guaranteed, that all requests will be transmitted to the gateway. For critical purposes you cannot rely on sending the signal once.
+
+Since all data packets are sent to the gateways via [UDP](https://en.wikipedia.org/wiki/User_Datagram_Protocol), it cannot be guaranteed, that all requests
+will be received by the gateway. For critical purposes you cannot rely on sending the signal once.
diff --git a/pyBrematic/__init__.py b/pyBrematic/__init__.py
index ee21da7..42d7f2e 100644
--- a/pyBrematic/__init__.py
+++ b/pyBrematic/__init__.py
@@ -1,5 +1,4 @@
-from .devices.device import Device
-from .gateways.brennenstuhl_gateway import BrennenstuhlGateway
 from .gateways.gateway import Gateway
+from .action import Action
 
-__all__ = ('Device', 'Gateway', 'BrennenstuhlGateway')
+__all__ = ["Gateway", "Action"]
diff --git a/pyBrematic/devices/action.py b/pyBrematic/action.py
similarity index 100%
rename from pyBrematic/devices/action.py
rename to pyBrematic/action.py
diff --git a/pyBrematic/devices/__init__.py b/pyBrematic/devices/__init__.py
index 48b1166..8f2fa25 100644
--- a/pyBrematic/devices/__init__.py
+++ b/pyBrematic/devices/__init__.py
@@ -1,7 +1,9 @@
 # -*- coding: utf-8 -*-
 
-from .action import Action
 from .autopairdevice import AutoPairDevice
 from .device import Device
 
-__all__ = ['Device', 'AutoPairDevice', 'Action']
+# Backwards compatibility
+from pyBrematic.action import Action
+
+__all__ = ["Device", "AutoPairDevice", "Action"]
diff --git a/pyBrematic/devices/autopairdevice.py b/pyBrematic/devices/autopairdevice.py
index a89458d..8731c7f 100644
--- a/pyBrematic/devices/autopairdevice.py
+++ b/pyBrematic/devices/autopairdevice.py
@@ -1,7 +1,7 @@
 # -*- coding: utf-8 -*-
 """Device class representing a remote-controllable receiver"""
 from pyBrematic.utils import Storage
-from .action import Action
+from pyBrematic.action import Action
 
 
 class AutoPairDevice(object):
@@ -22,6 +22,16 @@ def __init__(self, device_id, seed=None):
         storage = Storage()
         self.seed = seed or storage.get_seed(device_id)
 
-    def get_signal(self, gateway, action):
+    def get_signal(self, action):
         """Returns a signal which triggers a device to execute the intended action"""
         raise NotImplementedError("Subclasses must implement this method!")
+
+    @staticmethod
+    def join_list(lst, delimeter=","):
+        """Join a list together with a certain delimeter
+
+        :param lst: The list to join
+        :param delimeter: The delimeter to use to join the list elements
+        :return:
+        """
+        return delimeter.join(lst)
diff --git a/pyBrematic/devices/brennenstuhl/RCS1000N.py b/pyBrematic/devices/brennenstuhl/RCS1000N.py
index 6d34a09..c4d4b3a 100644
--- a/pyBrematic/devices/brennenstuhl/RCS1000N.py
+++ b/pyBrematic/devices/brennenstuhl/RCS1000N.py
@@ -1,7 +1,7 @@
 # -*- coding: utf-8 -*-
 
-from pyBrematic.devices import Device, Action
-from pyBrematic.gateways import BrennenstuhlGateway, IntertechnoGateway
+from pyBrematic.action import Action
+from pyBrematic.devices import Device
 
 
 # SYSTEM-CODE | unit code
@@ -17,71 +17,49 @@ class RCS1000N(Device):
     # TXP:0,0,10,5600,350,25,<bit_low>,<enc_system_code>,<enc_unit_code>,<bit_high>,1,1,16;
     # Check http://luckow.org/archive/brennenstuhl.html (german) for more info about this.
     # Parameters for the requests. Only change if you have good reasons for it
-    sRepeat = 10
-    sPauseBS = 5600
-    sPauseIT = 11200
-    sTune = 350
-    sBaudBS = 25
-    sBaudIT = 26
-    sSpeedBS = 16
-    sSpeedIT = 32
+    repeat = 10
+    tune = 350
     txversion = 1
 
-    headBSGW = "TXP:0,0,{},{},{},{},".format(sRepeat, sPauseBS, sTune, sBaudBS)
-    tailBSGW = "{},{};".format(txversion, sSpeedBS)
+    pause_BS = 5600
+    pause_IT = 11200
 
-    headITGW = "0,0,{},{},{},{},0,".format(sRepeat, sPauseIT, sTune, sBaudIT)
-    tailITGW = "{},{},0".format(txversion, sSpeedIT)
+    baud_BS = 25
+    baud_IT = 26
+
+    speed_BS = 16
+    speed_IT = 32
 
     # Values of high and low bits (binary -> encoded)
     # bit_low:  0 -> 1 | bit_high: 1 -> 3
-    lo = "1,"
-    hi = "3,"
+    lo = "1"
+    hi = "3"
 
-    seq_low = lo + hi + lo + hi
-    seq_high = lo + hi + hi + lo
+    seq_low = [lo, hi, lo, hi]
+    seq_high = [lo, hi, hi, lo]
 
     # These sequences are the commands for switching a device on or off
     # On:  01011 -> encoded: 13133
     # Off: 10010 -> encoded: 31131
     on = seq_low + seq_high
     off = seq_high + seq_low
+    supported_actions = {Action.ON: on, Action.OFF: off}
 
     def __init__(self, system_code, unit_code):
         super().__init__(system_code, unit_code)
 
-    # Method for encoding the system_code or unit_code from binary to a gateway-readable format
-    @staticmethod
-    def encode(code, seq_low, seq_high):
-        encoded_msg = ""
-        for bit in code:
-            if bit == "0":
-                encoded_msg += seq_high
-            elif bit == "1":
-                encoded_msg += seq_low
-            else:
-                raise ValueError("Invalid value in system_code or unit_code!")
-        return encoded_msg
-
-    def get_signal(self, gateway, action):
+    def get_signal(self, action):
         """Returns a signal which triggers a device to execute the intended action"""
         # Encoding the system_code and unit_code
         system_msg = self.encode(self.system_code, self.seq_low, self.seq_high)
         unit_msg = self.encode(self.unit_code, self.seq_low, self.seq_high)
 
-        if isinstance(gateway, BrennenstuhlGateway):
-            head = self.headBSGW
-            tail = self.tailBSGW
-        elif isinstance(gateway, IntertechnoGateway):
-            head = self.headITGW
-            tail = self.tailITGW
+        action_signal = self.supported_actions.get(action)
 
         # Build the payload of the UDP package depending on the action.
-        if action == Action.ON:
-            data = head + system_msg + unit_msg + self.on + tail
-        elif action == Action.OFF:
-            data = head + system_msg + unit_msg + self.off + tail
-        else:
+        if not action_signal:
             raise ValueError("Value of 'action' isn't valid!")
 
-        return data
+        data = system_msg + unit_msg + action_signal
+
+        return self.join_list(data)
diff --git a/pyBrematic/devices/brennenstuhl/README.md b/pyBrematic/devices/brennenstuhl/README.md
new file mode 100644
index 0000000..59f180d
--- /dev/null
+++ b/pyBrematic/devices/brennenstuhl/README.md
@@ -0,0 +1,8 @@
+# Supported Brennenstuhl devices
+|Model|Type|IC|Tested|
+|---|---|---|---|
+|RCR1000N|Plug switch|HX2262 / HX2272|Yes|
+|RCS1000N|Plug switch|HX2262 / HX2272|Yes|
+
+# Useful references
+- https://github.com/nicolashimmelmann/ESP_GWY433
diff --git a/pyBrematic/devices/brennenstuhl/__init__.py b/pyBrematic/devices/brennenstuhl/__init__.py
index 42d41f1..1110584 100644
--- a/pyBrematic/devices/brennenstuhl/__init__.py
+++ b/pyBrematic/devices/brennenstuhl/__init__.py
@@ -3,4 +3,4 @@
 from .RCR1000N import RCR1000N
 from .RCS1000N import RCS1000N
 
-__all__ = ['RCR1000N', 'RCS1000N']
+__all__ = ["RCR1000N", "RCS1000N"]
diff --git a/pyBrematic/devices/device.py b/pyBrematic/devices/device.py
index 8647a14..5c33fcd 100644
--- a/pyBrematic/devices/device.py
+++ b/pyBrematic/devices/device.py
@@ -1,6 +1,6 @@
 # -*- coding: utf-8 -*-
 """Device class representing a remote-controlled receiver"""
-from .action import Action
+from pyBrematic.action import Action
 
 
 class Device(object):
@@ -11,6 +11,29 @@ def __init__(self, system_code, unit_code):
         self.system_code = system_code
         self.unit_code = unit_code
 
-    def get_signal(self, gateway, action):
+    def get_signal(self, action):
         """Returns a signal which triggers a device to execute the intended action"""
         raise NotImplementedError("Subclasses must implement this method!")
+
+    @staticmethod
+    def encode(code, seq_low, seq_high):
+        """Method for encoding the system_code or unit_code from binary to a gateway-readable format"""
+        encoded_msg = []
+        for bit in code:
+            if bit == "0":
+                encoded_msg += seq_high
+            elif bit == "1":
+                encoded_msg += seq_low
+            else:
+                raise ValueError("Invalid value in system_code or unit_code!")
+        return encoded_msg
+
+    @staticmethod
+    def join_list(lst, delimeter=","):
+        """Join a list together with a certain delimeter
+
+        :param lst: The list to join
+        :param delimeter: The delimeter to use to join the list elements
+        :return:
+        """
+        return delimeter.join(lst)
diff --git a/pyBrematic/devices/elro/AB440SA.py b/pyBrematic/devices/elro/AB440SA.py
index 375dcea..29cecbd 100644
--- a/pyBrematic/devices/elro/AB440SA.py
+++ b/pyBrematic/devices/elro/AB440SA.py
@@ -1,69 +1,47 @@
 # -*- coding: utf-8 -*-
 
-from pyBrematic.devices import Device, Action
-from pyBrematic.gateways import BrennenstuhlGateway, IntertechnoGateway
+from pyBrematic.action import Action
+from pyBrematic.devices import Device
 
 
 class AB440SA(Device):
-    """Device class for the ELRO AB440SA  remote outlet"""
-    lo = "1,"
-    hi = "3,"
-    seq_low = lo + hi + lo + hi
-    seq_high = lo + hi + hi + lo
-
-    sRepeat = 10
-    sPauseBS = 5600
-    sPauseIT = 11200
-    sTune = 350
-    sBaudBS = 25
-    sBaudIT = 26
-    sSpeedBS = 14
-    sSpeedIT = 32
+    """Device class for the ELRO AB440SA remote outlet"""
+    repeat = 10
+    tune = 350
     txversion = 1
 
-    headITGW = "0,0,{},{},{},{},0,".format(sRepeat, sPauseIT, sTune, sBaudIT)
-    tailITGW = "{},{},0".format(txversion, sSpeedIT)
+    pause_BS = 5600
+    pause_IT = 11200
+
+    baud_BS = 25
+    baud_IT = 26
+
+    speed_BS = 14
+    speed_IT = 32
 
-    headBSGW = "TXP:0,0,{},{},{},{},".format(sRepeat, sPauseBS, sTune, sBaudBS)
-    tailBSGW = "{},{};".format(txversion, sSpeedBS)
+    lo = "1"
+    hi = "3"
+    seq_low = [lo, hi, lo, hi]
+    seq_high = [lo, hi, hi, lo]
 
     on = seq_low + seq_high
     off = seq_high + seq_low
+    supported_actions = {Action.ON: on, Action.OFF: off}
 
     def __init__(self, system_code, unit_code):
         super().__init__(system_code, unit_code)
 
-    @staticmethod
-    def encode(code, seq_low, seq_high):
-        """Method for encoding the system_code or unit_code from binary to a gateway-readable format"""
-        encoded_msg = ""
-        for bit in code:
-            if bit == "0":
-                encoded_msg += seq_high
-            elif bit == "1":
-                encoded_msg += seq_low
-            else:
-                raise ValueError("Invalid value in system_code or unit_code!")
-        return encoded_msg
-
-    def get_signal(self, gateway, action):
+    def get_signal(self, action):
         """Returns a signal which triggers a device to execute the intended action"""
         system_msg = self.encode(self.system_code, self.seq_low, self.seq_high)
         unit_msg = self.encode(self.unit_code, self.seq_low, self.seq_high)
 
-        if isinstance(gateway, BrennenstuhlGateway):
-            head = self.headBSGW
-            tail = self.tailBSGW
-        elif isinstance(gateway, IntertechnoGateway):
-            head = self.headITGW
-            tail = self.tailITGW
+        action_signal = self.supported_actions.get(action)
 
         # Build the payload of the UDP package depending on the action.
-        if action == Action.ON:
-            data = head + system_msg + unit_msg + self.on + tail
-        elif action == Action.OFF:
-            data = head + system_msg + unit_msg + self.off + tail
-        else:
+        if not action_signal:
             raise ValueError("Value of 'action' isn't valid!")
 
-        return data
+        data = system_msg + unit_msg + action_signal
+
+        return self.join_list(data)
diff --git a/pyBrematic/devices/elro/__init__.py b/pyBrematic/devices/elro/__init__.py
index d0fb93b..78ab670 100644
--- a/pyBrematic/devices/elro/__init__.py
+++ b/pyBrematic/devices/elro/__init__.py
@@ -2,4 +2,4 @@
 
 from .AB440SA import AB440SA
 
-__all__ = ['AB440SA']
+__all__ = ["AB440SA"]
diff --git a/pyBrematic/devices/intertechno/CMR1000.py b/pyBrematic/devices/intertechno/CMR1000.py
index cc93261..2194579 100644
--- a/pyBrematic/devices/intertechno/CMR1000.py
+++ b/pyBrematic/devices/intertechno/CMR1000.py
@@ -1,75 +1,51 @@
 # -*- coding: utf-8 -*-
-from pyBrematic.devices.device import Device
-from pyBrematic.exceptions import GatewayNotSupportedException
-from pyBrematic.gateways import BrennenstuhlGateway, IntertechnoGateway
+from pyBrematic.devices.intertechno import IntertechnoDevice
+from pyBrematic.action import Action
 
 
-class CMR1000(Device):
+class CMR1000(IntertechnoDevice):
     """Device class for the Intertechno CMR-1000 remote outlet"""
-    sRepeat = 6
-    sPause = 11125
-    sTune = 89
-    sBaudBS = 25
-    sBaudIT = 26
-    sSpeedBS = 140
-    sSpeedIT = 125
+    repeat = 6
+    tune = 89
     txversion = 1
 
-    headBSGW = "TXP:0,0,{},{},{},{},".format(sRepeat, sPause, sTune, sBaudBS)
-    tailBSGW = "{},{};".format(txversion, sSpeedBS)
+    pause_BS = 11125
+    pause_IT = 11125
 
-    headITGW = "0,0,{},{},{},{},0,".format(sRepeat, sPause, sTune, sBaudIT)
-    tailITGW = "{},{},0".format(txversion, sSpeedIT)
+    baud_BS = 25
+    baud_IT = 26
+
+    speed_BS = 140
+    speed_IT = 125
 
     # Values of high and low bits (binary -> encoded)
-    # bit_low:  0 -> 1 | bit_high: 1 -> 3
-    lo = "4,"
-    hi = "12,"
+    # bit_low:  0 -> 4 | bit_high: 1 -> 12
+    lo = "4"
+    hi = "12"
 
-    seq_low = lo + hi + lo + hi
-    seq_fl = lo + hi + hi + lo
+    seq_low = [lo, hi, lo, hi]
+    seq_fl = [lo, hi, hi, lo]
 
     on = seq_fl + seq_fl
     off = seq_fl + seq_low
     additional = seq_low + seq_fl
+    supported_actions = {Action.ON: on, Action.OFF: off}
 
     def __init__(self, system_code, unit_code):
         super().__init__(system_code, unit_code)
 
-    # Method for encoding the system_code or unit_code from binary to a gateway-readable format
-    @staticmethod
-    def encode(code, seq_low, seq_high):
-        encoded_msg = ""
-        for bit in code:
-            if bit == "0":
-                encoded_msg += seq_low
-            elif bit == "1":
-                encoded_msg += seq_high
-            else:
-                raise ValueError("Invalid value in system_code or unit_code!")
-        return encoded_msg
-
-    def get_signal(self, gateway, action):
+    def get_signal(self, action):
         """Returns a signal which triggers a device to execute the intended action"""
         # Encoding the system_code and unit_code
-        system_msg = self.encode(self.system_code, self.seq_low, self.seq_fl)
-        unit_msg = self.encode(self.unit_code, self.seq_low, self.seq_fl)
+        system_msg = self.encode(self.system_code, self.seq_fl, self.seq_low)
+        unit_msg = self.encode(self.unit_code, self.seq_fl, self.seq_low)
 
-        if isinstance(gateway, BrennenstuhlGateway):
-            head = self.headBSGW
-            tail = self.tailBSGW
-        elif isinstance(gateway, IntertechnoGateway):
-            head = self.headITGW
-            tail = self.tailITGW
-        else:
-            raise GatewayNotSupportedException
+        action_signal = self.supported_actions.get(action)
 
         # Build the payload of the UDP package depending on the action.
-        if action == self.ACTION_ON:
-            data = head + system_msg + unit_msg + self.additional + self.on + tail
-        elif action == self.ACTION_OFF:
-            data = head + system_msg + unit_msg + self.additional + self.off + tail
-        else:
+        if not action_signal:
             raise ValueError("Value of 'action' isn't valid!")
 
-        return data
+        data = system_msg + unit_msg + self.additional + action_signal
+
+        return self.join_list(data)
diff --git a/pyBrematic/devices/intertechno/CMR300.py b/pyBrematic/devices/intertechno/CMR300.py
new file mode 100644
index 0000000..b235919
--- /dev/null
+++ b/pyBrematic/devices/intertechno/CMR300.py
@@ -0,0 +1,12 @@
+# -*- coding: utf-8 -*-
+# Reference: https://github.com/Power-Switch/PowerSwitch_Android/blob/master/Smartphone/src/main/java/eu/power_switch/obj/receiver/device/intertechno/CMR300.java
+# Since this is a dimmer, I am not sure if this code also supports dimming or only switchig.
+
+from .CMR1000 import CMR1000
+
+
+class CMR300(CMR1000):
+    """Device class for the Intertechno CMR-300 dimmer"""
+
+    def __init__(self, system_code, unit_code):
+        super().__init__(system_code, unit_code)
diff --git a/pyBrematic/devices/intertechno/CMR500.py b/pyBrematic/devices/intertechno/CMR500.py
new file mode 100644
index 0000000..3f562bb
--- /dev/null
+++ b/pyBrematic/devices/intertechno/CMR500.py
@@ -0,0 +1,12 @@
+# -*- coding: utf-8 -*-
+# Reference: https://github.com/Power-Switch/PowerSwitch_Android/blob/master/Smartphone/src/main/java/eu/power_switch/obj/receiver/device/intertechno/CMR500.java
+# Since this is a dimmer, I am not sure if this code also supports dimming or only switchig.
+
+from .CMR1000 import CMR1000
+
+
+class CMR500(CMR1000):
+    """Device class for the Intertechno CMR-500 switch"""
+
+    def __init__(self, system_code, unit_code):
+        super().__init__(system_code, unit_code)
diff --git a/pyBrematic/devices/intertechno/ITL500.py b/pyBrematic/devices/intertechno/ITL500.py
index 1185481..00cc4c7 100644
--- a/pyBrematic/devices/intertechno/ITL500.py
+++ b/pyBrematic/devices/intertechno/ITL500.py
@@ -1,23 +1,29 @@
 # -*- coding: utf-8 -*-
-from pyBrematic.devices import AutoPairDevice, Action
-from pyBrematic.exceptions import GatewayNotSupportedException
-from pyBrematic.gateways import BrennenstuhlGateway, IntertechnoGateway
+from pyBrematic.action import Action
+from pyBrematic.devices import AutoPairDevice
 from pyBrematic.utils import Rand
 
 
 class ITL500(AutoPairDevice):
     """Device class for the Intertechno ITL-500 sun blind switch"""
-    headBSGW = "TXP:0,0,5,10976,98,66,3,29,"
-    tailBSGW = "3,126"
+    repeat = 5
+    tune = 98
+    txversion = 3
 
-    headITGW = "0,0,5,10976,98,67,0,3,29,"
-    tailITGW = "3,112,0"
+    pause_BS = 10976
+    pause_IT = 10976
 
-    lo = "3,"
-    hi = "15,"
+    baud_BS = 66
+    baud_IT = 67
 
-    seq_low = lo + lo + lo + hi
-    seq_fl = lo + hi + lo + lo
+    speed_IT = 112
+    speed_BS = 126
+
+    lo = "3"
+    hi = "15"
+
+    seq_low = [lo, lo, lo, hi]
+    seq_fl = [lo, hi, lo, lo]
 
     on = seq_low + seq_low + seq_fl
     off = seq_low + seq_low + seq_low
@@ -26,75 +32,49 @@ class ITL500(AutoPairDevice):
     def __init__(self, device_id, seed=None):
         super().__init__(device_id, seed)
 
-    def get_signal(self, gateway, action):
+    def get_signal(self, action):
         """Returns a signal which triggers a device to execute the intended action"""
         # Encoding the system_code and unit_code
         prng = Rand(self.seed)
-        signal = ""
-        data = ""
-
-        if isinstance(gateway, BrennenstuhlGateway):
-            head = self.headBSGW
-            tail = self.tailBSGW
-        elif isinstance(gateway, IntertechnoGateway):
-            head = self.headITGW
-            tail = self.tailITGW
-        else:
-            raise GatewayNotSupportedException
+        signal = ["3", "29"]
 
         # Build the payload of the UDP package depending on the action.
         if action == Action.UNPAIR_ALL:
             for i in range(24):
-                signal += self.lo
-
-            signal += self.lo + self.lo + self.hi + self.lo
-            signal += self.lo + self.lo + self.lo + self.lo
+                signal.append(self.lo)
 
-            data = head + signal + tail
+            signal += [self.lo, self.lo, self.hi, self.lo]
+            signal += [self.lo, self.lo, self.lo, self.lo]
 
         elif action == Action.UP or action == Action.PAIR:
             # When the receiver is in pairing mode, the first ON signal sent to the device will be stored by it.
             # Using a prng and a stored seed to generate the same number sequence in each request
-            signal += self.hi
+            signal.append(self.hi)
 
             for i in range(24):
-                if prng.next_bool():
-                    signal += self.hi
-                else:
-                    signal += self.lo
+                signal.append(self.hi if prng.next_bool() else self.lo)
 
             signal += self.on
 
             for i in range(2):
-                if prng.next_bool():
-                    signal += self.hi
-                else:
-                    signal += self.lo
+                signal.append(self.hi if prng.next_bool() else self.lo)
 
             signal += self.additional
 
-            data = head + signal + tail
         elif action == Action.DOWN or action == Action.UNPAIR:
-            signal += self.hi
+            signal.append(self.hi)
 
             for i in range(24):
-                if prng.next_bool():
-                    signal += self.hi
-                else:
-                    signal += self.lo
+                signal.append(self.hi if prng.next_bool() else self.lo)
 
             signal += self.off
 
             for i in range(2):
-                if prng.next_bool():
-                    signal += self.hi
-                else:
-                    signal += self.lo
+                signal.append(self.hi if prng.next_bool() else self.lo)
 
             signal += self.additional
 
-            data = head + signal + tail
         else:
             raise ValueError("Value of 'action' isn't valid!")
 
-        return data
+        return self.join_list(signal)
diff --git a/pyBrematic/devices/intertechno/ITR300.py b/pyBrematic/devices/intertechno/ITR300.py
new file mode 100644
index 0000000..a840e7c
--- /dev/null
+++ b/pyBrematic/devices/intertechno/ITR300.py
@@ -0,0 +1,9 @@
+# -*- coding: utf-8 -*-
+from pyBrematic.devices.intertechno import CMR1000
+
+
+class ITR300(CMR1000):
+    """Device class for the Intertechno ITR-300 remote outlet"""
+
+    def __init__(self, system_code, unit_code):
+        super().__init__(system_code, unit_code)
diff --git a/pyBrematic/devices/intertechno/README.md b/pyBrematic/devices/intertechno/README.md
new file mode 100644
index 0000000..30db167
--- /dev/null
+++ b/pyBrematic/devices/intertechno/README.md
@@ -0,0 +1,13 @@
+# Supported Intertechno devices
+|Model|Type|IC|Tested|
+|---|---|---|---|
+|CMR300|Switch||[Yes](https://github.com/d-Rickyy-b/pyBrematic/issues/14#issuecomment-752923204)|
+|CMR500|Dimmer||[Yes](https://github.com/d-Rickyy-b/pyBrematic/issues/14#issuecomment-752923204)|
+|CMR1000|Switch||[Yes](https://github.com/d-Rickyy-b/pyBrematic/issues/14#issuecomment-752923204)|
+|ITL500|Blind motor switch||[Yes](https://github.com/d-Rickyy-b/pyBrematic/issues/5#issuecomment-491618075)|
+|ITR300|Plug switch||No (but same as CMR1000)|
+|ITR3500|Plug switch||No (but same as CMR1000)|
+|PAR1500|Plug switch||[Yes](https://github.com/d-Rickyy-b/pyBrematic/issues/14#issuecomment-752923204)|
+
+# Useful references
+- https://wiki.fhem.de/wiki/Intertechno_Code_Berechnung#Stellen_8-9_.28Festwert_0F.29
diff --git a/pyBrematic/devices/intertechno/__init__.py b/pyBrematic/devices/intertechno/__init__.py
index 2d6ca5f..3f80e93 100644
--- a/pyBrematic/devices/intertechno/__init__.py
+++ b/pyBrematic/devices/intertechno/__init__.py
@@ -1,8 +1,12 @@
 # -*- coding: utf-8 -*-
 
+from .intertechnodevice import IntertechnoDevice
 from .CMR1000 import CMR1000
+from .CMR500 import CMR500
+from .CMR300 import CMR300
 from .ITL500 import ITL500
 from .ITR3500 import ITR3500
 from .PAR1500 import PAR1500
+from .tools import calc_systemcode, calc_unitcode, calc_system_and_unit_code
 
-__all__ = ['CMR1000', 'PAR1500', 'ITR3500', 'ITL500']
+__all__ = ["IntertechnoDevice", "CMR1000", "CMR500", "CMR300", "PAR1500", "ITR3500", "ITL500", "calc_systemcode", "calc_unitcode", "calc_system_and_unit_code"]
diff --git a/pyBrematic/devices/intertechno/intertechnodevice.py b/pyBrematic/devices/intertechno/intertechnodevice.py
new file mode 100644
index 0000000..ca6f038
--- /dev/null
+++ b/pyBrematic/devices/intertechno/intertechnodevice.py
@@ -0,0 +1,23 @@
+# -*- coding: utf-8 -*-
+
+from pyBrematic.devices.device import Device
+from .tools import calc_systemcode, calc_unitcode
+
+
+class IntertechnoDevice(Device):
+    """Base class for Intertechno devices using system/unit code"""
+
+    def __init__(self, system_code, unit_code):
+        super().__init__(system_code, unit_code)
+
+    def get_signal(self, action):
+        """Returns a signal which triggers a device to execute the intended action"""
+        super().get_signal(action)
+
+    @staticmethod
+    def calc_systemcode(master):
+        return calc_systemcode(master)
+
+    @staticmethod
+    def calc_unitcode(slave):
+        return calc_unitcode(slave)
diff --git a/pyBrematic/devices/intertechno/tools.py b/pyBrematic/devices/intertechno/tools.py
new file mode 100644
index 0000000..055cb06
--- /dev/null
+++ b/pyBrematic/devices/intertechno/tools.py
@@ -0,0 +1,54 @@
+# -*- coding: utf-8 -*-
+import re
+
+
+def calc_system_and_unit_code(code):
+    """Calculates the (binary) system and unit code from a given string such as "A5", "B11", "F8"
+
+    :param code: The alphanumeric code such as "D7"
+    :return: Tuple of string representation of binary values (system, unit) - e.g.: ("0100", "1000")
+    """
+    pattern = r"([A-P])(\d+)"
+    match = re.match(pattern, code)
+    if not match:
+        raise ValueError("Code must start with a letter A-P and end in a number 1-16!")
+
+    system_ascii = match.group(1)
+    unit_ascii = match.group(2)
+
+    system_bin = calc_systemcode(system_ascii)
+    unit_bin = calc_unitcode(int(unit_ascii))
+    return system_bin, unit_bin
+
+
+def calc_systemcode(master):
+    """Calculates the (binary) system code from a given string such as "A", "B", "F"
+
+    :param master: The system channel letter such as "D"
+    :return: String representation of binary value - e.g.: "0100"
+    """
+    if master == "":
+        raise ValueError("Value must not be empty!")
+
+    master = master.upper()
+
+    # Check if master is in range [A-P]
+    number = ord(master) - 65
+    if number not in range(0, 16 + 1):
+        raise ValueError("Master value '{}' is not ranged between [1, 16]".format(master))
+
+    systemcode = "{0:04b}".format(number)
+    return systemcode[::-1]
+
+
+def calc_unitcode(slave):
+    """Calculates the (binary) system code from a given channel number such as 3, 8, 15
+
+    :param slave: The unit channel number such as 11
+    :return: String representation of binary value - e.g.: "0100"
+    """
+    if slave not in range(1, 16 + 1):
+        raise ValueError("Slave value '{}' is not ranged between [1, 16]".format(slave))
+
+    unitcode = "{0:04b}".format(slave - 1)
+    return unitcode[::-1]
diff --git a/pyBrematic/example/example.py b/pyBrematic/example/example.py
index a052595..5fe8375 100644
--- a/pyBrematic/example/example.py
+++ b/pyBrematic/example/example.py
@@ -1,17 +1,23 @@
 # -*- coding: utf-8 -*-
 """This is an example usage file for the pyBrematic module"""
 
-from pyBrematic.devices import Action
+from pyBrematic import Action
 from pyBrematic.devices.brennenstuhl import RCS1000N
+from pyBrematic.devices.intertechno import ITR3500, calc_system_and_unit_code
 from pyBrematic.gateways import BrennenstuhlGateway
 
 # Set your system and unit codes
-system_code = "11110"  # Switches 1-4 are in the 'up' position, 5 is 'down'
-unit_code = "10000"  # Switch A is in the 'up' position, B-E are 'down'
+system_code = "11110"  # DIPs 1-4 are in the 'up' position, 5 is 'down'
+unit_code = "10000"  # DIP A is in the 'up' position, B-E are 'down'
 
 # Create a new device with the specified codes
 desk_lamp = RCS1000N(system_code, unit_code)
 
+# For Intertechno devices you can also use the following methods to get the code
+# via master/slave (letter/number) notation. Allowed values are "A-P" and "1-16".
+led_system_code, led_unit_code = calc_system_and_unit_code("A3")
+led_strip = ITR3500(led_system_code, led_unit_code)
+
 # Create a new gateway located at the specified IP
 gw = BrennenstuhlGateway("192.168.178.9")
 
diff --git a/pyBrematic/exceptions/__init__.py b/pyBrematic/exceptions/__init__.py
index 554bf62..ba24d27 100644
--- a/pyBrematic/exceptions/__init__.py
+++ b/pyBrematic/exceptions/__init__.py
@@ -2,4 +2,4 @@
 
 from .GatewayNotSupportedException import GatewayNotSupportedException
 
-__all__ = ['GatewayNotSupportedException']
+__all__ = ["GatewayNotSupportedException"]
diff --git a/pyBrematic/gateways/__init__.py b/pyBrematic/gateways/__init__.py
index 02e6a11..b86bcc1 100644
--- a/pyBrematic/gateways/__init__.py
+++ b/pyBrematic/gateways/__init__.py
@@ -1,7 +1,8 @@
 # -*- coding: utf-8 -*-
 
-from .brennenstuhl_gateway import BrennenstuhlGateway
 from .gateway import Gateway
+from .brennenstuhl_gateway import BrennenstuhlGateway
 from .intertechno_gateway import IntertechnoGateway
+from .connair_gateway import ConnAirGateway
 
-__all__ = ['Gateway', 'IntertechnoGateway', 'BrennenstuhlGateway']
+__all__ = ["Gateway", "IntertechnoGateway", "BrennenstuhlGateway", "ConnAirGateway"]
diff --git a/pyBrematic/gateways/brennenstuhl_gateway.py b/pyBrematic/gateways/brennenstuhl_gateway.py
index e7d7b17..142a454 100644
--- a/pyBrematic/gateways/brennenstuhl_gateway.py
+++ b/pyBrematic/gateways/brennenstuhl_gateway.py
@@ -9,11 +9,25 @@ class BrennenstuhlGateway(Gateway):
 
     def __init__(self, ip, port=49880):
         super().__init__(ip, port)
+        # Each UDP package sent to the gateway must contain certain parameters set in a header and a tail.
+        # These are format strings used to insert those parameters.
+        # For the Brematic GWY 433 the parameters look like that:
+        # head: "TXP:0,0,<repeat>,<pause>,<tune>,<baud>"
+        # tail: "<txversion>,<speed>;"
+        self.head_format = "TXP:0,0,{0},{1},{2},{3}"
+        self.tail_format = "{0},{1};"
+
+    def build_udp_payload(self, device, action):
+        head = self.get_head(device.repeat, device.pause_BS, device.tune, device.baud)
+        tail = self.get_tail(device.txversion, device.speed_BS)
+        payload = device.get_signal(self, action)
+
+        return ",".join([head, payload, tail])
 
     def send_request(self, device, action):
         """Sends the UDP request to the gateway"""
-        data = device.get_signal(self, action)
+        payload = self.build_udp_payload(device, action)
 
         s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
-        s.sendto(data.encode(), (self._ip, self._port))
+        s.sendto(payload.encode(), (self._ip, self._port))
         s.close()
diff --git a/pyBrematic/gateways/connair_gateway.py b/pyBrematic/gateways/connair_gateway.py
new file mode 100644
index 0000000..ef6fc88
--- /dev/null
+++ b/pyBrematic/gateways/connair_gateway.py
@@ -0,0 +1,8 @@
+# -*- coding: utf-8 -*-
+
+from .brennenstuhl_gateway import BrennenstuhlGateway
+
+
+class ConnAirGateway(BrennenstuhlGateway):
+    def __init__(self, ip, port=49880):
+        super().__init__(ip, port)
diff --git a/pyBrematic/gateways/gateway.py b/pyBrematic/gateways/gateway.py
index 45e9a25..d70ef12 100644
--- a/pyBrematic/gateways/gateway.py
+++ b/pyBrematic/gateways/gateway.py
@@ -13,6 +13,18 @@ class Gateway(object):
     def __init__(self, ip, port):
         self._ip = ip
         self._port = port
+        self.head_format = None
+        self.tail_format = None
+
+    def get_head(self, repeat, pause, tune, baud):
+        if not self.head_format:
+            raise ValueError("No head_format found for gateway")
+        return self.head_format.format(repeat, pause, tune, baud)
+
+    def get_tail(self, txversion, speed):
+        if not self.tail_format:
+            raise ValueError("No tail_format found for gateway")
+        return self.tail_format.format(txversion, speed)
 
     def send_request(self, device, action):
         raise NotImplementedError("Subclasses must implement this method!")
diff --git a/pyBrematic/gateways/intertechno_gateway.py b/pyBrematic/gateways/intertechno_gateway.py
index 1f728c0..edc6a45 100644
--- a/pyBrematic/gateways/intertechno_gateway.py
+++ b/pyBrematic/gateways/intertechno_gateway.py
@@ -9,11 +9,25 @@ class IntertechnoGateway(Gateway):
 
     def __init__(self, ip, port=49880):
         super().__init__(ip, port)
+        # Each UDP package sent to the gateway must contain certain parameters set in a header and a tail.
+        # These are format strings used to insert those parameters.
+        # For the Intertechno GWY 433 the parameters look like that:
+        # head: "0,0,<repeat>,<pause>,<tune>,<baud>"
+        # tail: "<txversion>,<speed>"
+        self.head_format = "0,0,{0},{1},{2},{3}"
+        self.tail_format = "{0},{1}"
+
+    def build_udp_payload(self, device, action):
+        head = self.get_head(device.repeat, device.pause_IT, device.tune, device.baud)
+        tail = self.get_tail(device.txversion, device.speed_IT)
+        payload = device.get_signal(self, action)
+
+        return ",".join([head, payload, tail])
 
     def send_request(self, device, action):
         """Sends the UDP request to the gateway"""
-        data = device.get_signal(self, action)
+        payload = self.build_udp_payload(device, action)
 
         s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
-        s.sendto(data.encode(), (self._ip, self._port))
+        s.sendto(payload.encode(), (self._ip, self._port))
         s.close()
diff --git a/pyBrematic/utils/__init__.py b/pyBrematic/utils/__init__.py
index 985c59f..f72c3c1 100644
--- a/pyBrematic/utils/__init__.py
+++ b/pyBrematic/utils/__init__.py
@@ -5,4 +5,4 @@
 from .singleton import singleton
 from .storage import Storage
 
-__all__ = ['singleton', 'Storage', 'DataEncoder', 'Rand']
+__all__ = ["singleton", "Storage", "DataEncoder", "Rand"]
diff --git a/pyBrematic/utils/singleton.py b/pyBrematic/utils/singleton.py
index e760b28..a2abc5d 100644
--- a/pyBrematic/utils/singleton.py
+++ b/pyBrematic/utils/singleton.py
@@ -6,14 +6,14 @@
 
 def singleton(cls):
     @wraps(cls)
-    def getInstance(*args, **kwargs):
+    def get_instance(*args, **kwargs):
         instance = __instances.get(cls, None)
         if not instance:
             instance = cls(*args, **kwargs)
             __instances[cls] = instance
         return instance
 
-    return getInstance
+    return get_instance
 
 
 def _delete_all_instances():
diff --git a/pyBrematic/version.py b/pyBrematic/version.py
index 6849410..c68196d 100644
--- a/pyBrematic/version.py
+++ b/pyBrematic/version.py
@@ -1,1 +1,1 @@
-__version__ = "1.1.0"
+__version__ = "1.2.0"
diff --git a/setup.cfg b/setup.cfg
index 224a779..660561d 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -1,2 +1,7 @@
 [metadata]
-description-file = README.md
\ No newline at end of file
+description-file = README.md
+
+[flake8]
+exclude = .git,__pycache__,build,dist,.env,.venv,venv
+max-line-length = 160
+ignore = E501


Test patch:
diff --git a/.github/workflows/python-lint-test.yml b/.github/workflows/python-lint-test.yml
new file mode 100644
index 0000000..64a89ab
--- /dev/null
+++ b/.github/workflows/python-lint-test.yml
@@ -0,0 +1,44 @@
+# This workflow will install Python dependencies, run tests and lint with a variety of Python versions
+# For more information see: https://help.github.com/actions/language-and-framework-guides/using-python-with-github-actions
+
+name: Run tests and lint
+
+on:
+  push:
+    branches: [ master, dev ]
+  pull_request:
+    branches: [ master ]
+
+jobs:
+  test-and-lint:
+
+    runs-on: ubuntu-latest
+    strategy:
+      matrix:
+        python-version: ['3.5', '3.6', '3.7', '3.8']
+
+    steps:
+    - uses: actions/checkout@v2
+    - name: Set up Python ${{ matrix.python-version }}
+      uses: actions/setup-python@v2
+      with:
+        python-version: ${{ matrix.python-version }}
+    - name: Install dependencies
+      run: |
+        python -m pip install --upgrade pip
+        python -m pip install flake8 pytest coveralls
+        if [ -f requirements.txt ]; then pip install -r requirements.txt; fi
+    - name: Lint with flake8
+      run: |
+        # stop the build if there are Python syntax errors or undefined names
+        flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics
+        # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide
+        flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics
+    - name: Test with pytest
+      run: |
+        coverage run --omit='*/virtualenv/*,*/site-packages/*' -m pytest
+    - name: Publish coverage
+      env:
+        COVERALLS_REPO_TOKEN: ${{ secrets.COVERALLS_REPO_TOKEN }}
+      run: |
+        coveralls
diff --git a/pyBrematic/devices/brennenstuhl/tests/brennenstuhl_test.py b/pyBrematic/devices/brennenstuhl/tests/brennenstuhl_test.py
index 27b413e..11deffd 100644
--- a/pyBrematic/devices/brennenstuhl/tests/brennenstuhl_test.py
+++ b/pyBrematic/devices/brennenstuhl/tests/brennenstuhl_test.py
@@ -2,7 +2,7 @@
 
 import unittest
 
-from pyBrematic.devices import Action
+from pyBrematic.action import Action
 from pyBrematic.devices.brennenstuhl import RCR1000N, RCS1000N
 from pyBrematic.gateways import BrennenstuhlGateway, IntertechnoGateway
 
@@ -20,30 +20,63 @@ def test_RCR1000N(self):
         """Test to check the functionality of the RCR1000N class"""
         dev = RCR1000N("10000", "00100")
 
-        on_signal_ITGW = "0,0,10,11200,350,26,0,1,3,1,3,1,3,3,1,1,3,3,1,1,3,3,1,1,3,3,1,1,3,3,1,1,3,3,1,1,3,1,3,1,3,3,1,1,3,3,1,1,3,1,3,1,3,3,1,1,32,0"
-        off_signal_ITGW = "0,0,10,11200,350,26,0,1,3,1,3,1,3,3,1,1,3,3,1,1,3,3,1,1,3,3,1,1,3,3,1,1,3,3,1,1,3,1,3,1,3,3,1,1,3,3,1,1,3,3,1,1,3,1,3,1,32,0"
+        on_signal = "1,3,1,3,1,3,3,1,1,3,3,1,1,3,3,1,1,3,3,1,1,3,3,1,1,3,3,1,1,3,1,3,1,3,3,1,1,3,3,1,1,3,1,3,1,3,3,1"
+        off_signal = "1,3,1,3,1,3,3,1,1,3,3,1,1,3,3,1,1,3,3,1,1,3,3,1,1,3,3,1,1,3,1,3,1,3,3,1,1,3,3,1,1,3,3,1,1,3,1,3"
 
-        self.assertEqual(on_signal_ITGW, dev.get_signal(self.itgw, Action.ON))
-        self.assertEqual(off_signal_ITGW, dev.get_signal(self.itgw, Action.OFF))
+        self.assertEqual(on_signal, dev.get_signal(Action.ON))
+        self.assertEqual(off_signal, dev.get_signal(Action.OFF))
 
-        on_signal_BSGW = "TXP:0,0,10,5600,350,25,1,3,1,3,1,3,3,1,1,3,3,1,1,3,3,1,1,3,3,1,1,3,3,1,1,3,3,1,1,3,1,3,1,3,3,1,1,3,3,1,1,3,1,3,1,3,3,1,1,16;"
-        off_signal_BSGW = "TXP:0,0,10,5600,350,25,1,3,1,3,1,3,3,1,1,3,3,1,1,3,3,1,1,3,3,1,1,3,3,1,1,3,3,1,1,3,1,3,1,3,3,1,1,3,3,1,1,3,3,1,1,3,1,3,1,16;"
+    def test_RCR1000N_10000_00000(self):
+        """Test to check the functionality of the RCR1000N class"""
+        # Taken from https://github.com/Power-Switch/PowerSwitch_Android/blob/54400f74230bb78f87b19cc89c0f174e080a3fa7/Smartphone/src/androidTest/java/eu/power_switch/obj/device/brennenstuhl/RCS1000NComfort_Test.java#L97
+        dev = RCR1000N("10000", "00000")
+
+        on_signal = "1,3,1,3,1,3,3,1,1,3,3,1,1,3,3,1,1,3,3,1,1,3,3,1,1,3,3,1,1,3,3,1,1,3,3,1,1,3,3,1,1,3,1,3,1,3,3,1"
+        off_signal = "1,3,1,3,1,3,3,1,1,3,3,1,1,3,3,1,1,3,3,1,1,3,3,1,1,3,3,1,1,3,3,1,1,3,3,1,1,3,3,1,1,3,3,1,1,3,1,3"
+
+        self.assertEqual(on_signal, dev.get_signal(Action.ON))
+        self.assertEqual(off_signal, dev.get_signal(Action.OFF))
+
+    def test_RCR1000N_10000_10000(self):
+        """Test to check the functionality of the RCR1000N class"""
+        dev = RCR1000N("10000", "10000")
 
-        self.assertEqual(on_signal_BSGW, dev.get_signal(self.bsgw, Action.ON))
-        self.assertEqual(off_signal_BSGW, dev.get_signal(self.bsgw, Action.OFF))
+        on_signal = "1,3,1,3,1,3,3,1,1,3,3,1,1,3,3,1,1,3,3,1,1,3,1,3,1,3,3,1,1,3,3,1,1,3,3,1,1,3,3,1,1,3,1,3,1,3,3,1"
+        off_signal = "1,3,1,3,1,3,3,1,1,3,3,1,1,3,3,1,1,3,3,1,1,3,1,3,1,3,3,1,1,3,3,1,1,3,3,1,1,3,3,1,1,3,3,1,1,3,1,3"
+
+        self.assertEqual(on_signal, dev.get_signal(Action.ON))
+        self.assertEqual(off_signal, dev.get_signal(Action.OFF))
 
     def test_RCS1000N(self):
         """Test to check the functionality of the RCS1000N class"""
         dev = RCS1000N("10000", "00100")
 
-        on_signal_ITGW = "0,0,10,11200,350,26,0,1,3,1,3,1,3,3,1,1,3,3,1,1,3,3,1,1,3,3,1,1,3,3,1,1,3,3,1,1,3,1,3,1,3,3,1,1,3,3,1,1,3,1,3,1,3,3,1,1,32,0"
-        off_signal_ITGW = "0,0,10,11200,350,26,0,1,3,1,3,1,3,3,1,1,3,3,1,1,3,3,1,1,3,3,1,1,3,3,1,1,3,3,1,1,3,1,3,1,3,3,1,1,3,3,1,1,3,3,1,1,3,1,3,1,32,0"
+        on_signal = "1,3,1,3,1,3,3,1,1,3,3,1,1,3,3,1,1,3,3,1,1,3,3,1,1,3,3,1,1,3,1,3,1,3,3,1,1,3,3,1,1,3,1,3,1,3,3,1"
+        off_signal = "1,3,1,3,1,3,3,1,1,3,3,1,1,3,3,1,1,3,3,1,1,3,3,1,1,3,3,1,1,3,1,3,1,3,3,1,1,3,3,1,1,3,3,1,1,3,1,3"
+
+        self.assertEqual(on_signal, dev.get_signal(Action.ON))
+        self.assertEqual(off_signal, dev.get_signal(Action.OFF))
+
+    def test_invalid_action(self):
+        """Test to check if invalid actions raise an exception"""
+        system_code = "10000"
+        unit_code = "00100"
+        device = RCS1000N(system_code=system_code, unit_code=unit_code)
+
+        with self.assertRaises(ValueError):
+            _ = device.get_signal("WrongAction!")
+
+    def test_valid_action(self):
+        """Test to check if valid actions are set up correctly"""
+        system_code = "10000"
+        unit_code = "00100"
+        device = RCS1000N(system_code=system_code, unit_code=unit_code)
 
-        self.assertEqual(on_signal_ITGW, dev.get_signal(self.itgw, Action.ON))
-        self.assertEqual(off_signal_ITGW, dev.get_signal(self.itgw, Action.OFF))
+        self.assertIsNotNone(device.supported_actions.get(Action.ON))
+        self.assertIsNotNone(device.supported_actions.get(Action.OFF))
 
-        on_signal_BSGW = "TXP:0,0,10,5600,350,25,1,3,1,3,1,3,3,1,1,3,3,1,1,3,3,1,1,3,3,1,1,3,3,1,1,3,3,1,1,3,1,3,1,3,3,1,1,3,3,1,1,3,1,3,1,3,3,1,1,16;"
-        off_signal_BSGW = "TXP:0,0,10,5600,350,25,1,3,1,3,1,3,3,1,1,3,3,1,1,3,3,1,1,3,3,1,1,3,3,1,1,3,3,1,1,3,1,3,1,3,3,1,1,3,3,1,1,3,3,1,1,3,1,3,1,16;"
+        self.assertEqual(device.on, device.supported_actions.get(Action.ON))
+        self.assertEqual(device.off, device.supported_actions.get(Action.OFF))
 
-        self.assertEqual(on_signal_BSGW, dev.get_signal(self.bsgw, Action.ON))
-        self.assertEqual(off_signal_BSGW, dev.get_signal(self.bsgw, Action.OFF))
+if __name__ == "__main__":
+    unittest.main()
diff --git a/pyBrematic/devices/elro/tests/elro_test.py b/pyBrematic/devices/elro/tests/elro_test.py
index 73e242d..a94c457 100644
--- a/pyBrematic/devices/elro/tests/elro_test.py
+++ b/pyBrematic/devices/elro/tests/elro_test.py
@@ -2,7 +2,7 @@
 
 import unittest
 
-from pyBrematic.devices import Action
+from pyBrematic.action import Action
 from pyBrematic.devices.elro import AB440SA
 from pyBrematic.gateways import BrennenstuhlGateway, IntertechnoGateway
 
@@ -21,14 +21,39 @@ def test_AB440SA(self):
         unit_code = "00100"
         device = AB440SA(system_code=system_code, unit_code=unit_code)
 
-        on_signal_ITGW = "0,0,10,11200,350,26,0,1,3,1,3,1,3,3,1,1,3,3,1,1,3,3,1,1,3,3,1,1,3,3,1,1,3,3,1,1,3,1,3,1,3,3,1,1,3,3,1,1,3,1,3,1,3,3,1,1,32,0"
-        off_signal_ITGW = "0,0,10,11200,350,26,0,1,3,1,3,1,3,3,1,1,3,3,1,1,3,3,1,1,3,3,1,1,3,3,1,1,3,3,1,1,3,1,3,1,3,3,1,1,3,3,1,1,3,3,1,1,3,1,3,1,32,0"
+        on_signal = "1,3,1,3,1,3,3,1,1,3,3,1,1,3,3,1,1,3,3,1,1,3,3,1,1,3,3,1,1,3,1,3,1,3,3,1,1,3,3,1,1,3,1,3,1,3,3,1"
+        off_signal = "1,3,1,3,1,3,3,1,1,3,3,1,1,3,3,1,1,3,3,1,1,3,3,1,1,3,3,1,1,3,1,3,1,3,3,1,1,3,3,1,1,3,3,1,1,3,1,3"
 
-        self.assertEqual(device.get_signal(self.itgw, Action.ON), on_signal_ITGW)
-        self.assertEqual(device.get_signal(self.itgw, Action.OFF), off_signal_ITGW)
+        self.assertEqual(on_signal, device.get_signal(Action.ON))
+        self.assertEqual(off_signal, device.get_signal(Action.OFF))
 
-        on_signal_BSGW = "TXP:0,0,10,5600,350,25,1,3,1,3,1,3,3,1,1,3,3,1,1,3,3,1,1,3,3,1,1,3,3,1,1,3,3,1,1,3,1,3,1,3,3,1,1,3,3,1,1,3,1,3,1,3,3,1,1,14;"
-        off_signal_BSGW = "TXP:0,0,10,5600,350,25,1,3,1,3,1,3,3,1,1,3,3,1,1,3,3,1,1,3,3,1,1,3,3,1,1,3,3,1,1,3,1,3,1,3,3,1,1,3,3,1,1,3,3,1,1,3,1,3,1,14;"
+        on_signal = "1,3,1,3,1,3,3,1,1,3,3,1,1,3,3,1,1,3,3,1,1,3,3,1,1,3,3,1,1,3,1,3,1,3,3,1,1,3,3,1,1,3,1,3,1,3,3,1"
+        off_signal = "1,3,1,3,1,3,3,1,1,3,3,1,1,3,3,1,1,3,3,1,1,3,3,1,1,3,3,1,1,3,1,3,1,3,3,1,1,3,3,1,1,3,3,1,1,3,1,3"
 
-        self.assertEqual(device.get_signal(self.bsgw, Action.ON), on_signal_BSGW)
-        self.assertEqual(device.get_signal(self.bsgw, Action.OFF), off_signal_BSGW)
+        self.assertEqual(on_signal, device.get_signal(Action.ON))
+        self.assertEqual(off_signal, device.get_signal(Action.OFF))
+
+    def test_invalid_action(self):
+        """Test to check if invalid actions raise an exception"""
+        system_code = "10000"
+        unit_code = "00100"
+        device = AB440SA(system_code=system_code, unit_code=unit_code)
+
+        with self.assertRaises(ValueError):
+            _ = device.get_signal("WrongAction!")
+
+    def test_valid_action(self):
+        """Test to check if valid actions are set up correctly"""
+        system_code = "10000"
+        unit_code = "00100"
+        device = AB440SA(system_code=system_code, unit_code=unit_code)
+
+        self.assertIsNotNone(device.supported_actions.get(Action.ON))
+        self.assertIsNotNone(device.supported_actions.get(Action.OFF))
+
+        self.assertEqual(device.on, device.supported_actions.get(Action.ON))
+        self.assertEqual(device.off, device.supported_actions.get(Action.OFF))
+
+
+if __name__ == "__main__":
+    unittest.main()
diff --git a/pyBrematic/devices/intertechno/tests/ITL500_test.py b/pyBrematic/devices/intertechno/tests/ITL500_test.py
index 8a8fb86..93a43de 100644
--- a/pyBrematic/devices/intertechno/tests/ITL500_test.py
+++ b/pyBrematic/devices/intertechno/tests/ITL500_test.py
@@ -1,69 +1,38 @@
 # -*- coding: utf-8 -*-
 import unittest
 
-from pyBrematic.devices import AutoPairDevice
+from pyBrematic import Action
 from pyBrematic.devices.intertechno import ITL500
-from pyBrematic.gateways import BrennenstuhlGateway, IntertechnoGateway
-from pyBrematic.exceptions import GatewayNotSupportedException
 from unittest.mock import Mock
 
 
 class TestITL500(unittest.TestCase):
     def setUp(self):
-        self.bsgw = BrennenstuhlGateway("192.168.178.2")
-        self.itgw = IntertechnoGateway("192.168.178.3")
-
         seed = 8712387
         self.dev = ITL500(123, seed)
 
-    def test_invalid_gateway(self):
-        with self.assertRaises(GatewayNotSupportedException):
-            fake_gateway = Mock()
-            self.dev.get_signal(fake_gateway, AutoPairDevice.ACTION_UP)
-
     def test_invalid_action(self):
         with self.assertRaises(ValueError):
             fake_action = Mock()
-            self.dev.get_signal(self.itgw, fake_action)
-
-    def test_up_intertechno(self):
-        up_signal_ITGW = "0,0,5,10976,98,67,0,3,29," + "15,3,15,15,3,15,15,3,15,15,3,3,15,3,3,15,15,15,3,15,15,15,15,3,15,3,3,3,15,3,3,3,15,3,15,3,3,3,3,3,3,3,15,3,3,3,15," + "3,112,0"
-        self.assertEqual(up_signal_ITGW, self.dev.get_signal(self.itgw, AutoPairDevice.ACTION_UP))
+            self.dev.get_signal(fake_action)
 
-    def test_up_brennenstuhl(self):
-        up_signal_BSGW = "TXP:0,0,5,10976,98,66,3,29," + "15,3,15,15,3,15,15,3,15,15,3,3,15,3,3,15,15,15,3,15,15,15,15,3,15,3,3,3,15,3,3,3,15,3,15,3,3,3,3,3,3,3,15,3,3,3,15," + "3,126"
-        self.assertEqual(up_signal_BSGW, self.dev.get_signal(self.bsgw, AutoPairDevice.ACTION_UP))
+    def test_up_signal(self):
+        up_signal = "3,29,15,3,15,15,3,15,15,3,15,15,3,3,15,3,3,15,15,15,3,15,15,15,15,3,15,3,3,3,15,3,3,3,15,3,15,3,3,3,3,3,3,3,15,3,3,3,15"
+        self.assertEqual(up_signal, self.dev.get_signal(Action.UP))
 
-    def test_down_intertechno(self):
-        down_signal_ITGW = "0,0,5,10976,98,67,0,3,29," + "15,3,15,15,3,15,15,3,15,15,3,3,15,3,3,15,15,15,3,15,15,15,15,3,15,3,3,3,15,3,3,3,15,3,3,3,15,3,3,3,3,3,15,3,3,3,15," + "3,112,0"
-        self.assertEqual(down_signal_ITGW, self.dev.get_signal(self.itgw, AutoPairDevice.ACTION_DOWN))
+    def test_down_signal(self):
+        down_signal = "3,29,15,3,15,15,3,15,15,3,15,15,3,3,15,3,3,15,15,15,3,15,15,15,15,3,15,3,3,3,15,3,3,3,15,3,3,3,15,3,3,3,3,3,15,3,3,3,15"
+        self.assertEqual(down_signal, self.dev.get_signal(Action.DOWN))
 
-    def test_down_brennenstuhl(self):
-        down_signal_BSGW = "TXP:0,0,5,10976,98,66,3,29," + "15,3,15,15,3,15,15,3,15,15,3,3,15,3,3,15,15,15,3,15,15,15,15,3,15,3,3,3,15,3,3,3,15,3,3,3,15,3,3,3,3,3,15,3,3,3,15," + "3,126"
-        self.assertEqual(down_signal_BSGW, self.dev.get_signal(self.bsgw, AutoPairDevice.ACTION_DOWN))
-
-    def test_other_seed_intertechno(self):
+    def test_other_seed(self):
         seed2 = 89090823173454879234
         dev_new = ITL500(1234, seed2)
 
-        up_signal_ITGW = "0,0,5,10976,98,67,0,3,29," + "15,3,15,15,3,3,3,3,3,15,3,3,15,15,3,3,15,15,3,3,15,15,15,3,15,3,3,3,15,3,3,3,15,3,15,3,3,3,3,3,3,3,15,3,3,3,15," + "3,112,0"
-        down_signal_ITGW = "0,0,5,10976,98,67,0,3,29," + "15,3,15,15,3,3,3,3,3,15,3,3,15,15,3,3,15,15,3,3,15,15,15,3,15,3,3,3,15,3,3,3,15,3,3,3,15,3,3,3,3,3,15,3,3,3,15," + "3,112,0"
-        self.assertEqual(up_signal_ITGW, dev_new.get_signal(self.itgw, AutoPairDevice.ACTION_UP))
-        self.assertEqual(down_signal_ITGW, dev_new.get_signal(self.itgw, AutoPairDevice.ACTION_DOWN))
-
-    def test_other_seed_brennenstuhl(self):
-        seed2 = 89090823173454879234
-        dev_new = ITL500(1234, seed2)
-
-        up_signal_BSGW = "TXP:0,0,5,10976,98,66,3,29," + "15,3,15,15,3,3,3,3,3,15,3,3,15,15,3,3,15,15,3,3,15,15,15,3,15,3,3,3,15,3,3,3,15,3,15,3,3,3,3,3,3,3,15,3,3,3,15," + "3,126"
-        down_signal_BSGW = "TXP:0,0,5,10976,98,66,3,29," + "15,3,15,15,3,3,3,3,3,15,3,3,15,15,3,3,15,15,3,3,15,15,15,3,15,3,3,3,15,3,3,3,15,3,3,3,15,3,3,3,3,3,15,3,3,3,15," + "3,126"
-        self.assertEqual(up_signal_BSGW, dev_new.get_signal(self.bsgw, AutoPairDevice.ACTION_UP))
-        self.assertEqual(down_signal_BSGW, dev_new.get_signal(self.bsgw, AutoPairDevice.ACTION_DOWN))
-
-    def test_unpair_all_intertechno(self):
-        unpair_signal_ITGW = "0,0,5,10976,98,67,0,3,29," + "3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,15,3,3,3,3,3," + "3,112,0"
-        self.assertEqual(unpair_signal_ITGW, self.dev.get_signal(self.itgw, AutoPairDevice.ACTION_UNPAIR_ALL))
+        up_signal = "3,29," + "15,3,15,15,3,3,3,3,3,15,3,3,15,15,3,3,15,15,3,3,15,15,15,3,15,3,3,3,15,3,3,3,15,3,15,3,3,3,3,3,3,3,15,3,3,3,15"
+        down_signal = "3,29," + "15,3,15,15,3,3,3,3,3,15,3,3,15,15,3,3,15,15,3,3,15,15,15,3,15,3,3,3,15,3,3,3,15,3,3,3,15,3,3,3,3,3,15,3,3,3,15"
+        self.assertEqual(up_signal, dev_new.get_signal(Action.UP))
+        self.assertEqual(down_signal, dev_new.get_signal(Action.DOWN))
 
-    def test_unpair_all_brennenstuhl(self):
-        unpair_signal_BSGW = "TXP:0,0,5,10976,98,66,3,29," + "3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,15,3,3,3,3,3," + "3,126"
-        self.assertEqual(unpair_signal_BSGW, self.dev.get_signal(self.bsgw, AutoPairDevice.ACTION_UNPAIR_ALL))
+    def test_unpair_all(self):
+        unpair_signal = "3,29," + "3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,15,3,3,3,3,3"
+        self.assertEqual(unpair_signal, self.dev.get_signal(Action.UNPAIR_ALL))
diff --git a/pyBrematic/devices/intertechno/tests/intertechno_test.py b/pyBrematic/devices/intertechno/tests/intertechno_test.py
index 387f569..20a190d 100644
--- a/pyBrematic/devices/intertechno/tests/intertechno_test.py
+++ b/pyBrematic/devices/intertechno/tests/intertechno_test.py
@@ -2,16 +2,14 @@
 
 import unittest
 
-from pyBrematic.devices import Action
+from pyBrematic.action import Action
 from pyBrematic.devices.intertechno import PAR1500, CMR1000, ITR3500
-from pyBrematic.gateways import BrennenstuhlGateway, IntertechnoGateway
 
 
 class TestIntertechno(unittest.TestCase):
 
     def setUp(self):
-        self.bsgw = BrennenstuhlGateway("192.168.178.2")
-        self.itgw = IntertechnoGateway("192.168.178.3")
+        pass
 
     def tearDown(self):
         pass
@@ -20,46 +18,84 @@ def test_PAR1500(self):
         """Test to check the functionality of the PAR1500 class"""
         dev = PAR1500("1000", "1111")
 
-        on_signal_ITGW = "0,0,6,11125,89,26,0,4,12,12,4,4,12,4,12,4,12,4,12,4,12,4,12,4,12,12,4,4,12,12,4,4,12,12,4,4,12,12,4,4,12,4,12,4,12,12,4,4,12,12,4,4,12,12,4,1,125,0"
-        off_signal_ITGW = "0,0,6,11125,89,26,0,4,12,12,4,4,12,4,12,4,12,4,12,4,12,4,12,4,12,12,4,4,12,12,4,4,12,12,4,4,12,12,4,4,12,4,12,4,12,12,4,4,12,12,4,4,12,4,12,1,125,0"
-
-        self.assertEqual(on_signal_ITGW, dev.get_signal(self.itgw, Action.ON))
-        self.assertEqual(off_signal_ITGW, dev.get_signal(self.itgw, Action.OFF))
+        on_signal = "4,12,12,4,4,12,4,12,4,12,4,12,4,12,4,12,4,12,12,4,4,12,12,4,4,12,12,4,4,12,12,4,4,12,4,12,4,12,12,4,4,12,12,4,4,12,12,4"
+        off_signal = "4,12,12,4,4,12,4,12,4,12,4,12,4,12,4,12,4,12,12,4,4,12,12,4,4,12,12,4,4,12,12,4,4,12,4,12,4,12,12,4,4,12,12,4,4,12,4,12"
 
-        on_signal_BSGW = "TXP:0,0,6,11125,89,25,4,12,12,4,4,12,4,12,4,12,4,12,4,12,4,12,4,12,12,4,4,12,12,4,4,12,12,4,4,12,12,4,4,12,4,12,4,12,12,4,4,12,12,4,4,12,12,4,1,140;"
-        off_signal_BSGW = "TXP:0,0,6,11125,89,25,4,12,12,4,4,12,4,12,4,12,4,12,4,12,4,12,4,12,12,4,4,12,12,4,4,12,12,4,4,12,12,4,4,12,4,12,4,12,12,4,4,12,12,4,4,12,4,12,1,140;"
-
-        self.assertEqual(on_signal_BSGW, dev.get_signal(self.bsgw, Action.ON))
-        self.assertEqual(off_signal_BSGW, dev.get_signal(self.bsgw, Action.OFF))
+        self.assertEqual(on_signal, dev.get_signal(Action.ON))
+        self.assertEqual(off_signal, dev.get_signal(Action.OFF))
 
     def test_CMR1000(self):
         """Test to check the functionality of the CMR1000 class"""
         dev = CMR1000("0000", "0010")  # binary representation of A5
 
-        on_signal_ITGW = "0,0,6,11125,89,26,0,4,12,4,12,4,12,4,12,4,12,4,12,4,12,4,12,4,12,4,12,4,12,4,12,4,12,12,4,4,12,4,12,4,12,4,12,4,12,12,4,4,12,12,4,4,12,12,4,1,125,0"
-        off_signal_ITGW = "0,0,6,11125,89,26,0,4,12,4,12,4,12,4,12,4,12,4,12,4,12,4,12,4,12,4,12,4,12,4,12,4,12,12,4,4,12,4,12,4,12,4,12,4,12,12,4,4,12,12,4,4,12,4,12,1,125,0"
+        on_signal = "4,12,4,12,4,12,4,12,4,12,4,12,4,12,4,12,4,12,4,12,4,12,4,12,4,12,12,4,4,12,4,12,4,12,4,12,4,12,12,4,4,12,12,4,4,12,12,4"
+        off_signal = "4,12,4,12,4,12,4,12,4,12,4,12,4,12,4,12,4,12,4,12,4,12,4,12,4,12,12,4,4,12,4,12,4,12,4,12,4,12,12,4,4,12,12,4,4,12,4,12"
+
+        self.assertEqual(on_signal, dev.get_signal(Action.ON))
+        self.assertEqual(off_signal, dev.get_signal(Action.OFF))
+
+        on_signal_2 = "4,12,4,12,4,12,4,12,4,12,4,12,4,12,4,12,4,12,4,12,4,12,4,12,4,12,12,4,4,12,4,12,4,12,4,12,4,12,12,4,4,12,12,4,4,12,12,4"
+        off_signal_2 = "4,12,4,12,4,12,4,12,4,12,4,12,4,12,4,12,4,12,4,12,4,12,4,12,4,12,12,4,4,12,4,12,4,12,4,12,4,12,12,4,4,12,12,4,4,12,4,12"
+
+        system_code = dev.calc_systemcode("A")
+        unit_code = dev.calc_unitcode(5)
+        dev2 = CMR1000(system_code, unit_code)
+
+        self.assertEqual(on_signal_2, dev.get_signal(Action.ON))
+        self.assertEqual(on_signal_2, dev2.get_signal(Action.ON))
+        self.assertEqual(off_signal_2, dev.get_signal(Action.OFF))
+        self.assertEqual(off_signal_2, dev2.get_signal(Action.OFF))
 
-        self.assertEqual(on_signal_ITGW, dev.get_signal(self.itgw, Action.ON))
-        self.assertEqual(off_signal_ITGW, dev.get_signal(self.itgw, Action.OFF))
+    def test_CMR1000_generation(self):
+        system_code = CMR1000.calc_systemcode("P")
+        unit_code = CMR1000.calc_unitcode(16)
+        dev = CMR1000(system_code, unit_code)
+        on_signal = "4,12,12,4,4,12,12,4,4,12,12,4,4,12,12,4,4,12,12,4,4,12,12,4,4,12,12,4,4,12,12,4,4,12,4,12,4,12,12,4,4,12,12,4,4,12,12,4"
+        off_signal = "4,12,12,4,4,12,12,4,4,12,12,4,4,12,12,4,4,12,12,4,4,12,12,4,4,12,12,4,4,12,12,4,4,12,4,12,4,12,12,4,4,12,12,4,4,12,4,12"
+        self.assertEqual(on_signal, dev.get_signal(Action.ON))
+        self.assertEqual(off_signal, dev.get_signal(Action.OFF))
 
-        on_signal_BSGW = "TXP:0,0,6,11125,89,25,4,12,4,12,4,12,4,12,4,12,4,12,4,12,4,12,4,12,4,12,4,12,4,12,4,12,12,4,4,12,4,12,4,12,4,12,4,12,12,4,4,12,12,4,4,12,12,4,1,140;"
-        off_signal_BSGW = "TXP:0,0,6,11125,89,25,4,12,4,12,4,12,4,12,4,12,4,12,4,12,4,12,4,12,4,12,4,12,4,12,4,12,12,4,4,12,4,12,4,12,4,12,4,12,12,4,4,12,12,4,4,12,4,12,1,140;"
+    def test_invalid_action(self):
+        """Test to check if invalid actions raise an exception"""
+        device = CMR1000("0000", "0010")
 
-        self.assertEqual(on_signal_BSGW, dev.get_signal(self.bsgw, Action.ON))
-        self.assertEqual(off_signal_BSGW, dev.get_signal(self.bsgw, Action.OFF))
+        with self.assertRaises(ValueError):
+            _ = device.get_signal("WrongAction!")
+
+    def test_valid_action(self):
+        """Test to check if valid actions are set up correctly"""
+        device = CMR1000("0000", "0010")
+
+        self.assertIsNotNone(device.supported_actions.get(Action.ON))
+        self.assertIsNotNone(device.supported_actions.get(Action.OFF))
+
+        self.assertEqual(device.on, device.supported_actions.get(Action.ON))
+        self.assertEqual(device.off, device.supported_actions.get(Action.OFF))
 
     def test_ITR3500(self):
         """Test to check the functionality of the ITR3500 class"""
         dev = ITR3500("0000", "0010")  # binary representation of A5
 
-        on_signal_ITGW = "0,0,6,11125,89,26,0,4,12,4,12,4,12,4,12,4,12,4,12,4,12,4,12,4,12,4,12,4,12,4,12,4,12,12,4,4,12,4,12,4,12,4,12,4,12,12,4,4,12,12,4,4,12,12,4,1,125,0"
-        off_signal_ITGW = "0,0,6,11125,89,26,0,4,12,4,12,4,12,4,12,4,12,4,12,4,12,4,12,4,12,4,12,4,12,4,12,4,12,12,4,4,12,4,12,4,12,4,12,4,12,12,4,4,12,12,4,4,12,4,12,1,125,0"
+        on_signal = "4,12,4,12,4,12,4,12,4,12,4,12,4,12,4,12,4,12,4,12,4,12,4,12,4,12,12,4,4,12,4,12,4,12,4,12,4,12,12,4,4,12,12,4,4,12,12,4"
+        off_signal = "4,12,4,12,4,12,4,12,4,12,4,12,4,12,4,12,4,12,4,12,4,12,4,12,4,12,12,4,4,12,4,12,4,12,4,12,4,12,12,4,4,12,12,4,4,12,4,12"
+
+        self.assertEqual(on_signal, dev.get_signal(Action.ON))
+        self.assertEqual(off_signal, dev.get_signal(Action.OFF))
+
+    def test_calc_unitcode(self):
+        with self.assertRaises(expected_exception=ValueError):
+            CMR1000.calc_unitcode(-1)
+
+        with self.assertRaises(expected_exception=ValueError):
+            CMR1000.calc_unitcode(17)
+
+    def test_calc_systemcode(self):
+        with self.assertRaises(expected_exception=ValueError):
+            CMR1000.calc_systemcode("Z")
 
-        self.assertEqual(on_signal_ITGW, dev.get_signal(self.itgw, Action.ON))
-        self.assertEqual(off_signal_ITGW, dev.get_signal(self.itgw, Action.OFF))
+        with self.assertRaises(expected_exception=ValueError):
+            CMR1000.calc_systemcode("-")
 
-        on_signal_BSGW = "TXP:0,0,6,11125,89,25,4,12,4,12,4,12,4,12,4,12,4,12,4,12,4,12,4,12,4,12,4,12,4,12,4,12,12,4,4,12,4,12,4,12,4,12,4,12,12,4,4,12,12,4,4,12,12,4,1,140;"
-        off_signal_BSGW = "TXP:0,0,6,11125,89,25,4,12,4,12,4,12,4,12,4,12,4,12,4,12,4,12,4,12,4,12,4,12,4,12,4,12,12,4,4,12,4,12,4,12,4,12,4,12,12,4,4,12,12,4,4,12,4,12,1,140;"
 
-        self.assertEqual(on_signal_BSGW, dev.get_signal(self.bsgw, Action.ON))
-        self.assertEqual(off_signal_BSGW, dev.get_signal(self.bsgw, Action.OFF))
+if __name__ == "__main__":
+    unittest.main()
diff --git a/pyBrematic/devices/intertechno/tests/intertechnodevice_test.py b/pyBrematic/devices/intertechno/tests/intertechnodevice_test.py
new file mode 100644
index 0000000..8dbb60e
--- /dev/null
+++ b/pyBrematic/devices/intertechno/tests/intertechnodevice_test.py
@@ -0,0 +1,111 @@
+# -*- coding: utf-8 -*-
+
+import unittest
+
+from pyBrematic.devices.intertechno import IntertechnoDevice
+
+
+class TestIntertechnoDevice(unittest.TestCase):
+
+    def setUp(self):
+        pass
+
+    def tearDown(self):
+        pass
+
+    def test_calc_unitcode_invalid_range(self):
+        """Test if exception raises on out of range values for calc_unitcode"""
+        with self.assertRaises(expected_exception=ValueError):
+            IntertechnoDevice.calc_unitcode(-1)
+
+        with self.assertRaises(expected_exception=ValueError):
+            IntertechnoDevice.calc_unitcode(17)
+
+    def test_calc_systemcode_invalid_range(self):
+        """Test if exception raises on out of range values for calc_systemcode"""
+        with self.assertRaises(expected_exception=ValueError):
+            IntertechnoDevice.calc_systemcode("Z")
+
+        with self.assertRaises(expected_exception=ValueError):
+            IntertechnoDevice.calc_systemcode("-")
+
+        with self.assertRaises(expected_exception=ValueError):
+            IntertechnoDevice.calc_systemcode("")
+
+    def test_calc_systemcode(self):
+        """Test if the system code calculation returns correct codes"""
+        dev = IntertechnoDevice("0000", "0000")
+
+        code = dev.calc_systemcode("A")
+        self.assertEqual(code, "0000")
+        code = dev.calc_systemcode("B")
+        self.assertEqual(code, "1000")
+        code = dev.calc_systemcode("C")
+        self.assertEqual(code, "0100")
+        code = dev.calc_systemcode("D")
+        self.assertEqual(code, "1100")
+        code = dev.calc_systemcode("E")
+        self.assertEqual(code, "0010")
+        code = dev.calc_systemcode("F")
+        self.assertEqual(code, "1010")
+        code = dev.calc_systemcode("G")
+        self.assertEqual(code, "0110")
+        code = dev.calc_systemcode("H")
+        self.assertEqual(code, "1110")
+        code = dev.calc_systemcode("I")
+        self.assertEqual(code, "0001")
+        code = dev.calc_systemcode("J")
+        self.assertEqual(code, "1001")
+        code = dev.calc_systemcode("K")
+        self.assertEqual(code, "0101")
+        code = dev.calc_systemcode("L")
+        self.assertEqual(code, "1101")
+        code = dev.calc_systemcode("M")
+        self.assertEqual(code, "0011")
+        code = dev.calc_systemcode("N")
+        self.assertEqual(code, "1011")
+        code = dev.calc_systemcode("O")
+        self.assertEqual(code, "0111")
+        code = dev.calc_systemcode("P")
+        self.assertEqual(code, "1111")
+
+    def test_calc_unitcode(self):
+        """Test if the unit code calculation returns correct codes"""
+        dev = IntertechnoDevice("0000", "0000")
+
+        code = dev.calc_unitcode(1)
+        self.assertEqual(code, "0000")
+        code = dev.calc_unitcode(2)
+        self.assertEqual(code, "1000")
+        code = dev.calc_unitcode(3)
+        self.assertEqual(code, "0100")
+        code = dev.calc_unitcode(4)
+        self.assertEqual(code, "1100")
+        code = dev.calc_unitcode(5)
+        self.assertEqual(code, "0010")
+        code = dev.calc_unitcode(6)
+        self.assertEqual(code, "1010")
+        code = dev.calc_unitcode(7)
+        self.assertEqual(code, "0110")
+        code = dev.calc_unitcode(8)
+        self.assertEqual(code, "1110")
+        code = dev.calc_unitcode(9)
+        self.assertEqual(code, "0001")
+        code = dev.calc_unitcode(10)
+        self.assertEqual(code, "1001")
+        code = dev.calc_unitcode(11)
+        self.assertEqual(code, "0101")
+        code = dev.calc_unitcode(12)
+        self.assertEqual(code, "1101")
+        code = dev.calc_unitcode(13)
+        self.assertEqual(code, "0011")
+        code = dev.calc_unitcode(14)
+        self.assertEqual(code, "1011")
+        code = dev.calc_unitcode(15)
+        self.assertEqual(code, "0111")
+        code = dev.calc_unitcode(16)
+        self.assertEqual(code, "1111")
+
+
+if __name__ == "__main__":
+    unittest.main()
diff --git a/pyBrematic/devices/intertechno/tests/tools_test.py b/pyBrematic/devices/intertechno/tests/tools_test.py
new file mode 100644
index 0000000..fca6597
--- /dev/null
+++ b/pyBrematic/devices/intertechno/tests/tools_test.py
@@ -0,0 +1,133 @@
+import unittest
+
+from pyBrematic.devices.intertechno.tools import calc_unitcode, calc_systemcode, calc_system_and_unit_code
+
+
+class TestIntertechnoTools(unittest.TestCase):
+
+    def test_calc_unitcode_invalid_range(self):
+        """Test if exception raises on out of range values for calc_unitcode"""
+        with self.assertRaises(expected_exception=ValueError):
+            calc_unitcode(-1)
+
+        with self.assertRaises(expected_exception=ValueError):
+            calc_unitcode(17)
+
+    def test_calc_systemcode_invalid_range(self):
+        """Test if exception raises on out of range values for calc_systemcode"""
+        with self.assertRaises(expected_exception=ValueError):
+            calc_systemcode("Z")
+
+        with self.assertRaises(expected_exception=ValueError):
+            calc_systemcode("-")
+
+        with self.assertRaises(expected_exception=ValueError):
+            calc_systemcode("")
+
+    def test_calc_systemcode(self):
+        """Test if the system code calculation returns correct codes"""
+        code = calc_systemcode("A")
+        self.assertEqual(code, "0000")
+        code = calc_systemcode("B")
+        self.assertEqual(code, "1000")
+        code = calc_systemcode("C")
+        self.assertEqual(code, "0100")
+        code = calc_systemcode("D")
+        self.assertEqual(code, "1100")
+        code = calc_systemcode("E")
+        self.assertEqual(code, "0010")
+        code = calc_systemcode("F")
+        self.assertEqual(code, "1010")
+        code = calc_systemcode("G")
+        self.assertEqual(code, "0110")
+        code = calc_systemcode("H")
+        self.assertEqual(code, "1110")
+        code = calc_systemcode("I")
+        self.assertEqual(code, "0001")
+        code = calc_systemcode("J")
+        self.assertEqual(code, "1001")
+        code = calc_systemcode("K")
+        self.assertEqual(code, "0101")
+        code = calc_systemcode("L")
+        self.assertEqual(code, "1101")
+        code = calc_systemcode("M")
+        self.assertEqual(code, "0011")
+        code = calc_systemcode("N")
+        self.assertEqual(code, "1011")
+        code = calc_systemcode("O")
+        self.assertEqual(code, "0111")
+        code = calc_systemcode("P")
+        self.assertEqual(code, "1111")
+
+    def test_calc_unitcode(self):
+        """Test if the unit code calculation returns correct codes"""
+        code = calc_unitcode(1)
+        self.assertEqual(code, "0000")
+        code = calc_unitcode(2)
+        self.assertEqual(code, "1000")
+        code = calc_unitcode(3)
+        self.assertEqual(code, "0100")
+        code = calc_unitcode(4)
+        self.assertEqual(code, "1100")
+        code = calc_unitcode(5)
+        self.assertEqual(code, "0010")
+        code = calc_unitcode(6)
+        self.assertEqual(code, "1010")
+        code = calc_unitcode(7)
+        self.assertEqual(code, "0110")
+        code = calc_unitcode(8)
+        self.assertEqual(code, "1110")
+        code = calc_unitcode(9)
+        self.assertEqual(code, "0001")
+        code = calc_unitcode(10)
+        self.assertEqual(code, "1001")
+        code = calc_unitcode(11)
+        self.assertEqual(code, "0101")
+        code = calc_unitcode(12)
+        self.assertEqual(code, "1101")
+        code = calc_unitcode(13)
+        self.assertEqual(code, "0011")
+        code = calc_unitcode(14)
+        self.assertEqual(code, "1011")
+        code = calc_unitcode(15)
+        self.assertEqual(code, "0111")
+        code = calc_unitcode(16)
+        self.assertEqual(code, "1111")
+
+    def test_calc_system_and_unit_code(self):
+        """Test if combined method does exactly the same as the individual methods"""
+        system1, unit1 = calc_system_and_unit_code("A5")
+        self.assertEqual(system1, calc_systemcode("A"))
+        self.assertEqual(unit1, calc_unitcode(5))
+
+        system2, unit2 = calc_system_and_unit_code("B11")
+        self.assertEqual(system2, calc_systemcode("B"))
+        self.assertEqual(unit2, calc_unitcode(11))
+
+        system3, unit3 = calc_system_and_unit_code("F8")
+        self.assertEqual(system3, calc_systemcode("F"))
+        self.assertEqual(unit3, calc_unitcode(8))
+
+    def test_calc_system_and_unit_code_issue(self):
+        """Test if wrong input raises a ValueError"""
+        with self.assertRaises(ValueError):
+            _, _ = calc_system_and_unit_code("Q5")
+
+        with self.assertRaises(ValueError):
+            _, _ = calc_system_and_unit_code("R18")
+
+        with self.assertRaises(ValueError):
+            _, _ = calc_system_and_unit_code("A18")
+
+        with self.assertRaises(ValueError):
+            _, _ = calc_system_and_unit_code("A0101110101")
+
+        with self.assertRaises(ValueError):
+            _, _ = calc_system_and_unit_code("11B")
+
+        with self.assertRaises(ValueError):
+            _, _ = calc_system_and_unit_code("0000100001")
+
+
+if __name__ == "__main__":
+    unittest.main()
diff --git a/pyBrematic/devices/tests/autopairdevice_test.py b/pyBrematic/devices/tests/autopairdevice_test.py
index f0ca129..4547861 100644
--- a/pyBrematic/devices/tests/autopairdevice_test.py
+++ b/pyBrematic/devices/tests/autopairdevice_test.py
@@ -2,7 +2,8 @@
 
 import unittest
 
-from pyBrematic.devices import AutoPairDevice, Action
+from pyBrematic.action import Action
+from pyBrematic.devices import AutoPairDevice
 from pyBrematic.gateways import BrennenstuhlGateway
 
 
@@ -18,4 +19,4 @@ def test_Device(self):
         """Test to make sure that call to method raises an exception"""
         dev = AutoPairDevice("10000", "00100")
         with self.assertRaises(NotImplementedError):
-            dev.get_signal(self.gw, Action.ON)
+            dev.get_signal(Action.ON)
diff --git a/pyBrematic/devices/tests/device_test.py b/pyBrematic/devices/tests/device_test.py
index cd00094..1b62305 100644
--- a/pyBrematic/devices/tests/device_test.py
+++ b/pyBrematic/devices/tests/device_test.py
@@ -2,14 +2,14 @@
 
 import unittest
 
-from pyBrematic.devices import Device, Action
-from pyBrematic.gateways import BrennenstuhlGateway
+from pyBrematic.action import Action
+from pyBrematic.devices import Device
 
 
 class TestDevice(unittest.TestCase):
 
     def setUp(self):
-        self.gw = BrennenstuhlGateway("192.168.178.2")
+        pass
 
     def tearDown(self):
         pass
@@ -18,4 +18,19 @@ def test_Device(self):
         """Test to make sure that call to method raises an exception"""
         dev = Device("10000", "00100")
         with self.assertRaises(NotImplementedError):
-            dev.get_signal(self.gw, Action.ON)
+            dev.get_signal(Action.ON)
+
+    def test_encode_error(self):
+        dev = Device("10000", "00100")
+        with self.assertRaises(ValueError):
+            dev.encode("000123000", "A", "B")
+
+    def test_encode(self):
+        dev = Device("10000", "00100")
+        encoded = dev.encode("000111", "A", "B")
+        self.assertEqual(list, type(encoded))
+        self.assertEqual(["B", "B", "B", "A", "A", "A"], encoded)
+
+
+if __name__ == "__main__":
+    unittest.main()
diff --git a/pyBrematic/gateways/tests/__init__.py b/pyBrematic/gateways/tests/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/pyBrematic/gateways/tests/brennenstuhl_gateway_test.py b/pyBrematic/gateways/tests/brennenstuhl_gateway_test.py
new file mode 100644
index 0000000..6eb9d84
--- /dev/null
+++ b/pyBrematic/gateways/tests/brennenstuhl_gateway_test.py
@@ -0,0 +1,87 @@
+# -*- coding: utf-8 -*-
+import socket
+import unittest
+from unittest.mock import Mock, patch
+
+from pyBrematic import Action
+from pyBrematic.gateways import BrennenstuhlGateway
+
+
+class TestBrennenstuhlGateway(unittest.TestCase):
+
+    def setUp(self):
+        self.ip = "192.168.1.100"
+        self.port = "1234"
+        self.gw = BrennenstuhlGateway(self.ip, self.port)
+
+    def test_default_port(self):
+        """Test if default port is correctly set"""
+        self.gw = BrennenstuhlGateway(self.ip)
+        self.assertEqual(49880, self.gw._port)
+
+    def test_build_udp_payload(self):
+        """Test if building the UDP payload for the Brennenstuhl GW works correctly"""
+        device = Mock()
+        device.repeat = 123
+        device.pause_BS = 987
+        device.tune = 567
+        device.baud = 999
+        device.txversion = 1
+        device.speed_BS = 234
+
+        function = Mock()
+        function.return_value = "SIGNAL-A,B,B,A-SIGNAL"
+        device.get_signal = function
+
+        payload = self.gw.build_udp_payload(device, Action.ON)
+        self.assertEqual("TXP:0,0,123,987,567,999,SIGNAL-A,B,B,A-SIGNAL,1,234;", payload)
+
+    def test_get_head(self):
+        """Test if formatting the 'head' string works with random data"""
+        self.gw.head_format = "1{0}----{1}++++{2}ASDF{3}#24/"
+        head = self.gw.get_head(777, "ASDF1", 313, "-A-")
+        self.assertEqual("1777----ASDF1++++313ASDF-A-#24/", head)
+
+    def test_get_tail(self):
+        """Test if formatting the 'tail' string works with random data"""
+        self.gw.tail_format = "1{0}---++---{1}++ASDF#24/"
+        tail = self.gw.get_tail("//-ASDF1", 313)
+        self.assertEqual("1//-ASDF1---++---313++ASDF#24/", tail)
+
+    def test_missing_head_format(self):
+        """Test if missing 'head' format is handled correctly"""
+        self.gw.head_format = None
+        with self.assertRaises(ValueError):
+            _ = self.gw.get_head("1", "2", "3", "4")
+
+    def test_invalid_tail_format(self):
+        """Test if missing 'tail' format is handled correctly"""
+        self.gw.tail_format = None
+        with self.assertRaises(ValueError):
+            _ = self.gw.get_tail("1", "2")
+
+    @patch("socket.socket")
+    def test_send_request(self, socket_class_mock):
+        device_mock = Mock()
+        # Mock method to prevent issues with that
+        build_udp_payload_mock = Mock()
+        payload_mock = Mock()
+        # Since "encode" is being called on the payload object later on, we need to set a return value
+        payload_mock.encode.return_value = "Test Payload"
+        build_udp_payload_mock.return_value = payload_mock
+        self.gw.build_udp_payload = build_udp_payload_mock
+        action_mock = Mock()
+
+        socket_instance_mock = Mock()
+        socket_class_mock.return_value = socket_instance_mock
+
+        self.gw.send_request(device_mock, action_mock)
+
+        build_udp_payload_mock.assert_called_once_with(device_mock, action_mock)
+
+        socket_class_mock.assert_called_once_with(socket.AF_INET, socket.SOCK_DGRAM)
+        socket_instance_mock.sendto.assert_called_once_with("Test Payload", (self.ip, self.port))
+
+
+if __name__ == '__main__':
+    unittest.main()
diff --git a/pyBrematic/gateways/tests/connair_gateway_test.py b/pyBrematic/gateways/tests/connair_gateway_test.py
new file mode 100644
index 0000000..30f29e7
--- /dev/null
+++ b/pyBrematic/gateways/tests/connair_gateway_test.py
@@ -0,0 +1,87 @@
+# -*- coding: utf-8 -*-
+import socket
+import unittest
+from unittest.mock import Mock, patch
+
+from pyBrematic import Action
+from pyBrematic.gateways import ConnAirGateway
+
+
+class TestConnAirGateway(unittest.TestCase):
+
+    def setUp(self):
+        self.ip = "192.168.1.100"
+        self.port = "1234"
+        self.gw = ConnAirGateway(self.ip, self.port)
+
+    def test_default_port(self):
+        """Test if default port is correctly set"""
+        self.gw = ConnAirGateway(self.ip)
+        self.assertEqual(49880, self.gw._port)
+
+    def test_build_udp_payload(self):
+        """Test if building the UDP payload for the ConnAir GW works correctly"""
+        device = Mock()
+        device.repeat = 2345
+        device.pause_BS = 917
+        device.tune = 557
+        device.baud = 91919
+        device.txversion = 1
+        device.speed_BS = 234
+
+        function = Mock()
+        function.return_value = "A,SIGNAL-A,B,B,A-SIGNAL,C"
+        device.get_signal = function
+
+        payload = self.gw.build_udp_payload(device, Action.ON)
+        self.assertEqual("TXP:0,0,2345,917,557,91919,A,SIGNAL-A,B,B,A-SIGNAL,C,1,234;", payload)
+
+    def test_get_head(self):
+        """Test if formatting the 'head' string works with random data"""
+        self.gw.head_format = "1{0}----{1}++++{2}ASDF{3}#24/"
+        head = self.gw.get_head(777, "ASDF1", 313, "-A-")
+        self.assertEqual("1777----ASDF1++++313ASDF-A-#24/", head)
+
+    def test_get_tail(self):
+        """Test if formatting the 'tail' string works with random data"""
+        self.gw.tail_format = "1{0}---++---{1}++ASDF#24/"
+        tail = self.gw.get_tail("//-ASDF1", 313)
+        self.assertEqual("1//-ASDF1---++---313++ASDF#24/", tail)
+
+    def test_missing_head_format(self):
+        """Test if missing 'head' format is handled correctly"""
+        self.gw.head_format = None
+        with self.assertRaises(ValueError):
+            _ = self.gw.get_head("1", "2", "3", "4")
+
+    def test_invalid_tail_format(self):
+        """Test if missing 'tail' format is handled correctly"""
+        self.gw.tail_format = None
+        with self.assertRaises(ValueError):
+            _ = self.gw.get_tail("1", "2")
+
+    @patch("socket.socket")
+    def test_send_request(self, socket_class_mock):
+        device_mock = Mock()
+        # Mock method to prevent issues with that
+        build_udp_payload_mock = Mock()
+        payload_mock = Mock()
+        # Since "encode" is being called on the payload object later on, we need to set a return value
+        payload_mock.encode.return_value = "Test Payload"
+        build_udp_payload_mock.return_value = payload_mock
+        self.gw.build_udp_payload = build_udp_payload_mock
+        action_mock = Mock()
+
+        socket_instance_mock = Mock()
+        socket_class_mock.return_value = socket_instance_mock
+
+        self.gw.send_request(device_mock, action_mock)
+
+        build_udp_payload_mock.assert_called_once_with(device_mock, action_mock)
+
+        socket_class_mock.assert_called_once_with(socket.AF_INET, socket.SOCK_DGRAM)
+        socket_instance_mock.sendto.assert_called_once_with("Test Payload", (self.ip, self.port))
+
+
+if __name__ == '__main__':
+    unittest.main()
diff --git a/pyBrematic/gateways/tests/intertechno_gateway_test.py b/pyBrematic/gateways/tests/intertechno_gateway_test.py
new file mode 100644
index 0000000..759c7cc
--- /dev/null
+++ b/pyBrematic/gateways/tests/intertechno_gateway_test.py
@@ -0,0 +1,88 @@
+# -*- coding: utf-8 -*-
+import socket
+import unittest
+from unittest.mock import Mock, patch
+
+from pyBrematic import Action
+from pyBrematic.gateways import IntertechnoGateway
+
+
+class TestIntertechnoGateway(unittest.TestCase):
+
+    def setUp(self):
+        self.ip = "192.168.1.100"
+        self.port = "1234"
+        self.gw = IntertechnoGateway(self.ip, self.port)
+
+    def test_default_port(self):
+        """Test if default port is correctly set"""
+        self.gw = IntertechnoGateway(self.ip)
+        self.assertEqual(49880, self.gw._port)
+
+    def test_build_udp_payload(self):
+        """Test if building the UDP payload for the Intertechno GW works correctly"""
+        device = Mock()
+        device.repeat = 2345
+        device.pause_BS = 917
+        device.pause_IT = 111
+        device.tune = 557
+        device.baud = 91919
+        device.txversion = 1
+        device.speed_BS = 234
+        device.speed_IT = 432
+
+        function = Mock()
+        function.return_value = "A,SIGNAL-A,B,B,A-SIGNAL,C"
+        device.get_signal = function
+
+        payload = self.gw.build_udp_payload(device, Action.ON)
+        self.assertEqual("0,0,2345,111,557,91919,A,SIGNAL-A,B,B,A-SIGNAL,C,1,432", payload)
+
+    def test_get_head(self):
+        """Test if formatting the 'head' string works with random data"""
+        self.gw.head_format = "1{0}----{1}++++{2}ASDF{3}#24/"
+        head = self.gw.get_head(777, "ASDF1", 313, "-A-")
+        self.assertEqual("1777----ASDF1++++313ASDF-A-#24/", head)
+
+    def test_get_tail(self):
+        """Test if formatting the 'tail' string works with random data"""
+        self.gw.tail_format = "1{0}---++---{1}++ASDF#24/"
+        tail = self.gw.get_tail("//-ASDF1", 313)
+        self.assertEqual("1//-ASDF1---++---313++ASDF#24/", tail)
+
+    def test_missing_head_format(self):
+        """Test if missing 'head' format is handled correctly"""
+        self.gw.head_format = None
+        with self.assertRaises(ValueError):
+            _ = self.gw.get_head("1", "2", "3", "4")
+
+    def test_invalid_tail_format(self):
+        """Test if missing 'tail' format is handled correctly"""
+        self.gw.tail_format = None
+        with self.assertRaises(ValueError):
+            _ = self.gw.get_tail("1", "2")
+
+    @patch("socket.socket")
+    def test_send_request(self, socket_class_mock):
+        device_mock = Mock()
+        # Mock method to prevent issues with that
+        build_udp_payload_mock = Mock()
+        payload_mock = Mock()
+        # Since "encode" is being called on the payload object later on, we need to set a return value
+        payload_mock.encode.return_value = "Test Payload"
+        build_udp_payload_mock.return_value = payload_mock
+        self.gw.build_udp_payload = build_udp_payload_mock
+        action_mock = Mock()
+
+        socket_instance_mock = Mock()
+        socket_class_mock.return_value = socket_instance_mock
+
+        self.gw.send_request(device_mock, action_mock)
+
+        build_udp_payload_mock.assert_called_once_with(device_mock, action_mock)
+
+        socket_class_mock.assert_called_once_with(socket.AF_INET, socket.SOCK_DGRAM)
+        socket_instance_mock.sendto.assert_called_once_with("Test Payload", (self.ip, self.port))
+
+if __name__ == '__main__':
+    unittest.main()
diff --git a/pyBrematic/utils/tests/storage_test.py b/pyBrematic/utils/tests/storage_test.py
index 28426e2..7835583 100644
--- a/pyBrematic/utils/tests/storage_test.py
+++ b/pyBrematic/utils/tests/storage_test.py
@@ -1,5 +1,6 @@
 # -*- coding: utf-8 -*-
 import json
+import logging
 import unittest
 
 from pyBrematic.utils import Storage
@@ -99,6 +100,8 @@ def test_add_device(self):
         self.assertEqual(dev.seed, seed)
 
     def test_duplicate_storage(self):
+        # Set log-level to "ERROR" to prevent text output during test execution
+        logging.getLogger().setLevel(logging.ERROR)
         self.storage.add_device(1337)
         self.assertEqual(1, len(self.storage.devices))