├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md └── workflows │ ├── AWS │ ├── CollectJson.yaml │ ├── aws_s3_policy.json │ ├── delete_old_objects.py │ └── lambda_function.py │ ├── BuildAndDeploy.yml │ └── generateJSON.sh ├── .gitignore ├── .gitpod.yml ├── .vscode ├── extensions.json └── settings.json ├── CODE_OF_CONDUCT.md ├── ChangeLog.md ├── FHEM ├── fhem.SoilMoisture.RaspberryPieZeroW.cfg ├── fhem.cfg ├── ftui_garden_sprinkle.html └── ftui_template_garden_sprinkle.html ├── LICENSE ├── README.md ├── circuit ├── ESP_PumpControl_PCB_v1.1.PDF ├── ESP_PumpControl_SCH_v1.1.PDF └── ESP_PumpControl_v1.1.zip ├── data └── web │ ├── Javascript.js │ ├── Style.css │ ├── baseconfig.html │ ├── baseconfig.js │ ├── handlefiles.html │ ├── handlefiles.js │ ├── index.html │ ├── navi.html │ ├── navi.js │ ├── reboot.html │ ├── sensorconfig.html │ ├── sensorconfig.js │ ├── status.html │ ├── status.js │ ├── update.html │ ├── update_response.html │ ├── valveconfig.html │ ├── valveconfig.js │ └── valvefunctions.js ├── esp_files ├── esp32 │ └── gpio.js └── esp8266 │ └── gpio.js ├── include └── _Release.h ├── partitions.csv ├── platformio.ini ├── scripts ├── build_flags.py └── prepareDataDir.py └── src ├── CommonLibs.h ├── MyWebServer.cpp ├── MyWebServer.h ├── README ├── TB6612.cpp ├── TB6612.h ├── baseconfig.cpp ├── baseconfig.h ├── handleFiles.cpp ├── handleFiles.h ├── main.cpp ├── mqtt.cpp ├── mqtt.h ├── sensor.cpp ├── sensor.h ├── valve.cpp ├── valve.h ├── valveHardware.cpp ├── valveHardware.h ├── valveStructure.cpp └── valveStructure.h /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Desktop (please complete the following information):** 27 | - OS: [e.g. iOS] 28 | - Browser [e.g. chrome, safari] 29 | - Version [e.g. 22] 30 | 31 | **Smartphone (please complete the following information):** 32 | - Device: [e.g. iPhone6] 33 | - OS: [e.g. iOS8.1] 34 | - Browser [e.g. stock browser, safari] 35 | - Version [e.g. 22] 36 | 37 | **Additional context** 38 | Add any other context about the problem here. 39 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.github/workflows/AWS/CollectJson.yaml: -------------------------------------------------------------------------------- 1 | AWSTemplateFormatVersion: '2010-09-09' 2 | Transform: 'AWS::Serverless-2016-10-31' 3 | Description: collecting all json files to "releases.json" 4 | Resources: 5 | CollectJson: 6 | Type: 'AWS::Serverless::Function' 7 | Properties: 8 | Handler: lambda_function.lambda_handler 9 | Runtime: python3.7 10 | CodeUri: . 11 | Description: collecting all json files to "releases.json" 12 | MemorySize: 128 13 | Timeout: 10 14 | Role: 'arn:aws:iam::328668909373:role/service-role/MyRole_ReadS3' 15 | Events: 16 | BucketEvent1: 17 | Type: S3 18 | Properties: 19 | Bucket: 20 | Ref: Bucket1 21 | Events: 22 | - 's3:ObjectCreated:Put' 23 | Filter: 24 | S3Key: 25 | Rules: 26 | - Name: suffix 27 | Value: .json 28 | Tags: 29 | 'lambda-console:blueprint': s3-get-object-python 30 | Bucket1: 31 | Type: 'AWS::S3::Bucket' 32 | -------------------------------------------------------------------------------- /.github/workflows/AWS/aws_s3_policy.json: -------------------------------------------------------------------------------- 1 | { 2 | "Version": "2012-10-17", 3 | "Statement": [ 4 | { 5 | "Sid": "VisualEditor0", 6 | "Effect": "Allow", 7 | "Action": [ 8 | "s3:PutObject", 9 | "s3:GetObject", 10 | "s3:ListBucket", 11 | "s3:DeleteObject", 12 | "s3:PutObjectAcl" 13 | ], 14 | "Resource": [ 15 | "arn:aws:s3:::tfa-releases", 16 | "arn:aws:s3:::tfa-releases/*" 17 | ] 18 | } 19 | ] 20 | } -------------------------------------------------------------------------------- /.github/workflows/AWS/delete_old_objects.py: -------------------------------------------------------------------------------- 1 | import boto3 2 | import re 3 | 4 | s3 = boto3.client('s3') 5 | 6 | #get_last_modified = lambda obj: int(obj['LastModified'].strftime('%Y%m%d%H%M%S')) 7 | save_versions = 5 8 | 9 | def delete_old_objects(bucketname, targetpath): 10 | resp = s3.list_objects(Bucket=bucketname,Prefix=targetpath + "/") 11 | if 'Contents' in resp: 12 | objs = resp['Contents'] 13 | 14 | # schreibe die Releasenummer/Github Action Nummer in ein eigenes Feld für Sortierung 15 | for o in resp['Contents']: 16 | try: 17 | found = re.search('[\d]+\.[\d]+\-(\d+)\.[DEV|PRE|PROD]', o['Key']).group(1) 18 | except AttributeError: 19 | found = '0' # apply your error handling 20 | 21 | o['MyNum'] = found 22 | 23 | # sortieren nach Dateidatum, fehlerhaft bei wiederherstellung aus Backup! 24 | #files = sorted(objs, key=get_last_modified, reverse=True) 25 | 26 | # sortieren nach Releasenummer 27 | files = sorted(objs, key=lambda i:i['MyNum'], reverse=True) 28 | 29 | # hilfstabellen 30 | hashtable = {} 31 | hashtable = {'ESP8266': {'DEV':[],'PRE':[],'PROD':[]}, 32 | 'ESP32': {'DEV':[],'PRE':[],'PROD':[]} 33 | } 34 | 35 | for key in files: 36 | key['save']=0 37 | 38 | for arch in hashtable.keys(): 39 | for stage in hashtable[arch].keys(): 40 | if key['Key'].find("."+arch+".") > 0 and key['Key'].find("."+stage+".") > 0 : 41 | if len(hashtable[arch][stage]) <= ((save_versions * 2) - 1) : # 4 Binaries + 4 Json (incl. dem jetzt kommenden) 42 | key['save']=1 43 | hashtable[arch][stage].append(key) 44 | #print ("Save Object #"+str(len(hashtable[arch][stage]))+" for "+arch+"/"+stage+": " + key['Key']) 45 | 46 | if key['save']==0: 47 | #print("Delete this Object: " + key['Key']) 48 | s3.delete_object(Bucket=bucketname, Key=key['Key']) 49 | 50 | -------------------------------------------------------------------------------- /.github/workflows/AWS/lambda_function.py: -------------------------------------------------------------------------------- 1 | from delete_old_objects import delete_old_objects 2 | import urllib.parse 3 | import re 4 | import boto3 5 | 6 | ressource = boto3.resource('s3') 7 | 8 | def lambda_handler(event, context): 9 | 10 | releaseJSON = ".+/releases.*\.json" 11 | 12 | # Name des Buckets in dem das S3 Put Event aufgetreten ist 13 | bucketname = event['Records'][0]['s3']['bucket']['name'] 14 | # Name der Datei die das Event ausgelöst hat 15 | key = urllib.parse.unquote_plus(event['Records'][0]['s3']['object']['key'], encoding='utf-8') 16 | 17 | # Prevent endless loop due writing releases.json 18 | #if key.endswith(releaseJSON): 19 | if re.match(releaseJSON, key): 20 | return "Do nothing because an releases json files was touching" 21 | 22 | bucket = ressource.Bucket(bucketname) 23 | path = key.split("/") 24 | path.pop() 25 | targetPath = "/".join(path) 26 | 27 | # delete old objects 28 | delete_old_objects(bucketname, targetPath) 29 | 30 | #exit("MyExit") 31 | CreateReleasesJson(bucketname, targetPath, "ESP32", 5) 32 | CreateReleasesJson(bucketname, targetPath, "ESP8266", 0) 33 | 34 | 35 | ############################################ 36 | # Parameters: 37 | # arch : Architecture, passend der Nomenklatur im Dateinamen -> ESP32|ESP8266 38 | # history: Anzahl der Releases die im releases.json aufgenommen werden sollen, 0 = nur die aktuelle Version 39 | ############################################ 40 | def CreateReleasesJson(BucketName, TargetPath, arch, history): 41 | s3 = boto3.client('s3') 42 | 43 | 44 | resp = s3.list_objects(Bucket=BucketName,Prefix=TargetPath + "/") 45 | if 'Contents' in resp: 46 | objs = resp['Contents'] 47 | 48 | # schreibe die Releasenummer/Github Action Nummer in ein eigenes Feld für Sortierung 49 | for o in resp['Contents']: 50 | try: 51 | found = re.search('[\d]+\.[\d]+\-(\d+)\.[DEV|PRE|PROD]', o['Key']).group(1) 52 | except AttributeError: 53 | found = '0' # apply your error handling 54 | 55 | o['MyNum'] = found 56 | 57 | # sortieren nach Releasenummer 58 | files = sorted(objs, key=lambda i:i['MyNum'], reverse=True) 59 | 60 | # hilfstabellen 61 | hashtable = {} 62 | hashtable = {'DEV':[],'PRE':[],'PROD':[] } 63 | myJSON = "[ \n" 64 | 65 | for key in files: 66 | for stage in hashtable.keys(): 67 | if re.match(".+\.json", key['Key']) and key['Key'].find("."+arch+".") > 0 and key['Key'].find("."+stage+".") > 0 : 68 | if len(hashtable[stage]) <= history : 69 | hashtable[stage].append(key) 70 | file_content = ressource.Object(BucketName, key['Key']).get()['Body'].read().decode('utf-8') 71 | myJSON += file_content + "," 72 | #print ("Save Object #"+str(len(hashtable[stage]))+" for "+arch+"/"+stage+": " + key['Key']) 73 | 74 | myJSON = myJSON[:-1] 75 | myJSON += "]" 76 | 77 | # Put JSON to S3 78 | # old releases file was deleted by "delete_old_objects" function 79 | object = ressource.Object(BucketName, TargetPath + "/releases_" + arch + ".json") 80 | object.put(Body=myJSON) 81 | 82 | # Enable public Access 83 | object_acl = ressource.ObjectAcl(BucketName, TargetPath + "/releases_" + arch + ".json") 84 | response = object_acl.put(ACL='public-read') 85 | 86 | -------------------------------------------------------------------------------- /.github/workflows/BuildAndDeploy.yml: -------------------------------------------------------------------------------- 1 | # https://github.com/marketplace/actions/test-compile-for-arduino 2 | # https://github.com/marketplace/actions/test-compile-for-arduino#multiple-boards-with-parameter-using-the-script-directly 3 | # https://github.com/actions/upload-release-asset/issues/17 4 | # https://github.com/dh1tw/remoteAudio/blob/master/.github/workflows/build.yml 5 | 6 | name: Build&Deploy 7 | on: 8 | push: 9 | branches: 10 | - master 11 | - prelive 12 | - development 13 | paths: 14 | - '**.cpp' 15 | - '**.h' 16 | - '**.yml' 17 | 18 | jobs: 19 | 20 | build: 21 | name: BuildAndDeploy-${{ matrix.variant }} 22 | runs-on: ubuntu-latest 23 | env: 24 | REPOSITORY: ${{ github.event.repository.name }} 25 | 26 | strategy: 27 | matrix: 28 | variant: 29 | - firmware_ESP8266 30 | 31 | include: 32 | - variant: firmware_ESP8266 33 | architecture: ESP8266 34 | 35 | steps: 36 | - name: checkout repository 37 | uses: actions/checkout@v2 38 | 39 | - name: Set up Python 40 | uses: actions/setup-python@main 41 | with: 42 | python-version: '3.x' 43 | 44 | - name: Install dependencies 45 | run: | 46 | pip install -U platformio 47 | pip install --upgrade pip 48 | 49 | - name: Run PlatformIO 50 | run: | 51 | platformio run -e ${{ matrix.variant }} 52 | platformio run --target buildfs -e ${{ matrix.variant }} 53 | 54 | - name: Display generated files 55 | run: | 56 | ls -R .pio/build/${{ matrix.variant }}/ 57 | 58 | - if: endsWith(github.ref, 'master') 59 | name: Set Environment Variable "PRODUCTION" 60 | run: echo "ENV_STAGE=PROD" >> $GITHUB_ENV 61 | 62 | - if: endsWith(github.ref, 'prelive') 63 | name: Set Environment Variable "PreLive" 64 | run: echo "ENV_STAGE=PRE" >> $GITHUB_ENV 65 | 66 | - if: endsWith(github.ref, 'development') 67 | name: Set Environment Variable "Development" 68 | run: echo "ENV_STAGE=DEV" >> $GITHUB_ENV 69 | 70 | # Script um JSON Datei zu erstellen 71 | - name: Schreibe Json File 72 | env: 73 | ENV_ARCH: ${{ matrix.architecture }} 74 | ENV_REPOSITORYNAME: ${{ env.REPOSITORY }} 75 | ENV_BINARYPATH: .pio/build/${{ matrix.variant }}/ 76 | ENV_RELEASEPATH: "release" 77 | ENV_ARTIFACTPATH: "artifacts" 78 | ENV_SUBVERSION: ${{ github.run_number }} 79 | ENV_RELEASEFILE: include/_Release.h 80 | run: | 81 | mkdir -p release artifacts 82 | chmod +x .github/workflows/generateJSON.sh 83 | .github/workflows/generateJSON.sh 84 | 85 | - name: Upload firmware artifacts 86 | uses: actions/upload-artifact@main 87 | with: 88 | name: "${{ matrix.variant }}.zip" 89 | path: artifacts/*.bin 90 | 91 | - name: Upload to AWS S3 92 | uses: jakejarvis/s3-sync-action@master 93 | with: 94 | args: '--acl public-read --follow-symlinks' 95 | env: 96 | AWS_S3_BUCKET: 'tfa-releases' 97 | AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} 98 | AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} 99 | AWS_REGION: 'eu-central-1' # optional: defaults to us-east-1 100 | SOURCE_DIR: 'release' # optional: defaults to entire repository 101 | DEST_DIR: ${{ env.REPOSITORY }} 102 | 103 | create_Release: 104 | needs: [build] 105 | 106 | runs-on: ubuntu-latest 107 | if: endsWith(github.ref, 'master') || endsWith(github.ref, 'prelive') 108 | 109 | env: 110 | GITHUB_REPOSITORY: ${{ github.event.repository.name }} 111 | 112 | steps: 113 | - name: Checkout Repository 114 | uses: actions/checkout@v2 115 | 116 | - name: Download all Artifacts 117 | uses: actions/download-artifact@main 118 | with: 119 | path: ./release 120 | 121 | - name: Display files 122 | run: | 123 | ls -R ./release 124 | 125 | - name: set Environment Variables 126 | id: set_env_var 127 | run: | 128 | VERSION=$(sed 's/[^0-9|.]//g' include/_Release.h) # zb. 2.4.2 129 | if [ ${{ github.ref }} == 'refs/heads/master' ]; then IS_PRE='false'; else IS_PRE='true'; fi 130 | if [ ${{ github.ref }} == 'refs/heads/master' ]; then POSTFIX='' ; else POSTFIX='PRE'; fi 131 | echo "version=${VERSION}" >> "$GITHUB_OUTPUT" 132 | echo "IS_PRERELEASE=${IS_PRE}" >> "$GITHUB_OUTPUT" 133 | echo "RELEASENAME_POSTFIX=${POSTFIX}" >> "$GITHUB_OUTPUT" 134 | RELEASEBODY=$(awk -v RS='Release ' '/'$VERSION':(.*)/ {print $0}' ChangeLog.md) 135 | echo "${RELEASEBODY}" > CHANGELOG.md 136 | 137 | 138 | - name: Create Release 139 | id: create_release 140 | uses: actions/create-release@v1 141 | env: 142 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # This token is provided by Actions, you do not need to create your own token 143 | with: 144 | tag_name: v.${{ steps.set_env_var.outputs.version }}-${{ steps.set_env_var.outputs.RELEASENAME_POSTFIX }}-${{ github.run_id }} 145 | release_name: Release ${{ steps.set_env_var.outputs.version }} ${{ steps.set_env_var.outputs.RELEASENAME_POSTFIX }} 146 | body_path: CHANGELOG.md 147 | draft: false 148 | prerelease: ${{ steps.set_env_var.outputs.IS_PRERELEASE }} 149 | 150 | - name: Upload Release Assets 151 | id: upload-release-assets 152 | uses: dwenegar/upload-release-assets@v1 153 | env: 154 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 155 | with: 156 | release_id: ${{ steps.create_release.outputs.id }} 157 | assets_path: ./release 158 | 159 | - name: Upload Changelog artifact 160 | uses: actions/upload-artifact@main 161 | with: 162 | name: CHANGELOG.md 163 | path: CHANGELOG.md 164 | -------------------------------------------------------------------------------- /.github/workflows/generateJSON.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | #Parameter 4 | 5 | REPOSITORYNAME="$1" # PumpControl 6 | SUBVERSION="$2" # Unique ID -> GITHUB_RUN_NUMBER 7 | STAGE="$3" # PROD|PRE|DEV 8 | BINARYPATH="$4" # Path of binaryFiles 9 | RELEASEPATH="$5" # Path of Destination, BIN and JSON Files 10 | RELEASEFILE="$6" # Path of ReleaseFile, contains versionnumber 11 | ARCH="$7" # Archtitcture, ESP8266|ESP32 12 | ARTIFACTPATH="$8" # Path of all Artifacts 13 | 14 | readonly NC='\033[0m' # No Color 15 | readonly RED='\033[0;31m' 16 | readonly GREEN='\033[0;32m' 17 | readonly YELLOW='\033[1;33m' 18 | readonly BLUE='\033[0;34m' 19 | 20 | # 21 | # Get env parameter with higher priority, which enables the script to run directly in a step 22 | # 23 | if [[ -n $ENV_BINARYPATH ]]; then BINARYPATH=$ENV_BINARYPATH; fi 24 | if [[ -n $ENV_SUBVERSION ]]; then SUBVERSION=$ENV_SUBVERSION; fi 25 | if [[ -n $ENV_STAGE ]]; then STAGE=$ENV_STAGE; fi 26 | if [[ -n $ENV_REPOSITORYNAME ]]; then REPOSITORYNAME=$ENV_REPOSITORYNAME; fi 27 | if [[ -n $ENV_RELEASEFILE ]]; then RELEASEFILE=$ENV_RELEASEFILE; fi 28 | if [[ -n $ENV_ARCH ]]; then ARCH=$ENV_ARCH; fi 29 | if [[ -n $ENV_RELEASEPATH ]]; then RELEASEPATH=$ENV_RELEASEPATH; fi 30 | if [[ -n $ENV_ARTIFACTPATH ]]; then ARTIFACTPATH=$ENV_ARTIFACTPATH; fi 31 | 32 | # 33 | # Echo input parameter 34 | # 35 | echo -e "\n\n"$YELLOW"Echo input parameter"$NC 36 | echo REPOSITORYNAME=$REPOSITORYNAME 37 | echo SUBVERSION=$SUBVERSION 38 | echo STAGE=$STAGE 39 | echo BINARYPATH=$BINARYPATH 40 | echo RELEASEPATH=$RELEASEPATH 41 | echo ARCHITECTURE=$ARCH 42 | echo RELEASEFILE=$RELEASEFILE 43 | echo ARTIFACTPATH=$ARTIFACTPATH 44 | 45 | if [[ ! -d $BINARYPATH ]]; then 46 | echo -e "\n\n"$RED"Binarypath $BINARYPATH not found\n"$NC 47 | exit 48 | fi 49 | 50 | if [[ ! -f $RELEASEFILE ]]; then 51 | echo -e "\n\n"$RED"Releasefile $RELEASEFILE not found\n"$NC 52 | exit 53 | fi 54 | 55 | if [[ ! -d $RELEASEPATH ]]; then 56 | mkdir -p $RELEASEPATH 57 | fi 58 | 59 | if [[ ! -d $ARTIFACTPATH ]]; then 60 | mkdir -p $ARTIFACTPATH 61 | fi 62 | 63 | VERSION=`sed 's/[^0-9|.]//g' $RELEASEFILE` # 2.4.2 64 | NUMBER=`sed 's/[^0-9]//g' $RELEASEFILE` # 242 65 | 66 | VERSION1=`echo $VERSION | cut -d '.' -f1` # 2 67 | VERSION2=`echo $VERSION | cut -d '.' -f2` # 4 68 | VERSION3=`echo $VERSION | cut -d '.' -f3` # 2 69 | 70 | let NUMBER=$(printf "%d%d%d%d" $VERSION1 $VERSION2 $VERSION3 $SUBVERSION) 71 | 72 | for FILE in `find $BINARYPATH/ -name firmware.bin` 73 | do 74 | 75 | FILENAME=${FILE%.*} 76 | FILEEXT=${FILE/*./} 77 | 78 | BINARYFILENAME=$(basename $FILENAME"."$ARCH".v"$VERSION"-"$SUBVERSION"."$STAGE) 79 | BINARYFILENAME=$(basename $FILENAME"."$ARCH".v"$VERSION"-"$SUBVERSION"."$STAGE) 80 | DOWNLOADURL="http://tfa-releases.s3-website.eu-central-1.amazonaws.com/"$REPOSITORYNAME"/"$BINARYFILENAME"."$FILEEXT 81 | 82 | JSON=' { 83 | "name":"Release '$VERSION'-'$STAGE'", 84 | "version":"'$VERSION'", 85 | "subversion":'$SUBVERSION', 86 | "number":'$NUMBER', 87 | "stage":"'$STAGE'", 88 | "arch":"'$ARCH'", 89 | "download-url":"'$DOWNLOADURL'" 90 | }' 91 | 92 | echo -e "\n\n"$GREEN"Echo json string"$NC 93 | echo $JSON 94 | 95 | echo $JSON > $RELEASEPATH/$BINARYFILENAME".json" 96 | cp $FILE $RELEASEPATH/$BINARYFILENAME"."$FILEEXT 97 | 98 | done 99 | 100 | # process the rest ob binaries into ARTIFACTPATH 101 | for FILE in `find $BINARYPATH/ -name *.bin` 102 | do 103 | FILENAME=${FILE%.*} 104 | FILEEXT=${FILE/*./} 105 | 106 | BINARYFILENAME=$(basename $FILENAME"."$ARCH".v"$VERSION"-"$SUBVERSION"."$STAGE) 107 | cp $FILE $ARTIFACTPATH/$BINARYFILENAME"."$FILEEXT 108 | 109 | done 110 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .pio 2 | .vscode 3 | data/web/esp 4 | -------------------------------------------------------------------------------- /.gitpod.yml: -------------------------------------------------------------------------------- 1 | tasks: 2 | - command: pip3 install -U platformio && platformio run && platformio run --target buildfs 3 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | // See http://go.microsoft.com/fwlink/?LinkId=827846 3 | // for the documentation about the extensions.json format 4 | "recommendations": [ 5 | "platformio.platformio-ide" 6 | ], 7 | "unwantedRecommendations": [ 8 | "ms-vscode.cpptools-extension-pack" 9 | ] 10 | } 11 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "cmake.configureOnOpen": false, 3 | "files.associations": { 4 | "array": "cpp", 5 | "atomic": "cpp", 6 | "bit": "cpp", 7 | "*.tcc": "cpp", 8 | "bitset": "cpp", 9 | "cctype": "cpp", 10 | "chrono": "cpp", 11 | "clocale": "cpp", 12 | "cmath": "cpp", 13 | "compare": "cpp", 14 | "concepts": "cpp", 15 | "cstdarg": "cpp", 16 | "cstddef": "cpp", 17 | "cstdint": "cpp", 18 | "cstdio": "cpp", 19 | "cstdlib": "cpp", 20 | "cstring": "cpp", 21 | "ctime": "cpp", 22 | "cwchar": "cpp", 23 | "cwctype": "cpp", 24 | "deque": "cpp", 25 | "list": "cpp", 26 | "map": "cpp", 27 | "set": "cpp", 28 | "unordered_map": "cpp", 29 | "vector": "cpp", 30 | "exception": "cpp", 31 | "algorithm": "cpp", 32 | "functional": "cpp", 33 | "iterator": "cpp", 34 | "memory": "cpp", 35 | "memory_resource": "cpp", 36 | "numeric": "cpp", 37 | "optional": "cpp", 38 | "random": "cpp", 39 | "ratio": "cpp", 40 | "regex": "cpp", 41 | "string": "cpp", 42 | "string_view": "cpp", 43 | "system_error": "cpp", 44 | "tuple": "cpp", 45 | "type_traits": "cpp", 46 | "utility": "cpp", 47 | "initializer_list": "cpp", 48 | "iosfwd": "cpp", 49 | "istream": "cpp", 50 | "limits": "cpp", 51 | "new": "cpp", 52 | "ostream": "cpp", 53 | "ranges": "cpp", 54 | "sstream": "cpp", 55 | "stdexcept": "cpp", 56 | "streambuf": "cpp", 57 | "cinttypes": "cpp", 58 | "typeinfo": "cpp", 59 | "variant": "cpp", 60 | "condition_variable": "cpp", 61 | "csignal": "cpp", 62 | "unordered_set": "cpp", 63 | "fstream": "cpp", 64 | "future": "cpp", 65 | "iomanip": "cpp", 66 | "iostream": "cpp", 67 | "mutex": "cpp", 68 | "stop_token": "cpp", 69 | "thread": "cpp" 70 | } 71 | } -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, sex characteristics, gender identity and expression, 9 | level of experience, education, socio-economic status, nationality, personal 10 | appearance, race, religion, or sexual identity and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at tobias.faust@gmx.net. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 72 | 73 | [homepage]: https://www.contributor-covenant.org 74 | 75 | For answers to common questions about this code of conduct, see 76 | https://www.contributor-covenant.org/faq 77 | -------------------------------------------------------------------------------- /ChangeLog.md: -------------------------------------------------------------------------------- 1 | Release 3.0.0: 2 | - +++++++ This Repo is only for ESP8266, for ESP32 use the other Repo +++++++ 3 | - change to Async Webserver 4 | - change ArduinoJson version 5.x to 6.x 5 | - change platform from Arduino-IDE to PlatformIO 6 | - reduce memory usage for handling json configs 7 | - derive custom mqtt handling from parent mqtt class 8 | - extract html-code into separate html-files, so everyone can customize his own instance 9 | - interact with Web-frontend by json 10 | - enable update filesystem at updatepage 11 | - saving configs by global upload function, no extra saveconfigfile functions anymore 12 | - data partition is now larger 13 | - change deprecated SPIFFS to LittleFS 14 | - move all webfiles (css,js) to FS 15 | - create new Webpage to maintain the FS-files, editing json registers on-the-fly is now possible 16 | - add ADS1115 moisture functionality 17 | - add page loader 18 | - Remove 1Wire, ADS1115, Oled, Relation Support to save heap memory 19 | 20 | Release 2.5.3: 21 | - Bug: Oled Typ selectionbox in GUI will now list correct 22 | - Bug: Reverse function works now as expected 23 | - ReScan i2cBus or 1WireBus will now change textcolor to red 24 | - Bug: ADS1115 Scan checks now if ADC is present to prevent ESP freezes 25 | - Feature: 1Wire ReScan at statuspage 26 | - Bug: bistable valve works again due an definition error in 2.5.2 27 | - Bug: ESP8266: reduce autoupdater to last version due RAM limitations by changing json file definition 28 | - Bug: valve sometimes doesnt switch back to OFF status after on-for-timer 29 | - Bug: in ValveConfig Ajax Change of enabling/disabling of a valve doesn recognized 30 | - Bug: 1Wire switches only one port 31 | 32 | Release 2.5.2: 33 | - changing 1wire pin without reboot now possible 34 | - show 1wire devices and controller at status page 35 | - Feature: OLED Type SSD1306 and SH1106 available 36 | - add ADS1115 ADC for a better level measurement (page sensor) 37 | - debugmode handling optimized (-> use it only from BaseConfig) 38 | 39 | Release 2.5.1: 40 | - Fix Firmware OTA Support for ESP32 41 | - Fix deleting stored WiFi Credentials 42 | - fixes some ESP32 bugs 43 | - refreshing i2c Seach in Basisconfig now working 44 | 45 | Release 2.5.0: 46 | - Feature: Add OneWire DS2408 hardware support 47 | - Feature: Add a keepalive message via MQTT, configurable at BasisConfig: 48 | - Feature: Add configurable DebugMode at BaseConfig 49 | - Feature: push out memory and rssi values via mqtt together with keepalive message if Debugmode >= 4 50 | - Feature: support for ESP32, still without OTA Update and Wifi Credential deletion 51 | 52 | Release 2.4.5: 53 | - Feature: deletion of WiFi credentials now possible 54 | - Feature: ESP Hostname now the configured Devicename 55 | - Bug: WIFI Mode forces to STATION-Mode, some devices has been ran in unsecured STA+AP Mode 56 | - Bug: security issue: dont show debug output of WiFi Connection (password has been shown) 57 | - Feature: valve reverse mode: enable if your valve act on LOW instead of ON 58 | - Feature: AutoOff: possibility to setup a security AutoOff 59 | - Bug: count of Threads now push out if an on-for-timer has been expired 60 | 61 | Release 2.4.4: 62 | - Feature: Issue #9: MQTT Client ID now configurable 63 | - MQTT now reconnect after DeviceName has been changed 64 | - MQTT LastWillTopic as device status configured by topic "/state [Offline|Online]" 65 | - Publish Release and Version after MQTT Connect by topic "/version" 66 | - Bugfix: Nullpointer to Hardwaredevice if multiple hardware devices are defined 67 | 68 | Release 2.4.3: 69 | - Bugfixing Automatische Releaseverteilung 70 | - Überarbeitung Github Workflow mit automatischer Releaseerstellung 71 | 72 | Release 2.4.2: 73 | - Bugfixing des TB6612 Handlings 74 | 75 | Release 2.4.1: 76 | - Added TB6612 Support 77 | - Added automatic Release Update 78 | 79 | Release 2.3: 80 | - solved some bugfixes 81 | - MQTT Commands setstate [on|off] now available 82 | 83 | Release 2.2: 84 | - some bugs resolved 85 | 86 | Release 2.1: 87 | Final Release! complete redesign, Its now easier to understand and add more functionality. 88 | Wiki is now up-to-date based on Release 2.1 89 | 90 | New functionality: 91 | - i2c Motordriver support for bistable valves 92 | - ESP8266 motordriverboard for bistable valves 93 | - Relations now added for complex garden 94 | - external and analog sensor support 95 | - changing valve status by Web-UI added 96 | 97 | Release 2.0: 98 | 1st Pre Release with completely new refactored code by completely class based. 99 | Tested with valves at PCF8575 and GPIO, LevelSensor HCSR04 and OLED 1306 100 | 101 | Release 1.0: 102 | this is the finale on first release. Works with optional OLED, optional LevelSensor. 103 | Supports valves at OnBoard GPIO Pins and PCF8574 Extender i2c-Shield 104 | -------------------------------------------------------------------------------- /FHEM/fhem.SoilMoisture.RaspberryPieZeroW.cfg: -------------------------------------------------------------------------------- 1 | attr global userattr cmdIcon devStateIcon devStateIcon:textField-long devStateStyle icon sortby webCmd webCmdLabel:textField-long widgetOverride 2 | attr global autoload_undefined_devices 1 3 | attr global autosave 0 4 | attr global logfile ./log/fhem-%Y-%m.log 5 | attr global modpath . 6 | 7 | 8 | attr global statefile ./log/fhem.save 9 | attr global updateInBackground 1 10 | attr global verbose 3 11 | 12 | define WEB FHEMWEB 8083 global 13 | 14 | # Fake FileLog entry, to access the fhem log from FHEMWEB 15 | define Logfile FileLog ./log/fhem-%Y-%m.log fakelog 16 | 17 | define autocreate autocreate 18 | attr autocreate filelog ./log/%NAME-%Y.log 19 | 20 | define eventTypes eventTypes ./log/eventTypes.txt 21 | 22 | # Disable this to avoid looking for new USB devices on startup 23 | define mqtt MQTT 192.168.10.10:1883 24 | 25 | define BF_TopfTreppe XiaomiBTLESens C4:7C:8D:64:3E:E1 26 | attr BF_TopfTreppe model flowerSens 27 | attr BF_TopfTreppe room XiaomiBTLESens 28 | attr BF_TopfTreppe stateFormat moisture 29 | 30 | define BF_WanneVorgarten XiaomiBTLESens C4:7C:8D:64:42:F5 31 | attr BF_WanneVorgarten model flowerSens 32 | attr BF_WanneVorgarten room XiaomiBTLESens 33 | attr BF_WanneVorgarten stateFormat moisture 34 | 35 | define allowed allowed 36 | 37 | define MQTT_BF_TopfTreppe MQTT_BRIDGE BF_TopfTreppe 38 | attr MQTT_BF_TopfTreppe IODev mqtt 39 | attr MQTT_BF_TopfTreppe publishReading_battery Garden/SoilMoisture/BF_TopfTreppe/battery 40 | attr MQTT_BF_TopfTreppe publishReading_batteryPercent Garden/SoilMoisture/BF_TopfTreppe/batteryLevel 41 | attr MQTT_BF_TopfTreppe publishReading_fertility Garden/SoilMoisture/BF_TopfTreppe/fertility 42 | attr MQTT_BF_TopfTreppe publishReading_lux Garden/SoilMoisture/BF_TopfTreppe/lux 43 | attr MQTT_BF_TopfTreppe publishReading_moisture Garden/SoilMoisture/BF_TopfTreppe/moisture 44 | attr MQTT_BF_TopfTreppe publishReading_temperature Garden/SoilMoisture/BF_TopfTreppe/temperature 45 | attr MQTT_BF_TopfTreppe retain 0 46 | attr MQTT_BF_TopfTreppe room XiaomiBTLESens 47 | 48 | define MQTT_BF_WanneVorgarten MQTT_BRIDGE BF_WanneVorgarten 49 | attr MQTT_BF_WanneVorgarten IODev mqtt 50 | attr MQTT_BF_WanneVorgarten publishReading_battery Garden/SoilMoisture/BF_WanneVorgarten/battery 51 | attr MQTT_BF_WanneVorgarten publishReading_batteryPercent Garden/SoilMoisture/BF_WanneVorgarten/batteryLevel 52 | attr MQTT_BF_WanneVorgarten publishReading_fertility Garden/SoilMoisture/BF_WanneVorgarten/fertility 53 | attr MQTT_BF_WanneVorgarten publishReading_lux Garden/SoilMoisture/BF_WanneVorgarten/lux 54 | attr MQTT_BF_WanneVorgarten publishReading_moisture Garden/SoilMoisture/BF_WanneVorgarten/moisture 55 | attr MQTT_BF_WanneVorgarten publishReading_temperature Garden/SoilMoisture/BF_WanneVorgarten/temperature 56 | attr MQTT_BF_WanneVorgarten retain 0 57 | attr MQTT_BF_WanneVorgarten room XiaomiBTLESens -------------------------------------------------------------------------------- /FHEM/fhem.cfg: -------------------------------------------------------------------------------- 1 | defmod PumpControl MQTT_DEVICE 2 | attr PumpControl DbLogExclude transmission-state,valve.*,Distance,WaterLevel:300 3 | attr PumpControl IODev mqtt 4 | attr PumpControl publishSet_VG-Hauptabsperrung-on-for-timer PumpControl/vorgarten/on-for-timer 5 | attr PumpControl publishSet_VG-Treppe-on-for-timer PumpControl_Treppe/Treppe/on-for-timer 6 | attr PumpControl publishSet_VG-Wanne-on-for-timer PumpControl_Treppe/Wanne/on-for-timer 7 | attr PumpControl room Aussen 8 | attr PumpControl stateFormat WaterLevel% 9 | attr PumpControl subscribeReading_Distance PumpControl/raw 10 | attr PumpControl subscribeReading_Frischwassernutzung PumpControl/3WegeVentil/state 11 | attr PumpControl subscribeReading_Frischwasserventil PumpControl/Frischwasserventil/state 12 | attr PumpControl subscribeReading_Threads PumpControl/Threads 13 | attr PumpControl subscribeReading_Ventil-VG-Hauptabsperrung PumpControl/vorgarten/state 14 | attr PumpControl subscribeReading_Ventil-VG-Treppe PumpControl_Treppe/Treppe/state 15 | attr PumpControl subscribeReading_Ventil-VG-Wanne PumpControl_Treppe/Wanne/state 16 | attr PumpControl subscribeReading_WaterLevel PumpControl/level 17 | 18 | defmod BF_VG_TopfTreppe MQTT_DEVICE 19 | attr BF_VG_TopfTreppe DbLogExclude transmission-state 20 | attr BF_VG_TopfTreppe IODev mqtt 21 | attr BF_VG_TopfTreppe room Aussen 22 | attr BF_VG_TopfTreppe stateFormat moisture 23 | attr BF_VG_TopfTreppe subscribeReading_batteryLevel Garden/SoilMoisture/BF_TopfTreppe/batteryLevel 24 | attr BF_VG_TopfTreppe subscribeReading_fertility Garden/SoilMoisture/BF_TopfTreppe/fertility 25 | attr BF_VG_TopfTreppe subscribeReading_moisture Garden/SoilMoisture/BF_TopfTreppe/moisture 26 | 27 | defmod BF_VG_Wanne MQTT_DEVICE 28 | attr BF_VG_Wanne DbLogExclude transmission-state 29 | attr BF_VG_Wanne IODev mqtt 30 | attr BF_VG_Wanne room Aussen 31 | attr BF_VG_Wanne stateFormat moisture 32 | attr BF_VG_Wanne subscribeReading_batteryLevel Garden/SoilMoisture/BF_WanneVorgarten/batteryLevel 33 | attr BF_VG_Wanne subscribeReading_fertility Garden/SoilMoisture/BF_WanneVorgarten/fertility 34 | attr BF_VG_Wanne subscribeReading_moisture Garden/SoilMoisture/BF_WanneVorgarten/moisture 35 | 36 | defmod AgroWeather PROPLANTA Berlin 37 | attr AgroWeather DbLogExclude .* 38 | attr AgroWeather INTERVAL 14400 39 | attr AgroWeather forecastDays 14 40 | attr AgroWeather room Aussen 41 | attr AgroWeather stateFormat Verdunstungsgrad: fc0_evapor / Regen heute: fc0_rain mm 42 | 43 | defmod DOIF_Bew_VG_TopfTreppe DOIF (([$SELF:1-time] ne "00:00" and [[$SELF:1-time]]) and [$SELF:2-treshold-moisture,0] > 0 and [?BF_VG_TopfTreppe:moisture]<=[$SELF:2-treshold-moisture,99] and [?AgroWeather:fc0_rain]<[$SELF:2-treshold-rain,99])\ 44 | (set PumpControl VG-Treppe-on-for-timer [$SELF:0-duration])\ 45 | DOELSEIF\ 46 | (([$SELF:1-time] ne "00:00" and [[$SELF:1-time]]) and [$SELF:2-treshold-moisture,0] == 0 and [?AgroWeather:fc0_rain]<[$SELF:2-treshold-rain,99])\ 47 | (set PumpControl VG-Treppe-on-for-timer [$SELF:0-duration])\ 48 | DOELSEIF\ 49 | (([$SELF:1-time] eq "00:00" and [$SELF:2-treshold-moisture,0] > 0 and [BF_VG_TopfTreppe:moisture]<=[$SELF:2-treshold-moisture,99] and [?AgroWeather:fc0_rain]<[$SELF:2-treshold-rain,99]))\ 50 | (set PumpControl VG-Treppe-on-for-timer [$SELF:0-duration])\ 51 | DOELSEIF\ 52 | ([$SELF:1-time] ne "00:00" and [$SELF:2-time] ne "00:00" and ([[$SELF:1-time]-[$SELF:2-time]]) and [$SELF:2-treshold-moisture,0] > 0 and [BF_VG_TopfTreppe:moisture]<=[$SELF:2-treshold-moisture,99])\ 53 | (set PumpControl VG-Treppe-on-for-timer [$SELF:0-duration])\ 54 | DOELSEIF\ 55 | ([$SELF:1-treshold-moisture,0] > 0 and [BF_VG_TopfTreppe:moisture]<=[$SELF:1-treshold-moisture,99] )\ 56 | (set PumpControl VG-Treppe-on-for-timer [$SELF:0-duration])\ 57 | DOELSE 58 | attr DOIF_Bew_VG_TopfTreppe DbLogExclude .* 59 | attr DOIF_Bew_VG_TopfTreppe comment cmd_1: Zeitpunkt und SollSensorFeuchte gesetzt\ cmd_2: Zeitpunkt gesetzt, keine SollSensorFeuchte\ cmd_3: Zeitpunkt nicht gesetzt, SollSensorFeuchte gesetzt\ cmd_4: Zeitraum und SollSensorFeuchte gesetzt\ cmd_5: Mindestfeuchte gesetzt, ohne Beruecksichtigung der Regenwarscheinlichkeit\ cmd_6: mache nix 60 | attr DOIF_Bew_VG_TopfTreppe do always 61 | attr DOIF_Bew_VG_TopfTreppe readingList 1-treshold-moisture 2-treshold-moisture 2-treshold-rain 0-duration 1-time 2-time 62 | attr DOIF_Bew_VG_TopfTreppe room Aussen 63 | attr DOIF_Bew_VG_TopfTreppe verbose 3 64 | 65 | setstate DOIF_Bew_VG_TopfTreppe 2020-04-27 13:24:28 0-duration 180 66 | setstate DOIF_Bew_VG_TopfTreppe 2020-04-27 12:42:59 1-time 00:00 67 | setstate DOIF_Bew_VG_TopfTreppe 2020-04-27 18:57:15 1-treshold-moisture 36 68 | setstate DOIF_Bew_VG_TopfTreppe 2020-04-27 12:42:59 2-time 00:00 69 | setstate DOIF_Bew_VG_TopfTreppe 2020-04-27 18:57:20 2-treshold-moisture 45 70 | setstate DOIF_Bew_VG_TopfTreppe 2020-04-27 13:24:21 2-treshold-rain 4.0 71 | 72 | 73 | defmod DOIF_Bew_VG_Wanne DOIF (([$SELF:1-time] ne "00:00" and [[$SELF:1-time]]) and [$SELF:2-treshold-moisture,0] > 0 and [?BF_VG_Wanne:moisture]<=[$SELF:2-treshold-moisture,99] and [?AgroWeather:fc0_rain]<[$SELF:2-treshold-rain,99])\ 74 | (set PumpControl VG-Wanne-on-for-timer [$SELF:0-duration])\ 75 | DOELSEIF\ 76 | (([$SELF:1-time] ne "00:00" and [[$SELF:1-time]]) and [$SELF:2-treshold-moisture,0] == 0 and [?AgroWeather:fc0_rain]<[$SELF:2-treshold-rain,99])\ 77 | (set PumpControl VG-Wanne-on-for-timer [$SELF:0-duration])\ 78 | DOELSEIF\ 79 | (([$SELF:1-time] eq "00:00" and [$SELF:2-treshold-moisture,0] > 0 and [BF_VG_Wanne:moisture]<=[$SELF:2-treshold-moisture,99] and [?AgroWeather:fc0_rain]<[$SELF:2-treshold-rain,99]))\ 80 | (set PumpControl VG-Wanne-on-for-timer [$SELF:0-duration])\ 81 | DOELSEIF\ 82 | ([$SELF:1-time] ne "00:00" and [$SELF:2-time] ne "00:00" and ([[$SELF:1-time]-[$SELF:2-time]]) and [$SELF:2-treshold-moisture,0] > 0 and [BF_VG_Wanne:moisture]<=[$SELF:2-treshold-moisture,99])\ 83 | (set PumpControl VG-Wanne-on-for-timer [$SELF:0-duration])\ 84 | DOELSEIF\ 85 | ([$SELF:1-treshold-moisture,0] > 0 and [BF_VG_Wanne:moisture]<=[$SELF:1-treshold-moisture,99] )\ 86 | (set PumpControl VG-Wanne-on-for-timer [$SELF:0-duration])\ 87 | DOELSE 88 | attr DOIF_Bew_VG_Wanne DbLogExclude .* 89 | attr DOIF_Bew_VG_Wanne comment cmd_1: Zeitpunkt und SollSensorFeuchte gesetzt\ cmd_2: Zeitpunkt gesetzt, keine SollSensorFeuchte\ cmd_3: Zeitpunkt nicht gesetzt, SollSensorFeuchte gesetzt\ cmd_4: Zeitraum und SollSensorFeuchte gesetzt\ cmd_5: Mindestfeuchte gesetzt, ohne Beruecksichtigung der Regenwarscheinlichkeit\ cmd_6: mache nix 90 | attr DOIF_Bew_VG_Wanne do always 91 | attr DOIF_Bew_VG_Wanne readingList 1-treshold-moisture 2-treshold-moisture 2-treshold-rain 0-duration 1-time 2-time 92 | attr DOIF_Bew_VG_Wanne room Aussen 93 | attr DOIF_Bew_VG_Wanne verbose 3 94 | 95 | 96 | setstate DOIF_Bew_VG_Wanne 2020-04-27 13:38:04 0-duration 180 97 | setstate DOIF_Bew_VG_Wanne 2020-04-27 13:28:49 1-time 00:00 98 | setstate DOIF_Bew_VG_Wanne 2020-04-27 18:56:47 1-treshold-moisture 40 99 | setstate DOIF_Bew_VG_Wanne 2020-04-27 13:28:49 2-time 00:00 100 | setstate DOIF_Bew_VG_Wanne 2020-04-27 18:56:53 2-treshold-moisture 45 101 | setstate DOIF_Bew_VG_Wanne 2020-04-27 13:28:49 2-treshold-rain 3 102 | 103 | 104 | 105 | 106 | 107 | -------------------------------------------------------------------------------- /FHEM/ftui_garden_sprinkle.html: -------------------------------------------------------------------------------- 1 |
Bewässerung: Topf auf der Treppe
2 |
9 |
10 | 11 |
Bewässerung: Blumenwanne im Vorgarten
12 |
19 |
-------------------------------------------------------------------------------- /FHEM/ftui_template_garden_sprinkle.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
letztmalig:
4 |
5 |
On/Off: 7 |
8 |
11 |
12 |
13 |
14 |
Sensor: 16 |
17 |
20 |
21 |
22 |
23 |
24 |
Status
25 |
33 |
34 |
35 |
36 |
aktuelle Feuchte
37 |
40 |
41 |
42 |
43 |
Nährstoffgehalt
44 |
47 |
48 |
49 |
50 |
Automatik:
51 |
57 |
58 |
59 |
63 |
64 |
68 |
71 |
72 |
73 |
Settings
74 |
75 |
76 |
77 |
Bewässern um (0=Off)
78 |
87 |
88 |
89 |
90 |
oder Zeitraum bis (0=Off)
91 |
100 |
101 |
102 |
103 |
104 |
Sollfeuchte
105 |
113 |
114 |
115 |
116 |
bei erwartetem Niederschlag weniger als (in mm)
117 |
126 |
127 |
128 |
129 |
130 |
Mindestfeuchte
131 |
139 |
140 |
141 |
142 |
Bewässerungsdauer in sek
143 |
152 |
153 |
154 |
155 |
156 |
157 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Bewässerungssteuerung für ein Hauswasserwerk sowie automatisierter Umschaltung bei leerer Zisterne auf Trinkwasser für ESP8266 2 | 3 | >Die Entwicklung dieser Firmware für den ESP8266 wurde eingestellt. Eine Weiterentwicklung erfolgt nur noch für den Esp32. Hierzu bitte ins [ESP32_Pumpcontrol](https://github.com/tobiasfaust/ESP32_PumpControl) Repository wechseln. 4 | 5 |
6 | 7 | [![license](https://img.shields.io/badge/Licence-GNU%20v3.0-green)](https://github.com/desktop/desktop/blob/master/LICENSE) 8 |
9 | ![GitHub Workflow Status](https://img.shields.io/github/actions/workflow/status/tobiasfaust/ESP8266_PumpControl/BuildAndDeploy.yml?label=build%20Development&branch=development) 10 | ![GitHub Workflow Status](https://img.shields.io/github/actions/workflow/status/tobiasfaust/ESP8266_PumpControl/BuildAndDeploy.yml?label=build%20Prelive&branch=prelive) 11 | ![GitHub Workflow Status](https://img.shields.io/github/actions/workflow/status/tobiasfaust/ESP8266_PumpControl/BuildAndDeploy.yml?label=build%20Master&branch=master) 12 | 13 |
14 | 15 | ![ESP8266 Architecture](https://img.shields.io/badge/Architecture-ESP8266-blue) 16 | ![GitHub release (latest by date including pre-releases)](https://img.shields.io/github/v/release/tobiasfaust/ESP8266_PumpControl?include_prereleases&style=plastic) 17 | ![GitHub All Releases](https://img.shields.io/github/downloads/tobiasfaust/ESP8266_PumpControl/total?style=plastic) 18 | 19 | 20 | Beschreibung siehe Wiki: https://github.com/tobiasfaust/ESP8266_PumpControl/wiki 21 | -------------------------------------------------------------------------------- /circuit/ESP_PumpControl_PCB_v1.1.PDF: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tobiasfaust/ESP8266_PumpControl/cf3438cae94dd96d2e7a2c8c94fdbbd492e48315/circuit/ESP_PumpControl_PCB_v1.1.PDF -------------------------------------------------------------------------------- /circuit/ESP_PumpControl_SCH_v1.1.PDF: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tobiasfaust/ESP8266_PumpControl/cf3438cae94dd96d2e7a2c8c94fdbbd492e48315/circuit/ESP_PumpControl_SCH_v1.1.PDF -------------------------------------------------------------------------------- /circuit/ESP_PumpControl_v1.1.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tobiasfaust/ESP8266_PumpControl/cf3438cae94dd96d2e7a2c8c94fdbbd492e48315/circuit/ESP_PumpControl_v1.1.zip -------------------------------------------------------------------------------- /data/web/Javascript.js: -------------------------------------------------------------------------------- 1 | 2 | var timer; // ID of setTimout Timer -> setResponse 3 | 4 | /*############################################################ 5 | # 6 | 7 | # activate all radioselections after pageload to hide unnecessary elements 8 | # 9 | ############################################################*/ 10 | function handleRadioSelections() { 11 | var objects = document.querySelectorAll('input[type=radio]:checked'); 12 | for( var i=0; i< objects.length; i++) { 13 | objects[i].click(); 14 | } 15 | } 16 | 17 | /*############################################################ 18 | # 19 | # central function to initiate data fetch 20 | # 21 | ############################################################*/ 22 | 23 | function requestData(json, highlight, callbackFn) { 24 | const data = new URLSearchParams(); 25 | data.append('json', json); 26 | 27 | fetch('/ajax', { 28 | method: 'POST', 29 | headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, 30 | body: data 31 | }) 32 | .then (response => response.json()) 33 | .then (json => { handleJsonItems(json, highlight, callbackFn)}); 34 | } 35 | 36 | /*############################################################ 37 | # 38 | # definition of applying jsondata to html templates 39 | # 40 | ############################################################*/ 41 | function applyKey (_obj, _key, _val, counter, tplHierarchie, highlight) { 42 | if (_obj.id == _key || _obj.id == tplHierarchie +"."+ _key) { 43 | if (['SPAN', 'DIV', 'TD', 'DFN'].includes(_obj.tagName)) { 44 | if (highlight && _obj.classList.contains('ajaxchange')) { 45 | _obj.classList.add('highlightOn'); 46 | _obj.innerHTML = _val; 47 | } if (!highlight && _obj.classList.contains('ajaxchange')) { 48 | _obj.classList.remove('highlightOn'); 49 | _obj.innerHTML = _val; 50 | } else { 51 | _obj.innerHTML = _val; 52 | } 53 | } else if (_obj.tagName == 'INPUT' && ['checkbox','radio'].includes(_obj.type)) { 54 | if (_val == true) _obj.checked = true; 55 | } else if (_obj.tagName == 'OPTION') { 56 | if (_val == true) _obj.selected = true; 57 | } else { 58 | _obj.value = _val; 59 | } 60 | } else { 61 | // using parenet object 62 | _obj[_key] = _val; 63 | } 64 | } 65 | 66 | /*############################################################################ 67 | json -> der json teil der angewendet werden soll 68 | _tpl -> das documentFragment auf welches das json agewendet werden soll 69 | ObjID -> ggf eine ID im _tpl auf die "key" und "value" des jsons agewendet werden soll 70 | wenn diese undefinied ist, ist die ID = json[key] 71 | counter -> gesetzt, wenn innerhalb eines _tpl arrays die ID des Objektes hochgezählt wurde 72 | highlight -> wenn in einem objekt die klasse "ajaxchange" gesetzt ist, so wird die Klasse "highlightOn" angewendet 73 | #############################################################################*/ 74 | 75 | function applyKeys(json, _tpl, ObjID, counter, tplHierarchie, highlight) { 76 | for (var key in json) { 77 | if (Array.isArray(json[key])) { 78 | applyTemplate((json[key]), key, _tpl, tplHierarchie, highlight); 79 | } else if (typeof json[key] === 'object') { 80 | applyKeys(json[key], _tpl, key, counter, tplHierarchie, highlight); 81 | } else { 82 | try { 83 | var _obj, _objID 84 | if (ObjID) { 85 | // obj is parent object, key ist jetzt eigenschaft, nicht ID 86 | if (ObjID + "_" + counter == _tpl.firstElementChild.id) { 87 | //die firstChildID ist schon das Object 88 | _objID = _tpl.firstElementChild.id; 89 | } else { 90 | _objID = (tplHierarchie?tplHierarchie +".":"") + ObjID; 91 | } 92 | } else { 93 | // key ist die ID 94 | _objID = (tplHierarchie?tplHierarchie +".":"") + key 95 | } 96 | 97 | if (document.getElementById(_objID)) { 98 | // exists already in DOM 99 | _obj = document.getElementById(_objID); 100 | } else if (_tpl.getElementById(_objID + "_" + counter)) { 101 | 102 | _obj = _tpl.getElementById(_objID + "_" + counter); 103 | } else { 104 | _obj = _tpl.getElementById(_objID); 105 | } 106 | applyKey(_obj, key, json[key], counter, tplHierarchie, highlight); 107 | } catch(e) {} 108 | } 109 | } 110 | } 111 | 112 | function applyTemplate(TemplateJson, templateID, doc, tplHierarchie, highlight) { 113 | if (Array.isArray(TemplateJson)) { 114 | for (var i=0; i < TemplateJson.length; i++) { 115 | if(doc.getElementById(templateID)) { 116 | var _tpl = document.importNode(doc.getElementById(templateID).content, true); 117 | var _parentObj = doc.getElementById(templateID).parentNode; 118 | try { 119 | 120 | //adjust id of first element, often not included in querySelectorAll statement (example: ) 121 | // firstchild.id contains hierarchie of templates to keep unique id´s 122 | if (_tpl.firstElementChild.id) { 123 | _tpl.firstElementChild.id = (tplHierarchie?tplHierarchie +".":"") + _tpl.firstElementChild.id + "_" + [i]; 124 | } else { 125 | _tpl.firstElementChild.id = (tplHierarchie?tplHierarchie +".":"") + templateID + "_" + [i]; 126 | } 127 | 128 | //adjust all id´s 129 | const o = _tpl.querySelectorAll("*"); 130 | for (var j=0; j bool => true = OK; false = Error 181 | // s => String => text to show 182 | // *********************************** 183 | function setResponse(b, s) { 184 | try { 185 | // clear if previous timer still run 186 | clearTimeout(timer); 187 | } catch(e) {} 188 | 189 | try { 190 | var r = document.getElementById("response"); 191 | var t = 2000; 192 | if (!b) t = 5000; // show errors longer 193 | 194 | r.innerHTML = s; 195 | if (b) { r.className = "oktext"; } else {r.className = "errortext";} 196 | timer = setTimeout(function() {document.getElementById("response").innerHTML=""}, 2000); 197 | } catch(e) {} 198 | } 199 | 200 | /*############################################################ 201 | # 202 | # definition of creating selectionlists from input fields 203 | # querySelector -> select input fields to convert 204 | # jsonLists -> define multiple predefined lists to set as option as array 205 | # blacklist -> simple list of ports (numbers) to set as disabled option 206 | # 207 | # example: 208 | # CreateSelectionListFromInputField('input[type=number][id^=AllePorts], input[type=number][id^=GpioPin]', 209 | # [gpio, gpio_analog], gpio_disabled); 210 | ############################################################*/ 211 | function CreateSelectionListFromInputField(querySelector, jsonLists, blacklist) { 212 | var _parent, _select, _option, i, j, k; 213 | var objects = document.querySelectorAll(querySelector); 214 | for( j=0; j< objects.length; j++) { 215 | _parent = objects[j].parentNode; 216 | _select = document.createElement('select'); 217 | _select.id = objects[j].id; 218 | _select.name = objects[j].name; 219 | for ( k = 0; k < jsonLists.length; k += 1 ) { 220 | for ( i = 0; i < jsonLists[k].length; i += 1 ) { 221 | _option = document.createElement( 'option' ); 222 | var p,v; 223 | if (jsonLists[k][i] instanceof Object) { 224 | p = jsonLists[k][i].port; 225 | v = jsonLists[k][i].name; 226 | } else { 227 | p = v = jsonLists[k][i]; 228 | } 229 | _option.value = p; 230 | _option.text = v; 231 | if(objects[j].value == p) { _option.selected = true;} 232 | if(blacklist && blacklist.indexOf(p)>=0) { 233 | _option.disabled = true; 234 | } 235 | _select.add( _option ); 236 | } 237 | } 238 | _parent.replaceChild(_select, objects[j]) 239 | } 240 | } 241 | 242 | /*############################################################ 243 | # returns, if a element is visible or not 244 | ############################################################*/ 245 | function isVisible(_obj) { 246 | var ret = true; 247 | if (_obj && (_obj.style.display == "none" || _obj.classList.contains('hide'))) { ret = false;} 248 | else if (_obj && _obj.parentNode && _obj.tagName != "HTML") ret = isVisible(_obj.parentNode); 249 | return ret; 250 | } 251 | 252 | /******************************* 253 | separator: 254 | regex of item ID to identify first element in row 255 | - if set, returned json is an array, all elements per row, example: "^myonoffswitch.*" 256 | - if emty, all elements at one level together, ONLY for small json´s (->memory issue) 257 | *******************************/ 258 | function onSubmit(DataForm, separator='') { 259 | // init json Objects 260 | var JsonData, tempData; 261 | 262 | if (separator.length == 0) { JsonData = {data: {}}; } 263 | else { JsonData = {data: []};} 264 | tempData = {}; 265 | 266 | var elems = document.getElementById(DataForm).elements; 267 | for(var i = 0; i < elems.length; i++){ 268 | if(elems[i].name && elems[i].value) { 269 | if (!isVisible(elems[i])) { continue; } 270 | 271 | // tempData -> JsonData if new row (first named element (-> match) in row) 272 | if (separator.length > 0 && elems[i].id.match(separator) && Object.keys(tempData).length > 0) { 273 | JsonData.data.push(tempData); 274 | tempData = {}; 275 | } else if (separator.length == 0 && Object.keys(tempData).length > 0) { 276 | JsonData.data[Object.keys(tempData)[0]] = tempData[Object.keys(tempData)[0]]; 277 | tempData = {}; 278 | } 279 | 280 | if (elems[i].type == "checkbox") { 281 | tempData[elems[i].name] = (elems[i].checked==true?1:0); 282 | } else if (elems[i].id.match(/^Alle.*/) || 283 | elems[i].id.match(/^GpioPin.*/) || 284 | elems[i].id.match(/^AnalogPin.*/) || 285 | elems[i].type == "number") { 286 | tempData[elems[i].name] = parseInt(elems[i].value); 287 | } else if (elems[i].type == "radio") { 288 | if (elems[i].checked==true) {tempData[elems[i].name] = elems[i].value;} 289 | } else { 290 | tempData[elems[i].name] = elems[i].value; 291 | } 292 | } 293 | } 294 | 295 | // ende elements 296 | if (separator.length > 0 && Object.keys(tempData).length > 0) { 297 | JsonData.data.push(tempData); 298 | tempData = {}; 299 | } else if (separator.length == 0 && Object.keys(tempData).length > 0) { 300 | JsonData.data[Object.keys(tempData)[0]] = tempData[Object.keys(tempData)[0]]; 301 | tempData = {}; 302 | } 303 | 304 | setResponse(true, "save ...") 305 | 306 | var filename = document.location.pathname.replace(/^.*[\\/]/, '') 307 | filename = filename.substring(0, filename.lastIndexOf('.')) || filename // without extension 308 | 309 | var textToSaveAsBlob = new Blob([JSON.stringify(JsonData)], {type:"text/plain"}); 310 | 311 | const formData = new FormData(); 312 | formData.append(filename + ".json", textToSaveAsBlob, '/' + filename + ".json"); 313 | 314 | fetch('/doUpload', { 315 | method: 'POST', 316 | body: formData, 317 | }) 318 | .then (response => response.json()) 319 | .then (json => { 320 | setResponse(true, json.text) 321 | }) 322 | .then (() => { 323 | var data = {}; 324 | data['action'] = "ReloadConfig"; 325 | data['subaction'] = filename; 326 | requestData(JSON.stringify(data), false); 327 | }); 328 | } 329 | 330 | /************************************** 331 | * Upload a blob as file 332 | ***************************************/ 333 | async function UploadFile(blob, filename, filepath) { 334 | const formData = new FormData(); 335 | formData.append(filename, blob, filepath + "/" + filename); 336 | 337 | await fetch('/doUpload', { 338 | method: 'POST', 339 | body: formData, 340 | }) 341 | .then (response => response.json()) 342 | .then (json => { 343 | setResponse(true, json.text) 344 | }); 345 | } 346 | 347 | /******************************* 348 | blendet Zeilen der Tabelle aus 349 | show: Array of shown IDs return true; 350 | hide: Array of hidden IDs 351 | *******************************/ 352 | function radioselection(show, hide) { 353 | for(var i = 0; i < show.length; i++){ 354 | if (document.getElementById(show[i])) {document.getElementById(show[i]).style.display = 'table-row';} 355 | } 356 | for(var j = 0; j < hide.length; j++){ 357 | if(document.getElementById(hide[j])) {document.getElementById(hide[j]).style.display = 'none';} 358 | } 359 | } 360 | 361 | -------------------------------------------------------------------------------- /data/web/Style.css: -------------------------------------------------------------------------------- 1 | /* https://www.peterkropff.de/site/css/kontext_selektoren.htm */ 2 | 3 | body { 4 | font-size: 140%; 5 | font-family: Verdana,Arial,Helvetica,sans-serif; 6 | } 7 | 8 | td.noborder { 9 | border: none !important; 10 | } 11 | 12 | input[type="number"] { 13 | width: 3em; 14 | } 15 | 16 | .inline { 17 | float: left; 18 | clear: both; 19 | } 20 | 21 | .hide { 22 | display: none; 23 | } 24 | 25 | input[type="submit"] { 26 | padding: 4px 16px; 27 | margin: 4px; 28 | background-color: #07D; 29 | color: #FFF; 30 | text-decoration: none; 31 | border-radius: 4px; 32 | border: none; 33 | } 34 | input[type="submit"]:hover { background: #336699; } 35 | 36 | input, select, textarea { 37 | margin: 4px; 38 | padding: 4px 8px; 39 | border-radius: 4px; 40 | background-color: #eee; 41 | border-style: solid; 42 | border-width: 1px; 43 | border-color: gray; 44 | } 45 | input:hover, select:hover, textarea:hover { background-color: #cccccc; } 46 | 47 | .editorDemoTable { 48 | border-spacing: 0; 49 | background-color: #FFF8C9; 50 | margin-left: auto; 51 | margin-right: auto; 52 | } 53 | .editorDemoTable thead { 54 | color: #000000; 55 | background-color: #2E6C80; 56 | text-align: center; 57 | } 58 | .editorDemoTable thead td { 59 | font-weight: bold; 60 | font-size: 13px; 61 | } 62 | .editorDemoTable td { 63 | border: 1px solid #777; 64 | margin: 0 !important; 65 | padding: 2px 3px; 66 | font-size: 11px; 67 | } 68 | 69 | .oktext { 70 | color: green; 71 | font-size: 13px; 72 | text-align: center; 73 | } 74 | 75 | .errortext { 76 | color: red; 77 | font-size: 13px; 78 | text-align: center; 79 | } 80 | 81 | .navi { 82 | border-bottom: 3px solid #777; 83 | padding: 5px; 84 | text-align: center; 85 | font-size: 13px; 86 | } 87 | 88 | .navi_active { 89 | background-color: #CCCCCC; 90 | border: 3px solid #777;; 91 | border-bottom: none; 92 | } 93 | 94 | .highlightOn { 95 | color: red; 96 | } 97 | 98 | .highlightOff { 99 | color: black; 100 | } 101 | 102 | .ButtonRefresh { 103 | font-size: 13px; 104 | background-color: #EEEEEE; 105 | color: #999999; 106 | } 107 | 108 | 109 | /* https://proto.io/freebies/onoff/ */ 110 | .onoffswitch { 111 | position: relative; width: 46px; 112 | -webkit-user-select:none; -moz-user-select:none; -ms-user-select: none; 113 | } 114 | .onoffswitch-checkbox { 115 | display: none; 116 | } 117 | .onoffswitch-label { 118 | display: block; overflow: hidden; cursor: pointer; 119 | border: 2px solid #999999; border-radius: 20px; 120 | } 121 | .onoffswitch-inner { 122 | display: block; width: 200%; margin-left: -100%; 123 | transition: margin 0.3s ease-in 0s; 124 | } 125 | .onoffswitch-inner:before, .onoffswitch-inner:after { 126 | display: block; float: left; width: 50%; height: 15px; padding: 0; line-height: 15px; 127 | font-size: 10px; color: white; font-family: Trebuchet, Arial, sans-serif; font-weight: bold; 128 | box-sizing: border-box; 129 | } 130 | .onoffswitch-inner:before { 131 | content: "ON"; 132 | padding-left: 5px; 133 | background-color: #34A7C1; color: #FFFFFF; 134 | } 135 | .onoffswitch-inner:after { 136 | content: "OFF"; 137 | padding-right: 5px; 138 | background-color: #EEEEEE; color: #999999; 139 | text-align: right; 140 | } 141 | .onoffswitch-switch { 142 | display: block; width: 8px; margin: 3.5px; 143 | background: #FFFFFF; 144 | position: absolute; top: 0; bottom: 0; 145 | right: 27px; 146 | border: 2px solid #999999; border-radius: 20px; 147 | transition: all 0.3s ease-in 0s; 148 | } 149 | .onoffswitch-checkbox:checked + .onoffswitch-label .onoffswitch-inner { 150 | margin-left: 0; 151 | } 152 | .onoffswitch-checkbox:checked + .onoffswitch-label .onoffswitch-switch { 153 | right: 0px; 154 | } 155 | 156 | /* https://wiki.selfhtml.org/wiki/CSS/Tutorials/Tooltips_mit_CSS */ 157 | /* used at modbusitem and rawdata page*/ 158 | 159 | .tooltip { 160 | color: #c32e04; 161 | text-decoration: underline; 162 | cursor: help; 163 | position: relative; 164 | font-style: normal; 165 | } 166 | 167 | .tooltip span[role=tooltip] { 168 | display: none; 169 | } 170 | 171 | .tooltip:hover span[role=tooltip] { 172 | display: block; 173 | position: absolute; 174 | bottom: 1em; 175 | left: -6em; 176 | width: 15em; 177 | padding: 0.5em; 178 | z-index: 100; 179 | color: #000; 180 | background-color: #ffebe6; 181 | border: solid 1px #c32e04; 182 | border-radius: 0.2em; 183 | } 184 | 185 | .tooltip_simple { 186 | font-style: normal; 187 | cursor: help; 188 | position: relative; 189 | } 190 | 191 | .tooltip_simple span[role=tooltip_simple] { 192 | display: none; 193 | } 194 | 195 | .tooltip_simple:hover span[role=tooltip_simple] { 196 | display: block; 197 | position: absolute; 198 | bottom: 1em; 199 | left: -6em; 200 | padding: 0.5em; 201 | z-index: 100; 202 | color: #000; 203 | background-color: #ffebe6; 204 | border: solid 1px #c32e04; 205 | border-radius: 0.2em; 206 | font-style: normal; 207 | } 208 | 209 | .texthover { 210 | background-color: transparent; 211 | } 212 | 213 | .texthover:hover { 214 | background-color: #cccccc; 215 | } 216 | 217 | /* show page loading */ 218 | /* https://dev.to/ziratsu/spinning-loader-in-pure-css-4dh */ 219 | #loader { 220 | position: absolute; 221 | top: 0; 222 | bottom: 0; 223 | left: 0; 224 | right: 0; 225 | margin: auto; 226 | width: 100px; 227 | height: 100px; 228 | border-radius: 50%; 229 | border: 4px solid crimson; 230 | border: 4px solid transparent; 231 | border-top-color: crimson; 232 | border-bottom-color: crimson; 233 | animation: spin 1s ease-in-out infinite; 234 | } 235 | 236 | @keyframes spin { 237 | to { 238 | transform: rotate(360deg); 239 | } 240 | } 241 | -------------------------------------------------------------------------------- /data/web/baseconfig.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | Modbus MQTT Gateway 12 | 13 | 14 |
15 |
16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 41 | 42 | 43 | 44 | 45 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 138 | 150 | 151 | 152 |
NameWert
Device Name
31 |
32 | 33 | 34 |
35 | 36 |
37 | 38 | 39 |
40 |
Select LAN Board 46 | 49 |
MQTT Server IP
MQTT Server Port
MQTT Authentification: Username (optional)
MQTT Authentification: Password (optional)
MQTT Topic Base Path (example: home/pumpcontrol)
79 |
80 | 81 | 82 |
83 |
84 | 85 | 86 |
87 |
Senden einer KeepAlive Message via MQTT (in sek > 10, 0=disabled)
LogLevel (0 [off] ... 5 [max])
Pin i2c SDA
Pin i2c SCL
112 |
113 | 114 | 115 |
116 | 117 |
118 | 119 | 120 |
121 |
3WegeVentil Trinkwasser Bypass
Update URL
verfügbare Releases 136 | 137 | 139 | 140 | 141 | 144 | 147 | 148 |
142 | 143 | 145 | 146 |
149 |
153 |
154 | 155 |
156 | 157 | 158 | 159 | -------------------------------------------------------------------------------- /data/web/baseconfig.js: -------------------------------------------------------------------------------- 1 | var ReleaseInfo; 2 | 3 | // ************************************************ 4 | window.addEventListener('DOMContentLoaded', init, false); 5 | function init() { 6 | GetInitData(); 7 | } 8 | 9 | // ************************************************ 10 | function GetInitData() { 11 | var data = {}; 12 | data.action = "GetInitData"; 13 | data.subaction = "baseconfig"; 14 | requestData(JSON.stringify(data), false, MyCallback); 15 | } 16 | 17 | // ************************************************ 18 | function MyCallback() { 19 | CreateSelectionListFromInputField('input[type=number][id^=GpioPin]', [gpio]); 20 | CreateSelectionListFromInputField('input[type=number][id*=ConfiguredPort]', [configuredPorts]); 21 | SetUpdateURL() 22 | FetchReleaseInfo(); 23 | document.querySelector("#loader").style.visibility = "hidden"; 24 | document.querySelector("body").style.visibility = "visible"; 25 | } 26 | 27 | // ************************************************ 28 | 29 | function SetUpdateURL() { 30 | var u = document.getElementById("au_url"); 31 | u.value = update_url; 32 | } 33 | 34 | function FetchReleaseInfo() { 35 | fetch( update_url, {}) 36 | .then (response => response.json()) 37 | .then (json => { 38 | ProcessReleaseInfo(json) 39 | }) 40 | } 41 | 42 | function ProcessReleaseInfo(json) { 43 | var _parent, _select, _optgroup_dev, _optgroup_pre, _optgroup_prd, _option; 44 | this.ReleaseInfo = json; 45 | 46 | _select = document.getElementById('releases'); 47 | _select.replaceChildren(); 48 | 49 | _optgroup_dev = document.createElement('optgroup'); 50 | _optgroup_pre = document.createElement('optgroup'); 51 | _optgroup_prd = document.createElement('optgroup'); 52 | 53 | _optgroup_dev.label = "Development"; 54 | _optgroup_pre.label = "Prelive"; 55 | _optgroup_prd.label = "Produktiv"; 56 | 57 | if (Array.isArray(json)) { 58 | for (var i=0; i < json.length; i++) { 59 | //console.log(json[i]) 60 | _option = document.createElement('option'); 61 | _option.value = json[i].subversion; //['download-url']; 62 | _option.text = json[i].name + " (" + json[i].subversion + ")"; 63 | //_option.selected 64 | if (json[i].stage == 'DEV') { _optgroup_dev.appendChild(_option); } 65 | if (json[i].stage == 'PRE') { _optgroup_pre.appendChild(_option); } 66 | if (json[i].stage == 'PROD') { _optgroup_prd.appendChild(_option); } 67 | } 68 | } 69 | 70 | _select.add( _optgroup_prd ); 71 | _select.add( _optgroup_pre ); 72 | _select.add( _optgroup_dev ); 73 | } 74 | 75 | async function FetchRelease() { 76 | var r = document.getElementById('releases') 77 | var releaseJson = GetSpecificReleaseJson(r.value); 78 | 79 | // show loader 80 | document.querySelector("#loader").style.visibility = "visible"; 81 | 82 | // fetch new release firmware 83 | const ReleaseBlob = await fetch( releaseJson['download-url'], { 84 | responseType: 'blob' 85 | }) 86 | .then (response => response.blob()) 87 | ; 88 | 89 | if(ReleaseBlob) { 90 | setResponse(true, "new Firmware successfully downloaded"); 91 | 92 | //upload releaseinformation (to show in Web-Header) 93 | var JsonText = JSON.stringify(releaseJson); 94 | var JsonBlob = new Blob([JsonText], {type:"text/plain"}); 95 | await UploadFile(JsonBlob, "ESPUpdate.json", ""); 96 | 97 | // Upload new release firmware 98 | setResponse(true, "please wait to upload new firmware"); 99 | await InstallRelease(ReleaseBlob); 100 | 101 | setResponse(true, "please wait a few seconds to reload"); 102 | 103 | setTimeout(() => { 104 | top.location.href="/"; 105 | }, 5000); 106 | 107 | } else { 108 | setResponse(false, "new Firmware download failed"); 109 | } 110 | } 111 | 112 | async function InstallRelease(BinaryBlob) { 113 | const formData = new FormData(); 114 | formData.append("firmware", BinaryBlob, "firmware.bin"); 115 | 116 | await fetch('/update', { 117 | method: 'POST', 118 | body: formData, 119 | }) 120 | } 121 | 122 | function GetSpecificReleaseJson(subversion) { 123 | if (Array.isArray(this.ReleaseInfo)) { 124 | for (var i=0; i < this.ReleaseInfo.length; i++) { 125 | if (subversion == this.ReleaseInfo[i].subversion) return this.ReleaseInfo[i]; 126 | } 127 | } 128 | } -------------------------------------------------------------------------------- /data/web/handlefiles.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | HandleFiles 16 | 17 | 18 |
19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 41 | 42 | 43 | 44 | 52 | 53 | 54 | 62 | 63 | 64 |
DateienInhalt
29 | 30 | 31 | 34 | 38 | 39 |
32 | path: 33 | 35 |
{path}
36 |
{fullpath}
37 |
40 |
45 | 50 |
51 |
filename: 55 | 56 | 57 | 58 | 59 | 60 | 61 |
65 | 66 | -------------------------------------------------------------------------------- /data/web/handlefiles.js: -------------------------------------------------------------------------------- 1 | // https://jsfiddle.net/tobiasfaust/uc1jfpgb/ 2 | 3 | var DirJson; 4 | 5 | window.addEventListener('load', initHandleFS, false); 6 | function initHandleFS() { 7 | document.querySelector("#loader").style.visibility = "hidden"; 8 | document.querySelector("body").style.visibility = "visible"; 9 | init("/"); 10 | } 11 | 12 | function init(startpath) { 13 | requestListDir(startpath); 14 | obj = document.getElementById('fullpath').innerHTML = ''; // div 15 | obj = document.getElementById('filename').value = ''; // input field 16 | obj = document.getElementById('content').value = ''; 17 | 18 | } 19 | 20 | // *********************************** 21 | // Ajax Request to update 22 | // *********************************** 23 | function requestListDir(startpath) { 24 | var data = {}; 25 | data['action'] = "handlefiles"; 26 | data['subaction'] = "listDir" 27 | //ajax_send(JSON.stringify(data)); 28 | 29 | var http = null; 30 | if (window.XMLHttpRequest) { http =new XMLHttpRequest(); } 31 | else { http =new ActiveXObject("Microsoft.XMLHTTP"); } 32 | 33 | if(!http){ alert("AJAX is not supported."); return; } 34 | 35 | var url = '/ajax'; 36 | var params = 'json=' + JSON.stringify(data); 37 | 38 | http.open('POST', url, true); 39 | http.setRequestHeader('Content-type', 'application/x-www-form-urlencoded'); 40 | http.onreadystatechange = function() { //Call a function when the state changes. 41 | if(http.readyState == 4 && http.status == 200) { 42 | DirJson = JSON.parse(http.responseText); 43 | listFiles(startpath); 44 | } 45 | } 46 | http.send(params); 47 | } 48 | 49 | // *********************************** 50 | // show content of fetched file 51 | // *********************************** 52 | function setContent(string, file) { 53 | obj = document.getElementById('fullpath').innerHTML = file; // div 54 | obj = document.getElementById('filename').value = basename(file); // input field 55 | 56 | if (file.endsWith("json")) { 57 | obj = document.getElementById('content').value = JSON.stringify(JSON.parse(string), null, 2); 58 | } else { 59 | obj = document.getElementById('content').value = string; 60 | } 61 | } 62 | 63 | // *********************************** 64 | // fetch file from host 65 | // *********************************** 66 | function fetchFile(file) { 67 | obj = document.getElementById('content').value = "loading "+file+"..."; 68 | 69 | fetch(file) 70 | .then(response => response.text()) 71 | .then(textString => setContent(textString, file)); 72 | } 73 | 74 | // *********************************** 75 | // show directory structure 76 | // *********************************** 77 | function listFiles(path) { 78 | var table = document.querySelector('#files'), 79 | row = document.querySelector('#NewRow'), 80 | tr_tpl, DirJsonLocal; 81 | 82 | // cleanup table 83 | table.replaceChildren(); 84 | 85 | // get the right part 86 | for(let i = 0; i < DirJson.length; i++) { 87 | if (DirJson[i].path == path) { 88 | DirJsonLocal = DirJson[i] 89 | } 90 | } 91 | 92 | // show path information 93 | document.getElementById('path').innerHTML = path; 94 | 95 | // set "back" item if not root 96 | if (path != '/') { 97 | tr_tpl = document.importNode(row.content, true); 98 | cells = tr_tpl.querySelectorAll("td"); 99 | cells.forEach(function (item, index) { 100 | var text = item.innerHTML; 101 | var oc = "listFiles('" + getParentPath(path) + "')" 102 | text = text.replaceAll("{file}", '..'); 103 | item.innerHTML = text; 104 | item.setAttribute('onClick', oc); 105 | }); 106 | table.appendChild(tr_tpl); 107 | } 108 | 109 | // show files 110 | DirJsonLocal.content.forEach(function (file) { 111 | // template "laden" (lies: klonen) 112 | tr_tpl = document.importNode(row.content, true); 113 | cells = tr_tpl.querySelectorAll("td"); 114 | cells.forEach(function (item, index) { 115 | var text = item.innerHTML; 116 | var oc; 117 | 118 | if(file.isDir == 0) { 119 | oc = item.getAttribute('onClick'); 120 | var newPath = DirJsonLocal.path + "/" + file.name; 121 | if (newPath.startsWith("//")) {newPath = newPath.substring(1)} 122 | oc = oc.replaceAll("{fullpath}", newPath); 123 | text = text.replaceAll("{file}", file.name); 124 | } else if(file.isDir == 1) { 125 | var newPath = DirJsonLocal.path + "/" + file.name; 126 | if (newPath.startsWith("//")) {newPath = newPath.substring(1)} 127 | oc = "listFiles('" + newPath + "')" 128 | text = text.replaceAll("{file}", file.name + "/"); 129 | } 130 | 131 | 132 | item.innerHTML = text; 133 | item.setAttribute('onClick', oc); 134 | }); 135 | table.appendChild(tr_tpl); 136 | }) 137 | } 138 | 139 | // *********************************** 140 | // returns parent path: '/regs/web' -> '/regs' 141 | // *********************************** 142 | function getParentPath(path) { 143 | var ParentPath, PathArray; 144 | 145 | PathArray = path.split('/') 146 | PathArray.pop() 147 | if (PathArray.length == 1) { ParentPath = '/' } 148 | else { ParentPath = PathArray.join('/')} 149 | return ParentPath 150 | } 151 | 152 | // *********************************** 153 | // extract the filename from path 154 | // *********************************** 155 | function basename(str) { 156 | return str.split('\\').pop().split('/').pop(); 157 | } 158 | 159 | // *********************************** 160 | // return true if valid json, otherwise false 161 | // *********************************** 162 | function validateJson(json) { 163 | try { 164 | JSON.parse(json); 165 | return true; 166 | } catch { 167 | return false; 168 | } 169 | } 170 | 171 | // *********************************** 172 | // download content of textarea as filename on local pc 173 | // *********************************** 174 | function downloadFile() { 175 | var textToSave = document.getElementById("content").value; 176 | var textToSaveAsBlob = new Blob([textToSave], {type:"text/plain"}); 177 | var textToSaveAsURL = window.URL.createObjectURL(textToSaveAsBlob); 178 | var fileNameToSaveAs = document.getElementById("filename").value; 179 | 180 | if (fileNameToSaveAs != '') { 181 | var downloadLink = document.createElement("a"); 182 | downloadLink.download = fileNameToSaveAs; 183 | downloadLink.innerHTML = "Download File"; 184 | downloadLink.href = textToSaveAsURL; 185 | 186 | downloadLink.onclick = destroyClickedElement; 187 | downloadLink.style.display = "none"; 188 | document.body.appendChild(downloadLink); 189 | downloadLink.click(); 190 | } else { setResponse(false, 'Filename is empty, Please define it.');} 191 | } 192 | 193 | function destroyClickedElement(event) 194 | { 195 | document.body.removeChild(event.target); 196 | } 197 | 198 | // *********************************** 199 | // store content of textarea 200 | // *********************************** 201 | function uploadAsFile() { 202 | var textToSave = document.getElementById("content").value; 203 | var textToSaveAsBlob = new Blob([textToSave], {type:"text/plain"}); 204 | var fileNameToSaveAs = document.getElementById("filename").value; 205 | var pathOfFile = document.getElementById('path').innerHTML; 206 | 207 | if (fileNameToSaveAs != '') { 208 | if (fileNameToSaveAs.toLowerCase().endsWith('.json')) { 209 | if (!validateJson(textToSave)) { 210 | setResponse(false, 'Json invalid') 211 | return; 212 | } 213 | } 214 | 215 | setResponse(true, 'Please wait for saving ...'); 216 | 217 | UploadFile(textToSaveAsBlob, fileNameToSaveAs, pathOfFile); 218 | 219 | } else { setResponse(false, 'Filename is empty, Please define it.');} 220 | } 221 | 222 | async function deleteAFile(file) { 223 | if (file != '') { 224 | var data = {}; 225 | data['action'] = 'handlefiles'; 226 | data['subaction'] = "deleteFile"; 227 | data['filename'] = file; 228 | 229 | setResponse(true, 'Please wait for deleting ...'); 230 | requestData(JSON.stringify(data)); 231 | } else { setResponse(false, 'Filename is empty, Please define it.');} 232 | } 233 | 234 | async function deleteFile() { 235 | var pathOfFile = document.getElementById('path').innerHTML; 236 | var fileName = document.getElementById("filename").value; 237 | 238 | await deleteAFile(pathOfFile + '/' + fileName); 239 | init(pathOfFile); 240 | } 241 | 242 | // *********************************** 243 | // backup complete filesystem of ESP by zipfile 244 | // 245 | // https://gist.github.com/noelvo/4502eea719f83270c8e9 246 | // *********************************** 247 | function backup() { 248 | var url = []; 249 | 250 | for(let i = 0; i < DirJson.length; i++) { 251 | DirJson[i].content.forEach(function (file) { 252 | if (file.isDir==0) { 253 | //console.log(DirJson[i].path, file.name) 254 | url.push(DirJson[i].path + "/" + file.name) 255 | } 256 | }) 257 | } 258 | compressed_img(url, "backup"); 259 | } 260 | 261 | function compressed_img(urls, nombre) { 262 | var zip = new JSZip(); 263 | var count = 0; 264 | var name = nombre + ".zip"; 265 | urls.forEach(function(url){ 266 | JSZipUtils.getBinaryContent(url, function (err, data) { 267 | if(err) { 268 | throw err; 269 | } 270 | zip.file(url, data, {binary:true}); 271 | count++; 272 | if (count == urls.length) { 273 | zip.generateAsync({type:'blob'}).then(function(content) { 274 | saveAs(content, name); 275 | }); 276 | } 277 | }); 278 | }); 279 | } -------------------------------------------------------------------------------- /data/web/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | PumpControl 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /data/web/navi.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | Modbus MQTT Gateway 10 | 11 | 12 | 13 | 14 | 17 | 18 | 21 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 |
15 |

Configuration

16 |
19 | () 20 | 22 | Release: 23 | 24 | 25 |
of 26 | 27 | 28 |
46 | 47 | -------------------------------------------------------------------------------- /data/web/navi.js: -------------------------------------------------------------------------------- 1 | // ************************************************ 2 | window.addEventListener('load', init, false); 3 | function init() { 4 | GetInitData(); 5 | } 6 | 7 | // ************************************************ 8 | function GetInitData() { 9 | var data = {}; 10 | data['action'] = "GetInitData"; 11 | data['subaction'] = "navi"; 12 | requestData(JSON.stringify(data)); 13 | } 14 | 15 | // ************************************************ 16 | function highlightNavi(item) { 17 | collection = document.getElementsByName('navi') 18 | 19 | for (let i = 0; i < collection.length; i++) { 20 | if (item.id == collection[i].id ) { 21 | document.getElementById(collection[i].id).classList.add('navi_active'); 22 | } else { 23 | document.getElementById(collection[i].id).classList.remove('navi_active'); 24 | } 25 | } 26 | 27 | top.frames["frame_main"].document.querySelector("#loader").style.visibility = "visible"; 28 | top.frames["frame_main"].document.querySelector("body").style.visibility = "hidden"; 29 | 30 | } 31 | 32 | // ************************************************ -------------------------------------------------------------------------------- /data/web/reboot.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 14 | Pumpcontrol 15 | 16 | Rebooting... 17 |

18 | 19 | 20 | -------------------------------------------------------------------------------- /data/web/sensorconfig.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | Pumpcontrol 12 | 13 | 14 |

15 |
16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 48 | 49 | 50 | 51 | 52 | 55 | 56 | 57 | 58 | 59 | 62 | 63 | 64 | 65 | 66 | 69 | 70 | 71 | 72 | 73 | 76 | 77 | 78 | 79 | 80 | 83 | 84 | 85 | 86 | 87 | 90 | 91 | 92 | 93 | 94 | 97 | 98 | 99 | 100 | 101 | 104 | 105 | 106 | 107 | 108 | 111 | 112 | 113 | 114 | 115 | 118 | 119 | 120 | 121 | 122 | 125 | 126 | 127 |
NameWert
27 |
28 | 29 | 30 |
31 | 32 |
33 | 34 | 35 |
36 | 37 |
38 | 39 | 40 |
41 | 42 |
43 | 44 | 45 |
46 | 47 |
Messintervall 53 | 54 |
Abstand Sensor min (in cm) 60 | 61 |
Abstand Sensor max (in cm) 67 | 68 |
Pin HC-SR04 Trigger 74 | 75 |
Pin HC-SR04 Echo 81 | 82 |
GPIO an welchem das Signal anliegt 88 | 89 |
Kalibrierung: 0% entspricht RAW Wert 95 | 96 |
Kalibrierung: 100% entspricht RAW Wert 102 | 103 |
MQTT-Topic des externen Sensors (Füllstand in %) 109 | 110 |
Sensor Treshold Min für 3WegeVentil 116 | 117 |
Sensor Treshold Max für 3WegeVentil 123 | 124 |
128 |
129 | 130 |
131 | 132 | 133 | 134 | -------------------------------------------------------------------------------- /data/web/sensorconfig.js: -------------------------------------------------------------------------------- 1 | // ************************************************ 2 | window.addEventListener('DOMContentLoaded', init, false); 3 | function init() { 4 | GetInitData(); 5 | } 6 | 7 | // ************************************************ 8 | function GetInitData() { 9 | var data = {}; 10 | data.action = "GetInitData"; 11 | data.subaction = "sensorconfig"; 12 | requestData(JSON.stringify(data), false, MyCallback); 13 | } 14 | 15 | // ************************************************ 16 | function MyCallback() { 17 | CreateSelectionListFromInputField('input[type=number][id^=GpioPin]', [gpio]); 18 | CreateSelectionListFromInputField('input[type=number][id^=AnalogPin]', [gpioanalog]); 19 | document.querySelector("#loader").style.visibility = "hidden"; 20 | document.querySelector("body").style.visibility = "visible"; 21 | 22 | } 23 | 24 | // ************************************************ 25 | -------------------------------------------------------------------------------- /data/web/status.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | status 10 | 11 | 12 |
13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 46 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 |
NameWert
IP-Adresse:
WiFi Name:
MAC:
WiFi RSSI:
i2c Bus: 44 | 45 | 47 |
48 |
MQTT Status:
aktuell geöffnete Ventile
Sensor RAW Value:
Füllstand in %:
Uptime:
Free Heap Memory:
Firmware Update
Device Reboot
Werkszustand herstellen (ohne WiFi)
WiFi Zugangsdaten entfernen
102 | 103 | -------------------------------------------------------------------------------- /data/web/status.js: -------------------------------------------------------------------------------- 1 | // ************************************************ 2 | window.addEventListener('DOMContentLoaded', init, false); 3 | function init() { 4 | GetInitData(); 5 | } 6 | 7 | // ************************************************ 8 | function GetInitData() { 9 | var data = {}; 10 | data['action'] = "GetInitData"; 11 | data['subaction'] = "status"; 12 | requestData(JSON.stringify(data), false, MyCallback); 13 | } 14 | 15 | // ************************************************ 16 | function MyCallback() { 17 | document.querySelector("#loader").style.visibility = "hidden"; 18 | document.querySelector("body").style.visibility = "visible"; 19 | } 20 | 21 | // ************************************************ 22 | -------------------------------------------------------------------------------- /data/web/update.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Pumpcontrol 8 | 9 | Update with your own custom firmware, for a precompiled standard firmware please go to baseconfig page:

10 |

11 | 12 | 13 |
14 | 25 | 26 | 30 |

please select 'data' directory: 31 | 32 |

33 | 34 | 35 | 80 | 81 | -------------------------------------------------------------------------------- /data/web/update_response.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 29 | Pumpcontrol 30 | 31 |
32 | Update Successful! Rebooting... 33 |

34 | 35 |

36 | 37 | 38 | -------------------------------------------------------------------------------- /data/web/valveconfig.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | Pumpcontrol 13 | 14 | 15 |

16 |

17 | 18 |

19 |
20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 99 | 100 | 101 |
NrActiveMQTT SubTopicPortTypeReverseAutoOffDeleteAction
102 |
103 |
104 | 105 | 106 | 107 | -------------------------------------------------------------------------------- /data/web/valveconfig.js: -------------------------------------------------------------------------------- 1 | // ************************************************ 2 | window.addEventListener('DOMContentLoaded', init, false); 3 | function init() { 4 | GetInitData(); 5 | } 6 | 7 | // ************************************************ 8 | function GetInitData() { 9 | var data = {}; 10 | data.action = "GetInitData"; 11 | data.subaction = "valveconfig"; 12 | requestData(JSON.stringify(data), false, MyCallback); 13 | } 14 | 15 | // ************************************************ 16 | function MyCallback() { 17 | CreateSelectionListFromInputField('input[type=number][id*=AllePorts]', [gpio, availablePorts], gpio_disabled); 18 | validate_identifiers("maintable"); 19 | document.querySelector("#loader").style.visibility = "hidden"; 20 | document.querySelector("body").style.visibility = "visible"; 21 | } 22 | 23 | // ************************************************ 24 | -------------------------------------------------------------------------------- /data/web/valvefunctions.js: -------------------------------------------------------------------------------- 1 | /******************************* 2 | copy first row of table and add it as clone 3 | *******************************/ 4 | function addrow(tableID) { 5 | var _table = document.getElementById(tableID); 6 | var firstrow; 7 | for( i=0; i< _table.rows.length; i++) { 8 | if (GetParentObject(_table.rows[i], "THEAD")) continue; 9 | firstrow = i; 10 | } 11 | 12 | _table.rows[firstrow].style.display = ''; 13 | var new_row = _table.rows[firstrow].cloneNode(true); 14 | _table.appendChild(new_row); 15 | validate_identifiers(tableID); 16 | } 17 | 18 | 19 | /******************************* 20 | delete a row in table 21 | *******************************/ 22 | function delrow(object) { 23 | var table = GetParentObject(object, 'TABLE'); 24 | var rowIndex = GetParentObject(object, 'TR').rowIndex; 25 | var rowFirst=0; 26 | for( i=0; i< table.rows.length; i++) { 27 | if (GetParentObject(table.rows[i], "THEAD")) continue; 28 | rowFirst = i; break; 29 | } 30 | if (table.rows.length > rowFirst+1) { 31 | // erste Zeile ist das Template + Header, darf nicht entfernt werden 32 | table.deleteRow(rowIndex) 33 | validate_identifiers(table.id); 34 | } 35 | } 36 | 37 | 38 | /******************************* 39 | recalculate all id´s, name´s 40 | *******************************/ 41 | function validate_identifiers(tableID) { 42 | table = document.getElementById(tableID); 43 | var counter=1; 44 | for( i=0; i< table.rows.length; i++) { 45 | row = table.rows[i]; 46 | if (GetParentObject(row, "THEAD")) continue; 47 | 48 | row.cells[0].innerHTML = counter; 49 | objects = row.querySelectorAll('label, input, select, div, td'); 50 | for( j=0; j< objects.length; j++) { 51 | if (objects[j].name) {objects[j].name = objects[j].name.replace(/(\d+)/, counter-1);} 52 | if (objects[j].id) {objects[j].id = objects[j].id.replace(/(\d+)/, counter-1);} 53 | if (objects[j].htmlFor) {objects[j].htmlFor = objects[j].htmlFor.replace(/(\d+)/, counter-1);} 54 | } 55 | counter++; 56 | } 57 | } 58 | 59 | /******************************* 60 | return the first parent object of tagName, e.g. TR 61 | *******************************/ 62 | function GetParentObject(object, TargetTagName) { 63 | if (object.tagName == TargetTagName) {return object;} 64 | else if (object.parentNode === null) { return false;} 65 | else { return GetParentObject(object.parentNode, TargetTagName); } 66 | } 67 | 68 | /******************************* 69 | return the port-value of selected row, 70 | object: anyone object of that row 71 | *******************************/ 72 | function GetPortOfRow(object) { 73 | var port = 0; 74 | var row = GetParentObject(object, 'TR') 75 | var objects = document.querySelectorAll('select[id*=AllePorts][name=port_a]'); 76 | 77 | for( var i=0; i< objects.length; i++) { 78 | if(isVisible(objects[i]) && row == GetParentObject(objects[i], 'TR')) { 79 | port = objects[i].value; 80 | } 81 | } 82 | 83 | return port; 84 | } 85 | 86 | /************************************************ 87 | the "active" checkbox has pressed 88 | *************************************************/ 89 | function ChangeEnabled(object) { 90 | var data = {}; 91 | 92 | data['action'] = "EnableValve" 93 | data['newState'] = object.checked; 94 | data['port'] = GetPortOfRow(object); 95 | 96 | requestData(JSON.stringify(data)); 97 | } 98 | 99 | /************************************************ 100 | the valbve type has changed 101 | *************************************************/ 102 | function ChangeValve(object) { 103 | btn = document.getElementById(object.id); 104 | var data = {}; 105 | 106 | data['action'] = "SetValve"; 107 | data['subaction'] = object.id; 108 | data['newState'] = btn.value.replace(/^Set\ (.*)/, "$1"); 109 | data['port'] = GetPortOfRow(object); 110 | 111 | requestData(JSON.stringify(data), false); 112 | } 113 | 114 | /************************************************ 115 | the "active" checkbox was press 116 | *************************************************/ 117 | function ChangeType(object) { 118 | var _obj_n, _obj_b; 119 | var row = GetParentObject(object, 'TR') 120 | val = object.value; 121 | 122 | var objects = document.querySelectorAll('div[id*=typ_]'); 123 | 124 | for( var i=0; i< objects.length; i++) { 125 | if(row == GetParentObject(objects[i], 'TR')) { 126 | if (objects[i].id.match(/typ_n/)) {_obj_n = objects[i];} 127 | if (objects[i].id.match(/typ_b/)) {_obj_b = objects[i];} 128 | } 129 | } 130 | 131 | if (val == 'b') { 132 | // Typ "Bistabil" 133 | _obj_n.classList = "hide"; 134 | _obj_b.classList = ""; 135 | } else if (val == 'n'){ 136 | // Typ "normal" 137 | _obj_n.classList = ""; 138 | _obj_b.classList = "hide"; 139 | } 140 | } 141 | -------------------------------------------------------------------------------- /esp_files/esp32/gpio.js: -------------------------------------------------------------------------------- 1 | gpio = [ {port: 204, name:'D24'}, 2 | {port: 213, name:'D13'}, 3 | {port: 216, name:'RX2'}, 4 | {port: 217, name:'TX2'}, 5 | {port: 218, name:'D18'}, 6 | {port: 219, name:'D19'}, 7 | {port: 221, name:'D21/SDA'}, 8 | {port: 222, name:'D22/SCL'}, 9 | {port: 223, name:'D23'}, 10 | {port: 225, name:'D25'}, 11 | {port: 226, name:'D26'}, 12 | {port: 227, name:'D27'}, 13 | {port: 232, name:'D32'}, 14 | {port: 233, name:'D33'}, 15 | ]; 16 | 17 | gpioanalog = [ {port: 236, name:'ADC1_CH0 - GPIO36'}, 18 | {port: 237, name:'ADC1_CH1 - GPIO37'}, 19 | {port: 238, name:'ADC1_CH2 - GPIO38'}, 20 | {port: 239, name:'ADC1_CH3 - GPIO39'}, 21 | {port: 232, name:'ADC1_CH4 - GPIO32'}, 22 | {port: 233, name:'ADC1_CH5 - GPIO33'}, 23 | {port: 234, name:'ADC1_CH6 - GPIO34'}, 24 | {port: 235, name:'ADC1_CH7 - GPIO35'}, 25 | {port: 204, name:'ADC2_CH0 - GPIO4'}, 26 | {port: 200, name:'ADC2_CH1 - GPIO0'}, 27 | {port: 202, name:'ADC2_CH2 - GPIO2'}, 28 | {port: 215, name:'ADC2_CH3 - GPIO15'}, 29 | {port: 213, name:'ADC2_CH4 - GPIO13'}, 30 | {port: 212, name:'ADC2_CH5 - GPIO12'}, 31 | {port: 214, name:'ADC2_CH6 - GPIO14'}, 32 | {port: 227, name:'ADC2_CH7 - GPIO27'}, 33 | {port: 225, name:'ADC2_CH8 - GPIO25'}, 34 | {port: 226, name:'ADC2_CH9 - GPIO26'} 35 | ]; 36 | 37 | update_url = ""; -------------------------------------------------------------------------------- /esp_files/esp8266/gpio.js: -------------------------------------------------------------------------------- 1 | gpio = [ {port: 216, name:'D0'}, 2 | {port: 205, name:'D1/SCL'}, 3 | {port: 204, name:'D2/SDA'}, 4 | {port: 200, name:'D3'}, 5 | {port: 202, name:'D4'}, 6 | {port: 214, name:'D5'}, 7 | {port: 212, name:'D6'}, 8 | {port: 213, name:'D7'}, 9 | {port: 215, name:'D8'}, 10 | {port: 201, name:'RX'}, 11 | {port: 203, name:'TX'} 12 | ]; 13 | 14 | gpioanalog = [ {port: 200, name:'A0'} 15 | ]; 16 | 17 | update_url = ""; 18 | -------------------------------------------------------------------------------- /include/_Release.h: -------------------------------------------------------------------------------- 1 | #define Release "3.0.0" 2 | -------------------------------------------------------------------------------- /partitions.csv: -------------------------------------------------------------------------------- 1 | # Name, Type, SubType, Offset, Size, Flags 2 | nvs, data, nvs, 0x9000, 0x5000, 3 | otadata, data, ota, 0xe000, 0x2000, 4 | app0, app, ota_0, 0x10000, 0x1A0000, 5 | app1, app, ota_1, , 0x1A0000, 6 | spiffs, data, spiffs, , 0x0A0000, 7 | coredump, data, coredump,0x3F0000, 0x10000, -------------------------------------------------------------------------------- /platformio.ini: -------------------------------------------------------------------------------- 1 | ; PlatformIO Project Configuration File 2 | ; 3 | ; Build options: build flags, source filter 4 | ; Upload options: custom upload port, speed and extra flags 5 | ; Library options: dependencies, extra library storages 6 | ; Advanced options: extra scripting 7 | ; 8 | ; Please visit documentation for the other options and examples 9 | ; https://docs.platformio.org/page/projectconf.html 10 | 11 | [env] 12 | monitor_speed = 115200 13 | upload_speed = 921600 14 | platform_packages = 15 | toolchain-riscv32-esp @ 8.4.0+2021r2-patch5 16 | board_build.partitions = partitions.csv 17 | lib_deps = 18 | https://github.com/KeithHanson/ESPAsyncWebServer 19 | https://github.com/alanswx/ESPAsyncWiFiManager 20 | https://github.com/knolleary/pubsubclient 21 | https://github.com/bblanchon/ArduinoJson 22 | https://github.com/bblanchon/ArduinoStreamUtils 23 | https://github.com/YiannisBourkelis/Uptime-Library 24 | 25 | https://github.com/wemos/WEMOS_Motor_Shield_Arduino_Library 26 | https://github.com/xreef/PCF8574_library 27 | https://github.com/tobiasfaust/i2cdetect 28 | 29 | extra_scripts = 30 | pre:scripts/prepareDataDir.py 31 | build_flags = 32 | -DASYNCWEBSERVER_REGEX 33 | -Ddbg=Serial 34 | !python scripts/build_flags.py git_branch 35 | !python scripts/build_flags.py git_repo 36 | 37 | ##################################################################### 38 | [env:firmware_ESP8266] 39 | platform = espressif8266 40 | board = nodemcuv2 41 | framework = arduino 42 | monitor_speed = ${env.monitor_speed} 43 | upload_speed = ${env.upload_speed} 44 | board_build.filesystem = littlefs 45 | lib_deps = 46 | ${env.lib_deps} 47 | https://github.com/me-no-dev/ESPAsyncTCP.git 48 | lib_ignore = 49 | 50 | build_flags = 51 | ${env.build_flags} 52 | -D USE_PCF8574=1 53 | -D USE_TB6612=1 54 | -------------------------------------------------------------------------------- /scripts/build_flags.py: -------------------------------------------------------------------------------- 1 | import subprocess; 2 | import sys; 3 | import os; 4 | 5 | def git_branch(): 6 | print('-D GIT_BRANCH=\\"%s\\"' % subprocess.check_output(['git', 'rev-parse', '--abbrev-ref', 'HEAD']).strip().decode()) 7 | 8 | def git_repo(): 9 | output = subprocess.check_output(['git', 'rev-parse', '--show-toplevel']) 10 | repo = os.path.basename(output).strip().decode() 11 | print('-D GIT_REPO=\\"%s\\"' % repo); 12 | 13 | 14 | 15 | if __name__ == '__main__': 16 | globals()[sys.argv[1]]() 17 | 18 | 19 | -------------------------------------------------------------------------------- /scripts/prepareDataDir.py: -------------------------------------------------------------------------------- 1 | Import("env"); 2 | import sys, os, re; 3 | from shutil import copytree; 4 | 5 | if (re.match(r".*ESP32.*", env["PIOENV"])): 6 | print("prepareDataDir.py: ESP32 detected"); 7 | esptype = "esp32" 8 | elif (re.match(r".*ESP8266.*", env["PIOENV"])): 9 | print("prepareDataDir.py: ESP8266 detected"); 10 | esptype = "esp8266" 11 | else: 12 | print("dont match any esp"); 13 | exit; 14 | 15 | if (esptype): 16 | data_master_dir = "esp_files"; 17 | data_dir = "data/web/esp"; 18 | 19 | if (os.path.exists(data_master_dir +"/"+ esptype)): 20 | copytree(data_master_dir +"/"+ esptype + "/" , data_dir, dirs_exist_ok=True); 21 | print("copy :<" + data_master_dir +"/"+ esptype + "> to <" + data_dir + ">"); 22 | else: 23 | print("path not exists: " + data_master_dir +"/"+ esptype + "/"); -------------------------------------------------------------------------------- /src/CommonLibs.h: -------------------------------------------------------------------------------- 1 | #ifndef COMMONLIBS_H 2 | #define COMMONLIBS_H 3 | 4 | #if defined(ARDUINO) && ARDUINO >= 100 5 | #include "Arduino.h" 6 | #else 7 | #include "WProgram.h" 8 | #endif 9 | 10 | #pragma once 11 | 12 | #ifdef ESP8266 13 | extern "C" { 14 | #include "user_interface.h" 15 | } 16 | 17 | #include 18 | #include 19 | #elif ESP32 20 | #include 21 | #include 22 | #include 23 | #endif 24 | 25 | #include 26 | #include 27 | #include 28 | #include 29 | #include 30 | 31 | #if defined(USE_PCF8574) || defined(USE_TB6612) 32 | #define USE_I2C 33 | #endif 34 | 35 | #ifdef ESP8266 36 | #define UPDATE_URL "http://tfa-releases.s3-website.eu-central-1.amazonaws.com/ESP8266_PumpControl/releases_ESP8266.json"; 37 | #define MY_ARCH "ESP8266" 38 | #elif ARDUINO_ESP32_DEV 39 | #define UPDATE_URL "http://tfa-releases.s3-website.eu-central-1.amazonaws.com/ESP8266_PumpControl/releases_ESP32.json"; 40 | #define MY_ARCH "ESP32" 41 | #elif ARDUINO_ESP32S2_DEV 42 | #define UPDATE_URL "http://tfa-releases.s3-website.eu-central-1.amazonaws.com/ESP8266_PumpControl/releases_ESP32-S2.json"; 43 | #define MY_ARCH "ESP32-S2" 44 | #elif ARDUINO_ESP32S3_DEV 45 | #define UPDATE_URL "http://tfa-releases.s3-website.eu-central-1.amazonaws.com/ESP8266_PumpControl/releases_ESP32-S3.json"; 46 | #define MY_ARCH "ESP32-S3" 47 | #elif ARDUINO_ESP32C3_DEV 48 | #define UPDATE_URL "http://tfa-releases.s3-website.eu-central-1.amazonaws.com/ESP8266_PumpControl/releases_ESP32-C3.json"; 49 | #define MY_ARCH "ESP32-C3" 50 | #endif 51 | 52 | #ifdef ESP8266 53 | #define ESP_getChipId() ESP.getChipId() 54 | #define ESP_GetMaxFreeAvailableBlock() ESP.getMaxFreeBlockSize() 55 | #define ARCH "ESP32" 56 | #elif ESP32 57 | #define ESP_getChipId() (uint32_t)ESP.getEfuseMac() // Unterschied zu ESP.getFlashChipId() ??? 58 | #define ESP_GetMaxFreeAvailableBlock() ESP.getMaxAllocHeap() 59 | #define ARCH "ESP8266" 60 | #endif 61 | 62 | 63 | #endif -------------------------------------------------------------------------------- /src/MyWebServer.cpp: -------------------------------------------------------------------------------- 1 | #include "MyWebServer.h" 2 | 3 | MyWebServer::MyWebServer(AsyncWebServer *server, DNSServer* dns): server(server), dns(dns), DoReboot(false) { 4 | 5 | fsfiles = new handleFiles(server); 6 | 7 | server->begin(); 8 | 9 | server->onNotFound(std::bind(&MyWebServer::handleNotFound, this, std::placeholders::_1)); 10 | server->on("/", HTTP_GET, std::bind(&MyWebServer::handleRoot, this, std::placeholders::_1)); 11 | server->on("/reboot", HTTP_GET, std::bind(&MyWebServer::handleReboot, this, std::placeholders::_1)); 12 | server->on("/reset", HTTP_GET, std::bind(&MyWebServer::handleReset, this, std::placeholders::_1)); 13 | server->on("/wifireset", HTTP_GET, std::bind(&MyWebServer::handleWiFiReset, this, std::placeholders::_1)); 14 | 15 | server->on("/parameter.js", HTTP_GET, std::bind(&MyWebServer::handleJSParam, this, std::placeholders::_1)); 16 | server->on("/ajax", HTTP_POST, std::bind(&MyWebServer::handleAjax, this, std::placeholders::_1)); 17 | server->on("/update", HTTP_POST, std::bind(&MyWebServer::handle_update_response, this, std::placeholders::_1), 18 | std::bind(&MyWebServer::handle_update_progress, this, std::placeholders::_1, 19 | std::placeholders::_2, 20 | std::placeholders::_3, 21 | std::placeholders::_4, 22 | std::placeholders::_5, 23 | std::placeholders::_6)); 24 | 25 | server->on("^/(.+).(css|js|html|json)$", HTTP_GET, std::bind(&MyWebServer::handleRequestFiles, this, std::placeholders::_1)); 26 | 27 | dbg.println(F("WebServer started...")); 28 | } 29 | 30 | void MyWebServer::handle_update_response(AsyncWebServerRequest *request) { 31 | request->send(LittleFS, "/web/update_response.html", "text/html"); 32 | } 33 | 34 | void MyWebServer::handle_update_progress(AsyncWebServerRequest *request, String filename, size_t index, uint8_t *data, size_t len, bool final) { 35 | 36 | if(!index){ 37 | dbg.printf("Update Start: %s\n", filename.c_str()); 38 | #ifdef ESP8266 39 | Update.runAsync(true); 40 | #endif 41 | /* 42 | if (filename == "filesystem") { 43 | if(!Update.begin(LittleFS.totalBytes(), U_SPIFFS)) { 44 | Update.printError(Serial); 45 | } 46 | } else { 47 | */ 48 | //content_len = request->contentLength() 49 | if(!Update.begin((ESP.getFreeSketchSpace() - 0x1000) & 0xFFFFF000), U_FLASH){ 50 | Update.printError(Serial); 51 | } 52 | //} 53 | } 54 | if(!Update.hasError()){ 55 | if(Update.write(data, len) != len){ 56 | Update.printError(Serial); 57 | } 58 | } else { 59 | Serial.printf("Progress: %d%%\n", (Update.progress()*100)/Update.size()); 60 | } 61 | 62 | if(final){ 63 | if(Update.end(true)){ 64 | dbg.printf("Update Success: %u Bytes\n", index+len); 65 | this->DoReboot = true;//Set flag so main loop can issue restart call 66 | } else { 67 | Update.printError(Serial); 68 | } 69 | } 70 | } 71 | 72 | void MyWebServer::loop() { 73 | //delay(1); // slow response Issue: https://github.com/espressif/arduino-esp32/issues/4348#issuecomment-695115885 74 | if (this->DoReboot) { 75 | dbg.println("Rebooting..."); 76 | delay(100); 77 | ESP.restart(); 78 | } 79 | } 80 | 81 | void MyWebServer::handleNotFound(AsyncWebServerRequest *request) { 82 | request->send(404, "text/plain", "404: Not found"); // Send HTTP status 404 (Not Found) when there's no handler for the URI in the request 83 | } 84 | 85 | void MyWebServer::handleRoot(AsyncWebServerRequest *request) { 86 | request->redirect("/web/index.html"); 87 | } 88 | 89 | void MyWebServer::handleRequestFiles(AsyncWebServerRequest *request) { 90 | if (Config->GetDebugLevel() >=3) { 91 | dbg.printf("Request file %s", ("/" + request->pathArg(0) + "." + request->pathArg(1)).c_str()); dbg.println(); 92 | } 93 | 94 | File f = LittleFS.open("/" + request->pathArg(0) + "." + request->pathArg(1), "r"); 95 | 96 | if (!f) { 97 | if (Config->GetDebugLevel() >=0) {dbg.printf("failed to open requested file: %s.%s", request->pathArg(0).c_str(), request->pathArg(1).c_str());} 98 | request->send(404, "text/plain", "404: Not found"); 99 | return; 100 | } 101 | 102 | f.close(); 103 | 104 | if (request->pathArg(1) == "css") { 105 | request->send(LittleFS, "/" + request->pathArg(0) + "." + request->pathArg(1), "text/css"); 106 | } else if (request->pathArg(1) == "js") { 107 | request->send(LittleFS, "/" + request->pathArg(0) + "." + request->pathArg(1), "text/javascript"); 108 | } else if (request->pathArg(1) == "html") { 109 | request->send(LittleFS, "/" + request->pathArg(0) + "." + request->pathArg(1), "text/html"); 110 | } else if (request->pathArg(1) == "json") { 111 | request->send(LittleFS, "/" + request->pathArg(0) + "." + request->pathArg(1), "text/json"); 112 | } else { 113 | request->send(LittleFS, "/" + request->pathArg(0) + "." + request->pathArg(1), "text/plain"); 114 | } 115 | 116 | } 117 | 118 | void MyWebServer::handleReboot(AsyncWebServerRequest *request) { 119 | request->send(LittleFS, "/web/reboot.html", "text/html"); 120 | this->DoReboot = true; 121 | } 122 | 123 | void MyWebServer::handleReset(AsyncWebServerRequest *request) { 124 | if (Config->GetDebugLevel() >= 3) { dbg.println("deletion of all config files was requested ...."); } 125 | //LittleFS.format(); // Werkszustand -> nur die config dateien loeschen, die web dateien muessen erhalten bleiben 126 | File root = LittleFS.open("/", "r"); 127 | File file = root.openNextFile(); 128 | while(file){ 129 | String path("/"); path.concat(file.name()); 130 | if (path.indexOf(".json") == -1) {dbg.println("Continue"); file = root.openNextFile(); continue;} 131 | file.close(); 132 | bool rm = LittleFS.remove(path); 133 | if (Config->GetDebugLevel() >= 3) { 134 | dbg.printf("deletion of configuration file '%s' %s\n", file.name(), (rm?"was successful":"has failed"));; 135 | } 136 | file = root.openNextFile(); 137 | } 138 | root.close(); 139 | 140 | this->handleReboot(request); 141 | } 142 | 143 | void MyWebServer::handleWiFiReset(AsyncWebServerRequest *request) { 144 | #ifdef ESP32 145 | WiFi.disconnect(true,true); 146 | #elif ESP8266 147 | ESP.eraseConfig(); 148 | #endif 149 | 150 | this->handleReboot(request); 151 | } 152 | 153 | void MyWebServer::handleJSParam(AsyncWebServerRequest *request) { 154 | AsyncResponseStream *response = request->beginResponseStream("text/javascript"); 155 | response->addHeader("Server","ESP Async Web Server"); 156 | 157 | VStruct->getWebJsParameter(response); 158 | request->send(response); 159 | } 160 | 161 | void MyWebServer::handleAjax(AsyncWebServerRequest *request) { 162 | char buffer[100] = {0}; 163 | memset(buffer, 0, sizeof(buffer)); 164 | String ret = (char*)0; 165 | bool RaiseError = false; 166 | String action, subaction, newState; 167 | String json = "{}"; 168 | uint8_t port = 0; 169 | 170 | AsyncResponseStream *response = request->beginResponseStream("text/json"); 171 | response->addHeader("Server","ESP Async Web Server"); 172 | 173 | if(request->hasArg("json")) { 174 | json = request->arg("json"); 175 | } 176 | 177 | JsonDocument jsonGet; 178 | DeserializationError error = deserializeJson(jsonGet, json.c_str()); 179 | 180 | JsonDocument jsonReturn; 181 | jsonReturn["response"].to(); 182 | 183 | if (Config->GetDebugLevel() >=4) { dbg.print("Ajax Json Empfangen: "); } 184 | if (!error) { 185 | if (Config->GetDebugLevel() >=4) { serializeJsonPretty(jsonGet, dbg); dbg.println(); } 186 | 187 | if (jsonGet.containsKey("action")) {action = jsonGet["action"].as();} 188 | if (jsonGet.containsKey("subaction")){subaction = jsonGet["subaction"].as();} 189 | if (jsonGet.containsKey("newState")) { newState = jsonGet["newState"].as(); } 190 | if (jsonGet.containsKey("port")) { port = jsonGet["port"].as(); } 191 | 192 | } else { 193 | snprintf(buffer, sizeof(buffer), "Ajax Json Command not parseable: %s -> %s", json.c_str(), error.c_str()); 194 | RaiseError = true; 195 | } 196 | 197 | if (RaiseError) { 198 | jsonReturn["response"]["status"] = 0; 199 | jsonReturn["response"]["text"] = buffer; 200 | serializeJson(jsonReturn, ret); 201 | response->print(ret); 202 | 203 | if (Config->GetDebugLevel() >=2) { 204 | dbg.println(FPSTR(buffer)); 205 | } 206 | 207 | return; 208 | 209 | } else if(action && action == "GetInitData") { 210 | if (subaction && subaction == "status") { 211 | this->GetInitDataStatus(response); 212 | } else if (subaction && subaction == "navi") { 213 | this->GetInitDataNavi(response); 214 | } else if (subaction && subaction == "baseconfig") { 215 | Config->GetInitData(response); 216 | } else if (subaction && subaction == "valveconfig") { 217 | VStruct->GetInitData(response); 218 | }else if (subaction && subaction == "sensorconfig") { 219 | LevelSensor->GetInitData(response); 220 | } 221 | 222 | } else if(action && action == "ReloadConfig") { 223 | if (subaction && subaction == "baseconfig") { 224 | Config->LoadJsonConfig(); 225 | } else if (subaction && subaction == "valveconfig") { 226 | VStruct->LoadJsonConfig(); 227 | } else if (subaction && subaction == "sensorconfig") { 228 | LevelSensor->LoadJsonConfig(); 229 | } 230 | 231 | jsonReturn["response"]["status"] = 1; 232 | jsonReturn["response"]["text"] = "new config reloaded sucessfully"; 233 | serializeJson(jsonReturn, ret); 234 | response->print(ret); 235 | 236 | } else if(action && action == "handlefiles") { 237 | fsfiles->HandleAjaxRequest(jsonGet, response); 238 | 239 | } else if (action && action == "SetValve") { 240 | if (newState && port && port > 0 && !VStruct->GetEnabled(port)) { 241 | jsonReturn["response"]["status"] = 0; 242 | jsonReturn["response"]["text"] = "Requested Port not enabled. Please enable first!"; 243 | serializeJson(jsonReturn, ret); 244 | response->print(ret); 245 | } 246 | else if (newState && port && port > 0 ) { 247 | if (newState == "On") { 248 | VStruct->SetOn(port); 249 | } 250 | if (newState == "Off") { 251 | VStruct->SetOff(port); 252 | } 253 | 254 | jsonReturn["response"]["status"] = 1; 255 | jsonReturn["response"]["text"] =(VStruct->GetState(port)?"Valve is now: ON":"Valve is now: OFF"); 256 | jsonReturn["data"][subaction] = (VStruct->GetState(port)?"Set Off":"Set On"); // subaction = button.id 257 | serializeJson(jsonReturn, ret); 258 | response->print(ret); 259 | } 260 | 261 | 262 | } else if (action && newState && action == "EnableValve") { 263 | if (port && port > 0 && newState) { 264 | if (strcmp(newState.c_str(),"true")==0) VStruct->SetEnable(port, true); 265 | if (strcmp(newState.c_str(),"false")==0) VStruct->SetEnable(port, false); 266 | jsonReturn["response"]["status"] = 1; 267 | jsonReturn["response"]["text"] = (VStruct->GetEnabled(port)?"valve now enabled":"valve now disabled"); 268 | serializeJson(jsonReturn, ret); 269 | response->print(ret); 270 | } 271 | 272 | #ifdef USE_I2C 273 | } else if (action && action == "RefreshI2C") { 274 | I2Cdetect->i2cScan(); 275 | 276 | jsonReturn["data"].to(); 277 | jsonReturn["data"]["showI2C"] = I2Cdetect->i2cGetAddresses(); 278 | jsonReturn["response"]["status"] = 1; 279 | jsonReturn["response"]["text"] = "successful"; 280 | serializeJson(jsonReturn, ret); 281 | response->print(ret); 282 | #endif 283 | 284 | } else { 285 | snprintf(buffer, sizeof(buffer), "Ajax Command unknown: %s - %s", action.c_str(), subaction.c_str()); 286 | jsonReturn["response"]["status"] = 0; 287 | jsonReturn["response"]["text"] = buffer; 288 | serializeJson(jsonReturn, ret); 289 | response->print(ret); 290 | 291 | if (Config->GetDebugLevel() >=1) { 292 | dbg.println(buffer); 293 | } 294 | } 295 | 296 | if (Config->GetDebugLevel() >=4) { dbg.print("Ajax Json Antwort: "); dbg.println(ret); } 297 | 298 | request->send(response); 299 | } 300 | 301 | void MyWebServer::GetInitDataNavi(AsyncResponseStream *response){ 302 | String ret; 303 | JsonDocument json; 304 | json["data"].to(); 305 | json["data"]["hostname"] = Config->GetMqttRoot(); 306 | json["data"]["releasename"] = Config->GetReleaseName(); 307 | json["data"]["releasedate"] = __DATE__; 308 | json["data"]["releasetime"] = __TIME__; 309 | 310 | json["response"].to(); 311 | json["response"]["status"] = 1; 312 | json["response"]["text"] = "successful"; 313 | serializeJson(json, ret); 314 | response->print(ret); 315 | } 316 | 317 | void MyWebServer::GetInitDataStatus(AsyncResponseStream *response) { 318 | String ret; 319 | JsonDocument json; 320 | 321 | json["data"].to(); 322 | json["data"]["ipaddress"] = mqtt->GetIPAddress().toString(); 323 | json["data"]["wifiname"] = (Config->GetUseETH()?"LAN":WiFi.SSID()); 324 | json["data"]["macaddress"] = WiFi.macAddress(); 325 | json["data"]["mqtt_status"] = (mqtt->GetConnectStatusMqtt()?"Connected":"Not Connected"); 326 | json["data"]["uptime"] = uptime_formatter::getUptime(); 327 | json["data"]["freeheapmem"] = ESP.getFreeHeap(); 328 | json["data"]["ValvesCount"] = VStruct->CountActiveThreads(); 329 | 330 | #ifdef USE_I2C 331 | json["data"]["showI2C"] = I2Cdetect->i2cGetAddresses(); 332 | #else 333 | json["data"]["tr_i2c"]["className"] = "hide"; 334 | #endif 335 | 336 | if (LevelSensor->GetType() != NONE && LevelSensor->GetType() != EXTERN) { 337 | json["data"]["SensorRawValue"] = LevelSensor->GetRaw(); 338 | } else { 339 | json["data"]["tr_sensRaw"]["className"] = "hide"; 340 | } 341 | 342 | if (LevelSensor->GetType() != NONE) { 343 | json["data"]["SensorLevel"] = LevelSensor->GetLvl(); 344 | } else { 345 | json["data"]["tr_sensLvl"]["className"] = "hide"; 346 | } 347 | 348 | #ifdef ESP32 349 | json["data"]["rssi"] = (Config->GetUseETH()?ETH.linkSpeed():WiFi.RSSI()), (Config->GetUseETH()?"Mbps":""); 350 | #else 351 | json["data"]["rssi"] = WiFi.RSSI(); 352 | #endif 353 | 354 | json["response"].to(); 355 | json["response"]["status"] = 1; 356 | json["response"]["text"] = "successful"; 357 | 358 | serializeJson(json, ret); 359 | response->print(ret); 360 | } 361 | 362 | -------------------------------------------------------------------------------- /src/MyWebServer.h: -------------------------------------------------------------------------------- 1 | // https://github.com/esp8266/Arduino/issues/3205 2 | // https://github.com/Hieromon/PageBuilder 3 | // https://www.mediaevent.de/tutorial/sonderzeichen.html 4 | // 5 | // https://byte-style.de/2018/01/automatische-updates-fuer-microcontroller-mit-gitlab-und-platformio/ 6 | // https://community.blynk.cc/t/self-updating-from-web-server-http-ota-firmware-for-esp8266-and-esp32/18544 7 | // https://forum.fhem.de/index.php?topic=50628.0 8 | 9 | #ifndef MYWEBSERVER_H 10 | #define MYWEBSERVER_H 11 | 12 | #include "CommonLibs.h" 13 | #include 14 | #include "uptime.h" // https://github.com/YiannisBourkelis/Uptime-Library/ 15 | #include "uptime_formatter.h" 16 | #include "handleFiles.h" 17 | 18 | #include "baseconfig.h" 19 | #include "sensor.h" 20 | #include "valveStructure.h" 21 | 22 | extern sensor* LevelSensor; 23 | extern valveStructure* VStruct; 24 | 25 | #ifdef USE_I2C 26 | extern i2cdetect* I2Cdetect; 27 | #endif 28 | 29 | #ifdef ESP8266 30 | #define ESPGPIO "gpio_esp8266.js" 31 | #elif ESP32 32 | #define ESPGPIO "gpio_esp32.js" 33 | #endif 34 | 35 | class MyWebServer { 36 | 37 | public: 38 | MyWebServer(AsyncWebServer *server, DNSServer* dns); 39 | 40 | void loop(); 41 | 42 | private: 43 | 44 | AsyncWebServer* server; 45 | DNSServer* dns; 46 | 47 | bool DoReboot; 48 | unsigned long RequestRebootTime; 49 | 50 | handleFiles* fsfiles; 51 | 52 | void handle_update_progress(AsyncWebServerRequest *request, String filename, size_t index, uint8_t *data, size_t len, bool final); 53 | void handle_update_response(AsyncWebServerRequest *request); 54 | void handleNotFound(AsyncWebServerRequest *request); 55 | void handleReboot(AsyncWebServerRequest *request); 56 | void handleReset(AsyncWebServerRequest *request); 57 | void handleWiFiReset(AsyncWebServerRequest *request); 58 | void handleRequestFiles(AsyncWebServerRequest *request); 59 | void handleRoot(AsyncWebServerRequest *request); 60 | void handleJSParam(AsyncWebServerRequest *request); 61 | 62 | void handleAjax(AsyncWebServerRequest *request); 63 | void GetInitDataStatus(AsyncResponseStream *response); 64 | void GetInitDataNavi(AsyncResponseStream *response); 65 | 66 | }; 67 | 68 | #endif 69 | -------------------------------------------------------------------------------- /src/README: -------------------------------------------------------------------------------- 1 | 2 | This directory is intended for project header files. 3 | 4 | A header file is a file containing C declarations and macro definitions 5 | to be shared between several project source files. You request the use of a 6 | header file in your project source file (C, C++, etc) located in `src` folder 7 | by including it, with the C preprocessing directive `#include'. 8 | 9 | ```src/main.c 10 | 11 | #include "header.h" 12 | 13 | int main (void) 14 | { 15 | ... 16 | } 17 | ``` 18 | 19 | Including a header file produces the same results as copying the header file 20 | into each source file that needs it. Such copying would be time-consuming 21 | and error-prone. With a header file, the related declarations appear 22 | in only one place. If they need to be changed, they can be changed in one 23 | place, and programs that include the header file will automatically use the 24 | new version when next recompiled. The header file eliminates the labor of 25 | finding and changing all the copies as well as the risk that a failure to 26 | find one copy will result in inconsistencies within a program. 27 | 28 | In C, the usual convention is to give header files names that end with `.h'. 29 | It is most portable to use only letters, digits, dashes, and underscores in 30 | header file names, and at most one dot. 31 | 32 | Read more about using header files in official GCC documentation: 33 | 34 | * Include Syntax 35 | * Include Operation 36 | * Once-Only Headers 37 | * Computed Includes 38 | 39 | https://gcc.gnu.org/onlinedocs/cpp/Header-Files.html 40 | -------------------------------------------------------------------------------- /src/TB6612.cpp: -------------------------------------------------------------------------------- 1 | #include "TB6612.h" 2 | 3 | tb6612::tb6612() { 4 | } 5 | 6 | void tb6612::init(uint8_t address) { 7 | M1 = new Motor(address,_MOTOR_A, 1000); 8 | M2 = new Motor(address,_MOTOR_B, 1000); 9 | dbg.println("TB6612 initialize"); 10 | } 11 | 12 | void tb6612::setOff(uint8_t port) { 13 | if (port==0) { 14 | M1->setmotor(_STOP); 15 | } else if (port==1) { 16 | M2->setmotor(_STOP); 17 | } 18 | //dbg.println("Motor Stop"); 19 | } 20 | 21 | void tb6612::setOn(uint8_t port, bool dir) { 22 | if (port==0) { 23 | M1->setmotor( (dir?_CW:_CCW)); 24 | } else if (port==1) { 25 | M2->setmotor( (dir?_CW:_CCW)); 26 | } 27 | //dbg.println("Motor On"); 28 | } 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /src/TB6612.h: -------------------------------------------------------------------------------- 1 | #ifndef TB6612_H 2 | #define TB6612_H 3 | 4 | #include "CommonLibs.h" 5 | #include "WEMOS_Motor.h" 6 | 7 | class tb6612 { 8 | 9 | public: 10 | tb6612(); 11 | void init(uint8_t address); 12 | void setOn(uint8_t port, bool dir); // Port: A=0; B=1 ; Direction: true=forward; false=backward 13 | void setOff(uint8_t port); 14 | 15 | private: 16 | Motor* M1; //Motor A 17 | Motor* M2; //Motor B 18 | }; 19 | 20 | #endif 21 | -------------------------------------------------------------------------------- /src/baseconfig.cpp: -------------------------------------------------------------------------------- 1 | #include "baseconfig.h" 2 | 3 | BaseConfig::BaseConfig(): 4 | mqtt_server ("test.mosquitto.org"), 5 | mqtt_port(1883), 6 | mqtt_root("PumpControl"), 7 | mqtt_basepath("home/"), 8 | mqtt_UseRandomClientID(true), 9 | keepalive(0), 10 | debuglevel(3), 11 | enable_3wege(false), 12 | ventil3wege_port(0), 13 | max_parallel(0), 14 | useETH(0) 15 | { 16 | 17 | #ifdef ESP8266 18 | this->pin_sda = 5; 19 | this->pin_scl = 4; 20 | #elif ESP32 21 | this->pin_sda = 21; 22 | this->pin_scl = 22, 23 | #endif 24 | 25 | LoadJsonConfig(); 26 | } 27 | 28 | void BaseConfig::LoadJsonConfig() { 29 | if (LittleFS.exists("/baseconfig.json")) { 30 | //file exists, reading and loading 31 | dbg.println(F("reading baseconfig.json file")); 32 | File configFile = LittleFS.open("/baseconfig.json", "r"); 33 | if (configFile) { 34 | if (this->GetDebugLevel() >=3) dbg.println(F("baseconfig.json is now open")); 35 | ReadBufferingStream stream{configFile, 64}; 36 | stream.find("\"data\":["); 37 | do { 38 | 39 | JsonDocument elem; 40 | DeserializationError error = deserializeJson(elem, stream); 41 | if (error) { 42 | if (this->GetDebugLevel() >=1) { 43 | dbg.printf("Failed to parse baseconfig.json data: %s, load default config\n", error.c_str()); 44 | } 45 | } else { 46 | // Print the result 47 | if (this->GetDebugLevel() >=5) {dbg.println(F("parsing partial JSON of baseconfig.json ok")); } 48 | if (this->GetDebugLevel() >=5) {serializeJsonPretty(elem, dbg);} 49 | 50 | if (elem.containsKey("mqttroot")) { this->mqtt_root = elem["mqttroot"].as();} 51 | if (elem.containsKey("mqttserver")) { this->mqtt_server = elem["mqttserver"].as();} 52 | if (elem.containsKey("mqttport")) { this->mqtt_port = elem["mqttport"].as();} 53 | if (elem.containsKey("mqttuser")) { this->mqtt_username = elem["mqttuser"].as();} 54 | if (elem.containsKey("mqttpass")) { this->mqtt_password = elem["mqttpass"].as();} 55 | if (elem.containsKey("mqttbasepath")) { this->mqtt_basepath = elem["mqttbasepath"].as();} 56 | if (elem.containsKey("sel_UseRandomClientID")){ if (strcmp(elem["sel_UseRandomClientID"], "none")==0) { this->mqtt_UseRandomClientID=false;} else {this->mqtt_UseRandomClientID=true;}} 57 | if (elem.containsKey("keepalive")) { if (elem["keepalive"].as() == 0) { this->keepalive = 0;} else { this->keepalive = _max(elem["keepalive"].as(), 10);}} 58 | if (elem.containsKey("debuglevel")) { this->debuglevel = _max(elem["debuglevel"].as(), 0);} 59 | if (elem.containsKey("pinsda")) { this->pin_sda = (elem["pinsda"].as()) - 200;} 60 | if (elem.containsKey("pinscl")) { this->pin_scl = (elem["pinscl"].as()) - 200;} 61 | if (elem.containsKey("sel_3wege")) { if (strcmp(elem["sel_3wege"], "none")==0) { this->enable_3wege=false;} else {this->enable_3wege=true;}} 62 | if (elem.containsKey("autoupdate_url")) { this->autoupdate_url = elem["autoupdate_url"].as(); } 63 | if (elem.containsKey("ventil3wege_port")) { this->ventil3wege_port = elem["ventil3wege_port"].as();} 64 | } 65 | } while (stream.findUntil(",","]")); 66 | } else { 67 | dbg.println("cannot open existing baseconfig.json config File, load default BaseConfig"); // -> constructor 68 | } 69 | } else { 70 | dbg.println("baseconfig.json config File not exists, load default BaseConfig"); 71 | } 72 | 73 | if (!this->autoupdate_url || this->autoupdate_url.length() < 10 ) { 74 | this->autoupdate_url = UPDATE_URL; 75 | } 76 | 77 | // Data Cleaning 78 | if(this->mqtt_basepath.endsWith("/")) { 79 | this->mqtt_basepath = this->mqtt_basepath.substring(0, this->mqtt_basepath.length()-1); 80 | } 81 | } 82 | 83 | const String BaseConfig::GetReleaseName() { 84 | return String(Release) + "(@" + GIT_BRANCH + ")"; 85 | } 86 | 87 | void BaseConfig::loop() { 88 | } 89 | 90 | /* https://cpp4arduino.com/2018/11/06/what-is-heap-fragmentation.html*/ 91 | size_t BaseConfig::getFragmentation() { 92 | return 100 - ESP_GetMaxFreeAvailableBlock() * 100 / ESP.getFreeHeap(); 93 | } 94 | 95 | void BaseConfig::GetInitData(AsyncResponseStream *response) { 96 | String ret; 97 | JsonDocument json; 98 | 99 | json["data"].to(); 100 | json["data"]["arch"] = ARCH; 101 | json["data"]["mqttroot"] = this->mqtt_root; 102 | json["data"]["mqttserver"] = this->mqtt_server; 103 | json["data"]["mqttport"] = this->mqtt_port; 104 | json["data"]["mqttuser"] = this->mqtt_username; 105 | json["data"]["mqttpass"] = this->mqtt_password; 106 | json["data"]["mqttbasepath"]= this->mqtt_basepath; 107 | json["data"]["debuglevel"] = this->debuglevel; 108 | json["data"]["sel_URCID1"] = ((this->mqtt_UseRandomClientID)?0:1); 109 | json["data"]["sel_URCID2"] = ((this->mqtt_UseRandomClientID)?1:0); 110 | json["data"]["keepalive"] = this->keepalive; 111 | 112 | #ifdef ESP32 113 | json["data"]["sel_wifi"] = ((this->useETH)?0:1); 114 | json["data"]["sel_eth"] = ((this->useETH)?1:0); 115 | #else 116 | json["data"]["tr_LAN"]["className"] = "hide"; 117 | json["data"]["SelectLAN"]["className"] = "hide"; 118 | #endif 119 | 120 | #ifdef USE_I2C 121 | json["data"]["GpioPin_0"] = this->pin_sda + 200; 122 | json["data"]["GpioPin_1"] = this->pin_scl + 200; 123 | #else 124 | json["data"]["tr_sda"]["className"] = "hide"; 125 | json["data"]["tr_scl"]["className"] = "hide"; 126 | #endif 127 | 128 | json["data"]["sel_3wege_0"] = ((this->enable_3wege)?0:1); 129 | json["data"]["sel_3wege_1"] = ((this->enable_3wege)?1:0); 130 | json["data"]["ConfiguredPort_0"] = this->ventil3wege_port; 131 | json["js"]["update_url"] = this->autoupdate_url; 132 | 133 | json["response"].to(); 134 | json["response"]["status"] = 1; 135 | json["response"]["text"] = "successful"; 136 | serializeJson(json, ret); 137 | response->print(ret); 138 | } 139 | -------------------------------------------------------------------------------- /src/baseconfig.h: -------------------------------------------------------------------------------- 1 | #ifndef BASECONFIG_H 2 | #define BASECONFIG_H 3 | 4 | #include "CommonLibs.h" 5 | #include "ArduinoJson.h" 6 | #include "_Release.h" 7 | 8 | class BaseConfig { 9 | 10 | public: 11 | BaseConfig(); 12 | void LoadJsonConfig(); 13 | void loop(); 14 | 15 | const uint8_t& GetPinSDA() const {return pin_sda;} 16 | const uint8_t& GetPinSCL() const {return pin_scl;} 17 | const String& GetMqttServer() const {return mqtt_server;} 18 | const uint16_t& GetMqttPort() const {return mqtt_port;} 19 | const String& GetMqttUsername()const {return mqtt_username;} 20 | const String& GetMqttPassword()const {return mqtt_password;} 21 | const String& GetMqttBasePath() const {return mqtt_basepath;} 22 | const String& GetMqttRoot() const {return mqtt_root;} 23 | const bool& UseRandomMQTTClientID() const { return mqtt_UseRandomClientID; } 24 | const uint8_t& Get3WegePort() const {return ventil3wege_port;} 25 | const bool& Enabled3Wege() const {return enable_3wege;} 26 | const uint8_t& GetMaxParallel() const {return max_parallel;} 27 | const uint16_t& GetKeepAlive() const {return keepalive;} 28 | const uint8_t& GetDebugLevel() const {return debuglevel;} 29 | const bool& GetUseETH() const { return useETH; } 30 | void GetInitData(AsyncResponseStream* response); 31 | const String& GetLANBoard() const {return LANBoard;} 32 | const String GetReleaseName(); 33 | size_t getFragmentation(); 34 | 35 | private: 36 | String mqtt_server; 37 | String mqtt_username; 38 | String mqtt_password; 39 | uint16_t mqtt_port; 40 | String mqtt_root; 41 | String mqtt_basepath; 42 | bool mqtt_UseRandomClientID; 43 | uint16_t keepalive; 44 | uint8_t debuglevel; 45 | uint8_t pin_sda; 46 | uint8_t pin_scl; 47 | bool enable_3wege; // wechsel Regen- /Trinkwasser 48 | uint8_t ventil3wege_port; // Portnummer des Ventils 49 | uint8_t max_parallel; 50 | String autoupdate_url; 51 | bool useETH; // otherwise use WIFI 52 | String LANBoard; 53 | }; 54 | 55 | extern BaseConfig* Config; 56 | 57 | #endif 58 | -------------------------------------------------------------------------------- /src/handleFiles.cpp: -------------------------------------------------------------------------------- 1 | #include "handleFiles.h" 2 | 3 | handleFiles::handleFiles(AsyncWebServer *server) { 4 | 5 | server->on("/doUpload", HTTP_POST, [](AsyncWebServerRequest *request) {}, 6 | std::bind(&handleFiles::handleUpload, this, std::placeholders::_1, 7 | std::placeholders::_2, 8 | std::placeholders::_3, 9 | std::placeholders::_4, 10 | std::placeholders::_5, 11 | std::placeholders::_6)); 12 | 13 | } 14 | 15 | //############################################################### 16 | // returns the complete folder structure 17 | //############################################################### 18 | void handleFiles::getDirList(JsonArray* json, String path) { 19 | JsonDocument doc; 20 | JsonObject jsonRoot = doc.to(); 21 | 22 | jsonRoot["path"] = path; 23 | JsonArray content = jsonRoot["content"].to(); 24 | 25 | File FSroot = LittleFS.open(path, "r"); 26 | File file = FSroot.openNextFile(); 27 | 28 | while (file) { 29 | JsonDocument doc1; 30 | JsonObject jsonObj = doc1.to(); 31 | String fname(file.name()); 32 | jsonObj["name"] = fname; 33 | 34 | if(file.isDirectory()){ 35 | jsonObj["isDir"] = 1; 36 | String p = path + "/" + fname; 37 | if (p.startsWith("//")) { p = p.substring(1); } 38 | this->getDirList(json, p); // recursive call 39 | } else { 40 | jsonObj["isDir"] = 0; 41 | } 42 | 43 | content.add(jsonObj); 44 | file.close(); 45 | file = FSroot.openNextFile(); 46 | } 47 | FSroot.close(); 48 | json->add(jsonRoot); 49 | } 50 | 51 | //############################################################### 52 | // returns the requested data via AJAX from Webserver.cpp 53 | //############################################################### 54 | void handleFiles::HandleAjaxRequest(JsonDocument& jsonGet, AsyncResponseStream* response) { 55 | String subaction = ""; 56 | if (jsonGet.containsKey("subaction")) {subaction = jsonGet["subaction"].as();} 57 | 58 | if (Config->GetDebugLevel() >= 3) { 59 | dbg.printf("handle Ajax Request in handleFiles.cpp: %s\n", subaction.c_str()); 60 | } 61 | 62 | if (subaction == "listDir") { 63 | JsonDocument doc; 64 | JsonArray content = doc.add(); 65 | 66 | this->getDirList(&content, "/"); 67 | String ret(""); 68 | serializeJson(content, ret); 69 | if (Config->GetDebugLevel() >= 5) { 70 | serializeJsonPretty(content, dbg); 71 | dbg.println(); 72 | } 73 | response->print(ret); 74 | } else if (subaction == "deleteFile") { 75 | String filename(""), ret(""); 76 | JsonDocument jsonReturn; 77 | 78 | if (Config->GetDebugLevel() >=3) { 79 | dbg.printf("Request to delete file %s", filename.c_str()); 80 | } 81 | if (jsonGet.containsKey("filename")) {filename = jsonGet["filename"].as();} 82 | 83 | if (LittleFS.remove(filename)) { 84 | jsonReturn["response_status"] = 1; 85 | jsonReturn["response_text"] = "deletion successful"; 86 | } else { 87 | jsonReturn["response_status"] = 0; 88 | jsonReturn["response_text"] = "deletion failed"; 89 | } 90 | if (Config->GetDebugLevel() >=3) { 91 | serializeJson(jsonReturn, Serial);dbg.println(); 92 | } 93 | serializeJson(jsonReturn, ret); 94 | response->print(ret); 95 | } 96 | } 97 | 98 | //############################################################### 99 | // store a file at Filesystem 100 | //############################################################### 101 | void handleFiles::handleUpload(AsyncWebServerRequest *request, String filename, size_t index, uint8_t *data, size_t len, bool final) { 102 | 103 | if (Config->GetDebugLevel() >=5) { 104 | dbg.printf("Client: %s %s\n", request->client()->remoteIP().toString().c_str(), request->url().c_str());; 105 | } 106 | 107 | if (!index) { 108 | // open the file on first call and store the file handle in the request object 109 | request->_tempFile = LittleFS.open(filename, "w"); 110 | if (Config->GetDebugLevel() >=5) { 111 | dbg.printf("Upload Start: %s\n", filename.c_str()); 112 | } 113 | } 114 | 115 | if (len) { 116 | // stream the incoming chunk to the opened file 117 | request->_tempFile.write(data, len); 118 | if (Config->GetDebugLevel() >=5) { 119 | dbg.printf("Writing file: %s ,index=%d len=%d bytes, FreeMem: %d\n", filename.c_str(), index, len, ESP.getFreeHeap()); 120 | } 121 | } 122 | 123 | if (final) { 124 | // close the file handle as the upload is now done 125 | request->_tempFile.close(); 126 | if (Config->GetDebugLevel() >=3) { 127 | dbg.printf("Upload Complete: %s ,size: %d Bytes\n", filename.c_str(), (index + len)); 128 | } 129 | 130 | AsyncResponseStream *response = request->beginResponseStream("text/json"); 131 | response->addHeader("Server","ESP Async Web Server"); 132 | 133 | JsonDocument jsonReturn; 134 | String ret; 135 | 136 | jsonReturn["status"] = 1; 137 | jsonReturn["text"] = "OK"; 138 | 139 | serializeJson(jsonReturn, ret); 140 | response->print(ret); 141 | request->send(response); 142 | 143 | if (Config->GetDebugLevel() >=5) { 144 | serializeJson(jsonReturn, Serial); 145 | } 146 | } 147 | } -------------------------------------------------------------------------------- /src/handleFiles.h: -------------------------------------------------------------------------------- 1 | #ifndef HANDLEFILES_H 2 | #define HANDLEFILES_H 3 | 4 | #include "CommonLibs.h" 5 | #include "baseconfig.h" 6 | 7 | class handleFiles { 8 | public: 9 | handleFiles(AsyncWebServer *server); 10 | 11 | void HandleAjaxRequest(JsonDocument& jsonGet, AsyncResponseStream* response); 12 | void handleUpload(AsyncWebServerRequest *request, String filename, size_t index, uint8_t *data, size_t len, bool final); 13 | 14 | private: 15 | void getDirList(JsonArray* json, String path); 16 | }; 17 | 18 | #endif 19 | -------------------------------------------------------------------------------- /src/main.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include "CommonLibs.h" 3 | #include "baseconfig.h" 4 | #include "mqtt.h" 5 | #include "MyWebServer.h" 6 | #include "sensor.h" 7 | 8 | 9 | #ifdef USE_I2C 10 | i2cdetect* I2Cdetect = NULL; 11 | #endif 12 | 13 | AsyncWebServer server(80); 14 | DNSServer dns; 15 | 16 | BaseConfig* Config = NULL; 17 | valveStructure* VStruct = NULL; 18 | MQTT* mqtt = NULL; 19 | sensor* LevelSensor = NULL; 20 | MyWebServer* mywebserver = NULL; 21 | 22 | /* debugmodes --> in der WebUI -> Basisconfig einstellbar 23 | 0 -> nothing 24 | 1 -> major and criticals 25 | 2 -> majors 26 | 3 -> standard 27 | 4 -> more details, plus: available RAM, RSSI via MQTT, WiFi Credentials via Serial 28 | 5 -> max details 29 | */ 30 | 31 | void myMQTTCallBack(char* topic, byte* payload, unsigned int length) { 32 | String msg; 33 | 34 | for (u_int16_t i = 0; i < length; i++) { 35 | msg.concat((char)payload[i]); 36 | } 37 | 38 | if (Config->GetDebugLevel() >= 4) { 39 | dbg.printf("Message arrived [%s]\nMessage: %s\n", topic, msg.c_str()); 40 | } 41 | 42 | if (LevelSensor->GetExternalSensor() && (strcmp(LevelSensor->GetExternalSensor().c_str(), topic)==0)) { 43 | LevelSensor->SetLvl(atoi(msg.c_str())); 44 | } 45 | else if (strstr(topic, "/raw") || strstr(topic, "/level") || strstr(topic, "/mem") || strstr(topic, "/rssi")) { 46 | /*SensorMeldungen - ignore!*/ 47 | } 48 | else { 49 | VStruct->ReceiveMQTT((String)topic, atoi(msg.c_str())); 50 | } 51 | } 52 | 53 | void setup() { 54 | Serial.begin(115200); 55 | Serial.println(""); 56 | Serial.println("ready"); 57 | 58 | #ifdef ESP8266 59 | LittleFS.begin(); 60 | #elif ESP32 61 | LittleFS.begin(true); // true: format LittleFS/NVS if mount fails 62 | #endif 63 | 64 | // Flash Write Issue 65 | // https://github.com/esp8266/Arduino/issues/4061#issuecomment-428007580 66 | //LittleFS.format(); 67 | 68 | Config = new BaseConfig(); 69 | //WebSerial.onMessage([](const String& msg) { Serial.println(msg); }); // dont works, workarround by using dbg definition in platformio.ini 70 | //WebSerial.begin(&server); 71 | 72 | #ifdef USE_I2C 73 | dbg.printf("Starting WIRE at (SDA, SCL)): %d, %d \n", Config->GetPinSDA(), Config->GetPinSCL()); 74 | Wire.begin(Config->GetPinSDA(), Config->GetPinSCL()); 75 | #endif 76 | 77 | dbg.println("Starting Wifi and MQTT"); 78 | mqtt = new MQTT(&server, &dns, 79 | Config->GetMqttServer().c_str(), 80 | Config->GetMqttPort(), 81 | Config->GetMqttBasePath().c_str(), 82 | Config->GetMqttRoot().c_str(), 83 | (char*)"AP_PumpControl", 84 | (char*)"password" 85 | ); 86 | 87 | mqtt->setCallback(myMQTTCallBack); 88 | 89 | #ifdef USE_I2C 90 | dbg.println("Starting I2CDetect"); 91 | I2Cdetect = new i2cdetect(Config->GetPinSDA(), Config->GetPinSCL()); 92 | #endif 93 | 94 | dbg.println("Starting Sensor"); 95 | LevelSensor = new sensor(); 96 | 97 | dbg.println("Starting Valve Structure"); 98 | VStruct = new valveStructure(Config->GetPinSDA(), Config->GetPinSCL()); 99 | 100 | dbg.println("Starting WebServer"); 101 | mywebserver = new MyWebServer(&server, &dns); 102 | 103 | //VStruct->OnForTimer("Valve1", 10); // Test 104 | 105 | dbg.println("Setup finished"); 106 | } 107 | 108 | void loop() { 109 | VStruct->loop(); 110 | mqtt->loop(); 111 | LevelSensor->loop(); 112 | mywebserver->loop(); 113 | Config->loop(); 114 | } 115 | -------------------------------------------------------------------------------- /src/mqtt.h: -------------------------------------------------------------------------------- 1 | #ifndef MQTT_H 2 | #define MQTT_H 3 | 4 | #include "CommonLibs.h" 5 | #include 6 | #include // https://github.com/alanswx/ESPAsyncWiFiManager 7 | #include 8 | #include "baseconfig.h" 9 | 10 | #ifdef ESP8266 11 | //#define SetHostName(x) wifi_station_set_hostname(x); 12 | #define ESP_getChipId() ESP.getChipId() 13 | #elif ESP32 14 | #include 15 | //#define SetHostName(x) WiFi.getHostname(x); --> MQTT.cpp TODO 16 | #define ESP_getChipId() (uint32_t)ESP.getEfuseMac() // Unterschied zu ESP.getFlashChipId() ??? 17 | #endif 18 | 19 | 20 | #ifdef ESP32 21 | typedef struct { 22 | String name; 23 | uint8_t PHY_ADDR; 24 | int PHY_POWER; 25 | int PHY_MDC; 26 | int PHY_MDIO; 27 | eth_phy_type_t PHY_TYPE; 28 | eth_clock_mode_t CLK_MODE; 29 | } eth_shield_t; 30 | #elif ESP8266 31 | typedef struct { 32 | String name; 33 | } eth_shield_t; 34 | #endif 35 | 36 | class MQTT: PubSubClient { 37 | 38 | #ifdef ESP32 39 | std::vector lan_shields = {{"WT32-ETH01", 1, 16, 23, 18, ETH_PHY_LAN8720, ETH_CLOCK_GPIO0_IN}, 40 | {"test", 1, 16, 23, 18, ETH_PHY_LAN8720, ETH_CLOCK_GPIO0_IN}}; 41 | #elif ESP8266 42 | std::vector lan_shields = {{"test1"}, 43 | {"test2"}}; 44 | #endif 45 | 46 | public: 47 | 48 | MQTT(AsyncWebServer* server, DNSServer *dns, const char* MqttServer, uint16_t MqttPort, String MqttBasepath, String MqttRoot, char* APName, char* APpassword); 49 | void loop(); 50 | void Publish_Bool(const char* subtopic, bool b, bool fulltopic); 51 | void Publish_Int(const char* subtopic, int number, bool fulltopic); 52 | void Publish_Float(const char* subtopic, float number, bool fulltopic); 53 | void Publish_String(const char* subtopic, String value, bool fulltopic); 54 | void Publish_IP(); 55 | String getTopic(String subtopic, bool fulltopic); 56 | void disconnect(); 57 | const String& GetRoot() const {return mqtt_root;}; 58 | const String& GetBasePath() const {return mqtt_basepath;}; 59 | void Subscribe(String topic); 60 | bool UnSubscribe(String topic); 61 | void ClearSubscriptions(); 62 | 63 | const bool& GetConnectStatusWifi() const {return ConnectStatusWifi;} 64 | const bool& GetConnectStatusMqtt() const {return ConnectStatusMqtt;} 65 | const IPAddress& GetIPAddress() const {return ipadresse;} 66 | 67 | using PubSubClient::setCallback; 68 | 69 | protected: 70 | void reconnect(); 71 | 72 | private: 73 | AsyncWebServer* server; 74 | DNSServer* dns; 75 | WiFiClient espClient; 76 | AsyncWiFiManager* wifiManager; 77 | 78 | std::vector* subscriptions = NULL; 79 | 80 | String mqtt_root = ""; 81 | String mqtt_basepath = ""; 82 | unsigned long mqttreconnect_lasttry = 0; 83 | unsigned long last_keepalive = 0; 84 | bool ConnectStatusWifi; 85 | bool ConnectStatusMqtt; 86 | IPAddress ipadresse; 87 | 88 | #ifdef ESP32 89 | void WifiOnEvent(WiFiEvent_t event); 90 | #endif 91 | 92 | void WaitForConnect(); 93 | 94 | eth_shield_t* GetEthShield(String ShieldName); 95 | }; 96 | 97 | extern MQTT* mqtt; 98 | 99 | #endif 100 | -------------------------------------------------------------------------------- /src/sensor.cpp: -------------------------------------------------------------------------------- 1 | #include "sensor.h" 2 | 3 | sensor::sensor() : 4 | Type(NONE), 5 | measureDistMin(0), 6 | measureDistMax(0), 7 | measurecycle(10), 8 | level(0), 9 | raw(0), 10 | pinTrigger(5), 11 | pinEcho(6), 12 | threshold_min(26), 13 | threshold_max(30), 14 | moistureEnabled(false) { 15 | 16 | #ifdef ESP8266 17 | uint8_t pinAnalogDefault = 0; 18 | #elif ESP32 19 | uint8_t pinAnalogDefault = 36; // ADC1_CH0 (GPIO 36) 20 | #endif 21 | 22 | this->pinAnalog = pinAnalogDefault; 23 | 24 | LoadJsonConfig(); 25 | } 26 | 27 | void sensor::init_analog(uint8_t pinAnalog) { 28 | setSensorType(ONBOARD_ANALOG); 29 | this->pinAnalog = pinAnalog; 30 | this->MAX_DIST=500; // is maximum by default 31 | } 32 | 33 | void sensor::init_hcsr04(uint8_t pinTrigger, uint8_t pinEcho) { 34 | setSensorType(HCSR04); 35 | this->MAX_DIST = 23200; // Anything over 400 cm (400*58 = 23200 us pulse) is "out of range" 36 | this->pinTrigger = pinTrigger; 37 | this->pinEcho = pinEcho; 38 | pinMode(this->pinTrigger, OUTPUT); 39 | pinMode(this->pinEcho, INPUT); 40 | } 41 | 42 | void sensor::init_extern(String externalSensor) { 43 | this->setSensorType(EXTERN); 44 | this->measurecycle = 10; 45 | mqtt->Subscribe(externalSensor); 46 | } 47 | 48 | void sensor::setSensorType(sensorType_t t) { 49 | this->Type = t; 50 | } 51 | 52 | void sensor::SetLvl(uint8_t lvl) { 53 | if (Config->GetDebugLevel() >= 4) { 54 | dbg.printf("Sensor: Set Level from extern: %d\n", lvl); 55 | } 56 | this->level = lvl; 57 | } 58 | 59 | void sensor::loop_analog() { 60 | this->raw = 0; 61 | this->level = 0; 62 | uint8_t pinanalog = this->pinAnalog; 63 | 64 | #ifdef ESP8266 65 | pinanalog = A0;; 66 | #endif 67 | 68 | if (Config->GetDebugLevel() >=4) dbg.printf("start measure, using analog Sensor pin: %d \n", pinanalog); 69 | 70 | this->raw = analogRead(pinanalog); 71 | 72 | this->level = map(this->raw, measureDistMin, measureDistMax, 0, 100); // 0-100% 73 | } 74 | 75 | void sensor::loop_hcsr04() { 76 | this->raw = 0; 77 | this->level = 0; 78 | 79 | digitalWrite(this->pinTrigger, LOW); 80 | delayMicroseconds(2); 81 | 82 | digitalWrite(this->pinTrigger, HIGH); 83 | delayMicroseconds(10); 84 | digitalWrite(this->pinTrigger, LOW); 85 | 86 | this->raw = pulseIn(this->pinEcho, HIGH, MAX_DIST); 87 | this->raw = (this->raw / 2) / 29.1; //Distance in CM's, use /148 for inches. 88 | 89 | if (this->raw == 0){//Reached timeout 90 | dbg.println("Out of range"); 91 | } else { 92 | if (this->measureDistMax - this->measureDistMin > 0) { 93 | this->level = (((this->measureDistMax - this->raw)*100)/(this->measureDistMax - this->measureDistMin)); 94 | } 95 | } 96 | } 97 | 98 | void sensor::loop() { 99 | /*start measuring sensor*/ 100 | if (millis() - this->previousMillis_sensor > this->measurecycle*1000) { 101 | this->previousMillis_sensor = millis(); 102 | 103 | if (this->Type == ONBOARD_ANALOG) {loop_analog();} 104 | 105 | if (this->Type == HCSR04) {loop_hcsr04();} 106 | 107 | if (this->Type != NONE && this->level !=0 && Config->Enabled3Wege()) { 108 | if (this->level < this->threshold_min) { VStruct->SetOn(Config->Get3WegePort()); } 109 | if (this->level > this->threshold_max) { VStruct->SetOff(Config->Get3WegePort()); } 110 | } 111 | if (this->Type != NONE && this->Type != EXTERN && mqtt) { 112 | if (this->raw > 0 ) { mqtt->Publish_Int((const char*)"raw", (int)this->raw, false); } 113 | if (this->level > 0 ) { mqtt->Publish_Int((const char*)"level", (int)this->level, false); } 114 | } 115 | 116 | if (this->Type != NONE && this->Type != EXTERN && Config->GetDebugLevel() >=4) { 117 | dbg.printf("measured sensor raw value: %d \n", this->raw); 118 | } 119 | } 120 | } 121 | 122 | void sensor::LoadJsonConfig() { 123 | mqtt->ClearSubscriptions(); 124 | 125 | String selection = ""; 126 | 127 | if (LittleFS.exists("/sensorconfig.json")) { 128 | //file exists, reading and loading 129 | dbg.println(F("reading sensorconfig.json file")); 130 | File configFile = LittleFS.open("/sensorconfig.json", "r"); 131 | if (configFile) { 132 | if (Config->GetDebugLevel() >=3) dbg.println(F("sensorconfig.json is now open")); 133 | ReadBufferingStream stream{configFile, 64}; 134 | stream.find("\"data\":["); 135 | do { 136 | 137 | JsonDocument elem; 138 | DeserializationError error = deserializeJson(elem, stream); 139 | if (error) { 140 | if (Config->GetDebugLevel() >=1) { 141 | dbg.printf("Failed to parse sensorconfig.json data: %s, load default config\n", error.c_str()); 142 | } 143 | } else { 144 | // Print the result 145 | if (Config->GetDebugLevel() >=5) {dbg.println(F("parsing partial JSON of sensorconfig.json ok")); } 146 | if (Config->GetDebugLevel() >=5) {serializeJsonPretty(elem, dbg);} 147 | 148 | if (elem.containsKey("measurecycle")) { this->measurecycle = _max(elem["measurecycle"].as(), 10);} 149 | if (elem.containsKey("measureDistMin")) { this->measureDistMin = elem["measureDistMin"].as();} 150 | if (elem.containsKey("measureDistMax")) { this->measureDistMax = elem["measureDistMax"].as();} 151 | if (elem.containsKey("pinhcsr04trigger")) { this->pinTrigger = elem["pinhcsr04trigger"].as() - 200;} 152 | if (elem.containsKey("pinhcsr04echo")) { this->pinEcho = elem["pinhcsr04echo"].as() - 200;} 153 | if (elem.containsKey("pinanalog")) { this->pinAnalog = elem["pinanalog"].as() - 200;} 154 | if (elem.containsKey("treshold_min")) { this->threshold_min = elem["treshold_min"].as();} 155 | if (elem.containsKey("treshold_max")) { this->threshold_max = elem["treshold_max"].as();} 156 | if (elem.containsKey("externalSensor")) { this->externalSensor = elem["externalSensor"].as();} 157 | if (elem.containsKey("selection")) { selection = elem["selection"].as(); } 158 | if (elem.containsKey("sel_moisture")) { if (elem["sel_moisture"].as() == "on") {this->moistureEnabled = true;} else {this->moistureEnabled = false;}} 159 | } 160 | } while (stream.findUntil(",","]")); 161 | 162 | } else { 163 | dbg.println("cannot open existing sensorconfig.json config File, load default SensorConfig"); // -> constructor 164 | } 165 | } else { 166 | dbg.println("sensorconfig.json config File not exists, load default SensorConfig"); 167 | } 168 | } 169 | 170 | void sensor::GetInitData(AsyncResponseStream *response) { 171 | String ret; 172 | JsonDocument json; 173 | 174 | json["data"].to(); 175 | json["data"]["sel0"] = ((this->Type==NONE)?1:0); 176 | json["data"]["sel1"] = ((this->Type==HCSR04)?1:0); 177 | json["data"]["sel2"] = ((this->Type==ONBOARD_ANALOG)?1:0); 178 | 179 | 180 | json["data"]["sel4"] = ((this->Type==EXTERN)?1:0); 181 | json["data"]["measurecycle"] = this->measurecycle; 182 | json["data"]["measureDistMin"] = this->measureDistMin; 183 | json["data"]["measureDistMax"] = this->measureDistMax; 184 | json["data"]["pinhcsr04trigger"] = this->pinTrigger + 200; 185 | json["data"]["pinhcsr04echo"] = this->pinEcho + 200; 186 | json["data"]["pinanalog"] = this->pinAnalog + 200; 187 | json["data"]["a_measureDistMin"] = this->measureDistMin; 188 | json["data"]["a_measureDistMax"] = this->measureDistMax; 189 | json["data"]["externalSensor"] = this->externalSensor; 190 | json["data"]["treshold_min"] = this->threshold_min; 191 | json["data"]["treshold_max"] = this->threshold_max; 192 | 193 | json["response"].to(); 194 | json["response"]["status"] = 1; 195 | json["response"]["text"] = "successful"; 196 | 197 | serializeJson(json, ret); 198 | response->print(ret); 199 | } 200 | -------------------------------------------------------------------------------- /src/sensor.h: -------------------------------------------------------------------------------- 1 | #ifndef SENSOR_H 2 | #define SENSOR_H 3 | 4 | #include "CommonLibs.h" 5 | #include "CommonLibs.h" 6 | #include 7 | #include 8 | #include "mqtt.h" 9 | #include "baseconfig.h" 10 | #include "valveStructure.h" 11 | 12 | extern valveStructure* VStruct; 13 | extern BaseConfig* Config; 14 | 15 | enum sensorType_t {NONE, EXTERN, HCSR04, ONBOARD_ANALOG}; 16 | 17 | class sensor { 18 | 19 | public: 20 | sensor(); 21 | void init_hcsr04(uint8_t pinTrigger, uint8_t pinEcho); 22 | void init_extern(String externalSensor); 23 | void init_analog(uint8_t pinAnalog) ; 24 | 25 | void setSensorType(sensorType_t t); 26 | void loop(); 27 | void SetLvl(uint8_t lvl); 28 | void LoadJsonConfig(); 29 | void GetInitData(AsyncResponseStream* response); 30 | 31 | const uint16_t& GetRaw() const {return raw;} 32 | const uint8_t& GetLvl() const {return level; } 33 | const sensorType_t& GetType() const {return Type; } 34 | const uint8_t& GetThresholdMin()const {return threshold_min;} 35 | const uint8_t& GetThresholdMax()const {return threshold_max;} 36 | const String& GetExternalSensor() const {return externalSensor;} 37 | 38 | private: 39 | void loop_analog(); 40 | void loop_hcsr04(); 41 | 42 | sensorType_t Type; 43 | 44 | uint16_t measureDistMin; 45 | uint16_t measureDistMax; 46 | uint16_t measurecycle; 47 | uint8_t level; 48 | uint16_t raw; 49 | uint8_t pinTrigger; 50 | uint8_t pinEcho; 51 | uint8_t pinAnalog; 52 | uint16_t MAX_DIST; 53 | uint8_t threshold_min; 54 | uint8_t threshold_max; 55 | String externalSensor; 56 | bool moistureEnabled; 57 | 58 | unsigned long previousMillis_sensor = 0; 59 | unsigned long previousMillis_moisture = 0; 60 | 61 | }; 62 | 63 | #endif 64 | -------------------------------------------------------------------------------- /src/valve.cpp: -------------------------------------------------------------------------------- 1 | #include "valve.h" 2 | 3 | valve::valve() : port1ms(10), port2ms(10), enabled(true), active(false), ValveType(NONE), autooff(0), reverse(false) { 4 | this->myHWdev = new HWdev_t(); 5 | this->myHWdev->i2cAddress = 0; 6 | } 7 | 8 | void valve::init(valveHardware* vHW, uint8_t Port, String SubTopic) { 9 | this->valveHWClass = vHW; 10 | bool ret = valveHWClass->RegisterPort(this->myHWdev, Port); 11 | if (!ret) { dbg.printf("Cannot locate port %d, set port as disabled \n", Port); this->enabled = false; } 12 | this->ValveType = NORMAL; 13 | this->port1 = Port; 14 | this->subtopic = SubTopic; 15 | } 16 | 17 | void valve::AddPort1(valveHardware* Device, uint8_t Port1) { 18 | this->valveHWClass = Device; 19 | bool ret = Device->RegisterPort(this->myHWdev, Port1); 20 | if (!ret) { dbg.printf("Cannot locate port %d, set port as disabled\n", Port1); this->enabled = false; } 21 | this->port1 = Port1; 22 | if (Config->GetDebugLevel()>=4) { 23 | dbg.printf("Registrierung für Port %d (0x%02x) abgeschlossen\n", this->GetPort1(), this->GetI2cAddress()); 24 | } 25 | } 26 | 27 | void valve::AddPort2(valveHardware* Device, uint8_t Port2) { 28 | bool ret = Device->RegisterPort(this->myHWdev, Port2); 29 | if (!ret) { dbg.printf("Cannot locate port %d, set port as disabled\n", Port2); this->enabled = false; } 30 | this->port2 = Port2; 31 | if (Config->GetDebugLevel()>=4) { 32 | dbg.printf("Registrierung für Port %d (0x%02x) abgeschlossen\n", this->GetPort2(), this->GetI2cAddress()); 33 | } 34 | } 35 | 36 | void valve::SetActive(bool value) { 37 | this->enabled = value; 38 | } 39 | 40 | void valve::SetReverse(bool value) { 41 | this->reverse = value; 42 | if (value) this->HandleSwitch(false, 0); // set OFF 43 | } 44 | 45 | void valve::SetAutoOff(uint16_t value) { 46 | this->autooff = value; 47 | } 48 | 49 | bool valve::OnForTimer(int duration) { 50 | bool ret= false; 51 | if (enabled && ActiveTimeLeft() < duration) {ret = this->HandleSwitch(true, duration);} 52 | if (duration == 0) { ret = this->SetOff(); } 53 | return ret; 54 | } 55 | 56 | bool valve::SetOn() { 57 | bool ret = false; 58 | if (this->enabled && !this->active) { 59 | if (this->autooff > 0) { 60 | ret = this->HandleSwitch(true, this->autooff); 61 | } else { 62 | ret = this->HandleSwitch(true, 0); 63 | } 64 | } 65 | return ret; 66 | } 67 | 68 | bool valve::SetOff() { 69 | bool ret = false; 70 | if (this->active) {ret = this->HandleSwitch(false, 0);} 71 | return ret; 72 | } 73 | 74 | bool valve::HandleSwitch (bool state, int duration) { 75 | char buffer[50] = {0}; 76 | memset(buffer, 0, sizeof(buffer)); 77 | 78 | if (this->ValveType == NORMAL) { 79 | valveHWClass->SetPort(this->myHWdev, this->port1, state, this->reverse); 80 | dbg.printf("Schalte Standard Ventil %s: Port %d (0x%02X) \n", (state?"An":"Aus"), this->port1, this->GetI2cAddress()); 81 | } else if (ValveType == BISTABIL) { 82 | valveHWClass->SetPort(this->myHWdev, this->port1, this->port2, state, this->reverse, (state?this->port1ms:this->port2ms)); 83 | dbg.printf("Schalte Bistabiles Ventil %s: Port %d/%d, ms: %d/%d (0x%02X) \n", (state?"An":"Aus"), port1, port2, port1ms, port2ms, this->GetI2cAddress()); 84 | } else { 85 | dbg.println("Unerwarteter Ventiltyp ?? Breche Schaltvorgang ab ....."); 86 | return false; 87 | } 88 | 89 | this->active = state; 90 | 91 | if (state && duration && duration>0) { 92 | this->startmillis = millis(); 93 | this->lengthmillis = duration * 1000; 94 | } else { 95 | this->startmillis = lengthmillis = 0; 96 | } 97 | 98 | if(mqtt) { 99 | snprintf (buffer, sizeof(buffer), "%s/state", this->subtopic.c_str()); 100 | mqtt->Publish_Bool(buffer, state, false); 101 | } 102 | 103 | return true; 104 | } 105 | 106 | int valve::ActiveTimeLeft() { 107 | // its wiered, _min function don work correct everytime 108 | if (!this->active) return 0; 109 | 110 | long t = this->lengthmillis - (millis() - this->startmillis); 111 | if (t > 0) return t; 112 | else return 0; 113 | } 114 | 115 | void valve::SetValveType(String type) { 116 | if (type == "n") { ValveType = NORMAL; } 117 | else if (type=="b") { ValveType = BISTABIL; } 118 | else { ValveType = NONE; } 119 | } 120 | 121 | String valve::GetValveType() { 122 | if (ValveType == NORMAL) { return "n"; } 123 | else if (ValveType == BISTABIL) { return "b"; } 124 | else { return ""; } 125 | } 126 | 127 | uint8_t valve::GetPort1() { 128 | return port1; 129 | } 130 | 131 | uint8_t valve::GetPort2() { 132 | return port2; 133 | } 134 | 135 | void valve::loop() { 136 | //if (this->active) dbg.printf("Check on-for-timer -> Time left: %d \n", this->ActiveTimeLeft()); 137 | 138 | if (this->active && this->lengthmillis >0 && this->ActiveTimeLeft()==0) { 139 | //dbg.printf("on-for-timer abgelaufen: Pin %d \n", this->port1); 140 | SetOff(); 141 | } 142 | } 143 | -------------------------------------------------------------------------------- /src/valve.h: -------------------------------------------------------------------------------- 1 | #ifndef VALVE_H 2 | #define VALVE_H 3 | 4 | #include "CommonLibs.h" 5 | #include "valveHardware.h" 6 | #include "mqtt.h" 7 | 8 | class valve { 9 | 10 | enum vType_t {NONE, BISTABIL, NORMAL}; 11 | 12 | public: 13 | valve(); 14 | 15 | void loop(); 16 | void init(valveHardware* Device, uint8_t Port, String SubTopic); 17 | 18 | bool OnForTimer(int duration); 19 | bool SetOn(); 20 | bool SetOff(); 21 | int ActiveTimeLeft(); 22 | void AddPort1(valveHardware* Device, uint8_t Port1); 23 | void AddPort2(valveHardware* Device, uint8_t Port2); 24 | void SetValveType(String type); 25 | void SetActive(bool value); 26 | void SetReverse(bool value); 27 | void SetAutoOff(uint16_t value); 28 | 29 | const bool& GetActive() const {return active;} 30 | const bool& GetEnabled() const {return enabled;} 31 | const bool& GetReverse() const {return reverse;} 32 | const uint16_t& GetAutoOff() const {return autooff;} 33 | const uint8_t& GetI2cAddress() const {return this->myHWdev->i2cAddress;} 34 | 35 | String GetValveType(); 36 | uint8_t GetPort1(); 37 | uint8_t GetPort2(); 38 | uint16_t port1ms; // millisekunden bei Type "b" für Port1: 10-999 39 | uint16_t port2ms; // millisekunden bei Type "b" für Port2: 10-999 40 | String subtopic; //ohne on-for-timer 41 | 42 | private: 43 | bool enabled; //grundsätzlich aktiviert in WebUI 44 | bool active; // Ventil ist gerade aktiv/geöffnet 45 | vType_t ValveType; 46 | uint16_t autooff; // anzahl sek wenn das Ventil nach einem ON automatisch spaetestens schliessen soll -> Sicherheitsabschaltung 47 | bool reverse; // Ventil schliesst auf ON, oeffnet auf OFF 48 | 49 | HWdev_t* myHWdev = NULL; //Pointer auf das Device 50 | valveHardware* valveHWClass = NULL; // Pointer auf die Klasse um auf die generischen Funktionen zugreifen zu können 51 | 52 | uint8_t port1; //0 - 220 53 | uint8_t port2; //0 - 220 , für bistabile Ventile 54 | uint32_t startmillis = 0; 55 | uint32_t lengthmillis = 0; 56 | 57 | bool HandleSwitch (bool state, int duration); 58 | }; 59 | 60 | #endif 61 | -------------------------------------------------------------------------------- /src/valveHardware.cpp: -------------------------------------------------------------------------------- 1 | #ifndef valve_h 2 | #define valve_h 3 | #include "valveHardware.h" 4 | #endif 5 | 6 | // Constructor 7 | valveHardware::valveHardware(uint8_t sda, uint8_t scl) 8 | : pin_sda(sda), pin_scl(scl) { 9 | 10 | //this->HWDevice = new std::vector{}; 11 | this->HWDevice = std::make_shared>(); 12 | 13 | // initial immer das GPIO HardwareDevice erstellen 14 | HWdev_t t; 15 | t.HWType=ONBOARD; 16 | t.i2cAddress=0x00; 17 | this->HWDevice->push_back(t); 18 | 19 | if (Config->GetDebugLevel() >=3) { 20 | char buffer[100] = {0}; 21 | memset(buffer, 0, sizeof(buffer)); 22 | sprintf(buffer, "Initialisiere HardwareDevice mit GPIO auf ic2Adresse 0x%02X", t.i2cAddress); 23 | dbg.println(buffer); 24 | } 25 | } 26 | 27 | void valveHardware::addI2CDevice(uint8_t i2cAddress) { 28 | if (!this->I2CIsPresent(i2cAddress)) { 29 | HWdev_t t; 30 | t.i2cAddress = i2cAddress; 31 | this->setHWType(&t); 32 | this->ConnectHWdevice(&t); 33 | this->HWDevice->push_back(t); 34 | } 35 | } 36 | 37 | bool valveHardware::I2CIsPresent(uint8_t i2cAddress) { 38 | char buffer[100] = {0}; 39 | //for (const auto &element : this->HWDevice) { 40 | for (uint8_t i=0; i < this->HWDevice->size(); i++) { 41 | if (Config->GetDebugLevel() >=5) { 42 | memset(buffer, 0, sizeof(buffer)); 43 | sprintf(buffer, "Pruefe ic2Adresse 0x%02X ob HW-Element 0x%02X schon existiert", i2cAddress, this->HWDevice->at(i).i2cAddress); 44 | dbg.println(buffer); 45 | } 46 | if (this->HWDevice->at(i).i2cAddress == i2cAddress) { 47 | if (Config->GetDebugLevel() >=4) { 48 | memset(buffer, 0, sizeof(buffer)); 49 | sprintf(buffer, "HW-Element von i2cAdresse 0x%02X gefunden", i2cAddress); 50 | dbg.println(buffer); 51 | } 52 | return true; 53 | } 54 | } 55 | return false; 56 | } 57 | 58 | HWdev_t* valveHardware::getI2CDevice(uint8_t i2cAddress) { 59 | for (uint8_t i=0; iHWDevice->size(); i++) { 60 | if (this->HWDevice->at(i).i2cAddress == i2cAddress) { 61 | return &this->HWDevice->at(i); 62 | } 63 | } 64 | return NULL; 65 | return NULL; 66 | } 67 | 68 | void valveHardware::ConnectHWdevice(HWdev_t* dev) { 69 | #ifdef USE_PCF8574 70 | if(dev->HWType == PCF) { 71 | PCF8574* pcf8574 = new PCF8574(dev->i2cAddress, this->pin_sda, this->pin_scl); 72 | pcf8574->begin(); 73 | dev->Device = pcf8574; 74 | } 75 | #endif 76 | #ifdef USE_TB6612 77 | if(dev->HWType == TB6612) { 78 | tb6612* motor = new tb6612(); 79 | motor->init(dev->i2cAddress); 80 | dev->Device = motor; 81 | } 82 | #endif 83 | 84 | if (Config->GetDebugLevel() >=3) { 85 | char buffer[100] = {0}; 86 | memset(buffer, 0, sizeof(buffer)); 87 | sprintf(buffer, "Hardwaredevice fuer Typ %d auf i2c-Adresse 0x%02X erfolgreich erstellt", dev->HWType, dev->i2cAddress); 88 | dbg.println(buffer); 89 | } 90 | } 91 | 92 | bool valveHardware::RegisterPort(HWdev_t*& dev, uint8_t Port) { 93 | return this->RegisterPort(dev, Port, false); 94 | } 95 | 96 | bool valveHardware::RegisterPort(HWdev_t*& dev, uint8_t Port, bool reverse) { 97 | char buffer[200] = {0}; 98 | bool success = false; 99 | if (Config->GetDebugLevel() >=4) { 100 | memset(buffer, 0, sizeof(buffer)); 101 | sprintf(buffer, "Fordere Registrierung Port %d an", Port); 102 | dbg.println(buffer); 103 | } 104 | 105 | PortMap_t PortMap; 106 | PortMap.Port = Port; 107 | this->PortMapping(&PortMap); // need i2cAddress and internalPort 108 | 109 | bool state = false ^ reverse; // default: OFF 110 | 111 | if (PortMap.Port !=0) { 112 | addI2CDevice(PortMap.i2cAddress); 113 | dev = getI2CDevice(PortMap.i2cAddress); 114 | #ifdef USE_PCF8574 115 | if(dev->HWType == PCF) { 116 | PCF8574* pcf8574 = static_cast(dev->Device); 117 | pcf8574->pinMode(PortMap.internalPort, OUTPUT); 118 | pcf8574->digitalWrite(PortMap.internalPort, !state); // normal: HIGH 119 | success = true; 120 | } 121 | #endif 122 | #ifdef USE_TB6612 123 | if (dev->HWType == TB6612) { 124 | tb6612* motor = static_cast(dev->Device); 125 | motor->setOff(PortMap.internalPort); 126 | success = true; 127 | } 128 | #endif 129 | 130 | if (dev->HWType == ONBOARD) { 131 | pinMode(PortMap.internalPort, OUTPUT); 132 | digitalWrite(PortMap.internalPort, state); // normal: LOW 133 | success = true; 134 | } 135 | } 136 | 137 | if (Config->GetDebugLevel() >=4) { 138 | memset(buffer, 0, sizeof(buffer)); 139 | if (success) { 140 | sprintf(buffer, "Port %d als internalPort %d fuer HardwareTyp %d auf i2c-Adresse 0x%02X erfolgreich registriert", Port, PortMap.internalPort, dev->HWType, dev->i2cAddress); 141 | } else { 142 | sprintf(buffer, "Fehler bei der Registrierung des Ports %d ", Port); 143 | } 144 | dbg.println(buffer); 145 | } 146 | 147 | if (success) { return true; } 148 | else { return false; } 149 | } 150 | 151 | bool valveHardware::IsValidPort(uint8_t Port) { 152 | PortMap_t PortMap; 153 | PortMap.Port = Port; 154 | PortMapping(&PortMap); 155 | if(PortMap.Port == Port) {return true;} else {return false;} 156 | } 157 | 158 | uint8_t valveHardware::GetI2CAddress(uint8_t Port) { 159 | PortMap_t PortMap; 160 | PortMap.Port = Port; 161 | PortMapping(&PortMap); 162 | return PortMap.i2cAddress; 163 | } 164 | 165 | void valveHardware::SetPort(HWdev_t* dev, uint8_t Port, bool state, bool reverse) { 166 | this->SetPort(dev, Port, 0 , state, reverse, 0); 167 | } 168 | 169 | void valveHardware::SetPort(HWdev_t* dev, uint8_t Port1, uint8_t Port2, bool state, bool reverse, uint16_t duration) { 170 | PortMap_t PortMap1, PortMap2; 171 | PortMap1.Port = Port1; PortMap2.Port = Port2; 172 | PortMapping(&PortMap1); PortMapping(&PortMap2); // need internalPort 173 | 174 | state = state ^ reverse; 175 | 176 | #ifdef USE_PCF8574 177 | if (dev->HWType == PCF) { //schaltet auf LOW 178 | PCF8574* pcf8574 = static_cast(dev->Device); // , pin_sda, pin_scl 179 | pcf8574->digitalWrite(PortMap1.internalPort, !state); // Normal: HIGH 180 | if (Port2 && Port2 > 0) { 181 | pcf8574->digitalWrite(PortMap2.internalPort, state); 182 | delay(duration); 183 | pcf8574->digitalWrite(PortMap1.internalPort, state); 184 | pcf8574->digitalWrite(PortMap2.internalPort, !state); 185 | } 186 | } 187 | #endif 188 | #ifdef USE_TB6612 189 | if (dev->HWType == TB6612) { 190 | tb6612* motor = static_cast(dev->Device); 191 | if (duration && duration > 0) { 192 | motor->setOn(PortMap1.internalPort, state); 193 | delay(duration); 194 | motor->setOff(PortMap1.internalPort); 195 | } 196 | // Port 2 nicht relevant 197 | } 198 | #endif 199 | 200 | if (dev->HWType == ONBOARD) { 201 | digitalWrite(PortMap1.internalPort, state); // Bistabil: set Direction 202 | if (Port2 && Port2 > 0) { 203 | digitalWrite(PortMap2.internalPort, true); // Bistabil: set ON 204 | delay(duration); 205 | digitalWrite(PortMap2.internalPort, false); // Bistabil: set OFF 206 | } 207 | } 208 | 209 | if (Config->GetDebugLevel() >=5) { 210 | char buffer[100] = {0}; 211 | memset(buffer, 0, sizeof(buffer)); 212 | sprintf(buffer, "Aenderung Port %d nach Status: %s ", Port1, vState(state)); 213 | dbg.println(buffer); 214 | } 215 | } 216 | 217 | void valveHardware::setHWType(HWdev_t* dev) { 218 | if (dev->i2cAddress >= 0x20 and dev->i2cAddress <= 0x27) { 219 | dev->HWType = PCF; 220 | } else if (dev->i2cAddress >= 0x38 and dev->i2cAddress <= 0x3F) { 221 | dev->HWType = PCF; 222 | } else if (dev->i2cAddress == 0x00) { 223 | dev->HWType = ONBOARD; 224 | } else if(dev->i2cAddress >= 0x2D and dev->i2cAddress <= 0x30) { 225 | dev->HWType = TB6612; 226 | } 227 | } 228 | 229 | // see Definition: https://www.letscontrolit.com/wiki/index.php/PCF8574 230 | void valveHardware::PortMapping(PortMap_t* Map) { 231 | if (Map->Port >=1 && Map->Port <=8) { 232 | Map->i2cAddress=0x20; 233 | Map->internalPort=Map->Port-1; 234 | Map->HWType = PCF; 235 | } else if (Map->Port >=9 && Map->Port <=16) { 236 | Map->i2cAddress=0x21; 237 | Map->internalPort=Map->Port-9; 238 | Map->HWType = PCF; 239 | } else if (Map->Port >=17 && Map->Port <=24) { 240 | Map->i2cAddress=0x22; 241 | Map->internalPort=Map->Port-17; 242 | Map->HWType = PCF; 243 | } else if (Map->Port >=25 && Map->Port <=32) { 244 | Map->i2cAddress=0x23; 245 | Map->internalPort=Map->Port-25; 246 | Map->HWType = PCF; 247 | } else if (Map->Port >=33 && Map->Port <=40) { 248 | Map->i2cAddress=0x24; 249 | Map->internalPort=Map->Port-33; 250 | Map->HWType = PCF; 251 | } else if (Map->Port >=41 && Map->Port <=48) { 252 | Map->i2cAddress=0x25; 253 | Map->internalPort=Map->Port-41; 254 | Map->HWType = PCF; 255 | } else if (Map->Port >=49 && Map->Port <=56) { 256 | Map->i2cAddress=0x26; 257 | Map->internalPort=Map->Port-49; 258 | Map->HWType = PCF; 259 | } else if (Map->Port >=57 && Map->Port <=64) { 260 | Map->i2cAddress=0x27; 261 | Map->internalPort=Map->Port-57; 262 | Map->HWType = PCF; 263 | } else if (Map->Port >=65 && Map->Port <=72) { 264 | Map->i2cAddress=0x38; 265 | Map->internalPort=Map->Port-65; 266 | Map->HWType = PCF; 267 | } else if (Map->Port >=73 && Map->Port <=80) { 268 | Map->i2cAddress=0x39; 269 | Map->internalPort=Map->Port-73; 270 | Map->HWType = PCF; 271 | } else if (Map->Port >=81 && Map->Port <=88) { 272 | Map->i2cAddress=0x3A; 273 | Map->internalPort=Map->Port-81; 274 | Map->HWType = PCF; 275 | } else if (Map->Port >=89 && Map->Port <=96) { 276 | Map->i2cAddress=0x3B; 277 | Map->internalPort=Map->Port-89; 278 | Map->HWType = PCF; 279 | } else if (Map->Port >=97 && Map->Port <=104) { 280 | Map->i2cAddress=0x3C; 281 | Map->internalPort=Map->Port-97; 282 | Map->HWType = PCF; 283 | } else if (Map->Port >=105 && Map->Port <=112) { 284 | Map->i2cAddress=0x3D; 285 | Map->internalPort=Map->Port-105; 286 | Map->HWType = PCF; 287 | } else if (Map->Port >=113 && Map->Port <=112) { 288 | Map->i2cAddress=0x3E; 289 | Map->internalPort=Map->Port-113; 290 | Map->HWType = PCF; 291 | } else if (Map->Port >=121 && Map->Port <=128) { 292 | Map->i2cAddress=0x3F; 293 | Map->internalPort=Map->Port-121; 294 | Map->HWType = PCF; 295 | } else if (Map->Port == 130) { 296 | Map->i2cAddress=0x2D; 297 | Map->internalPort=0; 298 | Map->HWType = TB6612; 299 | } else if (Map->Port == 131) { 300 | Map->i2cAddress=0x2D; 301 | Map->internalPort=1; 302 | Map->HWType = TB6612; 303 | } else if (Map->Port == 132) { 304 | Map->i2cAddress=0x2E; 305 | Map->internalPort=0; 306 | Map->HWType = TB6612; 307 | } else if (Map->Port == 133) { 308 | Map->i2cAddress=0x2E; 309 | Map->internalPort=1; 310 | Map->HWType = TB6612; 311 | } else if (Map->Port == 134) { 312 | Map->i2cAddress=0x2F; 313 | Map->internalPort=0; 314 | Map->HWType = TB6612; 315 | } else if (Map->Port == 135) { 316 | Map->i2cAddress=0x2F; 317 | Map->internalPort=1; 318 | Map->HWType = TB6612; 319 | } else if (Map->Port == 136) { 320 | Map->i2cAddress=0x30; 321 | Map->internalPort=0; 322 | Map->HWType = TB6612; 323 | } else if (Map->Port == 137) { 324 | Map->i2cAddress=0x30; 325 | Map->internalPort=1; 326 | Map->HWType = TB6612; 327 | } else if (Map->Port >=140 && Map->Port <=199) { 328 | // nur die Ports anzeigen die auch wirklich vorhanden sind 329 | 330 | } else if (Map->Port >=200 && Map->Port <=250) { 331 | // interne GPIO 332 | Map->i2cAddress=0x00; 333 | Map->internalPort=Map->Port-200; 334 | Map->HWType = ONBOARD; 335 | } else { 336 | Map->Port = 0; 337 | } 338 | } 339 | -------------------------------------------------------------------------------- /src/valveHardware.h: -------------------------------------------------------------------------------- 1 | #ifndef VALVEHARDWARE_H 2 | #define VALVEHARDWARE_H 3 | 4 | #include "CommonLibs.h" 5 | #include "baseconfig.h" 6 | #include 7 | #include 8 | 9 | #ifdef USE_PCF8574 10 | #include "PCF8574.h" // https://github.com/xreef/PCF8574_library 11 | #endif 12 | 13 | #ifdef USE_TB6612 14 | #include "TB6612.h" 15 | #endif 16 | 17 | extern BaseConfig* Config; 18 | 19 | enum HWType_t {ONBOARD, PCF, TB6612}; 20 | 21 | typedef struct { 22 | void* Device; 23 | HWType_t HWType; 24 | uint8_t i2cAddress; 25 | } HWdev_t; 26 | 27 | #define vState(x) ((x)?"An":"Aus") // Boolean in lesbare Ausgabe 28 | 29 | class valveHardware { 30 | 31 | // PortMapping Type 32 | typedef struct { 33 | uint8_t i2cAddress; 34 | HWType_t HWType; 35 | uint8_t Port; 36 | uint8_t internalPort; 37 | } PortMap_t; 38 | 39 | public: 40 | valveHardware(uint8_t sda, uint8_t scl); 41 | 42 | bool RegisterPort(HWdev_t*& dev, uint8_t Port); 43 | bool RegisterPort(HWdev_t*& dev, uint8_t Port, bool reverse); 44 | 45 | void SetPort(HWdev_t* dev, uint8_t Port, bool state, bool reverse); 46 | void SetPort(HWdev_t* dev, uint8_t Port1, uint8_t Port2, bool state, bool reverse, uint16_t duration); 47 | bool IsValidPort(uint8_t Port); 48 | uint8_t GetI2CAddress(uint8_t Port); 49 | 50 | private: 51 | 52 | // https://www.learncpp.com/cpp-tutorial/6-16-an-introduction-to-stdvector/ 53 | // https://www.learncpp.com/cpp-tutorial/7-10-stdvector-capacity-and-stack-behavior/ 54 | // https://de.wikibooks.org/wiki/C%2B%2B-Programmierung:_Vector 55 | //std::vector *HWDevice; //list of all created physical devices 56 | std::shared_ptr> HWDevice; //list of all created physical devices 57 | 58 | uint8_t pin_sda = SDA; 59 | uint8_t pin_scl = SCL; 60 | 61 | void setHWType(HWdev_t* dev); 62 | void ConnectHWdevice(HWdev_t* dev); 63 | void PortMapping(PortMap_t* Map); 64 | void addI2CDevice(uint8_t i2cAddress); 65 | bool I2CIsPresent(uint8_t i2cAddress); 66 | 67 | HWdev_t* getI2CDevice(uint8_t i2cAddress); 68 | 69 | 70 | }; 71 | 72 | #endif 73 | -------------------------------------------------------------------------------- /src/valveStructure.cpp: -------------------------------------------------------------------------------- 1 | #include "valveStructure.h" 2 | 3 | valveStructure::valveStructure(uint8_t sda, uint8_t scl) : 4 | pin_sda(sda), pin_scl(scl) { 5 | this->ValveHW = new valveHardware(sda, scl); 6 | 7 | this->Valves = std::make_shared>(); 8 | 9 | // loading twice, 1st valve is corrupted after 1st load, has to be investigate 10 | // TODO 11 | LoadJsonConfig(); 12 | LoadJsonConfig(); 13 | } 14 | 15 | void valveStructure::OnForTimer(String SubTopic, int duration) { 16 | valve* v = this->GetValveItem(SubTopic); 17 | if (v && v->OnForTimer(duration)) { 18 | if (mqtt) {mqtt->Publish_Int("Threads", (int)this->CountActiveThreads(), false); } 19 | } 20 | } 21 | 22 | void valveStructure::SetOff(String SubTopic) { 23 | valve* v = this->GetValveItem(SubTopic); 24 | if (v) { this->SetOff(GetValveItem(SubTopic)->GetPort1()); } 25 | } 26 | 27 | void valveStructure::SetOn(String SubTopic) { 28 | valve* v = this->GetValveItem(SubTopic); 29 | if (v) {this->SetOn(GetValveItem(SubTopic)->GetPort1()); } 30 | } 31 | 32 | void valveStructure::SetOn(uint8_t Port) { 33 | valve* v = this->GetValveItem(Port); 34 | if (v && v->SetOn() && mqtt) { mqtt->Publish_Int("Threads", (int)this->CountActiveThreads(), false); } 35 | } 36 | 37 | void valveStructure::SetOff(uint8_t Port) { 38 | valve* v = this->GetValveItem(Port); 39 | if (v) { v->SetOff(); } 40 | if (mqtt) { mqtt->Publish_Int("Threads", (int)this->CountActiveThreads(), false); } 41 | } 42 | 43 | bool valveStructure::GetState(uint8_t Port) { 44 | if (GetValveItem(Port)) { return GetValveItem(Port)->GetActive(); } 45 | return NULL; 46 | } 47 | 48 | bool valveStructure::GetEnabled(uint8_t Port) { 49 | if (GetValveItem(Port)) { return GetValveItem(Port)->GetEnabled();} 50 | return NULL; 51 | } 52 | 53 | void valveStructure::SetEnable(uint8_t Port, bool state) { 54 | if (GetValveItem(Port)) { GetValveItem(Port)->SetActive(state); } 55 | } 56 | 57 | void valveStructure::loop() { 58 | for (uint8_t i=0; isize(); i++) { 59 | Valves->at(i).loop(); 60 | } 61 | } 62 | 63 | void valveStructure::ReceiveMQTT(String topic, int value) { 64 | String SubTopic(topic); // nur das konfigurierte Subtopic, zb. "valve1" 65 | SubTopic = SubTopic.substring(SubTopic.lastIndexOf("/", SubTopic.lastIndexOf("/")-1)+1, SubTopic.lastIndexOf("/")); 66 | if (topic == "/test/on-for-timer") { Valves->at(0).OnForTimer(value); } 67 | 68 | if (topic.startsWith(mqtt->getTopic("", false)) && topic.endsWith("on-for-timer")) { this->OnForTimer(SubTopic, value); } 69 | if (topic.startsWith(mqtt->getTopic("", false)) && topic.endsWith("setstate") && value==1) { this->SetOn(SubTopic); } 70 | if (topic.startsWith(mqtt->getTopic("", false)) && topic.endsWith("setstate") && value==0) { this->SetOff(SubTopic); } 71 | if (topic.startsWith(mqtt->getTopic("", false)) && topic.endsWith("state") && value==0) { this->SetOff(SubTopic); } 72 | } 73 | 74 | valve* valveStructure::GetValveItem(uint8_t Port) { 75 | for (uint8_t i=0; isize(); i++) { 76 | if (Valves->at(i).GetPort1() == Port) {return &Valves->at(i);} 77 | } 78 | return NULL; 79 | } 80 | 81 | valve* valveStructure::GetValveItem(String SubTopic) { 82 | for (uint8_t i=0; isize(); i++) { 83 | if (Valves->at(i).subtopic == SubTopic) { return &Valves->at(i);} 84 | } 85 | return NULL; 86 | } 87 | 88 | uint8_t valveStructure::CountActiveThreads() { 89 | uint8_t count = 0; 90 | for (uint8_t i=0; isize(); i++) { 91 | if (Valves->at(i).GetActive() && (Valves->at(i).GetPort1() != Config->Get3WegePort() || !Config->Enabled3Wege() )) {count++;} 92 | } 93 | return count; 94 | } 95 | 96 | /* load json config from littlefs */ 97 | void valveStructure::LoadJsonConfig() { 98 | bool loadDefaultConfig = false; 99 | 100 | if (!Valves->empty()) { 101 | Valves->erase(Valves->begin(), Valves->end()); 102 | } 103 | 104 | if (LittleFS.exists("/valveconfig.json")) { 105 | //file exists, reading and loading 106 | if (Config->GetDebugLevel() >=3) dbg.println("reading valveconfig.json file...."); 107 | File configFile = LittleFS.open("/valveconfig.json", "r"); 108 | if (configFile) { 109 | if (Config->GetDebugLevel() >=3) dbg.println("valveconfig.json is now open"); 110 | 111 | ReadBufferingStream stream{configFile, 64}; 112 | stream.find("\"data\":["); 113 | do { 114 | JsonDocument elem; 115 | DeserializationError error = deserializeJson(elem, stream); 116 | 117 | if (error) { 118 | loadDefaultConfig = true; 119 | if (Config->GetDebugLevel() >=1) { 120 | dbg.printf("Failed to parse valveconfig.json data: %s, load default config\n", error.c_str()); 121 | } 122 | } else { 123 | // Print the result 124 | if (Config->GetDebugLevel() >=4) {dbg.println("parsing JSON ok"); } 125 | if (Config->GetDebugLevel() >=5) {serializeJsonPretty(elem, dbg);} 126 | 127 | valve myValve; 128 | 129 | String type = GetJsonKeyMatch(&elem, "type"); 130 | if (elem.containsKey("port_a") && elem["port_a"].as() > 0) { myValve.AddPort1(this->ValveHW, elem["port_a"].as()); } 131 | if (elem.containsKey(type)) {myValve.SetValveType(elem[type].as()); } 132 | if (elem["active"] && elem["active"] == 1) {myValve.SetActive(true);} else {myValve.SetActive(false);} 133 | if (elem.containsKey("mqtttopic")) {myValve.subtopic = elem["mqtttopic"].as();} 134 | if (elem.containsKey("port_b") && elem["port_b"].as() > 0) { myValve.AddPort2(ValveHW, elem["port_b"].as());} 135 | if (elem.containsKey("imp_a")) { myValve.port1ms = _max(10, _min(elem["imp_a"].as(), 999));} 136 | if (elem.containsKey("imp_b")) { myValve.port2ms = _max(10, _min(elem["imp_b"].as(), 999));} 137 | if (elem["reverse"] && elem["reverse"] == 1) {myValve.SetReverse(true);} else {myValve.SetReverse(false);} 138 | if (elem.containsKey("autooff") && elem["autooff"].as() > 0) { myValve.SetAutoOff(elem["autooff"].as()); } 139 | 140 | Valves->push_back(myValve); 141 | } 142 | 143 | } while (stream.findUntil(",","]")); 144 | } else { 145 | loadDefaultConfig = true; 146 | if (Config->GetDebugLevel() >=1) {dbg.println("failed to load valveconfig.json, load default config");} 147 | } 148 | } else { 149 | loadDefaultConfig = true; 150 | if (Config->GetDebugLevel() >=3) {dbg.println("valveconfig.json File not exists, load default config");} 151 | } 152 | 153 | if (loadDefaultConfig) { 154 | if (Config->GetDebugLevel() >=3) { dbg.println("lade Ventile DefaultConfig"); } 155 | valve myValve; 156 | 157 | myValve.init(this->ValveHW, 203, "Valve1"); 158 | this->Valves->push_back(myValve); 159 | 160 | myValve.init(this->ValveHW, 204, "Valve2"); 161 | this->Valves->push_back(myValve); 162 | } 163 | if (Config->GetDebugLevel() >=3) { 164 | dbg.printf("%d valves are now loaded \n", Valves->size()); 165 | } 166 | } 167 | 168 | /************************************** 169 | lookup with a pattern for a key 170 | returns the first matched key 171 | ***************************************/ 172 | String valveStructure::GetJsonKeyMatch(JsonDocument* doc, String key) { 173 | for (JsonPair kv : doc->as()) { 174 | if (strstr(kv.key().c_str(), key.c_str())) { 175 | return (String)kv.key().c_str(); 176 | } 177 | } 178 | return ""; 179 | } 180 | 181 | void valveStructure::GetInitData(AsyncResponseStream* response) { 182 | String ret; 183 | JsonDocument json; 184 | 185 | json["data"].to(); 186 | JsonArray row = json["data"]["rows"].to(); 187 | 188 | for(uint8_t i=0; isize(); i++) { 189 | row[i]["active"] = (Valves->at(i).GetEnabled()?1:0); 190 | row[i]["mqtttopic"] = Valves->at(i).subtopic; 191 | 192 | if (Valves->at(i).GetValveType() == "b") { 193 | row[i]["typ_n"]["className"] = "hide"; 194 | } else if (Valves->at(i).GetValveType() == "n") { 195 | row[i]["typ_b"]["className"] = "hide"; 196 | } 197 | 198 | row[i]["AllePorts_PortA"] = Valves->at(i).GetPort1(); 199 | row[i]["imp_a"] = Valves->at(i).port1ms; 200 | row[i]["AllePorts_PortB"] = Valves->at(i).GetPort2(); 201 | row[i]["imp_b"] = Valves->at(i).port2ms; 202 | row[i]["AllePorts"] = Valves->at(i).GetPort1(); 203 | 204 | String type_name("type_"); type_name.concat(i); 205 | row[i]["SelType_n"]["checked"] = (Valves->at(i).GetValveType()=="n"?1:0); 206 | row[i]["SelType_n"]["name"] = type_name; 207 | row[i]["SelType_b"]["checked"] = (Valves->at(i).GetValveType()=="b"?1:0); 208 | row[i]["SelType_b"]["name"] = type_name; 209 | row[i]["reverse"] = (Valves->at(i).GetReverse()?1:0); 210 | row[i]["autooff"] = Valves->at(i).GetAutoOff(); 211 | row[i]["action"] = (Valves->at(i).GetActive()?"Set Off":"Set On"); 212 | } 213 | 214 | json["response"].to(); 215 | json["response"]["status"] = 1; 216 | json["response"]["text"] = "successful"; 217 | 218 | serializeJson(json, ret); 219 | response->print(ret); 220 | } 221 | 222 | void valveStructure::getWebJsParameter(AsyncResponseStream *response) { 223 | 224 | // bereits belegte Ports, können nicht ausgewählt werden (zb.i2c-ports) 225 | // const gpio_disabled = Array(0,4); 226 | response->printf("const gpio_disabled = [%d,%d];\n", Config->GetPinSDA() + 200, Config->GetPinSCL() + 200); 227 | 228 | // anhand gefundener I2C Devices die verfügbaren Ports bereit stellen 229 | //const availablePorts = [65,72]; 230 | response->println("const availablePorts = ["); 231 | #ifdef USE_I2C 232 | uint8_t count=0; 233 | for (uint8_t p=1; p<=254; p++) { 234 | if (ValveHW->IsValidPort(p) && (I2Cdetect->i2cIsPresent(ValveHW->GetI2CAddress(p))) ) { 235 | // i2cDetect muss den ic2Port finden 236 | response->printf("%s%d", (count>0?",":"") , p); 237 | count++; 238 | } 239 | } 240 | #endif 241 | 242 | response->println("];\n"); 243 | 244 | //konfigurierte Ports / Namen 245 | //const configuredPorts = [ {port:65, name:"Ventil1"}, {port:67, name:"Ventil2"}] 246 | response->println("const configuredPorts = ["); 247 | for(uint8_t i=0; i < Valves->size(); i++) { 248 | response->printf("{port:%d, name:'%s'}%s", Valves->at(i).GetPort1() ,Valves->at(i).subtopic.c_str(), (isize()-1?",":"")); 249 | } 250 | response->println("];\n"); 251 | } -------------------------------------------------------------------------------- /src/valveStructure.h: -------------------------------------------------------------------------------- 1 | 2 | #ifndef VALVESTRUCTURE_H 3 | #define VALVESTRUCTURE_H 4 | 5 | #include "CommonLibs.h" 6 | #include "CommonLibs.h" 7 | #include 8 | #include "baseconfig.h" 9 | #include "valve.h" 10 | #include "mqtt.h" 11 | 12 | extern BaseConfig* Config; 13 | 14 | #ifdef USE_I2C 15 | #include 16 | extern i2cdetect* I2Cdetect; 17 | #endif 18 | 19 | class valveStructure { 20 | 21 | public: 22 | valveStructure(uint8_t sda, uint8_t scl); 23 | void loop(); 24 | void OnForTimer(String SubTopic, int duration); 25 | void SetOn(String SubTopic); 26 | void SetOn(uint8_t Port); 27 | void SetOff(String SubTopic); 28 | void SetOff(uint8_t Port); 29 | bool GetState(uint8_t Port); 30 | bool GetEnabled(uint8_t Port); 31 | void SetEnable(uint8_t Port, bool state); 32 | uint8_t CountActiveThreads(); 33 | void GetInitData(AsyncResponseStream* response); 34 | void LoadJsonConfig(); 35 | void getWebJsParameter(AsyncResponseStream *response); 36 | void ReceiveMQTT(String topic, int value); 37 | 38 | 39 | private: 40 | valve* GetValveItem(uint8_t Port); 41 | valve* GetValveItem(String SubTopic); 42 | String GetJsonKeyMatch(JsonDocument* doc, String key); 43 | 44 | valveHardware* ValveHW = NULL; 45 | std::shared_ptr> Valves; 46 | 47 | uint8_t pin_sda = SDA; 48 | uint8_t pin_scl = SCL; 49 | uint8_t parallelThreads = 0; 50 | }; 51 | 52 | #endif 53 | --------------------------------------------------------------------------------