Hallo Micha,
ich habe meine Oventrop Regumaq Frischwasserstation, die ebenfalls Resol V-Bus spricht über den Resol USB-Schnittstellenadapter an den TWS angebunden.
Darauf läuft im Docker Container das Python-Script von rellit (
https://github.com/rellit/resol-vbus-python), welches ich um eine MQTT-Anbindung in Python (paho-mqtt) erweitert habe. Damit werden die Daten des V-Bus ausgelesen und über MQTT an den TWS in die Timeseries gesendet.
Das Vorlage-Script muss angepasst werden, da vermutlich auch die Viessmann-spezifischen Paket-Header (wie auch bei Oventrop) nicht standardmäßig interpretiert werden können. Über den Schnittstellenadapter am PC lässt sich mit der Resol Service Center Software (
https://www.resol.de/de/software) allerdings der Datenstrom in Rohform ausgeben und die von deiner WP/FriWa genutzten Paketheader mit der V-Bus Spezifikation (
http://danielwippermann.github.io/resol-vbus/#/vsf) abgeglichen werden. Siehe dazu auch die von dir bereits verlinkte Seite von Hobbyelektronik. Mit diesen Infos kann dann das Python Script angepasst werden.
Viel Erfolg.
Gruß Moritz
Hier die angepassten Scripte als Lösungsansatz
config.py
Code: Alles auswählen
import os
# configure kind of connection "lan", "serial" or "stdin"
#connection = "lan"
connection = "serial"
#connection = "stdin"
# only used for "lan"
address = ("192.168.1.253", 7053)
vbus_pass = "vbus"
# only used for "serial"
port = "/dev/ttyACM0"
baudrate = 9600
spec_file = os.path.dirname(__file__) + '/spec/Regumaq-X45.json'
# expected amount of different source packets (see spec_file)
expected_packets = 1
debug = False
# MQTT Broker config
mqtt_host = "192.168.3.11"
#MQTT_PORT = 1883
#MQTT_KEEPALIVE_INTERVAL = 45
mqtt_topic = "regumaq"
resol3mqtt.py
Code: Alles auswählen
#!/usr/bin/env python3
#
# Talk with Resol VBUS over LAN or serial UART
#
import socket
import sys
import json
from decimal import *
import paho.mqtt.client as mqtt
import time
# Load settings
try:
import config
except:
sys.exit("config.py not found!")
if config.connection == "serial":
# import serial (pyserial) only if it's configured to not force installing it without needing
import serial
# Load Message specification
try:
import spec
except:
sys.exit("Could not load Message Specification")
# Logs in onto the DeltaSol BS Plus over LAN. Also starts (and maintains) the
# actual stream of data.
def login():
dat = recv()
#Check if device answered
if dat != "+HELLO\n":
return False
#Send Password
send("PASS %s\n" % config.vbus_pass)
dat = recv()
return dat.startswith("+OK")
def load_data():
if config.connection == "lan":
#Request Data
send("DATA\n")
dat = recv()
#Check if device is ready to send Data
if not dat.startswith("+OK"):
return
while len(result) < config.expected_packets:
buf = readstream()
msgs = splitmsg(buf)
if config.debug:
print(str(len(msgs))+" Messages, "+str(len(result))+" Resultlen")
for msg in msgs:
if config.debug: print(get_protocolversion(msg))
if "PV1" == get_protocolversion(msg):
if config.debug: print(format_message_pv1(msg))
parse_payload(msg)
elif "PV2" == get_protocolversion(msg):
if config.debug: print(format_message_pv2(msg))
# Receive 1024 bytes from stream
def recv():
if config.connection == "serial":
# timeout needs to be set to 0 (see init), or it will
# block until the requested number of bytes is read
dat = sock.read(1024)
elif config.connection == "lan":
dat = sock.recv(1024)
elif config.connection == "stdin":
dat = sock.buffer.read(1024)
else:
sys.exit('Unknown connection type. Please check config.')
return dat
# Sends given bytes over the stream. Adds debug
def send(dat):
sock.send(dat)
# Read data until minimum 1 message is received
# We cyclic get:
# '0xaa' '0x00' '0x00' '0x21' '0x74' '0x20' ...
# '0xaa' '0x15' '0x00' '0x21' '0x74' '0x10' ...
# '0xaa' '0x10' '0x00' '0x21' '0x74' '0x10' ...
# Only the last one is needed, so we need 4 times '0xaa'!
def readstream():
data = bytearray(b'')
data.extend(recv())
while data.count(0xAA) < 4:
data.extend(recv())
return data
#Split Messages on Sync Byte
def splitmsg(buf):
return buf.split(b'\xAA')[1:-1]
# Format 1 byte as String Hex string
def format_byte(byte):
return '0x' + ('0' if byte < 0x10 else '') + format(byte, 'x')
# Extract protocol Version from msg
def get_protocolversion(msg):
if msg[4] == 0x10: return "PV1"
if msg[4] == 0x20: return "PV2"
if msg[4] == 0x30: return "PV3"
return "UNKNOWN"
# Extract Destination from msg
def get_destination(msg):
return format_byte(msg[1]) + format_byte(msg[0])[2:]
#Extract source from msg
def get_source(msg):
return format_byte(msg[3]) + format_byte(msg[2])[2:]
# Extract command from msg
def get_command(msg):
return format_byte(msg[6]) + format_byte(msg[5])[2:]
# Get count of frames in msg
def get_frame_count(msg):
return gb(msg, 7, 8)
# Extract payload from msg
def get_payload(msg):
payload = bytearray(b'')
for i in range(get_frame_count(msg)):
payload.extend(integrate_septett(msg[9+(i*6):15+(i*6)]))
return payload
# parse payload and put result in result
def parse_payload(msg):
payload = get_payload(msg)
if config.debug:
print('ParsePacket Payload '+str(len(payload)))
for packet in spec.spec['packet']:
if packet['source'].lower() == get_source(msg).lower() and \
packet['destination'].lower() == get_destination(msg).lower() and \
packet['command'].lower() == get_command(msg).lower():
result[get_source_name(msg)] = {}
for field in packet['field']:
result[get_source_name(msg)][field['name'][0]] = str(
gb(payload, field['offset'], int(field['offset'])+((int(field['bitSize'])+1) / 8)) *
(Decimal(field['factor']) if 'factor' in field else 1)) + \
(field['unit'] if 'unit' in field else '')
def format_message_pv1(msg):
parsed = "PARSED: \n"
parsed += " ZIEL".ljust(15,'.')+": " + get_destination(msg) + "\n"
parsed += " QUELLE".ljust(15,'.')+": " + get_source(msg) + " " + get_source_name(msg) + "\n"
parsed += " PROTOKOLL".ljust(15,'.')+": " + get_protocolversion(msg) + "\n"
parsed += " BEFEHL".ljust(15,'.')+": " + get_command(msg) + "\n"
parsed += " ANZ_FRAMES".ljust(15,'.')+": " + str(get_frame_count(msg)) + "\n"
parsed += " CHECKSUM".ljust(15,'.')+": " + format_byte(msg[8]) + "\n"
for i in range(get_frame_count(msg)):
integrated = integrate_septett(msg[9+(i*6):15+(i*6)])
parsed += (" NB"+str(i*4+1)).ljust(15,'.')+": " + format_byte(msg[9+(i*6)]) + " - " + format_byte(integrated[0]) + "\n"
parsed += (" NB"+str(i*4+2)).ljust(15,'.')+": " + format_byte(msg[10+(i*6)]) + " - " + format_byte(integrated[1]) + "\n"
parsed += (" NB"+str(i*4+3)).ljust(15,'.')+": " + format_byte(msg[11+(i*6)]) + " - " + format_byte(integrated[2]) + "\n"
parsed += (" NB"+str(i*4+4)).ljust(15,'.')+": " + format_byte(msg[12+(i*6)]) + " - " + format_byte(integrated[3]) + "\n"
parsed += (" SEPTETT"+str(i+1)).ljust(15,'.')+": " + format_byte(msg[13+(i*6)]) + "\n"
parsed += (" CHECKSUM"+str(i+1)).ljust(15,'.')+": " + format_byte(msg[14+(i*6)]) + "\n"
parsed += " PAYLOAD".ljust(15,'.')+": " + (" ".join(format_byte(b) for b in get_payload(msg)))+"\n"
return parsed
def format_message_pv2(msg):
parsed = "PARSED: \n"
parsed += " ZIEL1".ljust(15,'.')+": " + format_byte(msg[0]) + "\n"
parsed += " ZIEL2".ljust(15,'.')+": " + format_byte(msg[1]) + "\n"
parsed += " QUELLE1".ljust(15,'.')+": " + format_byte(msg[2]) + "\n"
parsed += " QUELLE2".ljust(15,'.')+": " + format_byte(msg[3]) + "\n"
parsed += " PROTOKOLL".ljust(15,'.')+": " + format_byte(msg[4]) + "\n"
parsed += " BEFEHL1".ljust(15,'.')+": " + format_byte(msg[5]) + "\n"
parsed += " BEFEHL2".ljust(15,'.')+": " + format_byte(msg[6]) + "\n"
parsed += " ID1".ljust(15,'.')+": " + format_byte(msg[7]) + "\n"
parsed += " ID2".ljust(15,'.')+": " + format_byte(msg[8]) + "\n"
parsed += " WERT1".ljust(15,'.')+": " + format_byte(msg[9]) + "\n"
parsed += " WERT2".ljust(15,'.')+": " + format_byte(msg[10]) + "\n"
parsed += " WERT3".ljust(15,'.')+": " + format_byte(msg[11]) + "\n"
parsed += " WERT4".ljust(15,'.')+": " + format_byte(msg[12]) + "\n"
parsed += " SEPTETT".ljust(15,'.')+": " + format_byte(msg[13]) + "\n"
parsed += " CHECKSUM".ljust(15,'.')+": " + format_byte(msg[14]) + "\n"
return parsed
def get_compare_length(mask):
i = 1
while i < 6 and mask[i] != '0':
i += 1
return i+1
def get_source_name(msg):
src = format_byte(msg[3]) + format_byte(msg[2])[2:]
for device in spec.spec['device']:
if src[:get_compare_length(device['mask'])].lower() == device['address'][:get_compare_length(device['mask'])].lower():
return device['name'] if get_compare_length(device['mask']) == 7 else str(device['name']).replace('#',device['address'][get_compare_length(device['mask'])-1:],1)
return ""
def integrate_septett(frame):
data = bytearray(b'')
septet = frame[4]
for j in range(4):
if septet & (1 << j):
data.append(frame[j] | 0x80)
else:
data.append(frame[j])
return data
# Gets the numerical value of a set of bytes (respect Two's complement by value Range)
def gb(data, begin, end): # GetBytes
# convert begin and end to int whatever was passed to make enumerate work
begin = int(begin)
end = int(end)
wbg = sum([0xff << (i * 8) for i, b in enumerate(data[begin:end])])
s = sum([b << (i * 8) for i, b in enumerate(data[begin:end])])
if s >= wbg/2:
s = -1 * (wbg - s)
return s
if __name__ == '__main__':
if config.connection == "serial":
sock = serial.Serial(config.port, baudrate=config.baudrate, timeout=0)
elif config.connection == "lan":
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.connect(config.address)
login()
elif config.connection == "stdin":
sock = sys.stdin
else:
sys.exit('Unknown connection type. Please check config.')
while True:
result = dict()
load_data()
client = mqtt.Client("resol")
# client.connect(mqtt_host)
client.connect("192.168.3.11")
#print(json.dumps(result))
# client.publish(mqtt_topic,json.dumps(result))
client.publish("regumaq",json.dumps(result))
client.publish("regumaq/temperaturen/S1",str(result["Regumaq-X45"]["Temperatur Sensor 1"].split(" ",1)[0]))
client.publish("regumaq/temperaturen/s2",str(result["Regumaq-X45"]["Temperatur Sensor 2"].split(" ",1)[0]))
client.publish("regumaq/temperaturen/s3",str(result["Regumaq-X45"]["Temperatur Sensor 3"].split(" ",1)[0]))
client.publish("regumaq/temperaturen/s4",str(result["Regumaq-X45"]["Temperatur Sensor 4"].split(" ",1)[0]))
client.publish("regumaq/temperaturen/s5",str(result["Regumaq-X45"]["Temperatur Sensor 5"].split(" ",1)[0]))
client.publish("regumaq/temperaturen/s6",str(result["Regumaq-X45"]["Temperatur Sensor 6"].split(" ",1)[0]))
client.publish("regumaq/temperaturen/s7",str(result["Regumaq-X45"]["Temperatur Sensor 7"].split(" ",1)[0]))
client.publish("regumaq/temperaturen/s8",str(result["Regumaq-X45"]["Temperatur Sensor 8"].split(" ",1)[0]))
client.publish("regumaq/volumentstrom/trinkwasser",str(result["Regumaq-X45"]["Durchflusssensor"].split(" ",1)[0]))
client.publish("regumaq/pwm/pwm1",str(result["Regumaq-X45"]["Spannung PWM 1.1"].split(" ",1)[0]))
client.publish("regumaq/pwm/pwm2",str(result["Regumaq-X45"]["Spannung PWM 1.2"].split(" ",1)[0]))
client.publish("regumaq/pwm/pwm3",str(result["Regumaq-X45"]["Spannung PWM 2.1"].split(" ",1)[0]))
client.publish("regumaq/pwm/pwm4",str(result["Regumaq-X45"]["Spannung PWM 2.2"].split(" ",1)[0]))
client.publish("regumaq/relais/R1",str(result["Regumaq-X45"]["Relais 1"].split(" ",1)[0]))
client.publish("regumaq/relais/R2",str(result["Regumaq-X45"]["Relais 2"].split(" ",1)[0]))
client.publish("regumaq/relais/R3",str(result["Regumaq-X45"]["Relais 3"].split(" ",1)[0]))
client.publish("regumaq/relais/R4",str(result["Regumaq-X45"]["Relais 4"].split(" ",1)[0]))
client.publish("regumaq/ausgaenge/A1",str(result["Regumaq-X45"]["Ausgang 1"].split(" ",1)[0]))
client.publish("regumaq/ausgaenge/A2",str(result["Regumaq-X45"]["Ausgang 2"].split(" ",1)[0]))
client.publish("regumaq/ausgaenge/A3",str(result["Regumaq-X45"]["Ausgang 3"].split(" ",1)[0]))
client.publish("regumaq/ausgaenge/A4",str(result["Regumaq-X45"]["Ausgang 4"].split(" ",1)[0]))
client.publish("regumaq/ausgaenge/LinOut",str(result["Regumaq-X45"]["Lin Out"].split(" ",1)[0]))
client.publish("regumaq/fehler/s1",str(result["Regumaq-X45"]["Fehler S1"]))
client.publish("regumaq/fehler/s2",str(result["Regumaq-X45"]["Fehler S2"]))
client.publish("regumaq/fehler/s3",str(result["Regumaq-X45"]["Fehler S3"]))
client.publish("regumaq/fehler/s4",str(result["Regumaq-X45"]["Fehler S4"]))
client.publish("regumaq/fehler/s5",str(result["Regumaq-X45"]["Fehler S5"]))
client.publish("regumaq/fehler/s6",str(result["Regumaq-X45"]["Fehler S6"]))
client.publish("regumaq/fehler/s7",str(result["Regumaq-X45"]["Fehler S7"]))
client.publish("regumaq/fehler/s8",str(result["Regumaq-X45"]["Fehler S8"]))
client.disconnect()
time.sleep(10)
if config.connection == "lan":
try:
sock.shutdown(0)
except:
pass
sock.close()
sock = None
spec/Regumaq-X45.json
Code: Alles auswählen
{
"vbusSpecification": {
"device": [
{
"address": "0x1632",
"mask": "0xFFFF",
"name": "Regumaq-X45",
"isMaster": "true"
}
],
"packet": [
{
"destination": "0x0010",
"source": "0x1632",
"command": "0x0100",
"field": [
{
"offset": "0",
"name": [
"Temperatur Sensor 1",
{
"-lang": "en",
"#text": "Temperature sensor 1"
}
],
"bitSize": "15",
"factor": "0.1",
"unit": " °C"
},
{
"offset": "2",
"name": [
"Temperatur Sensor 2",
{
"-lang": "en",
"#text": "Temperature sensor 2"
}
],
"bitSize": "15",
"factor": "0.1",
"unit": " °C"
},
{
"offset": "4",
"name": [
"Temperatur Sensor 3",
{
"-lang": "en",
"#text": "Temperature sensor 3"
}
],
"bitSize": "15",
"factor": "0.1",
"unit": " °C"
},
{
"offset": "6",
"name": [
"Temperatur Sensor 4",
{
"-lang": "en",
"#text": "Temperature sensor 4"
}
],
"bitSize": "15",
"factor": "0.1",
"unit": " °C"
},
{
"offset": "8",
"name": [
"Temperatur Sensor 5",
{
"-lang": "en",
"#text": "Temperature sensor 5"
}
],
"bitSize": "15",
"factor": "0.1",
"unit": " °C"
},
{
"offset": "10",
"name": [
"Temperatur Sensor 6",
{
"-lang": "en",
"#text": "Temperature sensor 6"
}
],
"bitSize": "15",
"factor": "0.1",
"unit": " °C"
},
{
"offset": "12",
"name": [
"Temperatur Sensor 7",
{
"-lang": "en",
"#text": "Temperature sensor 7"
}
],
"bitSize": "15",
"factor": "0.1",
"unit": " °C"
},
{
"offset": "14",
"name": [
"Temperatur Sensor 8",
{
"-lang": "en",
"#text": "Temperature sensor 8"
}
],
"bitSize": "15",
"factor": "0.1",
"unit": " °C"
},
{
"offset": "16",
"name": [
"Spannung PWM 1.1",
{
"-lang": "en",
"#text": "Voltage / GFA 1.1"
}
],
"bitSize": "32",
"factor": "0.000001",
"unit": " V"
},
{
"offset": "20",
"name": [
"Spannung PWM 1.2",
{
"-lang": "en",
"#text": "Voltage / GFA 1.2"
}
],
"bitSize": "32",
"factor": "0.000001",
"unit": " V"
},
{
"offset": "24",
"name": [
"Spannung PWM 2.1",
{
"-lang": "en",
"#text": "Voltage / GFA 2.1"
}
],
"bitSize": "32",
"factor": "0.000001",
"unit": " V"
},
{
"offset": "28",
"name": [
"Spannung PWM 2.2",
{
"-lang": "en",
"#text": "Voltage / GFA 2.2"
}
],
"bitSize": "32",
"factor": "0.000001",
"unit": " V"
},
{
"offset": "32",
"name": [
"Durchflusssensor",
{
"-lang": "en",
"#text": "Frequency Sensor"
}
],
"bitSize": "15",
"unit": " l/h"
},
{
"offset": "35",
"name": [
"Relais 1",
{
"-lang": "en",
"#text": "Relay 1"
}
],
"bitSize": "8",
"unit": " %"
},
{
"offset": "36",
"name": [
"Relais 2",
{
"-lang": "en",
"#text": "Relay 2"
}
],
"bitSize": "8",
"unit": " %"
},
{
"offset": "37",
"name": [
"Relais 3",
{
"-lang": "en",
"#text": "Relay 3"
}
],
"bitSize": "8",
"unit": " %"
},
{
"offset": "38",
"name": [
"Relais 4",
{
"-lang": "en",
"#text": "Relay 4"
}
],
"bitSize": "8",
"unit": " %"
},
{
"offset": "39",
"name": [
"Relais 5",
{
"-lang": "en",
"#text": "Relay 5"
}
],
"bitSize": "8",
"unit": " %"
},
{
"offset": "40",
"name": [
"Ausgang 1",
{
"-lang": "en",
"#text": "Output 1"
}
],
"bitSize": "16",
"unit": " %"
},
{
"offset": "42",
"name": [
"Ausgang 2",
{
"-lang": "en",
"#text": "Output 2"
}
],
"bitSize": "16",
"unit": " %"
},
{
"offset": "44",
"name": [
"Ausgang 3",
{
"-lang": "en",
"#text": "Output 3"
}
],
"bitSize": "16",
"unit": " %"
},
{
"offset": "46",
"name": [
"Ausgang 4",
{
"-lang": "en",
"#text": "Output 4"
}
],
"bitSize": "16",
"unit": " %"
},
{
"offset": "50",
"name": [
"Lin Out",
{
"-lang": "en",
"#text": "Lin Out"
}
],
"bitSize": "16",
"factor": "0.1",
"unit": " %"
},
{
"offset": "60",
"name": [
"Fehler",
{
"-lang": "en",
"#text": "Error"
}
],
"bitSize": "16"
},
{
"offset": "60",
"name": ["Fehler S1"],
"bitSize": "1",
"bitPos": "1"
},
{
"offset": "60",
"name": ["Fehler S2"],
"bitSize": "1",
"bitPos": "2"
},
{
"offset": "60",
"name": ["Fehler S3"],
"bitSize": "1",
"bitPos": "3"
},
{
"offset": "60",
"name": ["Fehler S4"],
"bitSize": "1",
"bitPos": "4"
},
{
"offset": "60",
"name": ["Fehler S5"],
"bitSize": "1",
"bitPos": "5"
},
{
"offset": "60",
"name": ["Fehler S6"],
"bitSize": "1",
"bitPos": "6"
},
{
"offset": "60",
"name": ["Fehler S7"],
"bitSize": "1",
"bitPos": "7"
},
{
"offset": "60",
"name": ["Fehler S8"],
"bitSize": "1",
"bitPos": "8"
}
]
}
]
}
}
Hier noch der Docker Bauplan für das Image, basierend auf python-slim
die pyhton-scripts sind als persitenters Volume unter /app gemountet
Code: Alles auswählen
CMD ["bash"]
ENV PATH=/usr/local/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
ENV LANG=C.UTF-8
RUN set -eux; apt-get update; apt-get install -y --no-install-recommends ca-certificates netbase tzdata ; rm -rf /var/lib/apt/lists/*
ENV GPG_KEY=A035C8C19219BA821ECEA86B64E628F8D684696D
ENV PYTHON_VERSION=3.10.2
RUN set -eux; savedAptMark="$(apt-mark showmanual)"; apt-get update; apt-get install -y --no-install-recommends dpkg-dev gcc gnupg dirmngr libbluetooth-dev libbz2-dev libc6-dev libexpat1-dev libffi-dev libgdbm-dev liblzma-dev libncursesw5-dev libreadline-dev libsqlite3-dev libssl-dev make tk-dev uuid-dev wget xz-utils zlib1g-dev ; wget -O python.tar.xz "https://www.python.org/ftp/python/${PYTHON_VERSION%%[a-z]*}/Python-$PYTHON_VERSION.tar.xz"; wget -O python.tar.xz.asc "https://www.python.org/ftp/python/${PYTHON_VERSION%%[a-z]*}/Python-$PYTHON_VERSION.tar.xz.asc"; GNUPGHOME="$(mktemp -d)"; export GNUPGHOME; gpg --batch --keyserver hkps://keys.openpgp.org --recv-keys "$GPG_KEY"; gpg --batch --verify python.tar.xz.asc python.tar.xz; command -v gpgconf > /dev/null && gpgconf --kill all || :; rm -rf "$GNUPGHOME" python.tar.xz.asc; mkdir -p /usr/src/python; tar --extract --directory /usr/src/python --strip-components=1 --file python.tar.xz; rm python.tar.xz; cd /usr/src/python; gnuArch="$(dpkg-architecture --query DEB_BUILD_GNU_TYPE)"; ./configure --build="$gnuArch" --enable-loadable-sqlite-extensions --enable-optimizations --enable-option-checking=fatal --enable-shared --with-lto --with-system-expat --with-system-ffi --without-ensurepip ; nproc="$(nproc)"; make -j "$nproc" LDFLAGS="-Wl,--strip-all" ; make install; cd /; rm -rf /usr/src/python; find /usr/local -depth \( \( -type d -a \( -name test -o -name tests -o -name idle_test \) \) -o \( -type f -a \( -name '*.pyc' -o -name '*.pyo' -o -name '*.a' \) \) \) -exec rm -rf '{}' + ; ldconfig; apt-mark auto '.*' > /dev/null; apt-mark manual $savedAptMark; find /usr/local -type f -executable -not \( -name '*tkinter*' \) -exec ldd '{}' ';' | awk '/=>/ { print $(NF-1) }' | sort -u | xargs -r dpkg-query --search | cut -d: -f1 | sort -u | xargs -r apt-mark manual ; apt-get purge -y --auto-remove -o APT::AutoRemove::RecommendsImportant=false; rm -rf /var/lib/apt/lists/*; python3 --version
RUN set -eux; for src in idle3 pydoc3 python3 python3-config; do dst="$(echo "$src" | tr -d 3)"; [ -s "/usr/local/bin/$src" ]; [ ! -e "/usr/local/bin/$dst" ]; ln -svT "/usr/local/bin/$src" "/usr/local/bin/$dst"; done
ENV PYTHON_PIP_VERSION=21.2.4
ENV PYTHON_SETUPTOOLS_VERSION=58.1.0
ENV PYTHON_GET_PIP_URL=https://github.com/pypa/get-pip/raw/2caf84b14febcda8077e59e9b8a6ef9a680aa392/public/get-pip.py
ENV PYTHON_GET_PIP_SHA256=7c5239cea323cadae36083079a5ee6b2b3d56f25762a0c060d2867b89e5e06c5
RUN set -eux; savedAptMark="$(apt-mark showmanual)"; apt-get update; apt-get install -y --no-install-recommends wget; wget -O get-pip.py "$PYTHON_GET_PIP_URL"; echo "$PYTHON_GET_PIP_SHA256 *get-pip.py" | sha256sum -c -; apt-mark auto '.*' > /dev/null; [ -z "$savedAptMark" ] || apt-mark manual $savedAptMark > /dev/null; apt-get purge -y --auto-remove -o APT::AutoRemove::RecommendsImportant=false; rm -rf /var/lib/apt/lists/*; python get-pip.py --disable-pip-version-check --no-cache-dir "pip==$PYTHON_PIP_VERSION" "setuptools==$PYTHON_SETUPTOOLS_VERSION" ; pip --version; find /usr/local -depth \( \( -type d -a \( -name test -o -name tests -o -name idle_test \) \) -o \( -type f -a \( -name '*.pyc' -o -name '*.pyo' \) \) \) -exec rm -rf '{}' + ; rm -f get-pip.py
CMD ["python3"]
VOLUME [/app]
WORKDIR /app
RUN pip install pyserial paho-mqtt
ENTRYPOINT ["/bin/sh" "-c" "python3 /app/resol-vbus-python/resol3mqtt.py\"]"]