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 & 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