From d24e3c8fd5635838ff9c5c82254474e1f490d326 Mon Sep 17 00:00:00 2001
From: Philipp Klaus <klaus@physik.uni-frankfurt.de>
Date: Mon, 14 Aug 2017 17:33:36 +0200
Subject: [PATCH] opus20: goodbye for now, -> Github / PyPI

---
 opus20/MANIFEST.in                        |   2 -
 opus20/README.md                          | 114 +--
 opus20/gitignore                          |  10 -
 opus20/opus20/__init__.py                 |  10 -
 opus20/opus20/fakeserver.py               | 105 ---
 opus20/opus20/opus20.py                   | 847 ----------------------
 opus20/opus20/opus20_cli.py               | 110 ---
 opus20/opus20/opus20_discovery.py         |  45 --
 opus20/opus20/opus20_fakeserver.py        |  30 -
 opus20/opus20/opus20_web.py               |  45 --
 opus20/opus20/webapp/__init__.py          | 224 ------
 opus20/opus20/webapp/static/css/style.css |  23 -
 opus20/opus20/webapp/static/js/script.js  |   1 -
 opus20/opus20/webapp/views/about.jinja2   |  18 -
 opus20/opus20/webapp/views/base.jinja2    |  79 --
 opus20/opus20/webapp/views/debug.jinja2   |  10 -
 opus20/opus20/webapp/views/plots.jinja2   |  61 --
 opus20/opus20/webapp/views/status.jinja2  |  58 --
 opus20/setup.py                           |  48 --
 19 files changed, 5 insertions(+), 1835 deletions(-)
 delete mode 100644 opus20/MANIFEST.in
 delete mode 100644 opus20/gitignore
 delete mode 100644 opus20/opus20/__init__.py
 delete mode 100644 opus20/opus20/fakeserver.py
 delete mode 100644 opus20/opus20/opus20.py
 delete mode 100755 opus20/opus20/opus20_cli.py
 delete mode 100755 opus20/opus20/opus20_discovery.py
 delete mode 100755 opus20/opus20/opus20_fakeserver.py
 delete mode 100755 opus20/opus20/opus20_web.py
 delete mode 100755 opus20/opus20/webapp/__init__.py
 delete mode 100644 opus20/opus20/webapp/static/css/style.css
 delete mode 100644 opus20/opus20/webapp/static/js/script.js
 delete mode 100644 opus20/opus20/webapp/views/about.jinja2
 delete mode 100644 opus20/opus20/webapp/views/base.jinja2
 delete mode 100644 opus20/opus20/webapp/views/debug.jinja2
 delete mode 100644 opus20/opus20/webapp/views/plots.jinja2
 delete mode 100644 opus20/opus20/webapp/views/status.jinja2
 delete mode 100644 opus20/setup.py

diff --git a/opus20/MANIFEST.in b/opus20/MANIFEST.in
deleted file mode 100644
index 2c31157..0000000
--- a/opus20/MANIFEST.in
+++ /dev/null
@@ -1,2 +0,0 @@
-recursive-include opus20/webapp/views *
-recursive-include opus20/webapp/static *
diff --git a/opus20/README.md b/opus20/README.md
index 3845dd3..9d834ba 100644
--- a/opus20/README.md
+++ b/opus20/README.md
@@ -1,115 +1,11 @@
-
 ### opus20 - a Python interface to the OPUS20
 
 This is a *opus20*, a Python software to query the temperature / 
 humidity / air pressure logging device OPUS20 produced by Lufft.
 
