├── .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 |
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 | [](https://github.com/desktop/desktop/blob/master/LICENSE)
8 |
9 | 
10 | 
11 | 
12 |
13 |
14 |
15 | 
16 | 
17 | 
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 |
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 |
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 |
10 |
--------------------------------------------------------------------------------
/data/web/navi.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 | Modbus MQTT Gateway
10 |
11 |
12 |
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 |
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 | Name |
17 | Wert |
18 |
19 |
20 |
21 |
22 |
23 | IP-Adresse: |
24 | |
25 |
26 |
27 |
28 | WiFi Name: |
29 | |
30 |
31 |
32 |
33 | MAC: |
34 | |
35 |
36 |
37 |
38 | WiFi RSSI: |
39 | |
40 |
41 |
42 |
43 | i2c Bus:
44 | ↺
45 | |
46 |
47 |
48 | |
49 |
50 |
51 |
52 | MQTT Status: |
53 | |
54 |
55 |
56 |
57 | aktuell geöffnete Ventile |
58 | |
59 |
60 |
61 |
62 | Sensor RAW Value: |
63 | |
64 |
65 |
66 |
67 | Füllstand in %: |
68 | |
69 |
70 |
71 |
72 | Uptime: |
73 | |
74 |
75 |
76 |
77 | Free Heap Memory: |
78 | |
79 |
80 |
81 |
82 | Firmware Update |
83 | |
84 |
85 |
86 |
87 | Device Reboot |
88 | |
89 |
90 |
91 |
92 | Werkszustand herstellen (ohne WiFi) |
93 | |
94 |
95 |
96 |
97 | WiFi Zugangsdaten entfernen |
98 | |
99 |
100 |
101 |
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 |
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 |
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 |
--------------------------------------------------------------------------------