diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml
new file mode 100644
index 0000000..5f6b2b0
--- /dev/null
+++ b/.github/workflows/tests.yml
@@ -0,0 +1,45 @@
+name: Tests
+
+on:
+  push: ~
+  pull_request: ~
+
+jobs:
+  build:
+    runs-on: ubuntu-latest
+    timeout-minutes: 10  # Don't run forever when stale
+
+    strategy:
+      matrix:
+        python-version:
+          - 3.6
+          - 3.7
+          - 3.8
+          - 3.9
+
+    name: Python ${{ matrix.python-version }}
+    steps:
+      - uses: actions/checkout@v2
+
+      - name: Setup Python ${{ matrix.python-version }}
+        uses: actions/setup-python@v2
+        with:
+          python-version: ${{ matrix.python-version }}
+
+      - name: Cached PIP dependencies
+        uses: actions/cache@v2
+        with:
+          path: |
+            ~/.cache/pip
+            ~/.tox/python/.pytest_cache
+          key: pip-${{ matrix.python-version }}-${{ hashFiles('setup.py', 'tox.ini') }}
+          restore-keys: pip-${{ matrix.python-version }}-
+
+      - name: Install dependencies
+        run: pip install tox
+
+      - name: Run tests
+        run: tox
+
+      - name: Code coverage upload
+        uses: codecov/codecov-action@v1
diff --git a/.travis.yml b/.travis.yml
deleted file mode 100644
index bc6b513..0000000
--- a/.travis.yml
+++ /dev/null
@@ -1,18 +0,0 @@
-language: python
-
-python:
-  - 2.7
-  - 3.5
-  - 3.6
-  - 3.8
-  
-install: pip install tox-travis codecov
-
-script: tox
-
-after_success:
-  - codecov
-
-matrix:
-  allow_failures:
-    - python: 2.7
diff --git a/README.rst b/README.rst
index 403a723..b97d440 100644
--- a/README.rst
+++ b/README.rst
@@ -1,3 +1,6 @@
+**Notice:** this repository is in need of a new maintainer. If you are interested or have ideas about this, please let me know.
+
+
 DSMR Parser
 ===========
 
@@ -43,6 +46,25 @@ process because the code is blocking (not asynchronous):
 
 To be documented.
 
+**Socket client**
+
+Read a remote serial port (for example using ser2net) and work with the parsed telegrams.
+It should be run in a separate process because the code is blocking (not asynchronous):
+
+.. code-block:: python
+
+     from dsmr_parser import telegram_specifications
+     from dsmr_parser.clients import SocketReader
+    
+     socket_reader = SocketReader(
+         host='127.0.0.1',
+         port=2001,
+         telegram_specification=telegram_specifications.V4
+     )
+    
+     for telegram in socket_reader.read():
+         print(telegram)  # see 'Telegram object' docs below
+
 
 Parsing module usage
 --------------------
diff --git a/dsmr_parser/__main__.py b/dsmr_parser/__main__.py
index a24a6a2..9169318 100644
--- a/dsmr_parser/__main__.py
+++ b/dsmr_parser/__main__.py
@@ -16,8 +16,8 @@ def console():
                         help='alternatively connect using TCP host.')
     parser.add_argument('--port', default=None,
                         help='TCP port to use for connection')
-    parser.add_argument('--version', default='2.2', choices=['2.2', '4', '5', '5B', '5L', '5S'],
-                        help='DSMR version (2.2, 4, 5, 5B, 5L, 5S)')
+    parser.add_argument('--version', default='2.2', choices=['2.2', '4', '5', '5B', '5L', '5S', 'Q3D'],
+                        help='DSMR version (2.2, 4, 5, 5B, 5L, 5S, Q3D)')
     parser.add_argument('--verbose', '-v', action='count')
 
     args = parser.parse_args()
diff --git a/dsmr_parser/clients/__init__.py b/dsmr_parser/clients/__init__.py
index 7323ecd..9563399 100644
--- a/dsmr_parser/clients/__init__.py
+++ b/dsmr_parser/clients/__init__.py
@@ -1,5 +1,6 @@
 from dsmr_parser.clients.settings import SERIAL_SETTINGS_V2_2, \
     SERIAL_SETTINGS_V4, SERIAL_SETTINGS_V5
 from dsmr_parser.clients.serial_ import SerialReader, AsyncSerialReader
+from dsmr_parser.clients.socket_ import SocketReader
 from dsmr_parser.clients.protocol import create_dsmr_protocol, \
     create_dsmr_reader, create_tcp_dsmr_reader
