diff --git a/CHANGELOG.md b/CHANGELOG.md index 16fbc11..0a2e6e0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,8 +1,20 @@ # Changelog -### __[v2.5.2]__ - 01.08.2020 + +### __[v2.5.2]__ - unreleased +##### Added +- fhemCmd-Plugin: New plugin fhemCmd to execute commands in FHEM home automation. [#457](https://github.com/Schrolli91/BOSWatch/pull/457) +- Add field "ricFuncChar" in data structure for POC messages as a combination of "ric" and "functionChar" for using in RegEx filter. [#459](https://github.com/Schrolli91/BOSWatch/pull/459) +##### Changed +- Divera Plugin: Add individual alarms for FMS, ZVEI and POC. [#451](https://github.com/Schrolli91/BOSWatch/pull/451) +- install.sh: local git repo available at /opt/boswatch (or at your own path). Updates easier with `git pull` in /opt/boswatch. [#452](https://github.com/Schrolli91/BOSWatch/pull/452) +##### Deprecated +##### Removed ##### Fixed +- install.sh: old version of mysql-connector-python removed; add new via pip [#452](https://github.com/Schrolli91/BOSWatch/pull/452) [#445](https://github.com/Schrolli91/BOSWatch/issues/445) - GPIOcontrol plugin: Fixing indentation errors. [#465](https://github.com/Schrolli91/BOSWatch/pull/465) +##### Security + ### __[v2.5.1]__ - 28.04.2020 ##### Added diff --git a/README.md b/README.md index 4f4ed23..829bfdc 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ The software was developed using the Multimon-NG code, a function in the real op ### Fast support on https://bwcc.boswatch.de (Mattermost) -##### Work on BOSWatch 3 has started +### BW3 in first testing phase - see: https://github.com/BOSWatch/BW3-Core |Branch|Code Qualität|CI-Build| |---|---|---| diff --git a/config/config.template.ini b/config/config.template.ini index af5da01..8d2a9a4 100644 --- a/config/config.template.ini +++ b/config/config.template.ini @@ -182,6 +182,7 @@ yowsup = 0 hue = 0 Divera = 0 gpiocontrol = 0 +fhemCmd = 0 # for developing - template-module template = 0 @@ -494,17 +495,23 @@ SubD = poc_title = %DESCR%: %MSG% poc_text = %DATE% %TIME% - %DESCR%: %MSG% +# poc_ric is alarm-RIC in divera +poc_ric = %DESCR% # Section for ZVEI # default prio for all ZVEI - except you specify it different zvei_prio = true zvei_title = Alarm: %ZVEI% zvei_text = %DATE% %TIME%: %ZVEI% +# zvei_id working at 6 characters or more; later name of alarm-RIC in divera +zvei_id = %DESCR% # Section for FMS fms_prio = true fms_title = FMS: %FMS% fms_text = %DATE% %TIME%: %FMS%%BR%Status: %STATUS% - Direction: %DIRT% - TSI: %TSI% %LPAR%%DESCR%%RPAR% +# fms_vehicle is alarm-RIC in divera +fms_vehicle = %DESCR% [gpiocontrol] @@ -519,6 +526,26 @@ triggertime = 180 #POC Rics that trigger PIN (empty: allow all, separator ",") activerics = 1234567,1234568 +[fhemCmd] +# choose one of "http", "https" or "telnet" +protocol = http + +# servername or IP address +server = 192.168.0.1 + +# numeric port +port = 8083 + +# username if required +username = dummyUser + +# password if required +password = dummyPassword + +# desired command to execute +commandFMS = set SteckdoseSchlafzimmerEinsatz on-for-timer 90 +commandZVEI = +commandPOC = ##################### ##### Not ready yet # diff --git a/includes/decoders/poc.py b/includes/decoders/poc.py index e5b9b6d..bf906fd 100644 --- a/includes/decoders/poc.py +++ b/includes/decoders/poc.py @@ -163,6 +163,7 @@ def decode(freq, decoded): data["lat"] = lat # Add function as character a-d to dataset data["functionChar"] = data["function"].replace("1", "a").replace("2", "b").replace("3", "c").replace("4", "d") + data["ricFuncChar"] = data["ric"] + data["functionChar"] logging.info("POCSAG%s: %s %s %s ", data["bitrate"], data["ric"], data["function"], data["msg"]) diff --git a/includes/globalVars.py b/includes/globalVars.py index 05a1808..526ead2 100644 --- a/includes/globalVars.py +++ b/includes/globalVars.py @@ -9,9 +9,10 @@ Global variables """ # version info -versionNr = "2.5.1" -branch = "master" -buildDate = "28.04.2020" +versionNr = "2.5.2" +branch = "dev" +buildDate = "unreleased" + # Global variables config = 0 diff --git a/install.sh b/install.sh old mode 100644 new mode 100755 index 5470b24..0335aee --- a/install.sh +++ b/install.sh @@ -6,17 +6,17 @@ function exitcodefunction { module=$3 if [ $errorcode -ne "0" ]; then - echo "Action: $action on $module failed." >> $boswatchpath/install/setup_log.txt - echo "Exitcode: $errorcode" >> $boswatchpath/install/setup_log.txt + echo "Action: $action on $module failed." >> $boswatch_install_path/setup_log.txt + echo "Exitcode: $errorcode" >> $boswatch_install_path/setup_log.txt echo "" echo "Action: $action on $module failed." echo "Exitcode: $errorcode" echo "" echo " -> If you want to open an issue at https://github.com/Schrolli91/BOSWatch/issues" - echo " please post the logfile, located at $boswatchpath/install/setup_log.txt" + echo " please post the logfile, located at $boswatch_install_path/setup_log.txt" exit 1 else - echo "Action: $action on $module ok." >> $boswatchpath/install/setup_log.txt + echo "Action: $action on $module ok." >> $boswatch_install_path/setup_log.txt fi } @@ -44,9 +44,11 @@ echo "Caution, script does not install a webserver with PHP and MySQL" echo "So you have to make up manually if you want to use MySQL support" boswatchpath=/opt/boswatch +boswatch_install_path=/opt/boswatch_install reboot=false didBackup=false +# Checking for Backup # check for old version (for the old ones...) if [ -f $boswatchpath/BOSWatch/boswatch.py ]; then echo "Old installation found!" @@ -67,6 +69,7 @@ if [ -f $boswatchpath/boswatch.py ]; then didBackup=true fi +# Check for Flags in command line for (( i=1; i<=$#; i=$i+2 )); do t=$((i + 1)) eval arg=\$$i @@ -87,125 +90,115 @@ for (( i=1; i<=$#; i=$i+2 )); do esac done +# Create default paths mkdir -p $boswatchpath -mkdir -p $boswatchpath/install +mkdir -p $boswatch_install_path echo "" +# Update of computer tput cup 13 15 -echo "[ 1/10] [#---------]" +echo "[ 1/9] [#--------]" tput cup 15 5 echo "-> make an apt-get update................" -apt-get update -y > $boswatchpath/install/setup_log.txt 2>&1 +apt-get update -y > $boswatch_install_path/setup_log.txt 2>&1 +# download software tput cup 13 15 -echo "[ 2/10] [##--------]" +echo "[ 2/9] [##-------]" tput cup 15 5 echo "-> download GIT and other stuff.........." -apt-get -y install git cmake build-essential libusb-1.0 qt4-qmake qt4-default libpulse-dev libx11-dev sox >> $boswatchpath/install/setup_log.txt 2>&1 +apt-get -y install git cmake build-essential libusb-1.0 qt4-qmake qt4-default libpulse-dev libx11-dev sox python-pip >> $boswatch_install_path/setup_log.txt 2>&1 exitcodefunction $? download stuff +# download BOSWatch via git tput cup 13 15 -echo "[ 3/10] [###-------]" -tput cup 15 5 -echo "-> download rtl_fm......................" -cd $boswatchpath/install -git clone https://github.com/Schrolli91/rtl-sdr.git >> $boswatchpath/install/setup_log.txt 2>&1 -exitcodefunction $? git-clone rtl-sdr -cd rtl-sdr/ - -tput cup 13 15 -echo "[ 4/10] [####------]" -tput cup 15 5 -echo "-> compile rtl_fm......................" -mkdir -p build && cd build -cmake ../ -DINSTALL_UDEV_RULES=ON >> $boswatchpath/install/setup_log.txt 2>&1 -exitcodefunction $? cmake rtl-sdr - -make >> $boswatchpath/install/setup_log.txt 2>&1 -exitcodefunction $? make rtl-sdr - -make install >> $boswatchpath/install/setup_log.txt 2>&1 -exitcodefunction $? make-install rtl-sdr - -ldconfig >> $boswatchpath/install/setup_log.txt 2>&1 -exitcodefunction $? ldconfig rtl-sdr - - - -tput cup 13 15 -echo "[ 5/10] [#####-----]" -tput cup 15 5 -echo "-> download multimon-ng................" -cd $boswatchpath/install -git clone https://github.com/Schrolli91/multimon-ng.git multimonNG >> $boswatchpath/install/setup_log.txt 2>&1 -exitcodefunction $? git-clone multimonNG - - -cd $boswatchpath/install/multimonNG/ - -tput cup 13 15 -echo "[ 6/10] [######----]" -tput cup 15 5 -echo "-> compile multimon-ng................." -mkdir -p build -cd build -qmake ../multimon-ng.pro >> $boswatchpath/install/setup_log.txt 2>&1 -exitcodefunction $? qmake multimonNG - -make >> $boswatchpath/install/setup_log.txt 2>&1 -exitcodefunction $? make multimonNG - - -make install >> $boswatchpath/install/setup_log.txt 2>&1 -exitcodefunction $? qmakeinstall multimonNG - - -tput cup 13 15 -echo "[ 7/10] [#######---]" -tput cup 15 5 -echo "-> download MySQL connector for Python." -cd $boswatchpath/install -wget "http://dev.mysql.com/get/Downloads/Connector-Python/mysql-connector-python-1.0.9.tar.gz/from/http://cdn.mysql.com/" -O mysql-connector.tar >> $boswatchpath/install/setup_log.txt 2>&1 -exitcodefunction $? download mysql-connector - -tar xfv mysql-connector.tar >> $boswatchpath/install/setup_log.txt 2>&1 -exitcodefunction $? untar mysql-connector - -cd $boswatchpath/install/mysql-connector-python* - -tput cup 13 15 -echo "[ 8/10] [########--]" -tput cup 15 5 -echo "-> install MySQL connector for Python.." -chmod +x ./setup.py -./setup.py install >> $boswatchpath/install/setup_log.txt 2>&1 -exitcodefunction $? setup mysql-connector - - -tput cup 13 15 -echo "[ 9/10] [#########-]" +echo "[ 3/9] [###------]" tput cup 15 5 echo "-> download BOSWatch..................." cd $boswatchpath/ case $branch in - "dev") git clone -b develop https://github.com/Schrolli91/BOSWatch >> $boswatchpath/install/setup_log.txt 2>&1 && \ + "dev") git clone -b develop https://github.com/Schrolli91/BOSWatch . >> $boswatch_install_path/setup_log.txt 2>&1 && \ exitcodefunction $? git-clone BOSWatch-develop ;; - "beta") git clone -b beta https://github.com/Schrolli91/BOSWatch >> $boswatchpath/install/setup_log.txt 2>&1 && \ - exitcodefunction $? git-clone BOSWatch-beta ;; - *) git clone -b master https://github.com/Schrolli91/BOSWatch >> $boswatchpath/install/setup_log.txt 2>&1 && \ + *) git clone -b master https://github.com/Schrolli91/BOSWatch . >> $boswatch_install_path/setup_log.txt 2>&1 && \ exitcodefunction $? git-clone BOSWatch ;; esac +# Download RTL-SDR tput cup 13 15 -echo "[10/10] [##########]" +echo "[ 4/9] [####-----]" +tput cup 15 5 +echo "-> download rtl_fm......................" +cd $boswatch_install_path +git clone https://github.com/Schrolli91/rtl-sdr.git >> $boswatch_install_path/setup_log.txt 2>&1 +exitcodefunction $? git-clone rtl-sdr +cd rtl-sdr/ + +# Compie RTL-FM +tput cup 13 15 +echo "[ 5/9] [#####----]" +tput cup 15 5 +echo "-> compile rtl_fm......................" +mkdir -p build && cd build +cmake ../ -DINSTALL_UDEV_RULES=ON >> $boswatch_install_path/setup_log.txt 2>&1 +exitcodefunction $? cmake rtl-sdr + +make >> $boswatch_install_path/setup_log.txt 2>&1 +exitcodefunction $? make rtl-sdr + +make install >> $boswatch_install_path/setup_log.txt 2>&1 +exitcodefunction $? make-install rtl-sdr + +ldconfig >> $boswatch_install_path/setup_log.txt 2>&1 +exitcodefunction $? ldconfig rtl-sdr + + +# Download Multimon-NG +tput cup 13 15 +echo "[ 6/9] [######---]" +tput cup 15 5 +echo "-> download multimon-ng................" +cd $boswatch_install_path +git clone https://github.com/Schrolli91/multimon-ng.git multimonNG >> $boswatch_install_path/setup_log.txt 2>&1 +exitcodefunction $? git-clone multimonNG + +cd $boswatch_install_path/multimonNG/ + +# Compile Multimon-NG +tput cup 13 15 +echo "[ 7/9] [#######--]" +tput cup 15 5 +echo "-> compile multimon-ng................." +mkdir -p build +cd build +qmake ../multimon-ng.pro >> $boswatch_install_path/setup_log.txt 2>&1 +exitcodefunction $? qmake multimonNG + +make >> $boswatch_install_path/setup_log.txt 2>&1 +exitcodefunction $? make multimonNG + +make install >> $boswatch_install_path/setup_log.txt 2>&1 +exitcodefunction $? qmakeinstall multimonNG + +# Download & Install MySQL-Connector for Python via pip +tput cup 13 15 +echo "[ 8/9] [########-]" +tput cup 15 5 +echo "-> Download & Install MySQL connector for Python." +cd $boswatch_install_path +pip install mysql-connector-python >> $boswatch_install_path/setup_log.txt 2>&1 +exitcodefunction $? install mysql-connector + +# Blacklist DVB-Drivers +tput cup 13 15 +echo "[9/9] [#########]" tput cup 15 5 echo "-> configure..........................." cd $boswatchpath/ -chmod +x * echo $'# BOSWatch - blacklist the DVB drivers to avoid conflicts with the SDR driver\n blacklist dvb_usb_rtl28xxu \n blacklist rtl2830\n blacklist dvb_usb_v2\n blacklist dvb_core' >> /etc/modprobe.d/boswatch_blacklist_sdr.conf +# Installation is ready tput cup 17 1 echo "BOSWatch is now installed in $boswatchpath/" echo "Installation ready!" @@ -219,11 +212,8 @@ tput cnorm # cleanup mkdir $boswatchpath/log/install -p -mv $boswatchpath/install/setup_log.txt $boswatchpath/log/install/ -rm $boswatchpath/install/ -R - -mv $boswatchpath/BOSWatch/* $boswatchpath/ -rm $boswatchpath/BOSWatch -R +mv $boswatch_install_path/setup_log.txt $boswatchpath/log/install/ +rm $boswatch_install_path/ -R #copy the template config to run boswatch cp $boswatchpath/config/config.template.ini $boswatchpath/config/config.ini diff --git a/plugins/Divera/Divera.py b/plugins/Divera/Divera.py index 60092ed..5811f8f 100644 --- a/plugins/Divera/Divera.py +++ b/plugins/Divera/Divera.py @@ -16,6 +16,24 @@ from includes import globalVars # Global variables from includes.helper import configHandler from includes.helper import wildcardHandler +def isSignal(poc_id): + """ + @type poc_id: string + @param poc_id: POCSAG Ric + + @requires: Configuration has to be set in the config.ini + + @return: True if the Ric is Signal, other False + @exception: none + """ + # If RIC is Signal return True, else False + if globalVars.config.get("POC", "netIdent_ric"): + if poc_id in globalVars.config.get("POC", "netIdent_ric"): + logging.info("RIC %s is net ident", poc_id) + return True + else: + logging.info("RIC %s is no net ident", poc_id) + return False ## # @@ -61,6 +79,7 @@ def run(typ, freq, data): text = globalVars.config.get("Divera", "fms_text") title = globalVars.config.get("Divera", "fms_title") priority = globalVars.config.get("Divera", "fms_prio") + vehicle = globalVars.config.get("Divera", "fms_vehicle") elif typ == "ZVEI": # @@ -68,25 +87,33 @@ def run(typ, freq, data): # text = globalVars.config.get("Divera", "zvei_text") title = globalVars.config.get("Divera", "zvei_title") - priority = globalVars.config.get("Divera","zvei_std_prio") + priority = globalVars.config.get("Divera","zvei_prio") + zvei_id = globalVars.config.get("Divera","zvei_id") elif typ == "POC": - # - # building message for POC - # - if data["function"] == '1': - priority = globalVars.config.get("Divera", "SubA") - elif data["function"] == '2': - priority = globalVars.config.get("Divera", "SubB") - elif data["function"] == '3': - priority = globalVars.config.get("Divera", "SubC") - elif data["function"] == '4': - priority = globalVars.config.get("Divera", "SubD") + if isSignal(data["ric"]): + + logging.debug("RIC is net ident") + return else: - priority = '' + # + # building message for POC + # + if data["function"] == '1': + priority = globalVars.config.get("Divera", "SubA") + elif data["function"] == '2': + priority = globalVars.config.get("Divera", "SubB") + elif data["function"] == '3': + priority = globalVars.config.get("Divera", "SubC") + elif data["function"] == '4': + priority = globalVars.config.get("Divera", "SubD") + else: + priority = '' - text = globalVars.config.get("Divera", "poc_text") - title = globalVars.config.get("Divera", "poc_title") + text = globalVars.config.get("Divera", "poc_text") + title = globalVars.config.get("Divera", "poc_title") + ric = globalVars.config.get("Divera", "poc_ric") + else: logging.warning("Invalid type: %s", typ) @@ -97,30 +124,91 @@ def run(typ, freq, data): # Divera-Request # logging.debug("send Divera for %s", typ) - - # replace the wildcards - text = wildcardHandler.replaceWildcards(text, data) - title = wildcardHandler.replaceWildcards(title, data) - # Logging data to send + # Replace wildcards & Logging data to send + title = wildcardHandler.replaceWildcards(title, data) logging.debug("Title : %s", title) + text = wildcardHandler.replaceWildcards(text, data) logging.debug("Text : %s", text) - logging.debug("Priority: %s", priority) + + if typ == "FMS": + vehicle = wildcardHandler.replaceWildcards(vehicle, data) + logging.debug("Vehicle : %s", vehicle) + elif typ == "POC": + ric = wildcardHandler.replaceWildcards(ric, data) + logging.debug("RIC : %s", ric) + elif typ == "ZVEI": + zvei_id = wildcardHandler.replaceWildcards(zvei_id, data) + logging.debug("ZVEI_ID : %s", zvei_id) + else: + logging.info("No wildcards to replace and no Typ selected!") # check priority value if (priority != 'false') and (priority != 'true'): logging.info("No Priority set for type '%s'! Skipping Divera-Alarm!", typ) return - # start the connection - conn = httplib.HTTPSConnection("www.divera247.com:443") - conn.request("GET", "/api/alarm", - urllib.urlencode({ - "accesskey": globalVars.config.get("Divera", "accesskey"), - "title": title, - "text": text, - "priority": priority, - })) + # Check FMS + if typ == "FMS": + if (vehicle == ''): + logging.info("No Vehicle set!") + + # Check POC + elif typ == "POC": + if (ric == ''): + logging.info("No RIC set!") + + # Check ZVEI + elif typ == "ZVEI": + if (zvei_id == ''): + logging.info("No ZVEI_ID set!") + + else: + logging.info("No ZVEI, FMS or POC alarm") + + # start connection to Divera + if typ == "FMS": + # start the connection FMS + conn = httplib.HTTPSConnection("www.divera247.com:443") + conn.request("GET", "/api/fms", + urllib.urlencode({ + "accesskey": globalVars.config.get("Divera", "accesskey"), + "vehicle_ric": vehicle, + "status_id": data["status"], + "status_note": data["directionText"], + "title": title, + "text": text, + "priority": priority, + })) + + elif typ == "ZVEI": + # start connection ZVEI; zvei_id in Divera is alarm-RIC! + conn = httplib.HTTPSConnection("www.divera247.com:443") + conn.request("GET", "/api/alarm", + urllib.urlencode({ + "accesskey": globalVars.config.get("Divera", "accesskey"), + "title": title, + "ric": zvei_id, + "text": text, + "priority": priority, + })) + + elif typ == "POC": + # start connection POC + conn = httplib.HTTPSConnection("www.divera247.com:443") + conn.request("GET", "/api/alarm", + urllib.urlencode({ + "accesskey": globalVars.config.get("Divera", "accesskey"), + "title": title, + "ric": ric, + "text": text, + "priority": priority, + })) + + + else: + loggin.debug("No Type is set", exc_info=True) + return except: logging.error("cannot send Divera request") diff --git a/plugins/README.md b/plugins/README.md index be512ad..bafee9e 100644 --- a/plugins/README.md +++ b/plugins/README.md @@ -203,6 +203,7 @@ In the data map are the folowing informations: - ric - function - functionChar +- ricFuncChar - msg - bitrate - description diff --git a/plugins/fhemCmd/fhemCmd.py b/plugins/fhemCmd/fhemCmd.py new file mode 100644 index 0000000..2658e2e --- /dev/null +++ b/plugins/fhemCmd/fhemCmd.py @@ -0,0 +1,113 @@ +#!/usr/bin/python +# -*- coding: UTF-8 -*- + +""" +Plugin for calling FHEM home automation + +@author: Marco Schotthöfer + +@requires: python-fhem (pip install fhem) +""" + +# +# Imports +# +import logging # Global logger +from includes import globalVars # Global variables + +# Helper function, uncomment to use +#from includes.helper import timeHandler +#from includes.helper import wildcardHandler +from includes.helper import configHandler +from includes.helper import wildcardHandler +import fhem + +## +# +# onLoad (init) function of plugin +# will be called one time by the pluginLoader on start +# +def onLoad(): + """ + While loading the plugins by pluginLoader.loadPlugins() + this onLoad() routine is called one time for initialize the plugin + + @requires: nothing + + @return: nothing + @exception: Exception if init has an fatal error so that the plugin couldn't work + + """ + try: + ########## User onLoad CODE ########## + pass + ########## User onLoad CODE ########## + except: + logging.error("unknown error") + logging.debug("unknown error", exc_info=True) + raise + +## +# +# Main function of plugin +# will be called by the alarmHandler +# +def run(typ,freq,data): + """ + This function is the implementation of the Plugin. + + If necessary the configuration hast to be set in the config.ini. + + @type typ: string (FMS|ZVEI|POC) + @param typ: Typ of the dataset + @type data: map of data (structure see readme.md in plugin folder) + @param data: Contains the parameter for dispatch + @type freq: string + @keyword freq: frequency of the SDR Stick + + @requires: If necessary the configuration hast to be set in the config.ini. + + @return: nothing + @exception: nothing, make sure this function will never thrown an exception + """ + try: + if configHandler.checkConfig("fhemCmd"): #read and debug the config (let empty if no config used) + + protocol = globalVars.config.get("fhemCmd", "protocol") + logging.debug("protocol: %s", protocol) + + server = globalVars.config.get("fhemCmd", "server") + logging.debug("server: %s", server) + + port = globalVars.config.get("fhemCmd", "port") + logging.debug("port: %s", port) + + username = globalVars.config.get("fhemCmd", "username") + logging.debug("username: %s", username) + + password = globalVars.config.get("fhemCmd", "password") + logging.debug("password: %s", password) + + ########## User Plugin CODE ########## + if typ == "FMS": + fhemCommand = globalVars.config.get("fhemCmd", "commandFMS") + elif typ == "ZVEI": + fhemCommand = globalVars.config.get("fhemCmd", "commandZVEI") + elif typ == "POC": + fhemCommand = globalVars.config.get("fhemCmd", "commandPOC") + else: + logging.warning("Invalid Typ: %s", typ) + return False + + fhemCommand = wildcardHandler.replaceWildcards(fhemCommand, data) + logging.debug("fhemCommand: %s", fhemCommand) + + fh = fhem.Fhem(server=server, protocol=protocol, port=port, username=username, password=password) + + fh.send_cmd(fhemCommand) + del fh + ########## User Plugin CODE ########## + + except: + logging.error("unknown error") + logging.debug("unknown error", exc_info=True) diff --git a/plugins/fhemCmd/requirements.txt b/plugins/fhemCmd/requirements.txt new file mode 100644 index 0000000..a852eb5 --- /dev/null +++ b/plugins/fhemCmd/requirements.txt @@ -0,0 +1 @@ +fhem