-#### Requirements
-
-*opus20* depends (only) on Python version 3.3+.
-I thought about backporting it to Python 2.7+ but it's not done so far.
-
-The web interface requires a couple of Python packages:
-
-    pip install jinja2 bottle matplotlib pandas numpy
-
-Installing matplotlib may also require you to install
-the python development package (for Python 3).
-
-#### Installing
-
-This package can be installed via pip:
-
-    pip install --upgrade https://github.com/pklaus/opus20/archive/master.zip
-
-To install all requirements for the included plot web server, too, run this command instead:
-
-    pip install --upgrade https://github.com/pklaus/opus20/archive/master.zip#egg=opus20[webserver]
-
-#### Usage
-
-The Python package installs a command line tool to query the device
-for current values. It's called `opus20_cli`.
-
-Here is how to get a list of all available *channels* from the device:
-
-    philipp@lion:~> opus20_cli 192.168.1.55 list
-    Channel   100 (0x0064): CUR temperature         unit: °C    offset: ±10.0  logging: no
-    Channel   120 (0x0078): MIN temperature         unit: °C    offset: ±10.0  logging: no
-    Channel   140 (0x008C): MAX temperature         unit: °C    offset: ±10.0  logging: no
-    Channel   160 (0x00A0): AVG temperature         unit: °C    offset: ±10.0  logging: yes
-    Channel   105 (0x0069): CUR temperature         unit: °F    offset: 0.0    logging: no
-    Channel   125 (0x007D): MIN temperature         unit: °F    offset: 0.0    logging: no
-    Channel   145 (0x0091): MAX temperature         unit: °F    offset: 0.0    logging: no
-    Channel   165 (0x00A5): AVG temperature         unit: °F    offset: 0.0    logging: no
-    Channel   200 (0x00C8): CUR relative humidity   unit: %     offset: ±30.0  logging: no
-    Channel   220 (0x00DC): MIN relative humidity   unit: %     offset: ±30.0  logging: no
-    Channel   240 (0x00F0): MAX relative humidity   unit: %     offset: ±30.0  logging: no
-    Channel   260 (0x0104): AVG relative humidity   unit: %     offset: ±30.0  logging: yes
-    Channel   205 (0x00CD): CUR absolute humidity   unit: g/m³  offset: 0.0    logging: no
-    Channel   225 (0x00E1): MIN absolute humidity   unit: g/m³  offset: 0.0    logging: no
-    Channel   245 (0x00F5): MAX absolute humidity   unit: g/m³  offset: 0.0    logging: no
-    Channel   265 (0x0109): AVG absolute humidity   unit: g/m³  offset: 0.0    logging: yes
-    Channel   110 (0x006E): CUR dewpoint            unit: °C    offset: 0.0    logging: no
-    Channel   130 (0x0082): MIN dewpoint            unit: °C    offset: 0.0    logging: no
-    Channel   150 (0x0096): MAX dewpoint            unit: °C    offset: 0.0    logging: no
-    Channel   170 (0x00AA): AVG dewpoint            unit: °C    offset: 0.0    logging: yes
-    Channel   115 (0x0073): CUR dewpoint            unit: °F    offset: 0.0    logging: no
-    Channel   135 (0x0087): MIN dewpoint            unit: °F    offset: 0.0    logging: no
-    Channel   155 (0x009B): MAX dewpoint            unit: °F    offset: 0.0    logging: no
-    Channel   175 (0x00AF): AVG dewpoint            unit: °F    offset: 0.0    logging: no
-    Channel 10020 (0x2724): CUR battery voltage     unit: V     offset: 0.0    logging: no
-    Channel 10040 (0x2738): MIN battery voltage     unit: V     offset: 0.0    logging: no
-    Channel 10060 (0x274C): MAX battery voltage     unit: V     offset: 0.0    logging: no
-    Channel 10080 (0x2760): AVG battery voltage     unit: V     offset: 0.0    logging: yes
-
-
-Asking for the value of a channel works like this:
-
-    philipp@lion:~> opus20_cli 192.168.1.55 get 0x0064
-    24.712
-
-You can also download the values stored on the device and store them in a file:
-
-    philipp@lion:~> opus20_cli --loglevel INFO 192.168.1.55 download log_data.pickle
-    INFO:opus20.opus20:Connected to device with ID: EC9C0A06B183
-    INFO:opus_cli:script running time (net): 1.208517 seconds.
-    philipp@lion:~>
-
-Here is an overview of all the possible CLI commands:
-
-    # List all possible channels:
-    opus20_cli 192.168.1.55 list
-
-    # Get the values for the specified channels (CUR, MIN, MAX temperature in °C):
-    opus20_cli 192.168.1.55 get 0x0064 0x0078 0x008C
-
-    # Download the latest log data and merge it into a persistant data file:
-    opus20_cli 192.168.1.55 download opus20.PickleStore.p
-
-    # Check if logging in general is enabled on the device:
-    opus20_cli 192.168.1.55 logging status
-    opus20_cli 192.168.1.55 logging start
-    opus20_cli 192.168.1.55 logging stop
-    # Or clear the log:
-    opus20_cli 192.168.1.55 logging clear
-
-    # Enable or disable logging for individual channels:
-    opus20_cli 192.168.1.55 enable  0x0064 0x0078 0x008C
-    opus20_cli 192.168.1.55 disable 0x00CD 0x00E1 0x00F5
-
-#### Author
-
-* (c) 2015, Philipp Klaus  
-  <klaus@physik.uni-frankfurt.de>  
-  Ported the software to Python, extended and packaged it.
-* (c) 2012, [Ondics GmbH](http://www.ondics.de)  
-  <githubler@ondics.de>  
-  The author of the [original scripts][l2p_bash_scripts] (written as Bash scripts + netcat & gawk)
-
-#### License
-
-GPLv3
-
-[l2p_bash_scripts]: https://github.com/ondics/lufft-l2p-script-collection
+The source code is now managed on Github:
+https://github.com/pklaus/opus20).
 
+The Python package is also registered on PyPI, the Python Package Index:
+https://pypi.python.org/pypi/opus20
+for easy installation.
diff --git a/opus20/gitignore b/opus20/gitignore
deleted file mode 100644
index d1336e4..0000000
--- a/opus20/gitignore
+++ /dev/null
@@ -1,10 +0,0 @@
-.gitignore
-__pycache__
-ERRORS
-communication_protocols
-github
-test.py
-*.pyc
-lufft-l2p-script-collection
-log_data.pickle
-plot_log_data.py
diff --git a/opus20/opus20/__init__.py b/opus20/opus20/__init__.py
deleted file mode 100644
index 3545074..0000000
--- a/opus20/opus20/__init__.py
+++ /dev/null
@@ -1,10 +0,0 @@
-
-from .opus20 import Opus20, Frame, PickleStore
-from .opus20 import CHANNEL_SPEC as OPUS20_CHANNEL_SPEC
-from .opus20 import Opus20Exception, Opus20ConnectionException
-from .opus20 import Object
-from .opus20 import discover_OPUS20_devices
-from .fakeserver import Opus20FakeServer
-
-from .webapp import PlotWebServer
-
diff --git a/opus20/opus20/fakeserver.py b/opus20/opus20/fakeserver.py
deleted file mode 100644
index 3c833dd..0000000
--- a/opus20/opus20/fakeserver.py
+++ /dev/null
@@ -1,105 +0,0 @@
-
-
-from datetime import datetime, timedelta
-import socket
-
-from .opus20 import Frame
-
-class Opus20FakeServer(object):
-    """
-    A TCP server imitating (faking) the
-    behaviour of a Lufft OPUS20 device.
-    """
-    def __init__(self, host='', port=52015):
-        self.host = host
-        self.port = port
-        self.communication_samples = []
-
-    def bind_and_serve(self):
-        self.s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
-        self.s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
-        self.s.bind((self.host, self.port))
-        self.s.listen(1)
-        try:
-            while True:
-                conn, addr = self.s.accept()
-                print('Connected by', addr)
-                while True:
-                    data = conn.recv(1024)
-                    if not data: break
-                    input_frame = Frame(data)
-                    output_frame = self.react_to_input_frame(input_frame)
-                    conn.sendall(output_frame.data)
-                conn.close()
-        except KeyboardInterrupt:
-            pass
-        finally:
-            self.s.close()
-
-    def react_to_input_frame(self, input_frame):
-        output_frame = None
-        input_frame.validate()
-        in_props = input_frame.props
-        for sample in self.communication_samples:
-            sample_props = sample['in'].props
-            if in_props.cmd != sample_props.cmd:
-                continue
-            if in_props.payload != sample_props.payload:
-                continue
-            output_frame = sample['out']
-            break
-        if output_frame is None:
-            # 0x10 - Unknown CMD
-            output_frame = Frame.from_cmd_and_payload(in_props.cmd, b"\x10")
-        return output_frame
-
-    def feed_with_communication_log(self, l2p_frames_file):
-        """ Feed the fake server with l2p frames stored in a communication log file """
-        num_incoming, num_outgoing = 0, 0
-        num_short, num_long = 0, 0
-
-
-        def gt(dt_str):
-            dt, _, us= dt_str.partition(".")
-            dt= datetime.strptime(dt, "%Y-%m-%dT%H:%M:%S")
-            us= int(us.rstrip("Z"), 10)
-            return dt + timedelta(microseconds=us)
-            #return dt
-        
-        with open(l2p_frames_file) as fp:
-
-            timestamp = None
-            incoming = None
-            outgoing = None
-
-            for line in fp:
-
-                line = line.strip()
-                if not line: continue
-
-                kind = None
-                if line.startswith('Timestamp'):
-                    kind = 'timestamp'
-                    timestamp = gt(line.split('  ')[1].replace(' ', 'T'))
-                elif line.startswith('<- '):
-                    kind = 'incoming'
-                    num_incoming += 1
-                elif line.startswith('-> '):
-                    kind = 'outgoing'
-                    num_outgoing += 1
-
-                if kind in ('incoming', 'outgoing'):
-                    frame_bytes = bytes(int(byte, 16) for byte in line[3:].split())
-                    frm = Frame(frame_bytes)
-                    try:
-                        frm.validate()
-                    except Exception as e:
-                        print(e)
-                        pdb.set_trace()
-
-                if kind == 'incoming':
-                    incoming = frm
-                elif kind == 'outgoing':
-                    outgoing = frm
-                    self.communication_samples.append({'ts': timestamp, 'in': incoming, 'out': outgoing})
-
diff --git a/opus20/opus20/opus20.py b/opus20/opus20/opus20.py
deleted file mode 100644
index 076bce2..0000000
--- a/opus20/opus20/opus20.py
+++ /dev/null
@@ -1,847 +0,0 @@
-#!/usr/bin/env python
-
-import socket
-import select
-import pdb
-import struct
-import time
-import logging
-from datetime import datetime, timedelta
-import pickle
-import threading
-import ipaddress
-
-clock = time.perf_counter
-logger = logging.getLogger(__name__)
-
-class Opus20(object):
-
-    def __init__(self, host, port=52015, timeout=5.):
-
-        self.s = None
-
-        self.host = host
-        self.port = port
-        self.timeout = timeout
-
-        self.request_supported_channels()
-        self.request_device_status()
-
-    def connect(self):
-        try:
-            self.s = socket.create_connection((self.host, self.port), self.timeout)
-        except (ConnectionRefusedError, socket.gaierror) as e:
-            raise Opus20ConnectionException("Connection to host {} could not be established: {}".format(self.host, e))
-
-    def disconnect(self):
-        try:
-            # 0 = done receiving, 1 = done sending, 2 = both
-            self.s.shutdown(2)
-        except:
-            pass
-        try:
-            self.s.close()
-        except:
-            pass
-        self.s = None
-
-    @property
-    def connected(self):
-        if not self.s: return False
-        try:
-            ready_to_read, ready_to_write, in_error = \
-                select.select([self.s,], [self.s,], [], 5)
-        except select.error:
-            self.disconnect()
-            return False
-        return True
-
-    def query_frame(self, frame):
-        assert type(frame) == Frame
-        return self.query_bytes(frame.data)
-
-    def query_bytes(self, data : bytes):
-        if not self.connected: self.connect()
-        logger.debug("Sending the following {} bytes now: {}".format(len(data), hex_formatter(data)))
-        frame = None
-        num_tries = 3
-        while num_tries:
-            try:
-                self.s.sendall(data)
-                answer = self.s.recv(1024)
-                frame = Frame(answer)
-                frame.validate()
-                break
-            except IncompleteDataException:
-                answer += self.s.recv(1024)
-                frame = Frame(answer)
-                frame.validate()
-                break
-            except FrameValidationException as e:
-                logger.warning("The frame couldn't be validated: " + str(e))
-            num_tries -= 1
-            logger.warning("remaining tries: {}".format(num_tries))
-        if not frame.props: raise NameError("Couldn't get a valid answer.")
-        logger.debug("Received the following {} bytes as answer: {}".format(len(frame.data), hex_formatter(frame.data)))
-        return frame
-
-    def request_supported_channels(self):
-        frame = Frame.from_cmd_and_payload(0x31, b"\x16")
-        answer = self.query_frame(frame)
-        self.available_channels = answer.available_channels()
-
-    def request_channel_properties(self, channel: int):
-        query_frame = Frame.from_cmd_and_payload(0x31, b"\x30" + struct.pack('<H', channel))
-        answer_frame = self.query_frame(query_frame)
-        return answer_frame.channel_properties()
-
-    def request_device_status(self):
-        frame = Frame.from_cmd_and_payload(0x31, b"\x60")
-        answer = self.query_frame(frame)
-        self.device_id = ''.join("{:02X}".format(byte) for byte in answer.props.payload[2:2+6])
-        logger.info("Connected to device with ID: " + self.device_id)
-
-    def sync_datetime(self, new_datetime=None, tz_offset=None):
-        if not new_datetime: new_datetime = datetime.now().replace(microsecond=0)
-        if not tz_offset: tz_offset = round((datetime.now() - datetime.utcnow()).total_seconds())
-        offset_sign = '+' if tz_offset >= 0 else '-'
-        offset_hours = abs(tz_offset) // 3600;
-        offset_minutes = (abs(tz_offset) % 3600) // 60;
-        logger.info("Setting date & time on device to {}{:+03}{:02}".format(new_datetime.isoformat(), offset_hours, offset_minutes))
-        new_datetime = int(new_datetime.timestamp())
-        frame = Frame.from_cmd_and_payload(0x27, struct.pack('<ii', new_datetime, tz_offset))
-        #answer = self.query_frame(frame)
-        answer = Frame.from_cmd_and_payload(0x27, b"\x00")
-        answer.validate()
-        assert answer.props.cmd == 0x27
-        answer.assert_status()
-
-    def clear_log(self):
-        frame = Frame.from_cmd_and_payload(0x46, b"")
-        answer = self.query_frame(frame)
-        answer.validate()
-        assert answer.props.cmd == 0x46
-        answer.assert_status()
-
-    def start_logging(self):
-        self.set_logging_state(True)
-
-    def stop_logging(self):
-        self.set_logging_state(False)
-
-    def set_logging_state(self, enable_logging=True):
-        enable_logging = b"\x01" if enable_logging else b"\x00"
-        frame = Frame.from_cmd_and_payload(0x45, b"\x43" + enable_logging)
-        answer = self.query_frame(frame)
-        answer.validate()
-        assert answer.props.cmd == 0x45
-        answer.assert_status()
-
-    def get_logging_state(self):
-        frame = Frame.from_cmd_and_payload(0x44, b"\x43")
-        answer = self.query_frame(frame)
-        answer.validate()
-        props = answer.props
-        assert len(props.payload) == 3
-        sub_cmd, state = struct.unpack('<xB?', props.payload)
-        assert props.cmd == 0x44
-        answer.assert_status()
-        assert sub_cmd == 0x43
-        return state
-
-    def set_channel_logging_state(self, channel, enable_logging=True):
-        payload = b"\x22" + struct.pack('<H?', channel, enable_logging)
-        frame = Frame.from_cmd_and_payload(0x45, payload)
-        answer = self.query_frame(frame)
-        answer.validate()
-        props = answer.props
-        assert props.cmd == 0x45
-        assert len(props.payload) == 6
-        assert props.payload[1] == 0x22
-        answer.assert_status()
-        return struct.unpack('<I', props.payload[2:6])[0]
-
-    def get_channel_logging_state(self, channel):
-        payload = b"\x22" + struct.pack('<H', channel)
-        frame = Frame.from_cmd_and_payload(0x44, payload)
-        answer = self.query_frame(frame)
-        answer.validate()
-        props = answer.props
-        assert len(props.payload) == 5
-        sub_cmd, nch, state = struct.unpack('<xBH?', props.payload)
-        assert props.cmd == 0x44
-        answer.assert_status()
-        assert sub_cmd == 0x22
-        assert nch == channel
-        return state
-
-    def channel_value(self, channel: int):
-        query_frame = Frame.from_cmd_and_payload(0x23, struct.pack('<H', channel))
-        answer_frame = self.query_frame(query_frame)
-        return answer_frame.online_data_request_single()
-
-    def multi_channel_value(self, channels : list):
-        fmt = '<B' + 'H' * len(channels)
-        query_frame = Frame.from_cmd_and_payload(0x2f, struct.pack(fmt, len(channels), *channels))
-        answer_frame = self.query_frame(query_frame)
-        return answer_frame.online_data_request_multiple()
-
-    def download_logs(self, start_datetime=None):
-        if start_datetime:
-            # We convert to UNIX time and add one second
-            # ( otherwise the same 'last' datapoint could be fetched
-            #   and stored multiple times for subsequent calls. )
-            ts = int(start_datetime.timestamp()) + 1
-        else:
-            ts = 0
-        init_frame = Frame.from_cmd_and_payload(0x24, b'\x10' + struct.pack('<i', ts) + b'\x00\x00\x00\x00\x00')
-        init_answer_frame = self.query_frame(init_frame)
-        init_answer_frame.validate()
-        init_answer_frame.kind
-        num_answer_frames = struct.unpack('<I', init_answer_frame.props.payload[2:2+4])[0]
-        data_request_frame = Frame.from_cmd_and_payload(0x24, b'\x20\x01')
-        data = []
-        for i in range(num_answer_frames):
-            data_answer_frame = self.query_frame(data_request_frame)
-            data_answer_frame.validate()
-            data += data_answer_frame.kind.func()
-        return data
-
-class Opus20Exception(NameError):
-    """ An exception concerning Opu20 """
-
-class Opus20ConnectionException(Opus20Exception):
-    """ An Opus20 specific 'could not connect' exception """
-
-class FrameValidationException(Opus20Exception):
-    """ received invalid data """
-
-class IncompleteDataException(FrameValidationException):
-    """ received incomplete data """
-
-class LogStore(object):
-
-    def __init__(self):
-        raise NotImplementedError()
-
-    def max_ts(self):
-        raise NotImplementedError()
-
-    def get_device_ids(self):
-        raise NotImplementedError()
-
-    def get_data(self, device_id=None):
-        raise NotImplementedError()
-
-    def add_data(self):
-        raise NotImplementedError()
-
-    def persist(self):
-        raise NotImplementedError()
-
-class PickleStore(LogStore):
-
-    PICKLE_VERSION = 2
-
-    def __init__(self, pickle_file: str):
-        self.pickle_file = pickle_file
-
-        try:
-            with open(self.pickle_file, 'rb') as f:
-                self._data = pickle.load(f)
-        except FileNotFoundError:
-            self._data = {}
-
-    def max_ts(self):
-        max_ts = dict()
-        for device_id in self._data:
-            ts_list = [entry['ts'] for entry in self._data[device_id]]
-            max_ts[device_id] = max(ts_list)
-        return max_ts
-
-    def get_device_ids(self):
-        return tuple(self._data.keys())
-
-    def get_data(self, device_id=None):
-        if not device_id: return self._data
-        return self._data[device_id]
-
-    def add_data(self, device_id, new_data):
-        if device_id not in self._data:
-            self._data[device_id] = []
-        self._data[device_id] += new_data
-
-    def persist(self):
-        with open(self.pickle_file, 'wb') as f:
-            pickle.dump(self._data, f, self.PICKLE_VERSION)
-
-class Frame(object):
-
-    HEADER_SHORT = b"\x01\x10\x00\x00\x00\x00"
-    HEADER_LONG = b"\x01\x20\x00\x00\x00\x00"
-
-    STX = b"\x02"
-    ETX = b"\x03"
-    EOT = b"\x04"
-    SHORT_FRAME = 0x10
-    LONG_FRAME  = 0x20
-
-    def __init__(self, data=bytes()):
-        self.data = data
-
-    @classmethod
-    def from_cmd_and_payload(cls, cmd, payload, verc=0x10):
-        data = cls.HEADER_SHORT
-        data += bytes([2+len(payload)])
-        data += cls.STX
-        data += bytes([cmd, verc]) + payload
-        data += cls.ETX
-        crc = crc16(data)
-        # The byte order is LO HI
-        data += bytes([crc & 0xFF, crc >> 8])
-        data += cls.EOT
-
-        frame = Frame()
-        frame.data = data
-        return frame
-
-    def validate(self):
-
-        data = self.data
-
-        if len(data) < 12:
-            logger.warning("message incomplete? Expected at least 12 bytes, got {}. ".format(len(data)))
-            raise IncompleteDataException()
-
-        frame_style = None
-        offset = 0
-        # check header
-        if data[0:6] == self.HEADER_SHORT:
-            frame_style = self.SHORT_FRAME
-        elif data[0:6] == self.HEADER_LONG:
-            frame_style = self.LONG_FRAME
-        else:
-            raise FrameValidationException("l2p-header incorrect: " + str(data[0:6]))
-
-        # length of payload
-        length = 0
-        if frame_style == self.SHORT_FRAME:
-            length = data[6]
-        elif frame_style == self.LONG_FRAME:
-            length = struct.unpack('<H', data[6:8])[0]
-            offset += 1
-        logger.debug("length of payload={}".format(length))
-        if len(data) < 12 + offset + length:
-            # This 'problem' can occur regularly, thus we don't use .warning() but .info()
-            logger.debug("message incomplete? Expected {} bytes, got {}. ".format(12+length, len(data)) + str(data))
-            raise IncompleteDataException()
-
-        # stx ok?
-        if data[7+offset] != 0x02: raise FrameValidationException("l2p-stx incorrect")
-
-        # cmd/verc
-        cmd = data[8+offset]
-        verc = data[9+offset]
-        logger.debug("CMD=[0x{:02X}] VERC=[0x{:02X}]".format(cmd, verc))
-
-        # payload
-        payload = data[10+offset:10+offset+length-2]
-        logger.debug("Payload=[" + hex_formatter(payload) + "]")
-
-        # etx ok?
-        if data[8+offset+length] != 0x03: raise FrameValidationException("l2p-etx incorrect")
-
-        # chksum ok?
-        frame = data[0:9+offset+length]
-        chksum_calc = crc16(frame)
-        # The byte order is LO HI
-        chksum_calc = bytes([chksum_calc & 0xFF, chksum_calc >> 8])
-        chksum_found = data[9+offset+length:9+offset+length+2]
-        if chksum_calc != chksum_found:
-            logger.warning("Checksum: WRONG!")
-            raise FrameValidationException("l2p chksum incorrect: {} vs. {}".format(chksum_calc, chksum_found))
-        else:
-            logger.debug("Checksum: OK")
-
-        # eot ok?
-        if data[11+offset+length] != 0x04: raise FrameValidationException("l2p-eot incorrect")
-
-        props = Object()
-        props.cmd = cmd
-        props.verc = verc
-        props.length = length
-        props.payload = payload
-        props.frame_style = frame_style
-        props.chksum_found = chksum_found
-        self._props = props
-
-        return True
-
-    @property
-    def status(self):
-        return self.props.payload[0]
-
-    def assert_status(self):
-        # check the status byte
-        status = self.status
-        if status == 0x0:
-            logger.debug("Status: {}".format(STATUS_WORDS[status]))
-            return True
-        else:
-            logger.warning("Status: {}".format(STATUS_WORDS[status]))
-            msg = 'status not ok: 0x{:02X}'.format(status)
-            if status in STATUS_WORDS: msg += ' error code: {}'.format(STATUS_WORDS[status]['name'])
-            else: msg += ' error code: unknown'
-            raise NameError(msg)
-
-    @property
-    def props(self):
-        """ returns an object containing the basic properties of the Frame.
-            This object is created when calling Frame.validate() . """
-        try:
-            return self._props
-        except:
-            raise NameError('Props not available yet. Check Frame.validate() first.')
-
-
-    @property
-    def kind(self):
-        FRAME_KINDS = [
-          #
-          Object(cmd=0x1E, payload_check=[],            payload_length=   0, name='network discovery request'),
-          Object(cmd=0x1E, payload_check=[0x00],        payload_length=  35, name='network discovery answer',                   func=self.discovery_result),
-          #
-          Object(cmd=0x23, payload_check=[],            payload_length=   2, name='online single channel request'),
-          Object(cmd=0x23, payload_check=[0x00,],       payload_length=   8, name='online single channel answer',               func=self.online_data_request_single),
-          #
-          Object(cmd=0x24, payload_check=[0x10,],       payload_length=  10, name='initiate log download request'),
-          Object(cmd=0x24, payload_check=[0x00, 0x10],  payload_length=  10, name='initiate log download answer'),
-          #
-          Object(cmd=0x24, payload_check=[0x20, 0x01],  payload_length=   2, name='log download data request'),
-          Object(cmd=0x24, payload_check=[0x00, 0x20],  payload_length=None, name='log download data answer',                   func=self.get_log_data),
-          #
-          Object(cmd=0x27, payload_check=[],            payload_length=   8, name='update time request'),
-          Object(cmd=0x27, payload_check=[0x00,],       payload_length=   1, name='update time answer'),
-          #
-          Object(cmd=0x2F, payload_check=[],            payload_length=   2, name='online multiple channel request'),
-          Object(cmd=0x2F, payload_check=[0x00,],       payload_length=None, name='online multiple channel answer',             func=self.online_data_request_multiple),
-          #
-          Object(cmd=0x31, payload_check=[0x16,],       payload_length=   1, name='channel list request'),
-          Object(cmd=0x31, payload_check=[0x00, 0x16,], payload_length=None, name='channel list answer',                        func=self.available_channels),
-          #
-          Object(cmd=0x31, payload_check=[0x17,],       payload_length=   1, name='channel group list request'),
-          Object(cmd=0x31, payload_check=[0x00, 0x17,], payload_length=None, name='channel group list answer'),
-          #
-          Object(cmd=0x31, payload_check=[0x30,],       payload_length=   3, name='information on specific channel request'),
-          Object(cmd=0x31, payload_check=[0x00, 0x30,], payload_length=  85, name='information on specific channel answer',     func=self.channel_properties),
-          #
-          Object(cmd=0x31, payload_check=[0x10,],       payload_length=   1, name='advanced status request 0x10 (?)'),
-          Object(cmd=0x31, payload_check=[0x00, 0x10,], payload_length=None, name='advanced status answer 0x10 (?)'),
-          #
-          Object(cmd=0x31, payload_check=[0x13,],       payload_length=   1, name='advanced status request 0x13 (?)'),
-          Object(cmd=0x31, payload_check=[0x00, 0x13,], payload_length=None, name='advanced status answer 0x13 (?)'),
-          #
-          Object(cmd=0x31, payload_check=[0x60,],       payload_length=   1, name='device status request'),
-          Object(cmd=0x31, payload_check=[0x00, 0x60,], payload_length=  10, name='device status answer'),
-          #
-          Object(cmd=0x44, payload_check=[0x12,],       payload_length=   2, name='[r] value range of channel group request'),
-          Object(cmd=0x44, payload_check=[0x00, 0x12],  payload_length=  18, name='[r] value range of channel group answer'),
-          #
-          Object(cmd=0x44, payload_check=[0x22,],       payload_length=   3, name='[r] enable/disable logging of specific channel request'),
-          Object(cmd=0x44, payload_check=[0x00, 0x22],  payload_length=   5, name='[r] enable/disable logging of specific channel answer'),
-          Object(cmd=0x45, payload_check=[0x22,],       payload_length=   4, name='[w] enable/disable logging of specific channel request'),
-          Object(cmd=0x45, payload_check=[0x00, 0x22],  payload_length=   6, name='[w] enable/disable logging of specific channel answer'),
-          #
-          Object(cmd=0x44, payload_check=[0x41,],       payload_length=   1, name='[r] measuring/logging interval request'),
-          Object(cmd=0x44, payload_check=[0x00, 0x41],  payload_length=  14, name='[r] measuring/logging interval answer'),
-          Object(cmd=0x45, payload_check=[0x41,],       payload_length=   9, name='[w] measuring/logging interval request'),
-          Object(cmd=0x45, payload_check=[0x00, 0x41],  payload_length=   8, name='[w] measuring/logging interval answer'),
-          #
-          Object(cmd=0x44, payload_check=[0x43,],       payload_length=   1, name='[r] enable/disable logging request'),
-          Object(cmd=0x44, payload_check=[0x00, 0x43],  payload_length=   3, name='[r] enable/disable logging answer'),
-          Object(cmd=0x45, payload_check=[0x43,],       payload_length=   2, name='[w] enable/disable logging request'),
-          Object(cmd=0x45, payload_check=[0x00,],       payload_length=   1, name='[w] enable/disable logging answer'),
-          #
-          Object(cmd=0x46, payload_check=[],            payload_length=   0, name='clear log request'),
-          Object(cmd=0x46, payload_check=[0x00,],       payload_length=   1, name='clear log answer'),
-          #
-          ## Commands I don't understand right now:
-          # 31/31 : channel group specific, a 2+1+1+n-byte answer, the n-bytes are counting upwards
-          # 44/11 : channel group specific, a 2+1+1-byte answer (single flat?)
-          # 44/13 : channel group specific, a 2+1+1+4-byte answer with a float value of 0.0
-          # 44/21 : channel group specific, 2+81-byte answer with mostly 0x00 values
-          # 44/31 : global, 2+80-byte answer with mostly 0x00 values
-          # 44/61 : global, a 2+1-byte answer with 0x00 (single flag?)
-          # 44/62 : global, a 2+1-byte answer with 0x00 (single flag?)
-          # 44/70 : global, a 2+2-byte answer with 0x00 0x00
-          # 44/81 : global, contains the device ID like 31/60
-        ]
-
-        props = self.props
-
-        # all frames I have seen so far use verc = 0x10
-        if props.verc != 0x10: return None
-
-        for knd in FRAME_KINDS:
-            if knd.cmd != props.cmd: continue
-            if knd.payload_length != None and knd.payload_length != len(props.payload):
-                continue
-            if len(knd.payload_check) > len(props.payload): continue
-            payload_matches = True
-            for i in range(len(knd.payload_check)):
-                ref_byte = knd.payload_check[i]
-                check_byte = props.payload[i]
-                if ref_byte != None and ref_byte != check_byte:
-                    payload_matches = False
-                    break
-            if payload_matches: return knd
-        return None
-
-    def discovery_result(self):
-        props = self.props
-
-        assert props.cmd == 0x1E
-        assert len(props.payload) == 35
-        answer.assert_status()
-
-        dr = Object()
-        dr.device_id = ''.join("{:02X}".format(byte) for byte in props.payload[1:1+6])
-        dr.ip = ipaddress.IPv4Address(props.payload[9:9+4])
-        dr.gw = ipaddress.IPv4Address(props.payload[13:13+4])
-        dr.mask = ipaddress.IPv4Address(props.payload[17:17+4])
-        dr.net = ipaddress.IPv4Network('{}/{}'.format(dr.ip, dr.mask), strict=False)
-        return dr.to_dict()
-
-    def available_channels(self):
-        # cmd="31 10" (which channels are available in device?)
-
-        props = self.props
-
-        assert props.length >= 3, 'message too short for an answer containing the available channels'
-        assert props.cmd == 0x31 and  props.payload[1] == 0x16
-        self.assert_status()
-
-        logger.debug("Channel Query (31 10 16)")
-        num_channels = props.payload[2]
-        logger.debug("Number of available channels: {}".format(num_channels))
-        channels = []
-        for i in range(num_channels):
-            # Little Endian 16 bit:
-            channel = props.payload[3+2*i:3+2*i+2]
-            channel = channel[0] + (channel[1] << 8 )
-            if channel not in CHANNEL_SPEC:
-                # This is the case for channel 150 = 0x0096
-                continue
-            channels.append(channel)
-        return channels
-
-    def channel_properties(self):
-        props = self.props
-        assert len(props.payload) == 85
-        assert props.cmd == 0x31
-        self.assert_status()
-        assert props.payload[1] == 0x30
-        channel, group, name, unit, kind, min, max = struct.unpack('<HB40s30sBxff', props.payload[2:2+83])
-        name = name.decode('ascii').replace('\x00','').strip()
-        unit = unit.decode('utf-16-le').replace('\x00','').strip()
-        KIND_MAP = {0x10: 'CUR', 0x11: 'MIN', 0x12: 'MAX', 0x13: 'AVG'}
-        kind = KIND_MAP[kind]
-        return Object(channel=channel, name=name, group=group, unit=unit, kind=kind, min=min, max=max)
-
-    def online_data_request_single(self):
-        # cmd="23 10" (online data request, one channel)
-
-        props = self.props
-
-        assert props.length >= 3, 'message too short for an online data request with a single channel'
-        assert props.cmd == 0x23
-        self.assert_status()
-        logger.debug("Online Data Request (single channel) (23 10)")
-        channel_value = Frame.read_channel_value(props.payload, 1, length=7, status=self.status)
-        return channel_value.value
-
-    def online_data_request_multiple(self):
-        # cmd="2F 10" (online data request, multiple channels)
-
-        props = self.props
-
-        assert props.length >= 3, 'message too short for an online data request with multiple channels'
-        assert props.cmd == 0x2F
-        self.assert_status()
-        logger.debug("Online Data Request (multiple channels) (2F 10)")
-
-        offset = 1
-        num_channels = props.payload[offset]
-        logger.debug("Number of Channels={}".format(num_channels))
-        offset += 1
-
-        values = []
-
-        for i in range(num_channels):
-            channel_value = Frame.read_channel_value(props.payload, offset)
-            offset += 1 + channel_value.length
-            values.append(channel_value.value)
-
-        return values
-
-    def get_log_data(self):
-        props = self.props
-
-        assert props.length >= 21, 'message too short for a log data message'
-        assert props.cmd == 0x24 and props.payload[1] == 0x20
-        self.assert_status()
-
-        is_final, begin, end, interval, num_blocks = struct.unpack('<xx?xxxxiiIH', props.payload[0:21])
-        begin = datetime.fromtimestamp(begin)
-        end   = datetime.fromtimestamp(end)
-        interval = timedelta(seconds=interval)
-        logger.debug(str((is_final, begin, end, interval, num_blocks)))
-
-        ts = begin
-        offset = 21
-        table = []
-        for i in range(num_blocks):
-            num_entries = props.payload[offset]
-            offset += 1
-            row = {'ts': ts}
-            ts = ts + interval
-            for j in range(num_entries):
-                channel_value = Frame.read_channel_value(props.payload, offset)
-                row[channel_value.channel] = channel_value.value
-                offset += 9
-            table.append(row)
-        return table
-
-    @classmethod
-    def read_channel_value(cls, buf: bytes, offset: int, length=None, status=None):
-        if length is None:
-            length = buf[offset]
-            logger.debug("SubLen={} ({})".format(length, offset))
-            logger.debug("SubPayload: " + hex_formatter(buf[offset:offset+1+length]))
-            offset += 1
-
-        if status is None:
-            status = buf[offset]
-            logger.debug("SubStatus={}: ({})".format(status, offset))
-            offset += 1
-            if status != 0: raise FrameValidationException('Bad status of channel value: 0x{:02X}'.format(status))
-
-        channel = buf[offset] + (buf[offset+1] << 8)
-        if channel in CHANNEL_SPEC:
-            logger.debug("channel: {} ({:04X}) {}".format(channel, channel, CHANNEL_SPEC[channel]['name']))
-        else:
-            logger.debug("channel: {} ({:04X}) unknown?!".format(channel, channel))
-        offset += 2
-
-        dtype = buf[offset]
-        logger.debug("DataType=[0x{:02X}]".format(dtype))
-        offset += 1
-        if dtype == 0x16:
-            value = struct.unpack('<f', buf[offset:offset+4])[0]
-            offset += 4
-        else:
-            raise NameError("Data type 0x{:02X} not implemented".format(dtype))
-        logger.debug("Returned Value: " + str(value))
-
-        return Object(channel=channel, value=value, dtype=dtype, length=length, status=status)
-
-def hex_formatter(raw: bytes):
-    return ' '.join('{:02X}'.format(byte) for byte in raw)
-
-
-class UdpListenerThread(threading.Thread):
-    DETECTION_TIMEOUT = 0.25
-    def __init__ (self,port,callback):
-        """
-        Listens for packages via UDP. Calls callback for each response.
-            callback([frm, (ip, port), answer_time])
-        """
-        threading.Thread.__init__(self)
-        self.__port = port
-        self.__callback = callback
-        self.__start_time = clock()
-    def run(self):
-        addr = ('', self.__port)
-        UDPinsock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
-        UDPinsock.bind(addr)
-        UDPinsock.settimeout(self.DETECTION_TIMEOUT)
-        while True:
-            try:
-                """ Receive messages """
-                data, addr = UDPinsock.recvfrom(1024)
-                # keep timestamp of arriving package
-                answer_time = clock()
-            except:
-                """ server timeout """
-                break
-            frm = Frame(data)
-            frm.validate()
-            try:
-                frm.validate()
-            except:
-                logger.warning("received a response that didn't validate: " + repr(data))
-            self.__callback((frm, addr, (answer_time - self.__start_time)*1000))
-        UDPinsock.close()
-
-def discover_OPUS20_devices(callback, bind_addr=""):
-    dest = ('<broadcast>',52010)
-    myUDPintsockThread = UdpListenerThread(52005, callback)
-    myUDPintsockThread.start()
-
-    UDPoutsock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
-    # to allow broadcast communication:
-    UDPoutsock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
-    UDPoutsock.bind((bind_addr,0))
-    frm = Frame.from_cmd_and_payload(0x1e, b"")
-    UDPoutsock.sendto(frm.data, dest)
-
-    myUDPintsockThread.join()
-
-def crc16(data : bytes):
-    """ Calculates a CRC-16 CCITT checksum. data should be of type bytes() """
-    # https://en.wikipedia.org/wiki/Cyclic_redundancy_check
-    crc16_table = [
-        0x0000, 0x1189, 0x2312, 0x329B, 0x4624, 0x57AD, 0x6536, 0x74BF,
-        0x8C48, 0x9DC1, 0xAF5A, 0xBED3, 0xCA6C, 0xDBE5, 0xE97E, 0xF8F7,
-        0x1081, 0x0108, 0x3393, 0x221A, 0x56A5, 0x472C, 0x75B7, 0x643E,
-        0x9CC9, 0x8D40, 0xBFDB, 0xAE52, 0xDAED, 0xCB64, 0xF9FF, 0xE876,
-        0x2102, 0x308B, 0x0210, 0x1399, 0x6726, 0x76AF, 0x4434, 0x55BD,
-        0xAD4A, 0xBCC3, 0x8E58, 0x9FD1, 0xEB6E, 0xFAE7, 0xC87C, 0xD9F5,
-        0x3183, 0x200A, 0x1291, 0x0318, 0x77A7, 0x662E, 0x54B5, 0x453C,
-        0xBDCB, 0xAC42, 0x9ED9, 0x8F50, 0xFBEF, 0xEA66, 0xD8FD, 0xC974,
-        0x4204, 0x538D, 0x6116, 0x709F, 0x0420, 0x15A9, 0x2732, 0x36BB,
-        0xCE4C, 0xDFC5, 0xED5E, 0xFCD7, 0x8868, 0x99E1, 0xAB7A, 0xBAF3,
-        0x5285, 0x430C, 0x7197, 0x601E, 0x14A1, 0x0528, 0x37B3, 0x263A,
-        0xDECD, 0xCF44, 0xFDDF, 0xEC56, 0x98E9, 0x8960, 0xBBFB, 0xAA72,
-        0x6306, 0x728F, 0x4014, 0x519D, 0x2522, 0x34AB, 0x0630, 0x17B9,
-        0xEF4E, 0xFEC7, 0xCC5C, 0xDDD5, 0xA96A, 0xB8E3, 0x8A78, 0x9BF1,
-        0x7387, 0x620E, 0x5095, 0x411C, 0x35A3, 0x242A, 0x16B1, 0x0738,
-        0xFFCF, 0xEE46, 0xDCDD, 0xCD54, 0xB9EB, 0xA862, 0x9AF9, 0x8B70,
-        0x8408, 0x9581, 0xA71A, 0xB693, 0xC22C, 0xD3A5, 0xE13E, 0xF0B7,
-        0x0840, 0x19C9, 0x2B52, 0x3ADB, 0x4E64, 0x5FED, 0x6D76, 0x7CFF,
-        0x9489, 0x8500, 0xB79B, 0xA612, 0xD2AD, 0xC324, 0xF1BF, 0xE036,
-        0x18C1, 0x0948, 0x3BD3, 0x2A5A, 0x5EE5, 0x4F6C, 0x7DF7, 0x6C7E,
-        0xA50A, 0xB483, 0x8618, 0x9791, 0xE32E, 0xF2A7, 0xC03C, 0xD1B5,
-        0x2942, 0x38CB, 0x0A50, 0x1BD9, 0x6F66, 0x7EEF, 0x4C74, 0x5DFD,
-        0xB58B, 0xA402, 0x9699, 0x8710, 0xF3AF, 0xE226, 0xD0BD, 0xC134,
-        0x39C3, 0x284A, 0x1AD1, 0x0B58, 0x7FE7, 0x6E6E, 0x5CF5, 0x4D7C,
-        0xC60C, 0xD785, 0xE51E, 0xF497, 0x8028, 0x91A1, 0xA33A, 0xB2B3,
-        0x4A44, 0x5BCD, 0x6956, 0x78DF, 0x0C60, 0x1DE9, 0x2F72, 0x3EFB,
-        0xD68D, 0xC704, 0xF59F, 0xE416, 0x90A9, 0x8120, 0xB3BB, 0xA232,
-        0x5AC5, 0x4B4C, 0x79D7, 0x685E, 0x1CE1, 0x0D68, 0x3FF3, 0x2E7A,
-        0xE70E, 0xF687, 0xC41C, 0xD595, 0xA12A, 0xB0A3, 0x8238, 0x93B1,
-        0x6B46, 0x7ACF, 0x4854, 0x59DD, 0x2D62, 0x3CEB, 0x0E70, 0x1FF9,
-        0xF78F, 0xE606, 0xD49D, 0xC514, 0xB1AB, 0xA022, 0x92B9, 0x8330,
-        0x7BC7, 0x6A4E, 0x58D5, 0x495C, 0x3DE3, 0x2C6A, 0x1EF1, 0x0F78,
-    ]
-    crc_buff = 0xffff
-    for i in range(len(data)):
-        c = data[i]
-        crc_buff = (crc_buff >> 8) ^ crc16_table[ (c ^ ( crc_buff & 0xFF )) ]
-    return crc_buff
-
-CHANNEL_SPEC = {
-  100:   {'name': 'CUR temperature',        'unit': '°C',    'offset' : '±10.0', },
-  120:   {'name': 'MIN temperature',        'unit': '°C',    'offset' : '±10.0', },
-  140:   {'name': 'MAX temperature',        'unit': '°C',    'offset' : '±10.0', },
-  160:   {'name': 'AVG temperature',        'unit': '°C',    'offset' : '±10.0', },
-  105:   {'name': 'CUR temperature',        'unit': '°F',    'offset' : '0.0',   },
-  125:   {'name': 'MIN temperature',        'unit': '°F',    'offset' : '0.0',   },
-  145:   {'name': 'MAX temperature',        'unit': '°F',    'offset' : '0.0',   },
-  165:   {'name': 'AVG temperature',        'unit': '°F',    'offset' : '0.0',   },
-  110:   {'name': 'CUR dewpoint',           'unit': '°C',    'offset' : '0.0',   },
-  130:   {'name': 'MIN dewpoint',           'unit': '°C',    'offset' : '0.0',   },
-  150:   {'name': 'MAX dewpoint',           'unit': '°C',    'offset' : '0.0',   },
-  170:   {'name': 'AVG dewpoint',           'unit': '°C',    'offset' : '0.0',   },
-  115:   {'name': 'CUR dewpoint',           'unit': '°F',    'offset' : '0.0',   },
-  135:   {'name': 'MIN dewpoint',           'unit': '°F',    'offset' : '0.0',   },
-  155:   {'name': 'MAX dewpoint',           'unit': '°F',    'offset' : '0.0',   },
-  175:   {'name': 'AVG dewpoint',           'unit': '°F',    'offset' : '0.0',   },
-  200:   {'name': 'CUR relative humidity',  'unit': '%',     'offset' : '±30.0', },
-  220:   {'name': 'MIN relative humidity',  'unit': '%',     'offset' : '±30.0', },
-  240:   {'name': 'MAX relative humidity',  'unit': '%',     'offset' : '±30.0', },
-  260:   {'name': 'AVG relative humidity',  'unit': '%',     'offset' : '±30.0', },
-  205:   {'name': 'CUR absolute humidity',  'unit': 'g/m³',  'offset' : '0.0',   },
-  225:   {'name': 'MIN absolute humidity',  'unit': 'g/m³',  'offset' : '0.0',   },
-  245:   {'name': 'MAX absolute humidity',  'unit': 'g/m³',  'offset' : '0.0',   },
-  265:   {'name': 'AVG absolute humidity',  'unit': 'g/m³',  'offset' : '0.0',   },
-  300:   {'name': 'CUR abs. air pressure',  'unit': 'hPa',   'offset' : '±10.0', },
-  320:   {'name': 'MIN abs. air pressure',  'unit': 'hPa',   'offset' : '±10.0', },
-  340:   {'name': 'MAX abs. air pressure',  'unit': 'hPa',   'offset' : '±10.0', },
-  360:   {'name': 'AVG abs. air pressure',  'unit': 'hPa',   'offset' : '±10.0', },
-  305:   {'name': 'CUR abs. air pressure',  'unit': 'hPa',   'offset' : '0.0',   },
-  325:   {'name': 'MIN abs. air pressure',  'unit': 'hPa',   'offset' : '0.0',   },
-  345:   {'name': 'MAX abs. air pressure',  'unit': 'hPa',   'offset' : '0.0',   },
-  365:   {'name': 'AVG abs. air pressure',  'unit': 'hPa',   'offset' : '0.0',   },
-  10020: {'name': 'CUR battery voltage',    'unit': 'V',     'offset' : '0.0',   },
-  10040: {'name': 'MIN battery voltage',    'unit': 'V',     'offset' : '0.0',   },
-  10060: {'name': 'MAX battery voltage',    'unit': 'V',     'offset' : '0.0',   },
-  10080: {'name': 'AVG battery voltage',    'unit': 'V',     'offset' : '0.0',   },
-}
-STATUS_WORDS = {
-  0x00: {'name': "OK",                 'descr': "command successful"},
-  0x10: {'name': "UNKNOWN_CMD",        'descr': "unknown command"},
-  0x11: {'name': "INVALID_PARAM",      'descr': "invalid parameter"},
-  0x12: {'name': "INVALID_HEADER",     'descr': "invalid header version"},
-  0x13: {'name': "INVALID_VERC",       'descr': "invalid verion of the command"},
-  0x14: {'name': "INVALID_PW",         'descr': "invalid password for command"},
-  0x20: {'name': "READ_ERR",           'descr': "read error"},
-  0x21: {'name': "WRITE_ERR",          'descr': "write error"},
-  0x22: {'name': "TOO_LONG",           'descr': "too long"},
-  0x23: {'name': "INVALID_ADDRESS",    'descr': "invalid address"},
-  0x24: {'name': "INVALID_CHANNEL",    'descr': "invalid channel"},
-  0x25: {'name': "INVALID_CMD",        'descr': "command not possible in this mode"},
-  0x26: {'name': "UNKNOWN_CAL_CMD",    'descr': "unknown calibration command"},
-  0x27: {'name': "CAL_ERROR",          'descr': "calibration error"},
-  0x28: {'name': "BUSY",               'descr': "busy"},
-  0x29: {'name': "LOW_VOLTAGE",        'descr': "low voltage"},
-  0x2A: {'name': "HW_ERROR",           'descr': "hardware error"},
-  0x2B: {'name': "MEAS_ERROR",         'descr': "measurement error"},
-  0x2C: {'name': "INIT_ERROR",         'descr': "device initialization error"},
-  0x2D: {'name': "OS_ERROR",           'descr': "operating system error"},
-  0x30: {'name': "E2_DEFAULT_CONF",    'descr': "error. loading the default configuration."},
-  0x31: {'name': "E2_CAL_ERROR",       'descr': "calibration invalid - measurement impossible"},
-  0x32: {'name': "E2_CRC_CONF_ERR",    'descr': "CRC error. loading the default configuration."},
-  0x33: {'name': "E2_CRC_CAL_ERR",     'descr': "CRC error. calibration invalid - measurement impossible"},
-  0x34: {'name': "ADJ_STEP1",          'descr': "adjustment step 1"},
-  0x35: {'name': "ADJ_OK",             'descr': "adjustment OK"},
-  0x36: {'name': "CHANNEL_OFF",        'descr': "channel deactivated"},
-  0x50: {'name': "VALUE_OVERFLOW",     'descr': "measured value (+offset) is above the set value limit"},
-  0x51: {'name': "VALUE_UNDERFLOW",    'descr': ""},
-  0x52: {'name': "CHANNEL_OVERRANGE",  'descr': "measured value (physical) is above the measurable range (e.g. ADC saturation)"},
-  0x53: {'name': "CHANNEL_UNDERRANGE", 'descr': ""},
-  0x54: {'name': "DATA_ERROR",         'descr': "measurement data is invalid or doesn't exist"},
-  0x55: {'name': "MEAS_UNABLE",        'descr': "measurement impossible - check the environment conditions!"},
-  0x60: {'name': "FLASH_CRC_ERR",      'descr': "CRC error in the values stored in flash memory"},
-  0x61: {'name': "FLASH_WRITE_ERR",    'descr': "error on writing to flash memory"},
-  0x62: {'name': "FLASH_FLOAT_ERR",    'descr': "flash memory contains invalid float values"},
-  0x80: {'name': "FW_RECEIVE_ERR",     'descr': "Error activating firmware flash mode"},
-  0x81: {'name': "CRC_ERR",            'descr': "CRC error"},
-  0x82: {'name': "TIMEOUT_ERR",        'descr': "timeout occured"},
-  0xF0: {'name': "RESERVED",           'descr': "reserved"},
-  0xFF: {'name': "UNKNOWN_ERR",        'descr': "unknown error"},
-}
-
-class Object(object):
-    def __init__(self, **kwargs):
-        for key, val in kwargs.items():
-            setattr(self, key, val)
-
-    def __getitem__(self, key):
-        return getattr(self, key)
-
-    def __repr__(self):
-        return "Object.from_dict({})".format(self.__dict__)
-    def __str__(self):
-        return repr(self)
-
-    def to_dict(self):
-        return self.__dict__.copy()
-
-    @classmethod
-    def from_dict(cls, d):
-        o = Object()
-        for key, val in d:
-            setattr(o, key, val)
-        return o
-
diff --git a/opus20/opus20/opus20_cli.py b/opus20/opus20/opus20_cli.py
deleted file mode 100755
index 97c364e..0000000
--- a/opus20/opus20/opus20_cli.py
+++ /dev/null
@@ -1,110 +0,0 @@
-#!/usr/bin/env python
-
-import pdb
-import sys
-import time
-import argparse
-import logging
-
-from opus20 import Opus20, OPUS20_CHANNEL_SPEC, PickleStore, Opus20ConnectionException
-
-clock = time.perf_counter
-logger = logging.getLogger('opus20_cli')
-
-def extended_int(string):
-    if string.startswith('0x'):
-        return int(string, 16)
-    else:
-        return int(string)
-
-def main():
-
-    parser = argparse.ArgumentParser(description="CLI for the Lufft OPUS20. Note that the subcommands provide their own --help!")
-    parser.add_argument('host', help='hostname of the device')
-    parser.add_argument('--port', '-p', type=int, help='TCP port of the OPUS20')
-    parser.add_argument('--timeout', '-t', type=float, help='Timeout of the TCP connection in seconds')
-    parser.add_argument('--loglevel', choices=['CRITICAL', 'ERROR', 'WARNING', 'INFO', 'DEBUG'], help='Sets the verbosity of this script')
-    subparsers = parser.add_subparsers(title='commands', help='', dest='cmd')
-    parser_list = subparsers.add_parser('list', help='list all possible measurement channels')
-    parser_get = subparsers.add_parser('get', help='get the value(s) of specific channel(s)')
-    parser_get.add_argument('channel', type=extended_int, nargs='+', help='The selected channel(s)')
-    parser_download = subparsers.add_parser('download', help='download the logs and store them locally')
-    parser_download.add_argument('persistance_file', help='file to store the logs in')
-    parser_logging = subparsers.add_parser('logging', help='change or query global logging settings (start, stop, clear)')
-    subsubparsers = parser_logging.add_subparsers(help='Action to perform w/ respect to logging', dest='action')
-    parser_logging_action_status = subsubparsers.add_parser('status', help='Query the current logging status of the device')
-    parser_logging_action_start  = subsubparsers.add_parser('start', help='Start logging altogether on the device')
-    parser_logging_action_stop   = subsubparsers.add_parser('stop', help='Stop logging altogether on the device')
-    parser_logging_action_clear  = subsubparsers.add_parser('clear', help='Clear the log history on the device')
-    parser_enable = subparsers.add_parser('enable', help='enable logging for a specific channel')
-    parser_enable.add_argument('channel', type=extended_int, nargs='+', help='The selected channel(s)')
-    parser_disable = subparsers.add_parser('disable', help='disable logging for a specific channel')
-    parser_disable.add_argument('channel', type=extended_int, nargs='+', help='The selected channel(s)')
-    args = parser.parse_args()
-
-    if not args.cmd: parser.error('please select a command')
-    if args.cmd == 'logging' and not args.action: parser.error('please select a logging action')
-
-    if args.loglevel:
-        logging.basicConfig(level=getattr(logging, args.loglevel.upper()))
-
-    start = clock()
-    o20 = None
-    try:
-        kwargs = {}
-        if args.port: kwargs['port'] = args.port
-        if args.timeout: kwargs['timeout'] = args.timeout
-        o20 = Opus20(args.host, **kwargs)
-
-        if args.cmd == 'list':
-            for channel in o20.available_channels:
-                log_enabled = o20.get_channel_logging_state(channel)
-                log_enabled = 'yes' if log_enabled else 'no'
-                fmt = "Channel {:5d} (0x{:04X}): {name:22s}  unit: {unit:4s}  offset: {offset:5s}  logging: {log_enabled}"
-                print(fmt.format(channel, channel, log_enabled=log_enabled, **OPUS20_CHANNEL_SPEC[channel]))
-        if args.cmd == 'get':
-            if len(args.channel) > 1:
-                for channel in o20.multi_channel_value(args.channel):
-                    print("{:.3f}".format(channel))
-            else:
-                print("{:.3f}".format(o20.channel_value(args.channel[0])))
-        if args.cmd == 'download':
-            ps = PickleStore(args.persistance_file)
-            try:
-                max_ts = ps.max_ts()[o20.device_id]
-            except KeyError:
-                max_ts = None
-            log_data = o20.download_logs(start_datetime=max_ts)
-            ps.add_data(o20.device_id, log_data)
-            ps.persist()
-        if args.cmd == 'logging':
-            def logging_in_words(): return 'enabled' if o20.get_logging_state() else 'disabled'
-            if args.action == 'status':
-                print("Logging is currently " + logging_in_words() + ".")
-            elif args.action in ('start', 'stop'):
-                o20.set_logging_state(args.action == 'start')
-                logger.info("Logging is now " + logging_in_words() + ".")
-            elif args.action == 'clear':
-                o20.clear_log()
-                print('Clearing the log now. This will take a couple of minutes.')
-                print('You cannot make requests to the device during that time.')
-                o20.disconnect()
-        if args.cmd in ('enable', 'disable'):
-            enable = args.cmd == 'enable'
-            for channel in args.channel:
-                o20.set_channel_logging_state(enable)
-
-    except Opus20ConnectionException as e:
-        parser.error(str(e))
-
-    finally:
-        try:
-            o20.disconnect()
-            o20 = None
-        except:
-            pass
-    end = clock()
-    logger.info("script running time (net): {:.6f} seconds.".format(end-start))
-
-
-if __name__ == "__main__": main()
diff --git a/opus20/opus20/opus20_discovery.py b/opus20/opus20/opus20_discovery.py
deleted file mode 100755
index eee2973..0000000
--- a/opus20/opus20/opus20_discovery.py
+++ /dev/null
@@ -1,45 +0,0 @@
-#!/usr/bin/env python
-
-import sys
-import time
-import argparse
-import logging
-import functools
-
-from opus20 import Opus20, OPUS20_CHANNEL_SPEC, PickleStore, Opus20ConnectionException, discover_OPUS20_devices
-
-clock = time.perf_counter
-logger = logging.getLogger('opus20_discovery')
-
-def main():
-
-    parser = argparse.ArgumentParser(description="Discovery of Lufft OPUS20 devices on the local network")
-    parser.add_argument('bind_address', default="", nargs="?", help='The IP to bind to')
-    parser.add_argument('--loglevel', '-l', default="WARNING", choices=['CRITICAL', 'ERROR', 'WARNING', 'INFO', 'DEBUG'], help='log level')
-    args = parser.parse_args()
-
-    if args.loglevel:
-        logging.basicConfig(level=getattr(logging, args.loglevel.upper()))
-
-    found_devices = []
-    def full_callback(found_devices, answer):
-        frm, host, answer_time = answer
-        dev_props = frm.kind.func()
-        dev_props['answer_time'] = answer_time
-        found_devices.append(dev_props)
-    callback = functools.partial(full_callback, found_devices)
-
-    start = clock()
-
-    print("\nTrying to find devices on interface {}...\n".format(args.bind_address))
-    discover_OPUS20_devices(callback, bind_addr=args.bind_address)
-    for device in found_devices:
-        print("[{answer_time:.2f} ms] Device ID: {device_id}, IP: {ip}, Gateway: {gw}, Network: {net}".format(**device))
-    print("\nFound a total number of {} devices.\n".format(len(found_devices)))
-
-    end = clock()
-    logger.info("script running time (net): {:.6f} seconds.".format(end-start))
-
-    sys.exit(0 if found_devices else 1)
-
-if __name__ == "__main__": main()
diff --git a/opus20/opus20/opus20_fakeserver.py b/opus20/opus20/opus20_fakeserver.py
deleted file mode 100755
index f456ef4..0000000
--- a/opus20/opus20/opus20_fakeserver.py
+++ /dev/null
@@ -1,30 +0,0 @@
-#!/usr/bin/env python
-
-import argparse
-import logging
-import pdb
-
-from opus20 import Opus20FakeServer
-
-logger = logging.getLogger('opus20_fakeserver')
-
-def main():
-
-    parser = argparse.ArgumentParser(description="Discovery of Lufft OPUS20 devices on the local network")
-    parser.add_argument('bind_address', default="", nargs="?", help='The IP to bind to')
-    parser.add_argument('--feed_logfile', help='A log file to feed the fake server with l2p frames')
-    parser.add_argument('--loglevel', '-l', default="INFO", choices=['CRITICAL', 'ERROR', 'WARNING', 'INFO', 'DEBUG'], help='log level')
-    args = parser.parse_args()
-
-    if args.loglevel:
-        logging.basicConfig(level=getattr(logging, args.loglevel.upper()))
-
-    fs = Opus20FakeServer(args.bind_address)
-    if args.feed_logfile:
-        logger.info("Feeding server with l2p example communication...".format(args.bind_address))
-        fs.feed_with_communication_log(args.feed_logfile)
-    logger.info("Starting fake OPUS20 server on interface {}...".format(args.bind_address))
-    fs.bind_and_serve()
-    logger.info("Fake OPUS20 server closed.")
-
-if __name__ == "__main__": main()
diff --git a/opus20/opus20/opus20_web.py b/opus20/opus20/opus20_web.py
deleted file mode 100755
index 5984e85..0000000
--- a/opus20/opus20/opus20_web.py
+++ /dev/null
@@ -1,45 +0,0 @@
-#!/usr/bin/env python
-
-# local deps
-from opus20.webapp import PlotWebServer
-
-# std lib
-import argparse
-import logging
-
-logger = logging.getLogger('opus20_web')
-
-
-def main():
-
-    parser = argparse.ArgumentParser(description="Web interface for the Lufft OPUS20")
-    parser.add_argument('host', help='hostname of the device')
-    parser.add_argument('--port', '-p', type=int, help='port of the device for TCP connections')
-    parser.add_argument('--timeout', '-t', type=float, help='timeout for the TCP connection')
-    parser.add_argument('--loglevel', '-l', choices=['CRITICAL', 'ERROR', 'WARNING', 'INFO', 'DEBUG'], help='log level')
-    parser.add_argument('--debug', '-d', action='store_true', help='enable debugging')
-    args = parser.parse_args()
-
-    if args.loglevel:
-        logging.basicConfig(level=getattr(logging, args.loglevel.upper()))
-
-    plot_server = None
-    try:
-        kwargs = {}
-        if args.port: kwargs['port'] = args.port
-        if args.timeout: kwargs['timeout'] = args.timeout
-        kwargs['debug'] = args.debug
-        plot_server = PlotWebServer(args.host, '/tmp/opus20-plot-server.pickle', **kwargs)
-        plot_server.run(host='0.0.0.0', port=45067, debug=args.debug)
-
-    except ConnectionRefusedError as e:
-        parser.error("Could not connect to host {}: {}".format(args.host, e))
-
-    finally:
-        try:
-            plot_server.disconnect_opus20()
-        except:
-            pass
-
-
-if __name__ == "__main__": main()
diff --git a/opus20/opus20/webapp/__init__.py b/opus20/opus20/webapp/__init__.py
deleted file mode 100755
index 77aa216..0000000
--- a/opus20/opus20/webapp/__init__.py
+++ /dev/null
@@ -1,224 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-
-# local deps
-from opus20 import Opus20, OPUS20_CHANNEL_SPEC, PickleStore, Object
-
-# std lib
-import logging
-import os
-import time
-from datetime import datetime
-
-# external deps
-from bottle import Bottle, request, response, view, static_file, TEMPLATE_PATH, jinja2_view as view
-
-logger = logging.getLogger(__name__)
-
-# Find out where our resource files are located:
-try:
-    from pkg_resources import resource_filename, Requirement, require
-    PATH = resource_filename("opus20", "webapp")
-except:
-    PATH = './'
-
-TEMPLATE_PATH.insert(0, os.path.join(PATH, 'views'))
-
-clock = time.perf_counter
-
-class PlotWebServer(Bottle):
-
-    DPI = 72
-    TPL_GLOBALS = {}
-    MIME_MAP = {
-      'pdf': 'application/pdf',
-      'png': 'image/png',
-      'svg': 'image/svg+xml'
-    }
-
-    def __init__(self, host, log_file, **kwargs):
-        # check for different requirements at object instatiation
-        import matplotlib, jinja2, pandas, numpy, pillow
-        if 'debug' in kwargs:
-            self.debug = kwargs['debug']
-            del kwargs['debug']
-        else:
-            self.debug = False
-        self.TPL_GLOBALS['debug_mode'] = self.debug
-        self.o20 = Opus20(host, **kwargs)
-        self.o20.disconnect()
-        self.logfile = log_file
-        self.ps = PickleStore(log_file)
-        self._current_values_last_call = -1E13
-        self._download_device_data_last_call = -1E13
-        super(PlotWebServer, self).__init__()
-        self.route('/list/devices', callback = self._list_devices)
-        self.route('/download/<device_id>', callback = self._download_device_data)
-        self.route('/status/<device_id>', callback = self._status_device)
-        self.route('/plot/<device_id>_history.<fileformat>', callback = self._plot_history)
-        self.route('/static/<filename:path>', callback = self._serve_static)
-        if self.debug: self.route('/debug', callback = self._debug_page)
-        self.route('/plots', callback = self._plots_page)
-        self.route('/about', callback = self._about_page)
-        self.route('/', callback = self._status_page)
-
-    def _atg(self, vals):
-        """ Add template globals
-            A wrapper function for the templated routes decorated with a @view() """
-        vals.update(self.TPL_GLOBALS)
-        return vals
-
-    @view('status.jinja2')
-    def _status_page(self):
-        return self._atg({'device_id': self._connected_device, 'current_values': self.current_values, 'active': 'status'})
-
-    @view('about.jinja2')
-    def _about_page(self):
-        version = require("opus20")[0].version
-        return self._atg({'active': 'about', 'opus20_version': version})
-
-    @view('plots.jinja2')
-    def _plots_page(self):
-        return self._atg({'device_id': self._connected_device, 'active': 'plots'})
-
-    @view('debug.jinja2')
-    def _debug_page(self):
-        return self._atg({
-          'active': 'debug',
-          'debug_dict': {
-            'self._current_values_last_call': self._current_values_last_call,
-            'self._download_device_data_last_call': self._download_device_data_last_call,
-          }
-        })
-
-    @property
-    def current_values(self):
-        """ the current values """
-        if  clock() - self._current_values_last_call < 2.0:
-            return self._cached_current_values
-        self._current_values_last_call = clock()
-        cur = Object()
-        values = self.o20.multi_channel_value( [0x0064, 0x006E, 0x00C8, 0x00CD, 0x2724] )
-        cur.device_id =         self.o20.device_id
-        cur.temperature =       values[0]
-        cur.dewpoint =          values[1]
-        cur.relative_humidity = values[2]
-        cur.absolute_humidity = values[3]
-        cur.battery_voltage =   values[4]
-        cur.ts = datetime.now().replace(microsecond=0)
-        self._cached_current_values = cur
-        return cur
-
-    def _status_device(self, device_id):
-        assert device_id == self._connected_device
-        status = self.current_values.to_dict()
-        status['ts'] = status['ts'].isoformat()
-        return {
-                'success': True,
-                'status': status,
-               }
-
-    def _serve_static(self, filename):
-        return static_file(filename, root=os.path.join(PATH, 'static'))
-
-    @property
-    def _connected_device(self):
-        """ As long as the webserver can handle only a single
-            OPUS20 device, we return this single one here. """
-        return self.o20.device_id
-
-    def _list_devices(self):
-        return {
-                'success': True,
-                'devices': list(set([self._connected_device]) | set(self.ps.get_device_ids()))
-               }
-
-    def _download_device_data(self, device_id):
-        if  clock() - self._download_device_data_last_call < 10.0:
-            return {'success': True, 'cached': True}
-        self._download_device_data_last_call = clock()
-        try:
-            max_ts = self.ps.max_ts()[device_id]
-        except KeyError:
-            max_ts = None
-        log_data = self.o20.download_logs(start_datetime=max_ts)
-        self.o20.disconnect()
-        self.ps.add_data(self.o20.device_id, log_data)
-        self.ps.persist()
-        return {'success': True}
-
-    def _plot_history(self, device_id, fileformat):
-        from io import BytesIO
-        import matplotlib
-        matplotlib.use('Agg')
-        import matplotlib.pyplot as plt
-        import numpy as np
-        import pandas as pd
-
-        df = pd.DataFrame(self.ps.get_data(device_id))
-
-        df = df.set_index('ts', drop=True)
-        df.columns = [OPUS20_CHANNEL_SPEC[col]['name'] for col in df.columns]
-
-        # Handling of URL query variables
-        color = request.query.get('color', 'b,m,y,r,g,k').split(',')
-        ylabel =  request.query.get('ylabel',  'temperature [°C]')
-        y2label = request.query.get('y2label', 'humidity [%]')
-        q_range = request.query.range
-        if q_range:
-            if ',' in q_range:
-                q_range = q_range.split(',')
-                df = df[q_range[0]:q_range[1]]
-            else:
-                df = df[q_range]
-        else:
-            q_range = 'All Time'
-        figsize = request.query.figsize or '10,6'
-        figsize = (float(num) for num in figsize.split(','))
-        dpi = request.query.dpi or self.DPI
-        dpi = float(dpi)
-        #resample = request.query.resample or '2min'
-        measures = request.query.measures
-        if not measures:
-            measures = ('temperature', 'relative humidity')
-        else:
-            measures = measures.split(',')
-        right = request.query.get('right', None)
-        if right is None:
-            right = ('relative humidity',)
-        else:
-            right = right.split(',')
-        #selected_cols = df.columns
-        selected_cols = []
-        for measure in measures:
-            for col in df.columns:
-                if measure in col:
-                    if col not in selected_cols: selected_cols.append(col)
-        right_cols = []
-        for col in selected_cols:
-            for measure in right:
-                if measure in col:
-                    if col not in right_cols: right_cols.append(col)
-        # / End handling URL query variables
-
-        fig, ax = plt.subplots(figsize=figsize)
-        if len(selected_cols) == 1: color = color[0]
-        df.ix[:,selected_cols].plot(ax=ax, color=color, grid=True, secondary_y=right_cols, x_compat=True)
-        ax.set_xlabel('')
-        ax.set_ylabel(ylabel)
-        if len(right_cols): plt.ylabel(y2label)
-        ax.set_title("OPUS20 device: " + device_id)
-        #start, end = ax.get_xlim()
-        #ax.xaxis.set_ticks(np.arange(start, end, 1.0))
-        #ax.xaxis.grid(True, which="minor")
-        #ax.legend()
-
-        io = BytesIO()
-        plt.savefig(io, format=fileformat, dpi=dpi)
-        plt.close()
-        response.content_type = self.MIME_MAP[fileformat]
-        return io.getvalue()
-
-    def disconnect_opus20(self):
-        self.o20.close()
-
diff --git a/opus20/opus20/webapp/static/css/style.css b/opus20/opus20/webapp/static/css/style.css
deleted file mode 100644
index 71e459d..0000000
--- a/opus20/opus20/webapp/static/css/style.css
+++ /dev/null
@@ -1,23 +0,0 @@
-/*
- * Base structure
- */
-
-html,
-body {
-  padding-top: 70px;
-  background-color: #333;
-  text-align: center;
-  text-shadow: 0 1px 3px rgba(0,0,0,.5);
-}
-
-pre {
-  text-align: left;
-}
-
-.chart {
-  margin: 10px 20px;
-}
-
-.btn.disabled, .btn[disabled], fieldset[disabled] .btn {
-    cursor: wait;
-}
diff --git a/opus20/opus20/webapp/static/js/script.js b/opus20/opus20/webapp/static/js/script.js
deleted file mode 100644
index 8b13789..0000000
--- a/opus20/opus20/webapp/static/js/script.js
+++ /dev/null
@@ -1 +0,0 @@
-
diff --git a/opus20/opus20/webapp/views/about.jinja2 b/opus20/opus20/webapp/views/about.jinja2
deleted file mode 100644
index 6e4f815..0000000
--- a/opus20/opus20/webapp/views/about.jinja2
+++ /dev/null
@@ -1,18 +0,0 @@
-{% extends "base.jinja2" %}
-
-{% block title %}About{% endblock %}
-
-{% block content %}
-            <h1 class="cover-heading">About opus20</h1>
-            <p class="lead">
-              Environmental Monitoring with OPUS20
-            </p>
-            <p>Version: v{{opus20_version}}</p>
-            <p style="width: 23em; margin-left: auto; margin-right: auto;">
-              The Python package opus20 allows for easy environmental monitoring using the OPUS20 logger by Lufft.
-              It was written by <a href="mailto:klaus@physik.uni-frankfurt.de">Philipp Klaus</a> for use in the clean room laboratory of the <a href="https://www.uni-frankfurt.de/51839189/X-treme-Matter-Group">X-Matter Group</a> at the University of Frankfurt.
-            </p>
-            <!--<p class="lead">
-              <a href="#" class="btn btn-lg btn-default">Learn more</a>
-            </p>-->
-{% endblock %}
diff --git a/opus20/opus20/webapp/views/base.jinja2 b/opus20/opus20/webapp/views/base.jinja2
deleted file mode 100644
index 959195a..0000000
--- a/opus20/opus20/webapp/views/base.jinja2
+++ /dev/null
@@ -1,79 +0,0 @@
-<!DOCTYPE html>
-<html lang="en">
-  <head>
-    <meta charset="utf-8">
-    <meta http-equiv="X-UA-Compatible" content="IE=edge">
-    <meta name="viewport" content="width=device-width, initial-scale=1">
-    <!-- The above 3 meta tags *must* come first in the head; any other head content must come *after* these tags -->
-    <meta name="description" content="">
-    <meta name="author" content="">
-    <link rel="icon" href="favicon.ico">
-
-    <title>{% block title %}{% endblock %} - opus20</title>
-
-    <!-- Latest compiled and minified CSS -->
-    <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.5/css/bootstrap.min.css">
-
-    <!-- Optional theme -->
-    <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.5/css/bootstrap-theme.min.css">
-
-    <!-- Custom styles for this template -->
-    <link href="/static/css/style.css" rel="stylesheet">
-
-    <script type="text/javascript">
-          {% block top_javascript %}{% endblock %}
-    </script>
-  </head>
-
-  <body>
-
-    <!-- Fixed navbar -->
-    <nav class="navbar navbar-default navbar-fixed-top">
-      <div class="container">
-        <div class="navbar-header">
-          <button type="button" class="navbar-toggle collapsed" data-toggle="collapse" data-target="#navbar" aria-expanded="false" aria-controls="navbar">
-            <span class="sr-only">Toggle navigation</span>
-            <span class="icon-bar"></span>
-            <span class="icon-bar"></span>
-            <span class="icon-bar"></span>
-          </button>
-          <a class="navbar-brand {{'active' if active == 'status'}}" href="/">opus20 Status</a>
-        </div>
-        <div id="navbar" class="navbar-collapse collapse">
-          <ul class="nav navbar-nav">
-            <li class="{{'active' if active == 'plots'}}"><a href="/plots">Plots</a></li>
-          </ul>
-          <ul class="nav navbar-nav navbar-right">
-            {% if debug_mode %}
-            <li class="{{'active' if active == 'debug'}}"><a href="/debug">Debug</a></li>
-            {% endif %}
-            <li class="{{'active' if active == 'about'}}"><a href="/about">About</a></li>
-          </ul>
-        </div><!--/.nav-collapse -->
-      </div>
-    </nav>
-
-    <div class="container">
-
-      <!-- Main component for a primary marketing message or call to action -->
-      <div class="jumbotron">
-
-          {% block content %}{% endblock %}
-
-      </div>
-
-    </div> <!-- /container -->
-
-    <!-- Bootstrap core JavaScript
-    ================================================== -->
-    <script src="https://ajax.googleapis.com/ajax/libs/jquery/1.11.3/jquery.min.js"></script>
-    <script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.5/js/bootstrap.min.js"></script>
-    <script src="/static/js/script.js"></script>
-
-    <script type="text/javascript">
-          {% block bottom_javascript %}{% endblock %}
-    </script>
-    <!-- Placed at the end of the document so the pages load faster -->
-  </body>
-</html>
-
diff --git a/opus20/opus20/webapp/views/debug.jinja2 b/opus20/opus20/webapp/views/debug.jinja2
deleted file mode 100644
index 107ae30..0000000
--- a/opus20/opus20/webapp/views/debug.jinja2
+++ /dev/null
@@ -1,10 +0,0 @@
-{% extends "base.jinja2" %}
-
-{% block title %}Debug{% endblock %}
-
-{% block content %}
-            <h1 class="cover-heading">Debugging Information</h1>
-            <p class="lead">
-              <pre><code>{{ debug_dict | pprint }}</code></pre>
-            </p>
-{% endblock %}
diff --git a/opus20/opus20/webapp/views/plots.jinja2 b/opus20/opus20/webapp/views/plots.jinja2
deleted file mode 100644
index 8941a95..0000000
--- a/opus20/opus20/webapp/views/plots.jinja2
+++ /dev/null
@@ -1,61 +0,0 @@
-{% extends "base.jinja2" %}
-
-{% block title %}Plot{% endblock %}
-
-{% block top_javascript %}
-
-var device_id = "{{ device_id }}";
-
-{% endblock %}
-
-{% block bottom_javascript %}
-
-$(function(){
-  $('#fetchData').on('click', function(){
-    var $btn = $(this);
-    $btn.prop('disabled', true);
-    var $text = $btn[0].textContent;
-    $btn.prop('textContent', "Loading...");
-    $.ajax({
-      url: "/download/" + device_id,
-      type: 'get',
-      success: function (response) {
-        //console.log('response received');
-        $btn.prop('disabled', false);
-        $btn.prop('textContent', $text);
-        location.reload()
-      }, error: function (response) {
-        console.log('ajax request to fetch data failed');
-        alert('ajax request to fetch data failed');
-        $btn.prop('disabled', false);
-        $btn.prop('textContent', $text);
-      },
-    });
-  });
-});
-
-
-{% endblock %}
-
-{% block content %}
-            <h1 class="cover-heading">Plots of the Environment Values</h1> <br />
-            <h3>Combined Plot</h3>
-            <p class="lead">
-              <img class="chart" src="/plot/{{ device_id }}_history.png?figsize=10,7&measures=temperature,relative humidity&right=humidity" />
-            </p>
-            <h3>Temperature Plot</h3>
-            <p class="lead">
-              <img class="chart" src="/plot/{{ device_id }}_history.png?figsize=10,7&measures=temperature&color=blue" />
-            </p>
-            <h3>Relative Humidity Plot</h3>
-            <p class="lead">
-              <img class="chart" src="/plot/{{ device_id }}_history.png?figsize=10,7&measures=relative humidity&right=-&ylabel=humidity [%25]&color=magenta" />
-            </p>
-            <h3>Absolute Humidity Plot</h3>
-            <p class="lead">
-              <img class="chart" src="/plot/{{ device_id }}_history.png?figsize=10,7&measures=absolute humidity&right=-&ylabel=humidity [g%2Fm%B3]&color=cyan" />
-            </p>
-            <p class="lead">
-              <button id="fetchData" class="btn btn-lg btn-default" type="button">Fetch New Data</button>
-            </p>
-{% endblock %}
diff --git a/opus20/opus20/webapp/views/status.jinja2 b/opus20/opus20/webapp/views/status.jinja2
deleted file mode 100644
index 4128726..0000000
--- a/opus20/opus20/webapp/views/status.jinja2
+++ /dev/null
@@ -1,58 +0,0 @@
-{% extends "base.jinja2" %}
-
-{% block title %}Status{% endblock %}
-
-{% block top_javascript %}
-
-var update_every_ms = 2050;
-var device_id = "{{ device_id }}";
-
-{% endblock %}
-
-{% block bottom_javascript %}
-
-function updateStatus() {
-  $.getJSON("/status/" + device_id, function( data ) {
-    $("#device_id").text(data.status.device_id);
-    $("#temperature").text(data.status.temperature.toFixed(1));
-    $("#dewpoint").text(data.status.dewpoint.toFixed(1));
-    $("#relative_humidity").text(data.status.relative_humidity.toFixed(1));
-    $("#absolute_humidity").text(data.status.absolute_humidity.toFixed(1));
-    $("#battery_voltage").text(data.status.battery_voltage.toFixed(1));
-    $("#timestamp").text(data.status.ts.replace('T', ' - '));
-  });
-};
-
-function timedUpdate() {
-  updateStatus();
-  setTimeout(function() {
-    timedUpdate();
-  }, update_every_ms);
-};
-
-$(function() {
-  // on page load
-  updateStatus();
-  setTimeout(function() {
-    timedUpdate();
-  }, update_every_ms);
-});
-
-{% endblock %}
-
-{% block content %}
-            <h1 class="cover-heading">Current Status</h1>
-            <p class="lead">
-              <div>Device ID:                 <span id="device_id">{{ current_values.device_id                    }}</span>      </div>
-              <div>Temperature:             <span id="temperature">{{ current_values.temperature       | round(1) }}</span> °C   </div>
-              <div>Dewpoint:                   <span id="dewpoint">{{ current_values.dewpoint          | round(1) }}</span> °C   </div>
-              <div>Relative Humidity: <span id="relative_humidity">{{ current_values.relative_humidity | round(1) }}</span> %    </div>
-              <div>Absolute Humidity: <span id="absolute_humidity">{{ current_values.absolute_humidity | round(1) }}</span> g/m³ </div>
-              <div>Battery Voltage:     <span id="battery_voltage">{{ current_values.battery_voltage   | round(1) }}</span> V    </div>
-              <br />
-              <div>Date &amp; Time of this information: <span id="timestamp">{{ current_values.ts.isoformat().replace('T', ' - ') }}</span></div>
-            </p>
-            <!--<p class="lead">
-              <a href="#" class="btn btn-lg btn-default">Learn more</a>
-            </p>-->
-{% endblock %}
diff --git a/opus20/setup.py b/opus20/setup.py
deleted file mode 100644
index 5c6db36..0000000
--- a/opus20/setup.py
+++ /dev/null
@@ -1,48 +0,0 @@
-# -*- coding: utf-8 -*-
-
-"""
-Copyright (c) 2015, Philipp Klaus. All rights reserved.
-
-License: GPLv3
-"""
-
-from setuptools import setup
-
-setup(name='opus20',
-      version = '0.9.6',
-      description = 'Interface to Lufft OPUS20 devices',
-      long_description = '',
-      author = 'Philipp Klaus',
-      author_email = 'klaus@physik.uni-frankfurt.de',
-      url = '',
-      license = 'GPL',
-      packages = ['opus20', 'opus20.webapp'],
-      entry_points = {
-        'console_scripts': [
-          'opus20_cli = opus20.opus20_cli:main',
-          'opus20_web = opus20.opus20_web:main',
-          'opus20_discovery = opus20.opus20_discovery:main',
-          'opus20_fakeserver = opus20.opus20_fakeserver:main',
-        ],
-      },
-      install_requires = [],
-      extras_require = {
-          'webserver':  ["bottle", "matplotlib", "jinja2", "pandas", "numpy", "pillow"],
-      },
-      include_package_data = True,
-      zip_safe = True,
-      platforms = 'any',
-      keywords = 'Lufft Opus20',
-      classifiers = [
-          'Development Status :: 4 - Beta',
-          'Operating System :: OS Independent',
-          'License :: OSI Approved :: GPL License',
-          'Programming Language :: Python',
-          'Programming Language :: Python :: 3',
-          'Programming Language :: Python :: 3.2',
-          'Topic :: System :: Monitoring',
-          'Topic :: System :: Logging',
-      ]
-)
-
-
-- 
2.43.0