diff --git a/dsmr_parser/clients/protocol.py b/dsmr_parser/clients/protocol.py
index e996423..40cdfc3 100644
--- a/dsmr_parser/clients/protocol.py
+++ b/dsmr_parser/clients/protocol.py
@@ -30,6 +30,9 @@ def _create_dsmr_protocol(dsmr_version, telegram_callback, protocol, loop=None,
     elif dsmr_version == '4':
         specification = telegram_specifications.V4
         serial_settings = SERIAL_SETTINGS_V4
+    elif dsmr_version == '4+':
+        specification = telegram_specifications.V5
+        serial_settings = SERIAL_SETTINGS_V4
     elif dsmr_version == '5':
         specification = telegram_specifications.V5
         serial_settings = SERIAL_SETTINGS_V5
@@ -42,6 +45,9 @@ def _create_dsmr_protocol(dsmr_version, telegram_callback, protocol, loop=None,
     elif dsmr_version == "5S":
         specification = telegram_specifications.SWEDEN
         serial_settings = SERIAL_SETTINGS_V5
+    elif dsmr_version == "Q3D":
+        specification = telegram_specifications.Q3D
+        serial_settings = SERIAL_SETTINGS_V5
     else:
         raise NotImplementedError("No telegram parser found for version: %s",
                                   dsmr_version)
@@ -106,12 +112,16 @@ class DSMRProtocol(asyncio.Protocol):
 
     def data_received(self, data):
         """Add incoming data to buffer."""
-        data = data.decode('ascii')
+
+        # accept latin-1 (8-bit) on the line, to allow for non-ascii transport or padding
+        data = data.decode("latin1")
         self._active = True
         self.log.debug('received data: %s', data)
         self.telegram_buffer.append(data)
 
         for telegram in self.telegram_buffer.get_all():
+            # ensure actual telegram is ascii (7-bit) only (ISO 646:1991 IRV required in section 5.5 of IEC 62056-21)
+            telegram = telegram.encode("latin1").decode("ascii")
             self.handle_telegram(telegram)
 
     def keep_alive(self):
diff --git a/dsmr_parser/clients/socket_.py b/dsmr_parser/clients/socket_.py
new file mode 100644
index 0000000..6727979
--- /dev/null
+++ b/dsmr_parser/clients/socket_.py
@@ -0,0 +1,90 @@
+import logging
+import socket
+
+from dsmr_parser.clients.telegram_buffer import TelegramBuffer
+from dsmr_parser.exceptions import ParseError, InvalidChecksumError
+from dsmr_parser.parsers import TelegramParser
+from dsmr_parser.objects import Telegram
+
+
+logger = logging.getLogger(__name__)
+
+
+class SocketReader(object):
+
+    BUFFER_SIZE = 256
+
+    def __init__(self, host, port, telegram_specification):
+        self.host = host
+        self.port = port
+
+        self.telegram_parser = TelegramParser(telegram_specification)
+        self.telegram_buffer = TelegramBuffer()
+        self.telegram_specification = telegram_specification
+
+    def read(self):
+        """
+        Read complete DSMR telegram's from remote interface and parse it
+        into CosemObject's and MbusObject's
+
+        :rtype: generator
+        """
+        buffer = b""
+
+        with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as socket_handle:
+
+            socket_handle.connect((self.host, self.port))
+
+            while True:
+                buffer += socket_handle.recv(self.BUFFER_SIZE)
+
+                lines = buffer.splitlines(keepends=True)
+
+                if len(lines) == 0:
+                    continue
+
+                for data in lines:
+                    self.telegram_buffer.append(data.decode('ascii'))
+
+                for telegram in self.telegram_buffer.get_all():
+                    try:
+                        yield self.telegram_parser.parse(telegram)
+                    except InvalidChecksumError as e:
+                        logger.warning(str(e))
+                    except ParseError as e:
+                        logger.error('Failed to parse telegram: %s', e)
+
+                buffer = b""
+
+    def read_as_object(self):
+        """
+        Read complete DSMR telegram's from remote and return a Telegram object.
+
+        :rtype: generator
+        """
+        buffer = b""
+
+        with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as socket_handle:
+
+            socket_handle.connect((self.host, self.port))
+
+            while True:
+                buffer += socket_handle.recv(self.BUFFER_SIZE)
+
+                lines = buffer.splitlines(keepends=True)
+
+                if len(lines) == 0:
+                    continue
+
+                for data in lines:
+                    self.telegram_buffer.append(data.decode('ascii'))
+
+                    for telegram in self.telegram_buffer.get_all():
+                        try:
+                            yield Telegram(telegram, self.telegram_parser, self.telegram_specification)
+                        except InvalidChecksumError as e:
+                            logger.warning(str(e))
+                        except ParseError as e:
+                            logger.error('Failed to parse telegram: %s', e)
+
+                buffer = b""
diff --git a/dsmr_parser/obis_name_mapping.py b/dsmr_parser/obis_name_mapping.py
index 4028890..b224b7a 100644
--- a/dsmr_parser/obis_name_mapping.py
+++ b/dsmr_parser/obis_name_mapping.py
@@ -13,6 +13,7 @@ EN = {
     obis.ELECTRICITY_IMPORTED_TOTAL: 'ELECTRICITY_IMPORTED_TOTAL',
     obis.ELECTRICITY_USED_TARIFF_1: 'ELECTRICITY_USED_TARIFF_1',
     obis.ELECTRICITY_USED_TARIFF_2: 'ELECTRICITY_USED_TARIFF_2',
+    obis.ELECTRICITY_EXPORTED_TOTAL: 'ELECTRICITY_EXPORTED_TOTAL',
     obis.ELECTRICITY_DELIVERED_TARIFF_1: 'ELECTRICITY_DELIVERED_TARIFF_1',
     obis.ELECTRICITY_DELIVERED_TARIFF_2: 'ELECTRICITY_DELIVERED_TARIFF_2',
     obis.ELECTRICITY_ACTIVE_TARIFF: 'ELECTRICITY_ACTIVE_TARIFF',
@@ -51,10 +52,9 @@ EN = {
     obis.VALVE_POSITION_GAS: 'VALVE_POSITION_GAS',
     obis.BELGIUM_HOURLY_GAS_METER_READING: 'BELGIUM_HOURLY_GAS_METER_READING',
     obis.LUXEMBOURG_EQUIPMENT_IDENTIFIER: 'LUXEMBOURG_EQUIPMENT_IDENTIFIER',
-    obis.LUXEMBOURG_ELECTRICITY_USED_TARIFF_GLOBAL: 'LUXEMBOURG_ELECTRICITY_USED_TARIFF_GLOBAL',
-    obis.LUXEMBOURG_ELECTRICITY_DELIVERED_TARIFF_GLOBAL: 'LUXEMBOURG_ELECTRICITY_DELIVERED_TARIFF_GLOBAL',
-    obis.SWEDEN_ELECTRICITY_USED_TARIFF_GLOBAL: 'SWEDEN_ELECTRICITY_USED_TARIFF_GLOBAL',
-    obis.SWEDEN_ELECTRICITY_DELIVERED_TARIFF_GLOBAL: 'SWEDEN_ELECTRICITY_DELIVERED_TARIFF_GLOBAL',
+    obis.Q3D_EQUIPMENT_IDENTIFIER: 'Q3D_EQUIPMENT_IDENTIFIER',
+    obis.Q3D_EQUIPMENT_STATE: 'Q3D_EQUIPMENT_STATE',
+    obis.Q3D_EQUIPMENT_SERIALNUMBER: 'Q3D_EQUIPMENT_SERIALNUMBER',
 }
 
 REVERSE_EN = dict([(v, k) for k, v in EN.items()])
diff --git a/dsmr_parser/obis_references.py b/dsmr_parser/obis_references.py
index 5ac3b66..d4a4cbf 100644
--- a/dsmr_parser/obis_references.py
+++ b/dsmr_parser/obis_references.py
@@ -8,7 +8,6 @@ objects are introduced.
 """
 P1_MESSAGE_HEADER = r'\d-\d:0\.2\.8.+?\r\n'
 P1_MESSAGE_TIMESTAMP = r'\d-\d:1\.0\.0.+?\r\n'
-ELECTRICITY_IMPORTED_TOTAL = r'\d-\d:1\.8\.0.+?\r\n'
 ELECTRICITY_USED_TARIFF_1 = r'\d-\d:1\.8\.1.+?\r\n'
 ELECTRICITY_USED_TARIFF_2 = r'\d-\d:1\.8\.2.+?\r\n'
 ELECTRICITY_DELIVERED_TARIFF_1 = r'\d-\d:2\.8\.1.+?\r\n'
@@ -61,10 +60,13 @@ ELECTRICITY_DELIVERED_TARIFF_ALL = (
     ELECTRICITY_DELIVERED_TARIFF_2
 )
 
-# Alternate codes for foreign countries.
+# International generalized additions
+ELECTRICITY_IMPORTED_TOTAL = r'\d-\d:1\.8\.0.+?\r\n'  # Total imported energy register (P+)
+ELECTRICITY_EXPORTED_TOTAL = r'\d-\d:2\.8\.0.+?\r\n'  # Total exported energy register (P-)
+
+# International non generalized additions (country specific) / risk for necessary refactoring
 BELGIUM_HOURLY_GAS_METER_READING = r'\d-\d:24\.2\.3.+?\r\n'  # Different code, same format.
 LUXEMBOURG_EQUIPMENT_IDENTIFIER = r'\d-\d:42\.0\.0.+?\r\n'  # Logical device name
-LUXEMBOURG_ELECTRICITY_USED_TARIFF_GLOBAL = r'\d-\d:1\.8\.0.+?\r\n'  # Total imported energy register (P+)
-LUXEMBOURG_ELECTRICITY_DELIVERED_TARIFF_GLOBAL = r'\d-\d:2\.8\.0.+?\r\n'  # Total exported energy register (P-)
-SWEDEN_ELECTRICITY_USED_TARIFF_GLOBAL = r'\d-\d:1\.8\.0.+?\r\n'  # Total imported energy register (P+)
-SWEDEN_ELECTRICITY_DELIVERED_TARIFF_GLOBAL = r'\d-\d:2\.8\.0.+?\r\n'  # Total exported energy register (P-)
+Q3D_EQUIPMENT_IDENTIFIER = r'\d-\d:0\.0\.0.+?\r\n'  # Logical device name
+Q3D_EQUIPMENT_STATE = r'\d-\d:96\.5\.5.+?\r\n'  # Device state (hexadecimal)
+Q3D_EQUIPMENT_SERIALNUMBER = r'\d-\d:96\.1\.255.+?\r\n'  # Device Serialnumber
diff --git a/dsmr_parser/telegram_specifications.py b/dsmr_parser/telegram_specifications.py
index 5a06ce0..c8f05a5 100644
--- a/dsmr_parser/telegram_specifications.py
+++ b/dsmr_parser/telegram_specifications.py
@@ -153,18 +153,19 @@ BELGIUM_FLUVIUS['objects'].update({
 LUXEMBOURG_SMARTY = deepcopy(V5)
 LUXEMBOURG_SMARTY['objects'].update({
     obis.LUXEMBOURG_EQUIPMENT_IDENTIFIER: CosemParser(ValueParser(str)),
-    obis.LUXEMBOURG_ELECTRICITY_USED_TARIFF_GLOBAL: CosemParser(ValueParser(Decimal)),
-    obis.LUXEMBOURG_ELECTRICITY_DELIVERED_TARIFF_GLOBAL: CosemParser(ValueParser(Decimal)),
+    obis.ELECTRICITY_IMPORTED_TOTAL: CosemParser(ValueParser(Decimal)),
+    obis.ELECTRICITY_EXPORTED_TOTAL: CosemParser(ValueParser(Decimal)),
 })
 
-# Source: https://www.energiforetagen.se/globalassets/energiforetagen/det-erbjuder-vi/kurser-och-konferenser/elnat/branschrekommendation-lokalt-granssnitt-v2_0-201912.pdf # noqa
+# Source: https://www.energiforetagen.se/globalassets/energiforetagen/det-erbjuder-vi/kurser-och-konferenser/elnat/
+#         branschrekommendation-lokalt-granssnitt-v2_0-201912.pdf
 SWEDEN = {
     'checksum_support': True,
     'objects': {
         obis.P1_MESSAGE_HEADER: CosemParser(ValueParser(str)),
         obis.P1_MESSAGE_TIMESTAMP: CosemParser(ValueParser(timestamp)),
-        obis.SWEDEN_ELECTRICITY_USED_TARIFF_GLOBAL: CosemParser(ValueParser(Decimal)),
-        obis.SWEDEN_ELECTRICITY_DELIVERED_TARIFF_GLOBAL: CosemParser(ValueParser(Decimal)),
+        obis.ELECTRICITY_IMPORTED_TOTAL: CosemParser(ValueParser(Decimal)),
+        obis.ELECTRICITY_EXPORTED_TOTAL: CosemParser(ValueParser(Decimal)),
         obis.CURRENT_ELECTRICITY_USAGE: CosemParser(ValueParser(Decimal)),
         obis.CURRENT_ELECTRICITY_DELIVERY: CosemParser(ValueParser(Decimal)),
         obis.INSTANTANEOUS_ACTIVE_POWER_L1_POSITIVE: CosemParser(ValueParser(Decimal)),
@@ -181,3 +182,18 @@ SWEDEN = {
         obis.INSTANTANEOUS_CURRENT_L3: CosemParser(ValueParser(Decimal)),
     }
 }
+
+Q3D = {
+    "checksum_support": False,
+    "objects": {
+        obis.Q3D_EQUIPMENT_IDENTIFIER: CosemParser(ValueParser(str)),
+        obis.ELECTRICITY_IMPORTED_TOTAL: CosemParser(ValueParser(Decimal)),
+        obis.ELECTRICITY_EXPORTED_TOTAL: CosemParser(ValueParser(Decimal)),
+        obis.INSTANTANEOUS_ACTIVE_POWER_L1_POSITIVE: CosemParser(ValueParser(Decimal)),
+        obis.INSTANTANEOUS_ACTIVE_POWER_L2_POSITIVE: CosemParser(ValueParser(Decimal)),
+        obis.INSTANTANEOUS_ACTIVE_POWER_L3_POSITIVE: CosemParser(ValueParser(Decimal)),
+        obis.CURRENT_ELECTRICITY_USAGE: CosemParser(ValueParser(Decimal)),
+        obis.Q3D_EQUIPMENT_STATE: CosemParser(ValueParser(str)),
+        obis.Q3D_EQUIPMENT_SERIALNUMBER: CosemParser(ValueParser(str)),
+    },
+}
diff --git a/test/example_telegrams.py b/test/example_telegrams.py
index f74ed16..1ccb8ce 100644
--- a/test/example_telegrams.py
+++ b/test/example_telegrams.py
@@ -128,3 +128,44 @@ TELEGRAM_V5 = (
     '0-2:96.1.0()\r\n'
     '!6EEE\r\n'
 )
+
+# EasyMeter via COM-1 Ethernet Gateway
+# Q3D Manual (german) https://www.easymeter.com/downloads/products/zaehler/Q3D/Easymeter_Q3D_DE_2016-06-15.pdf
+#  - type code on page 8
+#  - D0-Specs on page 20
+#
+# last two lines are added by the COM-1 Ethernet Gateway
+
+TELEGRAM_ESY5Q3DB1024_V304 = (
+    '/ESY5Q3DB1024 V3.04\r\n'
+    '\r\n'
+    '1-0:0.0.0*255(0272031312565)\r\n'
+    '1-0:1.8.0*255(00052185.7825309*kWh)\r\n'
+    '1-0:2.8.0*255(00019949.3221493*kWh)\r\n'
+    '1-0:21.7.0*255(000747.85*W)\r\n'
+    '1-0:41.7.0*255(000737.28*W)\r\n'
+    '1-0:61.7.0*255(000639.73*W)\r\n'
+    '1-0:1.7.0*255(002124.86*W)\r\n'
+    '1-0:96.5.5*255(80)\r\n'
+    '0-0:96.1.255*255(1ESY1313002565)\r\n'
+    '!\r\n'
+    '  25803103\r\n'
+    '\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff'
+    '\xff\xff\xff\xff\xff\r\n'
+)
+
+TELEGRAM_ESY5Q3DA1004_V304 = (
+    '/ESY5Q3DA1004 V3.04\r\n'
+    '\r\n'
+    '1-0:0.0.0*255(1336001560)\r\n'
+    '1-0:1.8.0*255(00032549.5061662*kWh)\r\n'
+    '1-0:21.7.0*255(000557.29*W)\r\n'
+    '1-0:41.7.0*255(000521.62*W)\r\n'
+    '1-0:61.7.0*255(000609.30*W)\r\n'
+    '1-0:1.7.0*255(001688.21*W)\r\n'
+    '1-0:96.5.5*255(80)\r\n'
+    '0-0:96.1.255*255(1ESY1336001560)\r\n'
+    '!\r\n'
+    '  25818685\r\n'
+    'DE0000000000000000000000000000003\r\n'
+)
diff --git a/tox.ini b/tox.ini
index a9a403d..27fc713 100644
--- a/tox.ini
+++ b/tox.ini
@@ -1,13 +1,9 @@
-[tox]
-envlist = py35,py36,py37,py38,py39
-
 [testenv]
 deps=
   pytest
   pytest-cov
   pylama
   pytest-asyncio
-  pytest-catchlog
   pytest-mock
 commands=
   py.test --cov=dsmr_parser test {posargs}