From: Philipp Klaus
Date: Mon, 14 Aug 2017 15:33:36 +0000 (+0200)
Subject: opus20: goodbye for now, -> Github / PyPI
X-Git-Url: https://jspc29.x-matter.uni-frankfurt.de/git/?a=commitdiff_plain;h=d24e3c8fd5635838ff9c5c82254474e1f490d326;p=labtools.git
opus20: goodbye for now, -> Github / PyPI
---
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
-
- Ported the software to Python, extended and packaged it.
-* (c) 2012, [Ondics GmbH](http://www.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('= 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('> 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('> 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('= 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('',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/', callback = self._download_device_data)
- self.route('/status/', callback = self._status_device)
- self.route('/plot/_history.', callback = self._plot_history)
- self.route('/static/', 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 %}
-
About opus20
-
- Environmental Monitoring with OPUS20
-
-
Version: v{{opus20_version}}
-
- The Python package opus20 allows for easy environmental monitoring using the OPUS20 logger by Lufft.
- It was written by Philipp Klaus for use in the clean room laboratory of the X-Matter Group at the University of Frankfurt.
-