ich habe mein erstes Python-Script erstellt und hätte gerne die Meinung und Verbesserungsvorschläge der Python-Könner.
Zur Ausgangssituation: Ich möchte meinen Ölstandssensor (https://www.proteus.at/em-27437.htm) in openHab einbinden. Leider gibt es dazu kein passendes Modul. Also habe ich mir einen Raspberry Zero geholt und den Monitor dort angeschlossen. Der Monitor sendet prinzipiell jede Stunde (kann variieren) die Sensordaten. Ich habe erst mit einem Bashscript versucht die Daten abzufragen und an openHab zu schicken, was prinzipiell auch funktioniert, aber mir fehlte dann ein gewisser Komfort bzw. Überprüfungsmöglichkeiten. Also versuche ich es jetzt mit Python.
Das Script funktioniert, aber ich würde das gerne auch anderen zur Verfügung stellen und da möchte ich sicher sein, dass es korrekt und sauber ist.
Deshalb bin ich um jeden Input froh, der das Script verbessert.
Hab mir die ganzen Infos aus verschiedenen Tutorials zusammengesucht und deshalb denke ich, dass es einiges an Overhead und Verbesserungsmöglichkeiten gibt.
Grundsatzüberlegung(en):
- Das Script startet mit dem Booten des Raspberry
- Die Daten des Monitors sollen ständig empfangen werden können
- Die Daten sollen nach einem Ausfall des Raspberry immer noch vorhanden sein
- Die Daten werden mit einer REST-API im JSON-Format zur Verfügung gestellt und können jederzeit abgerufen werden
Martin
Code: Alles auswählen
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from flask import Flask, request, jsonify, render_template
from waitress import serve
import re
import pickle
import serial
import datetime
import logging
import threading
# Define data file
ecometer_data_file = '/home/pi/eco_data.pkl'
# Define Logger
logger = logging.getLogger('Ecometer_Log')
logger.setLevel(logging.DEBUG)
fh = logging.FileHandler('ecometer.log')
fh.setLevel(logging.DEBUG)
logger.addHandler(fh)
formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
fh.setFormatter(formatter)
logger.addHandler(fh)
# Define Webserver
app = Flask(__name__)
# Thread for reading the data
def ecometer_reading_data():
logger.info('Start Thread ecometer_reading_data')
# Port of Ecometer-Device
ecometer_serial = '/dev/ttyUSB0'
# Open serial device for reading, it is 115200 baud, 8N1
# https://sarnau.info/communication-protocol-of-the-proteus-ecometer-tek603/
ser = serial.Serial(ecometer_serial, 115200)
while True:
# make sure that are no old bytes left in the input buffer
ser.reset_input_buffer()
logger.info('Thread: Waiting for Data...')
# FYI: the devices sends one package with 22 Bytes per hour(?) or at irregular intervals
# Convert the Bytes in a Hex-String for extracting the needed values (Don't know how to make it directly)
hex_string = ser.read(22).hex()
# each packet starts with 2 Bytes, which are 'SI' (in HEX = '5349' on Position 0-4)
if hex_string[0:4] != '5349': # Proteus Ecometer detected?
continue
logger.info('Thread: Data is read in')
# The next 2 Bytes are the Length of the complete Package (16 bit, big-endian)
# The next 1 Byte is a Command (1: data send to the device, 2: data received from the device)
# The next 1 Byte are Flags (bit 0: set the clock (hour/minutes/seconds) in the device on upload, bit 1: force reset the device (set before an update of the device),
# bit 2: a non-empty payload is send to the device, bit 3: force recalculate the device (set on upload after changing the Sensor Offset, Outlet Height or the lookup table)
# bit 4: live data received from the device, bit 5: n/a, bit 6: n/a, bit 7: n/a)
# The next 3 Bytes are the Time from Ecometer: 1 Byte = Hour, 1 Byte = Minute, 1 Byte = Minute (in HEX = character 12-17)
time = '%02d:%02d:%02d' % (int(hex_string[12:14], 16), int(hex_string[14:16], 16), int(hex_string[16:18], 16))
time_dict = {
'id' : 0,
'item' : 'Time',
'value' : time
}
# The next 2 Bytes are the EEPROM Start (16 bit, big-endian) – unused in live data
# The next 2 Bytes are the EEPROM End (16 bit, big-endian)
# The next 1 Byte are the Temperature in Farenheit (in HEX = character 26-27)
temp_f = int(hex_string[26:28], 16)
tempF_dict = {
'id' : 1,
'item' : 'Temp_F',
'value' : temp_f
}
# Calculating the Temp in °C
temp_c = ((temp_f - 40 - 32) / 1.8)
tempC_dict = {
'id' : 2,
'item' : 'Temp_C',
'value' : temp_c
}
# The next 2 Bytes are the Sensor Level in cm (Ullage) (16-bit, big-endian) (in HEX = character 28-31)
ullage = int(hex_string[28:32], 16)
ullage_dict = {
'id' : 3,
'item' : 'Ullage',
'value' : ullage
}
# The next 2 Bytes are the Usable Level (Available Qantity) in Liter (16-bit, big-endian) (in HEX = character 32-35)
usableLevel = int(hex_string[32:36], 16)
usableLevel_dict = {
'id' : 4,
'item' : 'UseableLevel',
'value' : usableLevel
}
# The next 2 Bytes are the Totale Capacity in Liter (16-bit, big-endian) (in HEX = character 36-39)
capacity = int(hex_string[36:40], 16)
capacity_dict = {
'id' : 6,
'item' : 'UseableCapacity',
'value' : capacity
}
# Calculating the available Qantity in %
usablePercent = usableLevel / capacity * 100.01
usablePercent_dict = {
'id' : 5,
'item' : 'UseablePercent',
'value' : usablePercent
}
# Crdate (When was the data generated?)
now = datetime.datetime.now()
crdate_dict = {
'id' : 7,
'item' : 'Timestamp',
'value' : now.timestamp()
}
eco_tuple = (time_dict,tempF_dict,tempC_dict,ullage_dict,usableLevel_dict,usablePercent_dict,capacity_dict,crdate_dict)
logger.info('Thread: Data ready to save')
# Pickle the Tuple
eco_file = open(ecometer_data_file,"bw")
logger.info('Thread: Storage file opened for writing')
pickle.dump(eco_tuple,eco_file)
logger.info('Thread: Data has been saved')
eco_file.close()
logger.info('Thread: Storage file closed')
# Deleting the Tuple for new Measurement
del eco_tuple
logger.info('Thread: Storage data discarded')
logger.info('End Thread ecometer_reading_data')
# Index-Seite
@app.route("/", methods=['GET'])
def home():
return render_template("home.html")
# REST-API
@app.route('/rest/item/<id>', methods=['GET'])
def api_id(id):
# Check if an ID was provided as part of the URL.
# If ID is provided, assign it to a variable.
# If no ID is provided, display an error in the browser.
if int(id) in range(8):
id = int(id)
else:
return "Error: Die gewählte ID wird nicht unterstützt. Gültige IDs sind von 0 bis 7."
# Read Pickle File for Data
eco_file = open(ecometer_data_file,"rb")
logger.info('Webserver: Storage file opened for reading')
ecometer_items = pickle.load(eco_file)
logger.info('Webserver: Data has been read')
# Create an empty list for our results
results = []
# Loop through the data and match results that fit the requested ID.
# IDs are unique, but other fields might return many results
for ecometer_item in ecometer_items:
if ecometer_item['id'] == id:
results.append(ecometer_item)
# Use the jsonify function from Flask to convert our list of
# Python dictionaries to the JSON format.
return jsonify(results)
# 404
@app.errorhandler(404)
def page_not_found(e):
return "<h1>404</h1><p>Die angeforderte Seite gibt es nicht.</p>", 404
# Webserver starten
if __name__ == '__main__':
# Start Threat for reading Data
ecometer_get_data = threading.Thread(target=ecometer_reading_data)
logger.info('Main: Thread is called')
ecometer_get_data.start()
serve(app, host='0.0.0.0', port=5000)