diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json new file mode 100644 index 00000000..bdc7166f --- /dev/null +++ b/.devcontainer/devcontainer.json @@ -0,0 +1,13 @@ +{ + "name": "SolaxModbusGateway", + "image": "mcr.microsoft.com/devcontainers/python:3.11", + "onCreateCommand": "pip3 install -U platformio", + "postStartCommand": "platformio run && platformio run --target buildfs", + "customizations": { + "vscode": { + "extensions": [ + "platformio.platformio-ide" + ] + } + } +} \ No newline at end of file diff --git a/.github/scripts/createManifest.py b/.github/scripts/createManifest.py index 9d79caf5..d83f067c 100644 --- a/.github/scripts/createManifest.py +++ b/.github/scripts/createManifest.py @@ -10,6 +10,7 @@ parser.add_argument('-s', '--build', type=str, help='Buildnummer der GithubAction (Unique ID)') parser.add_argument('-t', '--stage', type=str, help='Stage (Branch name)') parser.add_argument('-bp', '--binarypath', type=str, help='Path of binary files') +parser.add_argument('-fn', '--firmwarename', type=str, help='Name of the firmware') parser.add_argument('-rp', '--releasepath', type=str, default="release", help='Path of destination, BIN and JSON files') parser.add_argument('-rf', '--releasefile', type=str, help='Path of release file, contains version number') parser.add_argument('-a', '--arch', type=str, help='Architecture (ESP8266|ESP32|ESP32-S2|ESP32-C3|...)') @@ -26,6 +27,7 @@ logging.info(f"BUILDNUMMER={args.build}") logging.info(f"STAGE={args.stage}") logging.info(f"BINARYPATH={args.binarypath}") + logging.info(f"FIRMWARENAME={args.firmwarename}") logging.info(f"RELEASEPATH={args.releasepath}") logging.info(f"RELEASEFILE={args.releasefile}") logging.info(f"ARCHITECTURE={args.arch}") @@ -57,9 +59,7 @@ for file in files: if file == 'firmware.bin': FILENAME = os.path.splitext(file)[0] - FILEEXT = os.path.splitext(file)[1][1:] - FIRMWARENAME = args.binarypath.split(os.sep)[-2] # get the name of the firmware folder - + FILEEXT = os.path.splitext(file)[1][1:] DOWNLOADURL = f"https://tobiasfaust.github.io/{args.repository}/firmware/{FILENAME}.{FileExtension}.{FILEEXT}" ################ Create custom json ################## @@ -98,40 +98,40 @@ if os.path.isfile(os.path.join(args.binarypath, "merged-firmware.bin")): manifest_data["parts"].append({ - "path": f"https://tobiasfaust.github.io/{args.repository}/firmware/{SubDir}/{FIRMWARENAME}/merged-firmware.{FileExtension}.bin", + "path": f"https://tobiasfaust.github.io/{args.repository}/firmware/{SubDir}/{args.firmwarename}/merged-firmware.{FileExtension}.bin", "offset": 0 }) else: # fuer ESP8266 manifest_data["parts"].append({ - "path": f"https://tobiasfaust.github.io/{args.repository}/firmware/{SubDir}/{FIRMWARENAME}/{FILENAME}.{FileExtension}.{FILEEXT}", + "path": f"https://tobiasfaust.github.io/{args.repository}/firmware/{SubDir}/{args.firmwarename}/{FILENAME}.{FileExtension}.{FILEEXT}", "offset": 0 }) # process files.json if os.path.isfile(os.path.join(args.binarypath, "bootloader.bin")): files_data["parts"].append({ - "path": f"https://tobiasfaust.github.io/{args.repository}/firmware/{SubDir}/{FIRMWARENAME}/bootloader.{FileExtension}.bin", + "path": f"https://tobiasfaust.github.io/{args.repository}/firmware/{SubDir}/{args.firmwarename}/bootloader.{FileExtension}.bin", "offset": 4096, "filetype": "bootloader" }) if os.path.isfile(os.path.join(args.binarypath, "partitions.bin")): files_data["parts"].append({ - "path": f"https://tobiasfaust.github.io/{args.repository}/firmware/{SubDir}/{FIRMWARENAME}/partitions.{FileExtension}.bin", + "path": f"https://tobiasfaust.github.io/{args.repository}/firmware/{SubDir}/{args.firmwarename}/partitions.{FileExtension}.bin", "offset": 32768, "filetype": "partitions" }) if os.path.isfile(os.path.join(args.binarypath, "littlefs.bin")): files_data["parts"].append({ - "path": f"https://tobiasfaust.github.io/{args.repository}/firmware/{SubDir}/{FIRMWARENAME}/littlefs.{FileExtension}.bin", - "offset": int(readOffsetFromPartitionCSV("partitions.csv", "spiffs"), 16), + "path": f"https://tobiasfaust.github.io/{args.repository}/firmware/{SubDir}/{args.firmwarename}/littlefs.{FileExtension}.bin", + "offset": int(readOffsetFromPartitionCSV("partitions.csv", "webdata"), 16), "filetype": "filesystem" }) OFFSET = 0 if "ESP8266" in args.arch else int(readOffsetFromPartitionCSV("partitions.csv", "app0"), 16) files_data["parts"].append({ - "path": f"https://tobiasfaust.github.io/{args.repository}/firmware/{SubDir}/{FIRMWARENAME}/{FILENAME}.{FileExtension}.{FILEEXT}", + "path": f"https://tobiasfaust.github.io/{args.repository}/firmware/{SubDir}/{args.firmwarename}/{FILENAME}.{FileExtension}.{FILEEXT}", "offset": OFFSET, "filetype": "firmware" }) diff --git a/.github/scripts/createMergedFirmware.py b/.github/scripts/createMergedFirmware.py index 301c66eb..d34f4c18 100644 --- a/.github/scripts/createMergedFirmware.py +++ b/.github/scripts/createMergedFirmware.py @@ -9,7 +9,7 @@ -b, --BuildDir (str): The build directory containing the built firmware binaries. Required. -p, --PathOfPartitionsCSV (str): The path to the partitions.csv file. Default is 'partitions.csv'. Required. Functions: - readOffsetFromPartitionCSV(path: str, name: str) -> int: + readOffsetFromPartitionCSV(path: str, partitionLabel: str) -> int: Args: Returns: Example: @@ -31,31 +31,46 @@ args = parser.parse_args() result = None +# Bootloader offsets vary by chip +bootloader_offsets = { + "ESP32": "0x1000", + "ESP32-S2": "0x1000", + "ESP32-S3": "0x0", + "ESP32-C2": "0x0", + "ESP32-C3": "0x0", + "ESP32-C5": "0x2000", + "ESP32-C6": "0x0", + "ESP32-H2": "0x0", + "ESP32-P4": "0x2000", +} + if(not os.path.isfile(args.PathOfPartitionsCSV)): logging.error(f'File {args.PathOfPartitionsCSV} does not exist') exit(1) if args.BuildDir and os.path.isdir(args.BuildDir): if 'ESP32' in args.ChipFamily: - result = f'esptool.py --chip {args.ChipFamily} merge_bin \ + bootloader_offset = bootloader_offsets[args.ChipFamily] + + result = f'esptool --chip {args.ChipFamily} merge-bin \ --output {args.BuildDir}/merged-firmware.bin \ - --flash_mode dout \ - --flash_freq 80m \ - --flash_size 4MB \ - 0x1000 {args.BuildDir}/bootloader.bin \ + --flash-mode dout \ + --flash-freq 80m \ + --flash-size 4MB \ + {bootloader_offset} {args.BuildDir}/bootloader.bin \ 0x8000 {args.BuildDir}/partitions.bin \ {readOffsetFromPartitionCSV("partitions.csv", "app0")} {args.BuildDir}/firmware.bin' if os.path.isfile(f'{args.BuildDir}/littlefs.bin'): - result += f' {readOffsetFromPartitionCSV("partitions.csv", "spiffs")} {args.BuildDir}/littlefs.bin' + result += f' {readOffsetFromPartitionCSV("partitions.csv", "webdata")} {args.BuildDir}/littlefs.bin' elif 'ESP8266' in args.ChipFamily and os.path.isfile(f'{args.BuildDir}/littlefs.bin'): - result = f'esptool.py --chip {args.ChipFamily} merge_bin \ + result = f'esptool --chip {args.ChipFamily} merge-bin \ --output {args.BuildDir}/merged-firmware.bin \ - --flash_mode dout \ - --flash_freq 40m \ - --flash_size 4MB \ + --flash-mode dout \ + --flash-freq 40m \ + --flash-size 4MB \ 0x0000 {args.BuildDir}/firmware.bin \ - {readOffsetFromPartitionCSV("partitions.csv", "spiffs")} {args.BuildDir}/littlefs.bin' + {readOffsetFromPartitionCSV("partitions.csv", "webdata")} {args.BuildDir}/littlefs.bin' print(f'command={result}') \ No newline at end of file diff --git a/.github/scripts/myUtils.py b/.github/scripts/myUtils.py index 5f80746a..c2d4b957 100644 --- a/.github/scripts/myUtils.py +++ b/.github/scripts/myUtils.py @@ -288,7 +288,7 @@ def deleteVersions(root: str, keepVersions: int, json: list = None) -> None: # Lade die 'versions.json' Datei versions_file = os.path.join(root, 'versions.json') json = read_json_file(versions_file) - if versions is None: + if json is None: logging.error(f"Fehler beim Verarbeiten der Datei {versions_file}") return @@ -358,18 +358,18 @@ def changeURL(root: str, url: str) -> None: except Exception as e: logging.error(f"Fehler beim Verarbeiten der Datei {manifest_path}: {e}") -def readOffsetFromPartitionCSV(path: str, name: str) -> int: +def readOffsetFromPartitionCSV(path: str, partitionLabel: str) -> int: """ - Reads a CSV file and calculates the offset for a given name. + Reads a CSV file and calculates the offset for a given partition label. This function reads the specified CSV file with a delimiter of ','. It fills in the Offset column if it is empty by adding the previous offset and the value from the Size column of the previous row. It returns the calculated - offset from the row where the value in the first column matches the given name as a hexadecimal number. + offset from the row where the value in the first column matches the given partition label as a hexadecimal number. If the file does not exist or cannot be read, it returns None. Args: path (str): The path to the CSV file. - name (str): The name to search for in the first column. + partitionLabel (str): The partition label to search for in the first column. Returns: int: The calculated offset as a hexadecimal number, or None if the file cannot be read. @@ -379,7 +379,7 @@ def readOffsetFromPartitionCSV(path: str, name: str) -> int: with open(path, 'r') as file: lines = file.readlines() headers = lines[0].strip().split(',') - name_index = 0 + partition_label_index = 0 offset_index = 3 size_index = 4 @@ -391,7 +391,7 @@ def readOffsetFromPartitionCSV(path: str, name: str) -> int: columns[offset_index] = str(previous_offset + previous_size) previous_size = int(columns[size_index], 0) previous_offset = int(columns[offset_index], 0) - if columns[name_index] == name: + if columns[partition_label_index] == partitionLabel: return hex(previous_offset) except (FileNotFoundError, IndexError, ValueError): return None \ No newline at end of file diff --git a/.github/workflows/BuildAndDeploy.yml b/.github/workflows/BuildAndDeploy.yml index aa6cdc1c..0562661d 100644 --- a/.github/workflows/BuildAndDeploy.yml +++ b/.github/workflows/BuildAndDeploy.yml @@ -17,6 +17,11 @@ on: - '**.yml' - '**.sh' - '**.py' + - '**.json' + - '**.js' + - '**.css' + - '**.html' + - '**.ini' jobs: build: @@ -46,9 +51,6 @@ jobs: - firmware: firmware_ESP32-C3 architecture: ESP32-C3 subvariant: standard - - firmware: firmware_ESP32-WebSerial - architecture: ESP32 - subvariant: webSerial steps: - name: checkout repository @@ -68,13 +70,18 @@ jobs: - name: Run PlatformIO id: buildFw run: | + mkdir -p firmware/ platformio run -e ${{ matrix.variant.firmware }} + cp .pio/build/${{ matrix.variant.firmware }}/firmware.bin firmware/ + cp .pio/build/${{ matrix.variant.firmware }}/partitions.bin firmware/ + cp .pio/build/${{ matrix.variant.firmware }}/bootloader.bin firmware/ if [ -d "data" ]; then platformio run --target buildfs -e ${{ matrix.variant.firmware }} + cp .pio/build/${{ matrix.variant.firmware }}/littlefs.bin firmware/ fi python .github/scripts/createMergedFirmware.py \ --ChipFamily ${{ matrix.variant.architecture }} \ - --BuildDir .pio/build/${{ matrix.variant.firmware }} \ + --BuildDir firmware \ --PathOfPartitionsCSV partitions.csv \ >> "$GITHUB_OUTPUT" @@ -85,7 +92,7 @@ jobs: - name: Display generated files run: | - ls -R .pio/build/${{ matrix.variant.firmware }}/ + ls -R firmware/ - name: Schreibe Json/Manifest File id: json_params @@ -96,7 +103,8 @@ jobs: --variant ${{ matrix.variant.subvariant }} \ --stage ${{ env.BRANCH_NAME }} \ --repository ${{ env.REPOSITORY }} \ - --binarypath .pio/build/${{ matrix.variant.firmware }}/ \ + --binarypath firmware/ \ + --firmwarename ${{ matrix.variant.firmware }} \ --releasepath release \ --artifactpath artifacts \ --build ${{ github.run_number }} \ @@ -108,19 +116,6 @@ jobs: with: name: ${{ matrix.variant.firmware }} path: artifacts/* - -# - name: Upload to AWS S3 -# uses: jakejarvis/s3-sync-action@master -# with: -# args: '--acl public-read --follow-symlinks' -# env: -# AWS_S3_BUCKET: 'tfa-releases' -# AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} -# AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} -# AWS_REGION: 'eu-central-1' # optional: defaults to us-east-1 -# SOURCE_DIR: 'release' # optional: defaults to entire repository -# DEST_DIR: ${{ env.REPOSITORY }} - web-installer: needs: [build] @@ -250,7 +245,7 @@ jobs: continue fi echo "Uploading $file" - gh release upload -R tobiasfaust/${{ env.GITHUB_REPOSITORY }} ${{ steps.set_env_var.outputs.tagname }} "$file" --clobber + gh release upload -R ${{env.GITHUB_OWNER}}/${{ env.GITHUB_REPOSITORY }} ${{ steps.set_env_var.outputs.tagname }} "$file" --clobber fi done done @@ -263,18 +258,18 @@ jobs: GH_TOKEN: ${{ github.token }} run: | mkdir -p manifests - tagNames=$(gh release ls -R tobiasfaust/${{ env.GITHUB_REPOSITORY }} --json tagName) + tagNames=$(gh release ls -R ${{env.GITHUB_OWNER}}/${{ env.GITHUB_REPOSITORY }} --json tagName) for tag in $(echo "$tagNames" | jq -r '.[].tagName'); do - for asset in $(gh release view $tag -R tobiasfaust/${{ env.GITHUB_REPOSITORY }} --json assets --jq '.assets[].name' | grep -E 'manifestAll|filesAll'); do - echo "Downloading file: gh release download $tag -R tobiasfaust/${{ env.GITHUB_REPOSITORY }} --pattern '$asset' --dir 'manifests/${tag}/'" + for asset in $(gh release view $tag -R ${{env.GITHUB_OWNER}}/${{ env.GITHUB_REPOSITORY }} --json assets --jq '.assets[].name' | grep -E 'manifestAll|filesAll'); do + echo "Downloading file: gh release download $tag -R ${{env.GITHUB_OWNER}}/${{ env.GITHUB_REPOSITORY }} --pattern '$asset' --dir 'manifests/${tag}/'" gh release download $tag \ - -R tobiasfaust/${{ env.GITHUB_REPOSITORY }} \ + -R ${{env.GITHUB_OWNER}}/${{ env.GITHUB_REPOSITORY }} \ --pattern "$asset" \ --dir "manifests/${tag}/" || true done mkdir -p manifests/${tag}/ - gh release view $tag -R tobiasfaust/${{ env.GITHUB_REPOSITORY }} --json assets > "manifests/${tag}/assets.json" + gh release view $tag -R ${{env.GITHUB_OWNER}}/${{ env.GITHUB_REPOSITORY }} --json assets > "manifests/${tag}/assets.json" done python app/.github/scripts/createReleaseVersions.py \ --ManifestDir manifests \ diff --git a/.gitignore b/.gitignore index 84b3952b..2d5045c7 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,7 @@ .pio/ .vscode/ .git/ +data/web/handlefiles.* +data/web/webserial.* +include/board.h +data/web/gpio.js diff --git a/.gitpod.yml b/.gitpod.yml deleted file mode 100644 index 72a03cf1..00000000 --- a/.gitpod.yml +++ /dev/null @@ -1,3 +0,0 @@ -tasks: - - command: pip3 install -U platformio && platformio run && platformio run --target buildfs - \ No newline at end of file diff --git a/CPPLINT.cfg b/CPPLINT.cfg new file mode 100644 index 00000000..3c255f5e --- /dev/null +++ b/CPPLINT.cfg @@ -0,0 +1,3 @@ +linelength=120 +root=src +filter=-build/include_what_you_use \ No newline at end of file diff --git a/ChangeLog.md b/ChangeLog.md index 1ce829e9..719b4cc1 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -1,9 +1,49 @@ +Release 3.4.3: + - ... + +Release 3.4.2: + - feature: specific board settings via board.h, important for ESP32-C3 + - feature: add new Deye SG05LP3 (thanks to @MagicSven81) (#171) + - feature: add new openWB topics up from version 2.1.8 (thanks to @AndreasNewbie) (#169) + +Release 3.4.1: + - fix BatVoltage in Growatt-SPH.json, thanks @Motl1 (#164) + - feature: add options "+unit" for modbus websocket streaming to show unit in output (#164) + - Fix: change gitpod to github codespace + +Release 3.4.0: + +++++++ This version is not OTA compatible with older versions +++++++ + +++++++ Please do a fresh installation by web-installer +++++++ + - add python.env file with HTML_DIR variable + - splitting LittleFS partitions: Separating system files (web UI, static assets) and user configuration/data (settings, user uploads) into distinct partitions (sysFS/configFS) improves reliability, allows safe firmware updates without overwriting user data, and enables easier backup/restore of user settings independently from system files. + - feature: add new webserial log monitor page for all variants + - fix broken ethernet connection handling by adding global gpio handling + - openwb: add more topics to Growatt-SPH (thanks to @AndreasNewbie) (#160) + +Release 3.3.4: + - Change default pins for ESP32-C3 (#149), thanks to @NHellFire + - remove deprecated backup functionality from Elegant-OTA + +Release 3.3.3: + - improve logging functionality methods + - fixing some bugs + - updated Solax-X3 json (thanks to @Lazgar) + +Release 3.3.2: + - new feature: add confirmation dialog for ESP reset + - migrate from old ajax communication to standard websocket communication + - new feature: show set topics in WebUI (thanks to @laszgar) + - new feature: add "newUpdate available" info in WebUI header + - new feature: add toggle buttons at ModbusItems WebUI to change all items at once (#96) + - new Inverter: add Growatt-SPH-V124 register file (thanks to @StefanNouza) (#109) + Release 3.3.1: - new Feature: datatype "binary" now available for json register definitions (PR #115) - BugFix: fix null-terminationof string handling (#96) - new feature: support for OpenWB 2.0 Api (#100) - bugfix: fix esp crash for /getitems if using an huge register table (#76) - fix CORS Issue when download a stable release + - bugfix: fix register id definition (#113) Release 3.3.0: - new feature: WebSerial as remote serial output (#74) diff --git a/README.md b/README.md index ed163182..d003afd0 100644 --- a/README.md +++ b/README.md @@ -25,6 +25,7 @@ Currently the following Inverters are with thier special registers integrated: * Growatt SPH * Sofar-KTL Solarmax-SGA * Deye Sun SG04LP3 +* Deye Sun SG05LP3 * QVolt-HYP-G3-3P If your Solar Inverter is not listed, feel free to add the special register simply, please check th [wiki page](configuration-register) or contact me by opening a [new issue](https://github.com/tobiasfaust/SolaxModbusGateway/issues) in github. diff --git a/data/misc/openwb.json b/data/misc/openwb.json index bece0478..bf982262 100644 --- a/data/misc/openwb.json +++ b/data/misc/openwb.json @@ -3,23 +3,57 @@ "version": "1.9", "topics": [ { "setpvw": "openWB/set/pv/#InverterID#/W" }, + { "setpvwh": "openWB/set/pv/#InverterID#/WhCounter"}, + { "setcoexp": "openWB/set/houseMeter/WhExported" }, + { "setcocur": "openWB/set/houseMeter/Currents" }, + { "setcofre": "openWB/set/houseMeter/Frequency" }, + { "setcovol": "openWB/set/houseMeter/Voltages" }, + { "setcopow": "openWB/set/houseMeter/Powers" }, + { "setcopowf": "openWB/set/houseMeter/PowerFactors" }, + { "setcoimp": "openWB/set/houseMeter/WhImported" }, + { "setcow": "openWB/set/houseMeter/W" }, { "setbatw": "openWB/set/houseBattery/W"}, { "setbatsoc": "openWB/set/houseBattery/%Soc"}, { "setbatexpwh": "openWB/set/houseBattery/WhExported"}, - { "setbatimpwh": "openWB/set/houseBattery/WhImported"}, - { "setcounterwh": "openWB/set/pv/#InverterID#/WhCounter"} + { "setbatimpwh": "openWB/set/houseBattery/WhImported"} ] }, { "version": "2.0", "topics": [ { "setpvw": "openWB/set/pv/#InverterID#/get/power" }, - { "setbatw": "openWB/set/bat/#BatteryID#/get/power"}, - { "setbatsoc": "openWB/set/bat/#BatteryID#/get/soc"}, - { "setbatexpwh": "openWB/set/bat/#BatteryID#/get/exported"}, - { "setbatimpwh": "openWB/set/bat/#BatteryID#/get/imported"}, - { "setcounterwh": " openWB/set/pv/#InverterID#/get/exported"} - + { "setpvwh": "openWB/set/pv/#InverterID#/get/exported" }, + { "setbatw": "openWB/set/bat/#BatteryID#/get/power" }, + { "setbatsoc": "openWB/set/bat/#BatteryID#/get/soc" }, + { "setbatexpwh": "openWB/set/bat/#BatteryID#/get/exported" }, + { "setbatimpwh": "openWB/set/bat/#BatteryID#/get/imported" }, + { "setcoexp": "openWB/set/counter/#SmartMeterID#/get/exported" }, + { "setcocur": "openWB/set/counter/#SmartMeterID#/get/currents" }, + { "setcofre": "openWB/set/counter/#SmartMeterID#/get/frequency" }, + { "setcovol": "openWB/set/counter/#SmartMeterID#/get/voltages" }, + { "setcopow": "openWB/set/counter/#SmartMeterID#/get/power" }, + { "setcopowf": "openWB/set/counter/#SmartMeterID#/get/power_factors" }, + { "setcoimp": "openWB/set/counter/#SmartMeterID#/get/imported" }, + { "setcow": "openWB/set/counter/#SmartMeterID#/get/power" } ] - } + }, + { + "version": "2.1.8", + "topics": [ + { "setpvw": "openWB/set/mqtt/pv/#InverterID#/get/power" }, + { "setpvwh": "openWB/set/mqtt/pv/#InverterID#/get/exported" }, + { "setbatw": "openWB/set/mqtt/bat/#BatteryID#/get/power" }, + { "setbatsoc": "openWB/set/mqtt/bat/#BatteryID#/get/soc" }, + { "setbatexpwh": "openWB/set/mqtt/bat/#BatteryID#/get/exported" }, + { "setbatimpwh": "openWB/set/mqtt/bat/#BatteryID#/get/imported" }, + { "setcoexp": "openWB/set/mqtt/counter/#SmartMeterID#/get/exported" }, + { "setcocur": "openWB/set/mqtt/counter/#SmartMeterID#/get/currents" }, + { "setcofre": "openWB/set/mqtt/counter/#SmartMeterID#/get/frequency" }, + { "setcovol": "openWB/set/mqtt/counter/#SmartMeterID#/get/voltages" }, + { "setcopow": "openWB/set/mqtt/counter/#SmartMeterID#/get/power" }, + { "setcopowf": "openWB/set/mqtt/counter/#SmartMeterID#/get/power_factors" }, + { "setcoimp": "openWB/set/mqtt/counter/#SmartMeterID#/get/imported" }, + { "setcow": "openWB/set/mqtt/counter/#SmartMeterID#/get/power" } + ] + } ] \ No newline at end of file diff --git a/data/regs/Deye-SUN-SG05LP3.json b/data/regs/Deye-SUN-SG05LP3.json new file mode 100644 index 00000000..34a67e40 --- /dev/null +++ b/data/regs/Deye-SUN-SG05LP3.json @@ -0,0 +1,201 @@ +{ + "Deye-SUN-xxK-SG05LP3-EU-SM2": { + "config": { + "author": "MagicSven81", + "RequestLiveData": [ + [ + "#ClientID", + "0x03", + "0x02", + "0x44", + "0x00", + "0x66" + ] + ], + "ClientIdPos": 0, + "LiveDataFunctionCodePos": 1, + "LiveDataFunctionCode": "0x03", + "LiveDataStartsAtPos": 3, + "LiveDataSuccessCode": "0x03" + }, + "data": { + "livedata": [ + + { + "position": [17, 18], + "name": "BatV", + "realname": "Batterie Spannung", + "datatype": "float", + "factor": 0.01, + "unit": "V" + }, + { + "position": [19, 20], + "name": "BatSOC", + "realname": "Batterie SOC", + "datatype": "integer", + "unit": "%" + }, + { + "position": [21, 22], + "name": "BatCurrent", + "realname": "Batterie Strom", + "datatype": "float", + "factor": 0.01, + "unit": "A" + }, + { + "position": [23, 24], + "name": "BatP", + "realname": "Batterie Leistung", + "datatype": "integer", + "unit": "W" + }, + { + "position": [25, 26], + "name": "BatTemp", + "realname": "Batterie Temperatur", + "datatype": "float", + "factor": 0.1, + "valueAdd": -100, + "unit": "°C" + }, + { + "position": [69, 70], + "name": "GridV_L1", + "realname": "Netzspannung L1", + "datatype": "float", + "factor": 0.1, + "unit": "V" + }, + { + "position": [71, 72], + "name": "GridV_L2", + "realname": "Netzspannung L2", + "datatype": "float", + "factor": 0.1, + "unit": "V" + }, + { + "position": [73, 74], + "name": "GridV_L3", + "realname": "Netzspannung L3", + "datatype": "float", + "factor": 0.1, + "unit": "V" + }, + { + "position": [75, 76], + "name": "GridP_L1", + "realname": "Netz L1 Leistung", + "datatype": "integer", + "unit": "W" + }, + { + "position": [77, 78], + "name": "GridP_L2", + "realname": "Netz L2 Leistung", + "datatype": "integer", + "unit": "W" + }, + { + "position": [79, 80], + "name": "GridP_L3", + "realname": "Netz L3 Leistung", + "datatype": "integer", + "unit": "W" + }, + { + "position": [81, 82], + "name": "Grid_Total", + "realname": "Netz Gesamtleistung", + "datatype": "integer", + "unit": "W" + }, + { + "position": [83, 84], + "name": "GridFreq", + "realname": "Netzfrequenz", + "datatype": "float", + "factor": 0.01, + "unit": "Hz" + }, + { + "position": [145, 146], + "name": "Load_L1", + "realname": "Hausverbrauch L1", + "datatype": "integer", + "unit": "W" + }, + { + "position": [147, 148], + "name": "Load_L2", + "realname": "Hausverbrauch L2", + "datatype": "integer", + "unit": "W" + }, + { + "position": [149, 150], + "name": "Load_Total", + "realname": "Hausverbrauch Gesamt", + "datatype": "integer", + "unit": "W" + }, + { + "position": [151, 152], + "name": "Load_L3", + "realname": "Hausverbrauch L3", + "datatype": "integer", + "unit": "W" + }, + { + "position": [187, 188], + "name": "PV1_P", + "realname": "PV1 Leistung", + "datatype": "integer", + "unit": "W" + }, + { + "position": [189, 190], + "name": "PV2_P", + "realname": "PV2 Leistung", + "datatype": "integer", + "unit": "W" + }, + { + "position": [193, 194], + "name": "PV1_V", + "realname": "PV1 Spannung", + "datatype": "float", + "factor": 0.1, + "unit": "V" + }, + { + "position": [195, 196], + "name": "PV1_I", + "realname": "PV1 Strom", + "datatype": "float", + "factor": 0.1, + "unit": "A" + }, + { + "position": [197, 198], + "name": "PV2_V", + "realname": "PV2 Spannung", + "datatype": "float", + "factor": 0.1, + "unit": "V" + }, + { + "position": [199, 200], + "name": "PV2_I", + "realname": "PV2 Strom", + "datatype": "float", + "factor": 0.1, + "unit": "A" + } + + ] + } + } +} diff --git a/data/regs/Deye_SUN_SG04LP3.json b/data/regs/Deye_SUN_SG04LP3.json index d291499b..cf5d3358 100644 --- a/data/regs/Deye_SUN_SG04LP3.json +++ b/data/regs/Deye_SUN_SG04LP3.json @@ -336,7 +336,7 @@ "realname": "Total PV Power in Wh", "datatype": "float", "factor": 100, - "openwbtopic": "setcounterwh", + "openwbtopic": "setpvwh", "unit": "KWh" }, { diff --git a/data/regs/Growatt-MOD.json b/data/regs/Growatt-MOD.json index e4e858a9..74d74715 100644 --- a/data/regs/Growatt-MOD.json +++ b/data/regs/Growatt-MOD.json @@ -331,7 +331,7 @@ "name": "TotalEnergyGeneratedWh", "realname": "Erzeugte Energie Gesamt in Wh", "datatype": "integer", - "openwbtopic": "setcounterwh", + "openwbtopic": "setpvwh", "factor": 100, "unit": "KWh" }, diff --git a/data/regs/Growatt-SPH-V124.json b/data/regs/Growatt-SPH-V124.json new file mode 100644 index 00000000..922dbb5d --- /dev/null +++ b/data/regs/Growatt-SPH-V124.json @@ -0,0 +1,691 @@ +{ + "Growatt-SPH-V124": { + "config": { + "author": [ + "StefanNouza", + "AndreasNewbie" + ], + "RequestLiveData": [ + ["#ClientID", "0x04", "0x00", "0x00", "0x00", "0x77"], + ["#ClientID", "0x04", "0x03", "0xF1", "0x00", "0x7D"] + ], + "RequestIdData": [ + ["#ClientID", "0x03", "0x00", "0x00", "0x00", "0x1E"] + ], + "ClientIdPos": 0, + "LiveDataFunctionCodePos": 1, + "LiveDataFunctionCode": "0x04", + "IdDataFunctionCodePos": 1, + "IdDataFunctionCode": "0x03", + "LiveDataStartsAtPos": 3, + "IdDataStartsAtPos": 3, + "LiveDataErrorPos": 1, + "LiveDataErrorCode": "0x84", + "IdDataErrorPos": 1, + "IdDataErrorCode": "0x83", + "LiveDataSuccessPos": 1, + "LiveDataSuccessCode": "0x04", + "IdDataSuccessPos": 1, + "IdDataSuccessCode": "0x03" + }, + "data": { + "livedata": [ + { + "position": [3, 4], + "name": "Inverter_Status", + "realname": "Inverter status (number)", + "datatype": "integer", + "unit": "" + }, + { + "position": [3, 4], + "name": "Inverter_StatusText", + "realname": "Inverter status (text)", + "datatype": "integer", + "mapping": [[0,"0: waiting"], + [1,"1: normal"], + [2,"2:"], + [3,"3: fault"], + [4,"4:"], + [5,"5:"], + [6,"6:"], + [7,"7:"], + [8,"8:"], + [9,"9:"]], + "unit": "" + }, + { + "position": [5, 6, 7, 8], + "name": "Power_PV_Input_W", + "realname": "Power of PV input", + "openwbtopic": "setpvw", + "datatype": "float", + "factor": -0.1, + "unit": "W" + }, + { + "position": [9, 10], + "name": "PV1_InputVoltage_V", + "realname": "PV1 input Voltage", + "datatype": "float", + "factor": 0.1, + "unit": "V" + }, + { + "position": [11, 12], + "name": "PV1_InputCurrent_A", + "realname": "PV1 input Current", + "datatype": "float", + "factor": 1.0, + "unit": "A" + }, + { + "position": [13, 14, 15, 16], + "name": "Power_PV1_Input_W", + "realname": "Power of PV1 input", + "datatype": "float", + "factor": 0.1, + "unit": "W" + }, + { + "position": [17, 18], + "name": "PV2_InputVoltage_V", + "realname": "PV2 input Voltage", + "datatype": "float", + "factor": 0.1, + "unit": "V" + }, + { + "position": [19, 20], + "name": "PV2_InputCurrent_A", + "realname": "PV2 input Current", + "datatype": "float", + "factor": 1.0, + "unit": "A" + }, + { + "position": [21, 22, 23, 24], + "name": "Power_PV2_Input_W", + "realname": "Power of PV2 input", + "datatype": "float", + "factor": 0.1, + "unit": "W" + }, + { + "position": [73, 74, 75, 76], + "name": "AC_OutputPower_W", + "realname": "AC output Power", + "datatype": "float", + "factor": 0.1, + "unit": "W" + }, + { + "position": [77, 78], + "name": "AC_GridFrequency_Hz", + "realname": "Grid Frequency", + "openwbtopic": "setcofre", + "datatype": "float", + "factor": 0.01, + "unit": "Hz" + }, + { + "position": [109, 110, 111, 112], + "name": "Energy_Generated_today_kWh", + "realname": "generated Energy today", + "openwbtopic": "setpvwh", + "datatype": "float", + "factor": 0.1, + "unit": "kWh" + }, + { + "position": [113, 114, 115, 116], + "name": "Energy_Generated_total_kWh", + "realname": "generated Energy total", + "datatype": "float", + "factor": 0.1, + "unit": "kWh" + }, + { + "position": [117, 118, 119, 120], + "name": "Inverter_WorkTime_total_s", + "realname": "Inverter work time total", + "datatype": "float", + "factor": 0.5, + "unit": "s" + }, + { + "position": [121, 122, 123, 124], + "name": "Energy_Generated_PV1_today_kWh", + "realname": "generated Energy today PV1", + "datatype": "float", + "factor": 0.1, + "unit": "kWh" + }, + { + "position": [125, 126, 127, 128], + "name": "Energy_Generated_PV1_total_kWh", + "realname": "generated Energy total PV1", + "datatype": "float", + "factor": 0.1, + "unit": "kWh" + }, + { + "position": [129, 130, 131, 132], + "name": "Energy_Generated_PV2_today_kWh", + "realname": "generated Energy today PV2", + "datatype": "float", + "factor": 0.1, + "unit": "kWh" + }, + { + "position": [133, 134, 135, 136], + "name": "Energy_Generated_PV2_total_kWh", + "realname": "generated Energy total PV2", + "datatype": "float", + "factor": 0.1, + "unit": "kWh" + }, + { + "position": [185, 186, 187, 188], + "name": "Energy_Generated_PV_total_kWh", + "realname": "generated Energy total PV", + "datatype": "float", + "factor": 0.1, + "unit": "kWh" + }, + { + "position": [211, 212], + "name": "Inverter_DeratingMode", + "realname": "Inverter derating mode", + "datatype": "integer", + "mapping": [[0,"no derate"], + [1,"PV derate"], + [2,"derate-2"], + [3,"Vac derate"], + [4,"Fac derate"], + [5,"Tboost derate"], + [6,"Tinv derate"], + [7,"Control derate"], + [8,"derate-8"], + [9,"OverBack By Time derate"]], + "unit": "" + }, + { + "position": [213, 214], + "name": "Inverter_FaultCode", + "realname": "Inverter fault code (number)", + "datatype": "integer", + "unit": "" + }, + { + "position": [215, 216, 217, 218], + "name": "Inverter_FaultCodeText", + "realname": "Inverter fault code bits (text)", + "datatype": "integer", + "mapping": [[1,"b00 "], + [2,"b01 communication error"], + [4,"b02 "], + [8,"b03 StrReverse or StrShort fault"], + [16,"b04 model init fault"], + [32,"b05 grid volt sample different"], + [64,"b06 ISO sample different"], + [128,"b07 GFCI sample different"], + [256,"b08 "], + [512,"b09 "], + [1024,"b10 "], + [2048,"b11 "], + [4096,"b12 AFCI fault"], + [8192,"b13 "], + [16384,"b14 AFCI module fault"], + [32768,"b15 "], + [65536,"b16 "], + [131072,"b17 relay check fault"], + [262144,"b18 "], + [524288,"b19 "], + [1048576,"b20 "], + [2097152,"b21 communication error"], + [4194304,"b22 bus voltage error"], + [8388608,"b23 auto-test fail"], + [16777216,"b24 no utility"], + [33554432,"b25 PV isolation low"], + [67108864,"b26 residual I high"], + [134217728,"b27 output high DCI"], + [268435456,"b28 PV voltage high"], + [536870912,"b29 AC V outrange"], + [1073741824,"b30 AC F outrange"], + [-2147483648,"b31 temperature high"]], + "unit": "" + }, + { + "position": [227, 228, 229, 230], + "name": "Energy_ChargeFromGrid_today_kWh", + "realname": "Charge Energy from Grid today", + "datatype": "float", + "factor": 0.1, + "unit": "kWh" + }, + { + "position": [231, 232, 233, 234], + "name": "Energy_ChargeFromGrid_total_kWh", + "realname": "Charge Energy from Grid total", + "datatype": "float", + "factor": 0.1, + "unit": "kWh" + }, + { + "position": [239, 240], + "name": "Inverter_Priority", + "realname": "Priority of power-distribution by inverter (number)", + "datatype": "integer", + "unit": "" + }, + { + "position": [239, 240], + "name": "Inverter_Priority_Text", + "realname": "Priority of power-distribution by inverter (text)", + "datatype": "integer", + "mapping": [[0,"0: Load first"], + [1,"1: Batt first"], + [2,"2: Grid first"]], + "unit": "" + }, + { + "position": [246, 247, 248, 249], + "name": "Power_Battery_Discharge_W", + "realname": "Discharge Power", + "openwbtopic": "setbatexpwh", + "datatype": "float", + "factor": 0.1, + "unit": "W" + }, + { + "position": [250, 251, 252, 253], + "name": "Power_Battery_Charge_W", + "realname": "Charge Power", + "openwbtopic": "setbatimpwh", + "datatype": "float", + "factor": 0.1, + "unit": "W" + }, + { + "position": [246, 247, 248, 249], + "position2": [250, 251, 252, 253], + "name": "BatChargingPower", + "realname": "Battery Charging Power", + "openwbtopic": "setbatw", + "datatype": "float", + "factor": -0.1, + "unit": "W" + }, + { + "position": [254, 255], + "name": "Battery_Voltage_V", + "realname": "Battery Voltage", + "datatype": "float", + "factor": 0.1, + "unit": "V" + }, + { + "position": [256, 257], + "name": "Battery_SOC_Percent", + "realname": "Battery State Of Charge", + "openwbtopic": "setbatsoc", + "datatype": "float", + "factor": 1.0, + "unit": "%" + }, + { + "position": [270, 271, 272, 273], + "name": "Power_AC_GridToUser_W", + "realname": "SmartMeter: AC Power Grid to User", + "datatype": "float", + "factor": 0.1, + "unit": "W" + }, + { + "position": [286, 287, 288, 289], + "name": "Power_AC_UserToGrid_W", + "realname": "SmartMeter: AC Power User to Grid", + "datatype": "float", + "factor": 0.1, + "unit": "W" + }, + { + "position": [270, 271, 272, 273], + "position2": [286, 287, 288, 289], + "name": "Power_AC_W", + "realname": "SmartMeter: Power", + "openwbtopic": "setcopow", + "datatype": "float", + "factor": 0.1, + "unit": "W" + }, + { + "position": [302, 303, 304, 305], + "name": "Power_AC_toLocalLoad_W", + "realname": "Power to Local Load", + "datatype": "float", + "factor": 0.1, + "unit": "W" + }, + { + "position": [308, 309], + "name": "Battery_Temperature_degC", + "realname": "Battery Temperature", + "datatype": "float", + "factor": 0.1, + "unit": "°C" + }, + { + "position": [316, 317, 318, 319], + "name": "Energy_toUser_today_kWh", + "realname": "Energy to User today", + "datatype": "float", + "factor": 0.1, + "unit": "kWh" + }, + { + "position": [320, 321, 322, 323], + "name": "Energy_toUser_total_kWh", + "realname": "Energy to User total", + "openwbtopic": "setcoimp", + "datatype": "float", + "factor": 0.1, + "unit": "kWh" + }, + { + "position": [324, 325, 326, 327], + "name": "Energy_toGrid_today_kWh", + "realname": "Energy to Grid today", + "datatype": "float", + "factor": 0.1, + "unit": "kWh" + }, + { + "position": [328, 329, 330, 331], + "name": "Energy_toGrid_total_kWh", + "realname": "Energy to Grid total", + "openwbtopic": "setcoexp", + "datatype": "float", + "factor": 0.1, + "unit": "kWh" + }, + { + "position": [332, 333, 334, 335], + "name": "Energy_Discharge_today_kWh", + "realname": "Discharge Energy today", + "datatype": "float", + "factor": 0.1, + "unit": "kWh" + }, + { + "position": [336, 337, 338, 339], + "name": "Energy_Discharge_total_kWh", + "realname": "Discharge Energy total", + "datatype": "float", + "factor": 0.1, + "unit": "kWh" + }, + { + "position": [340, 341, 342, 343], + "name": "Energy_Charge_today_kWh", + "realname": "Charge Energy today", + "datatype": "float", + "factor": 0.1, + "unit": "kWh" + }, + { + "position": [344, 345, 346, 347], + "name": "Energy_Charge_total_kWh", + "realname": "Charge Energy total", + "datatype": "float", + "factor": 0.1, + "unit": "kWh" + }, + { + "position": [348, 349, 350, 351], + "name": "Energy_LocalLoad_today_kWh", + "realname": "Energy Local Load today", + "datatype": "float", + "factor": 0.1, + "unit": "kWh" + }, + { + "position": [352, 353, 354, 355], + "name": "Energy_LocalLoad_total_kWh", + "realname": "Energy Local Load total", + "datatype": "float", + "factor": 0.1, + "unit": "kWh" + }, + { + "position": [362, 363], + "name": "UPS_Frequency_Hz", + "realname": "UPS Frequency", + "datatype": "float", + "factor": 0.01, + "unit": "Hz" + }, + { + "position": [364, 365], + "name": "UPS_VoltageL1_V", + "realname": "UPS Voltage L1", + "datatype": "float", + "factor": 0.1, + "unit": "V" + }, + { + "position": [366, 367], + "name": "UPS_CurrentL1_A", + "realname": "UPS Current L1", + "datatype": "float", + "factor": 0.1, + "unit": "A" + }, + { + "position": [368, 369, 370, 371], + "name": "UPS_PowerL1_VA", + "realname": "UPS Power L1", + "datatype": "float", + "factor": 0.1, + "unit": "VA" + }, + { + "position": [372, 373], + "name": "UPS_VoltageL2_V", + "realname": "UPS Voltage L2", + "datatype": "float", + "factor": 0.1, + "unit": "V" + }, + { + "position": [374, 375], + "name": "UPS_CurrentL2_A", + "realname": "UPS Current L2", + "datatype": "float", + "factor": 0.1, + "unit": "A" + }, + { + "position": [376, 377, 378, 379], + "name": "UPS_PowerL2_VA", + "realname": "UPS Power L2", + "datatype": "float", + "factor": 0.1, + "unit": "VA" + }, + { + "position": [380, 381], + "name": "UPS_VoltageL3_V", + "realname": "UPS Voltage L3", + "datatype": "float", + "factor": 0.1, + "unit": "V" + }, + { + "position": [382, 383], + "name": "UPS_CurrentL3_A", + "realname": "UPS Current L3", + "datatype": "float", + "factor": 0.1, + "unit": "A" + }, + { + "position": [384, 385, 386, 387], + "name": "UPS_PowerL3_VA", + "realname": "UPS Power L3", + "datatype": "float", + "factor": 0.1, + "unit": "VA" + }, + { + "position": [388, 389], + "name": "UPS_Load_Percent", + "realname": "UPS Load Percent", + "datatype": "float", + "factor": 1.0, + "unit": "%" + }, + { + "position": [400, 401], + "name": "Battery_SOC_BMS_Percent", + "realname": "Battery State Of Charge from BMS", + "datatype": "float", + "factor": 1.0, + "unit": "%" + }, + { + "position": [402, 403], + "name": "Battery_Voltage_BMS_V", + "realname": "Battery Voltage from BMS", + "datatype": "float", + "factor": 0.1, + "unit": "V" + }, + { + "position": [404, 405], + "name": "Battery_Current_BMS_A", + "realname": "Battery Current from BMS", + "datatype": "float", + "factor": 0.1, + "unit": "A" + }, + { + "position": [406, 407], + "name": "Battery_Temperature_BMS_degC", + "realname": "Battery Temperature from BMS", + "datatype": "float", + "factor": 0.1, + "unit": "°C" + }, + { + "position": [418, 419], + "name": "Battery_CycleCount_BMS", + "realname": "Cycle count from BMS", + "datatype": "integer", + "unit": "" + }, + { + "position": [420, 421], + "name": "Battery_SOH_BMS_Percent", + "realname": "Battery State Of Health from BMS", + "datatype": "float", + "factor": 1.0, + "unit": "%" + }, + { + "position": [444, 445], + "name": "BMS_MaxCellVoltage_V", + "realname": "Highest cell voltage", + "datatype": "float", + "factor": 0.001, + "unit": "V" + }, + { + "position": [446, 447], + "name": "BMS_MinCellVoltage_V", + "realname": "Lowest cell voltage", + "datatype": "float", + "factor": 0.001, + "unit": "V" + }, + { + "position": [452, 453], + "name": "BMS_MaxVoltageCellNr", + "realname": "Number of cell with highest voltage", + "datatype": "integer", + "unit": "" + }, + { + "position": [454, 455], + "name": "BMS_MinVoltageCellNr", + "realname": "Number of cell with lowest voltage", + "datatype": "integer", + "unit": "" + }, + { + "position": [456, 457], + "name": "BMS_MaxCellTemperature_degC", + "realname": "Highest cell temperature", + "datatype": "float", + "factor": 0.1, + "unit": "°C" + }, + { + "position": [458, 459], + "name": "BMS_MinCellTemperature_degC", + "realname": "Lowest cell temperature", + "datatype": "float", + "factor": 0.1, + "unit": "°C" + }, + { + "position": [460, 461], + "name": "BMS_MaxTemperatureCellNr", + "realname": "Number of cell with highest temp.", + "datatype": "integer", + "unit": "" + }, + { + "position": [462, 463], + "name": "BMS_MinTemperatureCellNr", + "realname": "Number of cell with lowest temp.", + "datatype": "integer", + "unit": "" + }, + { + "position": [476, 477, 478, 479], + "name": "AC_ChargeEnergy_today_kWh", + "realname": "AC charging energy today", + "datatype": "float", + "factor": 0.1, + "unit": "kWh" + }, + { + "position": [480, 481, 482, 483], + "name": "AC_ChargeEnergy_total_kWh", + "realname": "AC charging energy total", + "datatype": "float", + "factor": 0.1, + "unit": "kWh" + }, + { + "position": [484, 485, 486, 487], + "name": "AC_ChargePower_W", + "realname": "AC charging power", + "datatype": "float", + "factor": 1.0, + "unit": "W" + } + ], + "id": [ + { + "position": [49, 50, 51, 52, 53, 54, 55, 56, 57, 58], + "name": "InverterSN", + "realname": "Inverter SerialNumber", + "datatype": "string" + } + ] + } + } +} \ No newline at end of file diff --git a/data/regs/Growatt-SPH.json b/data/regs/Growatt-SPH.json index a4c4aa56..6850ec43 100644 --- a/data/regs/Growatt-SPH.json +++ b/data/regs/Growatt-SPH.json @@ -76,7 +76,7 @@ ], "name": "TotalEnergyPV", "realname": "Erzeugte Energie PV", - "openwbtopic": "setcounterwh", + "openwbtopic": "setpvwh", "datatype": "integer", "factor": 100, "unit": "Wh" @@ -114,7 +114,7 @@ "unit": "KWh" }, { - "position": [183, 284], + "position": [272, 273], "name": "BatVoltage", "realname": "Battery Voltage", "datatype": "float", diff --git a/data/regs/QVolt.json b/data/regs/QVolt.json index 74fe1065..c67fd503 100644 --- a/data/regs/QVolt.json +++ b/data/regs/QVolt.json @@ -447,7 +447,7 @@ "name": "EnergyTotalToGridWh", "realname": "Total Energy to Grid in Wh", "datatype": "integer", - "openwbtopic": "setcounterwh", + "openwbtopic": "setpvwh", "factor": 100, "unit": "Wh" }, diff --git a/data/regs/Solax-MIC-Pro.json b/data/regs/Solax-MIC-Pro.json index e4820ecf..6eb9f89a 100644 --- a/data/regs/Solax-MIC-Pro.json +++ b/data/regs/Solax-MIC-Pro.json @@ -303,7 +303,7 @@ ], "name": "EnergyTotalToGridWh", "realname": "Total Energy to Grid in Wh", - "openwbtopic": "setcounterwh", + "openwbtopic": "setpvwh", "datatype": "integer", "factor": 100, "unit": "Wh" diff --git a/data/regs/Solax-MIC.json b/data/regs/Solax-MIC.json index b7b1db3f..dee86b9f 100644 --- a/data/regs/Solax-MIC.json +++ b/data/regs/Solax-MIC.json @@ -197,7 +197,7 @@ "position": [75, 76, 73, 74], "name": "EnergyTotalToGridWh", "realname": "Total Energy to Grid in Wh", - "openwbtopic": "setcounterwh", + "openwbtopic": "setpvwh", "datatype": "integer", "factor": 100, "unit": "Wh" diff --git a/data/regs/Solax-X1.json b/data/regs/Solax-X1.json index 65a8f3fc..22c7b6d2 100644 --- a/data/regs/Solax-X1.json +++ b/data/regs/Solax-X1.json @@ -405,7 +405,7 @@ ], "name": "EnergyTotalToGridWh", "realname": "Total Energy to Grid in Wh", - "openwbtopic": "setcounterwh", + "openwbtopic": "setpvwh", "datatype": "integer", "factor": 100, "unit": "Wh" @@ -515,14 +515,46 @@ ] }, "set": [ + { + "name": "setUnlockSettings", + "realname": "Unlock Settings", + "info": "send the 4 digit advanced password", + "request": [ + "#ClientID", + "0x06", + "0x00", + "0x00" + ] + }, { - "name": "TargetBatSOC", - "request": [ - "#ClientID", - "0x06", - "0x00", - "0x83" + "name": "setTargetBatSOC", + "realname": "Target SoC", + "info": "set battery SOC: 0 - 100 in percent", + "request": [ + "#ClientID", + "0x06", + "0x00", + "0x83" ] + }, + { + "name": "setOperationMode", + "realname": "Operation Mode", + "info": "setting of 6 possible operation modes", + "mapping": [ + [ "SelfUse", 0 ], + [ "FeedInPriority", 1 ], + [ "BackupMode", 2 ], + [ "ManuelMode", 3 ], + [ "PeakShaving", 4 ], + [ "TUOMode", 5 ] + ], + "request": [ + "#ClientID", + "0x06", + "0x00", + "0x1f" + ] } ] } diff --git a/data/regs/Solax-X3.json b/data/regs/Solax-X3.json index 1ff20640..13f30ee4 100644 --- a/data/regs/Solax-X3.json +++ b/data/regs/Solax-X3.json @@ -1,874 +1,1967 @@ { - "Solax-X3": { - "config": { - "author": "Lazgar", - "RequestLiveData": [ - [ - "#ClientID", - "0x04", - "0x00", - "0x00", - "0x00", - "0x78" - ], - [ - "#ClientID", - "0x04", - "0x00", - "0x78", - "0x00", - "0x77" - ] - ], - "RequestIdData": [ - [ - "#ClientID", - "0x03", - "0x00", - "0x00", - "0x00", - "0x14" - ] - ], - "ClientIdPos": 0, - "LiveDataFunctionCodePos": 1, - "LiveDataFunctionCode": "0x04", - "IdDataFunctionCodePos": 1, - "IdDataFunctionCode": "0x03", - "LiveDataStartsAtPos": 3, - "IdDataStartsAtPos": 3, - "LiveDataErrorPos": 1, - "LiveDataErrorCode": "0x84", - "IdDataErrorPos": 1, - "IdDataErrorCode": "0x83", - "LiveDataSuccessPos": 1, - "LiveDataSuccessCode": "0x04", - "IdDataSuccessPos": 1, - "IdDataSuccessCode": "0x03" - }, - "data": { - "livedata": [ - { - "position": [ - 215, - 216 - ], - "name": "GridVoltage_R", - "realname": "Grid Voltage L1", - "datatype": "float", - "factor": 0.1, - "unit": "V" - }, - { - "position": [ - 217, - 218 - ], - "name": "GridCurrent_R", - "realname": "Grid Current L1", - "datatype": "float", - "factor": 0.1, - "unit": "A" - }, - { - "position": [ - 219, - 220 - ], - "name": "GridPower_R", - "realname": "Grid Power L1", - "datatype": "integer", - "unit": "W" - }, - { - "position": [ - 221, - 222 - ], - "name": "GridFrequency_R", - "realname": "Grid Frequency L1", - "datatype": "float", - "factor": 0.01, - "unit": "Hz" - }, - { - "position": [ - 223, - 224 - ], - "name": "GridVoltage_S", - "realname": "Grid Voltage L2", - "datatype": "float", - "factor": 0.1, - "unit": "V" - }, - { - "position": [ - 225, - 226 - ], - "name": "GridCurrent_S", - "realname": "Grid Current L2", - "datatype": "float", - "factor": 0.1, - "unit": "A" - }, - { - "position": [ - 227, - 228 - ], - "name": "GridPower_S", - "realname": "Grid Power L2", - "datatype": "integer", - "unit": "W" - }, - { - "position": [ - 229, - 230 - ], - "name": "GridFrequency_S", - "realname": "Grid Frequency L2", - "datatype": "float", - "factor": 0.01, - "unit": "Hz" - }, - { - "position": [ - 231, - 232 - ], - "name": "GridVoltage_T", - "realname": "Grid Voltage L3", - "datatype": "float", - "factor": 0.1, - "unit": "V" - }, - { - "position": [ - 233, - 234 - ], - "name": "GridCurrent_T", - "realname": "Grid Current L3", - "datatype": "float", - "factor": 0.1, - "unit": "A" - }, - { - "position": [ - 235, - 236 - ], - "name": "GridPower_T", - "realname": "Grid Power L3", - "datatype": "integer", - "unit": "W" - }, - { - "position": [ - 237, - 238 - ], - "name": "GridFrequency_T", - "realname": "Grid Frequency L3", - "datatype": "float", - "factor": 0.01, - "unit": "Hz" - }, - { - "position": [ - 9, - 10 - ], - "name": "PvVoltage1", - "realname": "Pv Voltage 1", - "datatype": "float", - "factor": 0.1, - "unit": "V" - }, - { - "position": [ - 11, - 12 - ], - "name": "PvVoltage2", - "realname": "Pv Voltage 2", - "datatype": "float", - "factor": 0.1, - "unit": "V" - }, - { - "position": [ - 13, - 14 - ], - "name": "PvCurrent1", - "realname": "Pv Current 1", - "datatype": "float", - "factor": 0.1, - "unit": "A" - }, - { - "position": [ - 15, - 16 - ], - "name": "PvCurrent2", - "realname": "Pv Current 2", - "datatype": "float", - "factor": 0.1, - "unit": "A" - }, - { - "position": [ - 19, - 20 - ], - "name": "Temperature", - "realname": "Temperature", - "datatype": "integer", - "unit": "°C" - }, - { - "position": [ - 21, - 22 - ], - "name": "InverterStatus", - "realname": "Inverter Status", - "datatype": "integer", - "mapping": [ - [ - 0, - "WaitMode" - ], - [ - 1, - "CheckMode" - ], - [ - 2, - "NormalMode" - ], - [ - 3, - "FaultMode" - ], - [ - 4, - "PermanentFaultMode" - ], - [ - 5, - "UpdateMode" - ], - [ - 6, - "EPSCheckMode" - ], - [ - 7, - "EPSMode" - ], - [ - 8, - "SelfTest" - ], - [ - 9, - "IdleMode" - ] - ] - }, - { - "position": [ - 23, - 24 - ], - "name": "PowerPv1", - "realname": "Power PV 1", - "datatype": "integer", - "unit": "W" - }, - { - "position": [ - 25, - 26 - ], - "name": "PowerPv2", - "realname": "Power PV 2", - "datatype": "integer", - "unit": "W" - }, - { - "position": [ - 43, - 44 - ], - "name": "BatVoltage", - "realname": "Battery Voltage", - "datatype": "float", - "factor": 0.1, - "unit": "V" - }, - { - "position": [ - 45, - 46 - ], - "name": "BatCurrent", - "realname": "Battery Current", - "datatype": "float", - "factor": 0.1, - "unit": "A" - }, - { - "position": [ - 47, - 48 - ], - "name": "BatPower", - "realname": "Battery Power", - "datatype": "integer", - "openwbtopic": "setbatimpwh", - "unit": "W" - }, - { - "position": [ - 51, - 52 - ], - "name": "BatTemp", - "realname": "Battery Temperature", - "datatype": "integer", - "unit": "°C" - }, - { - "position": [ - 55, - 56 - ], - "name": "GridStatus", - "realname": "Grid Status", - "datatype": "integer", - "mapping": [ - [ - 0, - "OnGrid" - ], - [ - 1, - "OffGrid" - ] - ] - }, - { - "position": [ - 59, - 60 - ], - "name": "BatCapacity", - "realname": "Battery Capacity", - "datatype": "integer", - "openwbtopic": "setbatsoc", - "unit": "%" - }, - { - "position": [ - 63, - 64, - 61, - 62 - ], - "name": "OutputEnergyChargeWh", - "realname": "Output Energy Charge (Wh)", - "datatype": "integer", - "openwbtopic": "setbatexpwh", - "factor": 100, - "unit": "Wh" - }, - { - "position": [ - 63, - 64, - 61, - 62 - ], - "name": "OutputEnergyChargeKWh", - "realname": "Output Energy Charge (KWh)", - "datatype": "float", - "factor": 0.1, - "unit": "KWh" - }, - { - "position": [ - 67, - 68 - ], - "name": "OutputEnergyChargeToday", - "realname": "Output Energy Charge Today", - "datatype": "float", - "factor": 0.1, - "unit": "KWh" - }, - { - "position": [ - 71, - 72, - 69, - 70 - ], - "name": "InputEnergyChargeWh", - "realname": "Input Energy Charge (Wh)", - "datatype": "integer", - "openwbtopic": "setbatimpwhhImported", - "factor": 100, - "unit": "Wh" - }, - { - "position": [ - 71, - 72, - 69, - 70 - ], - "name": "InputEnergyChargeKWh", - "realname": "Input Energy Charge (KWh)", - "datatype": "float", - "factor": 0.1, - "unit": "KWh" - }, - { - "position": [ - 73, - 74 - ], - "name": "InputEnergyChargeToday", - "realname": "Input Energy Charge Today", - "datatype": "float", - "factor": 0.1, - "unit": "kWh" - }, - { - "position": [ - 145, - 146, - 143, - 144 - ], - "name": "feedinPower", - "realname": "FeedIn Energy Power to Grid", - "datatype": "integer", - "unit": "W" - }, - { - "position": [ - 149, - 150, - 147, - 148 - ], - "name": "feedinEnergyTotal", - "realname": "FeedIn Energy Total", - "datatype": "float", - "factor": 0.01, - "unit": "kWh" - }, - { - "position": [ - 153, - 154, - 151, - 152 - ], - "name": "consumedEnergyTotal", - "realname": "Consumed Energy Total", - "datatype": "float", - "factor": 0.01, - "unit": "kWh" - }, - { - "position": [ - 163, - 164 - ], - "name": "EnergyTodayToGrid", - "realname": "Today Energy to Grid", - "datatype": "float", - "factor": 0.1, - "unit": "kWh" - }, - { - "position": [ - 169, - 170, - 167, - 168 - ], - "name": "EnergyTotalToGridKwh", - "realname": "Total Energy to Grid in KWh", - "datatype": "float", - "factor": 0.1, - "unit": "kWh" - }, - { - "position": [ - 169, - 170, - 167, - 168 - ], - "name": "EnergyTotalToGridWh", - "realname": "Total Energy to Grid in Wh", - "datatype": "integer", - "openwbtopic": "setcounterwh", - "factor": 100, - "unit": "Wh" - }, - { - "position": [ - 282, - 283, - 280, - 281 - ], - "name": "OnGridRunTime", - "realname": "OnGrid RunTime", - "datatype": "float", - "factor": 0.1, - "unit": "h" - }, - { - "position": [ - 286, - 287, - 284, - 285 - ], - "name": "OffGridRunTime", - "realname": "OffGrid RunTime", - "datatype": "float", - "factor": 0.1, - "unit": "h" - }, - { - "position": [ - 239, - 240 - ], - "name": "OffGridVoltage_R", - "realname": "Off Grid Voltage L1", - "datatype": "float", - "factor": 0.1, - "unit": "V" - }, - { - "position": [ - 241, - 242 - ], - "name": "OffGridCurrent_R", - "realname": "Off Grid Current L1", - "datatype": "float", - "factor": 0.1, - "unit": "A" - }, - { - "position": [ - 250, - 251 - ], - "name": "OffGridPowerActive_R", - "realname": "Off Grid Power L1", - "datatype": "integer", - "unit": "W" - }, - { - "position": [ - 254, - 255 - ], - "name": "OffGridVoltage_S", - "realname": "Off Grid Voltage L2", - "datatype": "float", - "factor": 0.1, - "unit": "V" - }, - { - "position": [ - 256, - 257 - ], - "name": "OffGridCurrent_S", - "realname": "Off Grid Current L2", - "datatype": "float", - "factor": 0.1, - "unit": "A" - }, - { - "position": [ - 258, - 259 - ], - "name": "OffGridPowerActive_S", - "realname": "Off Grid Power L2", - "datatype": "integer", - "unit": "W" - }, - { - "position": [ - 262, - 263 - ], - "name": "OffGridVoltage_T", - "realname": "Off Grid Voltage L3", - "datatype": "float", - "factor": 0.1, - "unit": "V" - }, - { - "position": [ - 264, - 265 - ], - "name": "OffGridCurrent_T", - "realname": "Off Grid Current L3", - "datatype": "float", - "factor": 0.1, - "unit": "A" - }, - { - "position": [ - 266, - 267 - ], - "name": "OffGridPowerActive_T", - "realname": "Off Grid Power L3", - "datatype": "integer", - "unit": "W" - }, - { - "position": [ - 270, - 271, - 268, - 269 - ], - "name": "FeedInPowerPhase_R", - "realname": "FeedIn Power Phase L1", - "datatype": "integer", - "unit": "W" - }, - { - "position": [ - 274, - 275, - 272, - 273 - ], - "name": "FeedInPowerPhase_S", - "realname": "FeedIn Power Phase L2", - "datatype": "integer", - "unit": "W" - }, - { - "position": [ - 278, - 279, - 276, - 277 - ], - "name": "FeedInPowerPhase_T", - "realname": "FeedIn Power Phase L3", - "datatype": "integer", - "unit": "W" - }, - { - "position": [ - 294, - 295, - 292, - 293 - ], - "name": "OffGridYieldTotal", - "realname": "OffGrid Yield Total", - "datatype": "float", - "factor": 0.1, - "unit": "kWh" - }, - { - "position": [ - 296, - 297 - ], - "name": "OffGridYieldToday", - "realname": "OffGrid Yield Today", - "datatype": "float", - "factor": 0.1, - "unit": "kWh" - }, - { - "position": [ - 298, - 299 - ], - "name": "EChargeToday", - "realname": "ECharge Today", - "datatype": "float", - "factor": 0.1, - "unit": "kWh" - }, - { - "position": [ - 302, - 303, - 300, - 301 - ], - "name": "EChargeTotal", - "realname": "ECharge Total", - "datatype": "float", - "factor": 0.1, - "unit": "kWh" - }, - { - "position": [ - 306, - 307, - 304, - 305 - ], - "name": "SolarEnergyTotal", - "realname": "SolarEnergy Total", - "datatype": "float", - "factor": 0.1, - "unit": "kWh" - }, - { - "position": [ - 308, - 309 - ], - "name": "SolarEnergyToday", - "realname": "SolarEnergy Today", - "datatype": "float", - "factor": 0.1, - "unit": "kWh" - }, - { - "position": [ - 314, - 315, - 312, - 313 - ], - "name": "EnergyFeedin", - "realname": "EnergyFeedin Today", - "datatype": "float", - "factor": 0.01, - "unit": "kWh" - }, - { - "position": [ - 318, - 319, - 316, - 317 - ], - "name": "EnergyConsum", - "realname": "EnergyConsum Today", - "datatype": "float", - "factor": 0.01, - "unit": "kWh" - }, - { - "position": [ - 384, - 385 - ], - "name": "CellVoltageHigh", - "realname": "Cell Voltage High", - "datatype": "float", - "factor": 0.001, - "unit": "V" - }, - { - "position": [ - 386, - 387 - ], - "name": "CellVoltageLow", - "realname": "Cell Voltage Low", - "datatype": "float", - "factor": 0.001, - "unit": "V" - } - ], - "id": [ - { - "position": [ - 3, - 4, - 5, - 6, - 7, - 8, - 9, - 10, - 11, - 12, - 13, - 14, - 15, - 16 - ], - "name": "InverterSN", - "realname": "Inverter SerialNumber", - "datatype": "string" - }, - { - "position": [ - 17, - 18, - 19, - 20, - 21, - 22, - 23, - 24, - 25, - 26, - 27, - 28, - 29, - 30 - ], - "name": "FactoryName", - "realname": "Factory Name", - "datatype": "string" - }, - { - "position": [ - 31, - 32, - 33, - 34, - 35, - 36, - 37, - 38, - 39, - 40, - 41, - 42 - ], - "name": "ModuleName", - "realname": "Module Name", - "datatype": "string" - } + "Solax-X3": { + "config": { + "author": "Lazgar", + "RequestLiveData": [ + [ + "#ClientID", + "0x04", + "0x00", + "0x00", + "0x00", + "0x79" + ], + [ + "#ClientID", + "0x04", + "0x00", + "0x79", + "0x00", + "0x79" + ], + [ + "#ClientID", + "0x04", + "0x00", + "0xf2", + "0x00", + "0x41" + ] + ], + "RequestIdData": [ + [ + "#ClientID", + "0x03", + "0x00", + "0x00", + "0x00", + "0x79" + ], + [ + "#ClientID", + "0x03", + "0x00", + "0x79", + "0x00", + "0x79" + ], + [ + "#ClientID", + "0x03", + "0x00", + "0xf2", + "0x00", + "0x79" + ], + [ + "#ClientID", + "0x03", + "0x01", + "0x6b", + "0x00", + "0x0c" + ] + ], + "ClientIdPos": 0, + "LiveDataFunctionCodePos": 1, + "LiveDataFunctionCode": "0x04", + "IdDataFunctionCodePos": 1, + "IdDataFunctionCode": "0x03", + "LiveDataStartsAtPos": 3, + "IdDataStartsAtPos": 3, + "LiveDataErrorPos": 1, + "LiveDataErrorCode": "0x84", + "IdDataErrorPos": 1, + "IdDataErrorCode": "0x83", + "LiveDataSuccessPos": 1, + "LiveDataSuccessCode": "0x04", + "IdDataSuccessPos": 1, + "IdDataSuccessCode": "0x03" + }, + "data": { + "livedata": [ + { + "position": [ + 9, + 10 + ], + "name": "PvVoltage1", + "realname": "Pv Voltage 1", + "datatype": "float", + "factor": 0.1, + "unit": "V" + }, + { + "position": [ + 11, + 12 + ], + "name": "PvVoltage2", + "realname": "Pv Voltage 2", + "datatype": "float", + "factor": 0.1, + "unit": "V" + }, + { + "position": [ + 13, + 14 + ], + "name": "PvCurrent1", + "realname": "Pv Current 1", + "datatype": "float", + "factor": 0.1, + "unit": "A" + }, + { + "position": [ + 15, + 16 + ], + "name": "PvCurrent2", + "realname": "Pv Current 2", + "datatype": "float", + "factor": 0.1, + "unit": "A" + }, + { + "position": [ + 23, + 24 + ], + "name": "PvPower1", + "realname": "Pv Power 1", + "datatype": "integer", + "unit": "W" + }, + { + "position": [ + 25, + 26 + ], + "name": "PvPower2", + "realname": "Pv Power 2", + "datatype": "integer", + "unit": "W" + }, + { + "position": [ + 19, + 20 + ], + "name": "Temperature", + "realname": "Temperature", + "datatype": "integer", + "unit": "°C" + }, + { + "position": [ + 21, + 22 + ], + "name": "InverterStatus", + "realname": "Inverter Status", + "datatype": "integer", + "mapping": [ + [ + 0, + "Waiting" + ], + [ + 1, + "Checking" + ], + [ + 2, + "Normal" + ], + [ + 3, + "Fault" + ], + [ + 4, + "Permanent Fault" + ], + [ + 5, + "Update" + ], + [ + 6, + "Off-Grid Waiting" + ], + [ + 7, + "Off-Grid" + ], + [ + 8, + "Self Testing" + ], + [ + 9, + "Idle" + ], + [ + 10, + "Standby" ] + ] + }, + { + "position": [ + 133, + 134, + 131, + 132 + ], + "name": "InverterFaultMessage", + "realname": "Inverter Fault Message", + "datatype": "binary", + "mapping": [ + "TZ Protect Fault", + "Grid Lost Fault", + "Grid Volt Fault", + "Grid Freq Fault", + "PV Volt Fault", + "Bus Volt Fault", + "Bat Volt Fault", + "AC10mins Volt Fault", + "DCI OCP Fault", + "DCV OCP Fault", + "SW OCP Fault", + "RC OCP Fault", + "Isolation Fault", + "Temp Over Fault", + "BatConnDir Fault", + "Off-Grid Overload", + "Overload", + "Bat Power Low", + "BMS Lost", + "Fan Fault", + "Low Temp Fault", + "Parallel Fault", + "Hard Limit Fault", + "INV Volt Sample Fault", + "Inner Comm Fault", + "INV EEPROM Fault", + "RCD Fault", + "Grid Relay Fault", + "Off-grid Relay Fault", + "PV Conndir Fault", + "Charger Relay Fault", + "Earth Relay Fault", + "no Error" + ] + }, + { + "position": [ + 137, + 138 + ], + "name": "ManagerFaultMessage", + "realname": "Manager Fault Message", + "datatype": "binary", + "mapping": [ + "Power Type Fault", + "Port OC Warning", + "Mgr EEPROM Fault", + "Reserve3", + "NTC Sample Invalid", + "Bat Temp Low", + "Bat Temp High", + "Reserve7", + "Reserve8", + "Meter Fault", + "Bypass Relay Fault", + "Fan 2 Fault", + "Reserve12", + "Reserve13", + "Reserve14", + "Reserve15", + "no Error" + ] + }, + { + "position": [ + 141, + 142, + 139, + 140 + ], + "name": "BMSFaultMessage", + "realname": "BMS Fault Message", + "datatype": "binary", + "mapping": [ + "BMS_External_Err", + "BMS_Internal_Err", + "BMS_OverVoltage", + "BMS_LowerVoltage", + "BMS_ChargeOCP", + "BMS_DischargeOCP", + "BMS_TemHigh", + "BMS_TemLow", + "BMS_CellImbalance", + "BMS_Hardware_Protect", + "BMS_Circuit_Fault", + "BMS_ISO_Fault", + "BMS_VolSen_Fault", + "BMS_TempSen_Fault", + "BMS_CurSen_Fault", + "BMS_Relay_Fault", + "BMS_Type_Unmatch", + "BMS_Ver_Unmathch", + "BMS_MFR_Unmathch", + "BMS_SW_Unmathch", + "BMS_M&S_Unmatch", + "BMS_CR_NORespond", + "BMS_SW_Protect", + "BMS_536_Fault", + "BMS_SelfcheckErr", + "BMS_TempdiffErr", + "BMS_BreakFault", + "BMS_Flash_Fault", + "BMS_Precharge_Fault", + "BMS_AirSwitch_Break", + "Rev", + "Rev", + "no Error" + ] + }, + { + "position": [ + 171, + 172 + ], + "name": "InverterSettings", + "realname": "Inverter Settings", + "datatype": "integer", + "mapping": [ + [ + 0, + "Locked" + ], + [ + 1, + "Unlocked" + ] + ] + }, + { + "position": [ + 55, + 56 + ], + "name": "GridStatus", + "realname": "Grid Status", + "datatype": "integer", + "mapping": [ + [ + 0, + "OnGrid" + ], + [ + 1, + "OffGrid" + ] + ] + }, + { + "position": [ + 53, + 54 + ], + "name": "BatStatus", + "realname": "Battery Status", + "datatype": "integer", + "mapping": [ + [ + 0, + "Discharge" + ], + [ + 1, + "Charge" + ], + [ + 2, + "Stop" + ] + ] + }, + { + "position": [ + 43, + 44 + ], + "name": "BatVoltage", + "realname": "Battery Voltage", + "datatype": "float", + "factor": 0.1, + "unit": "V" + }, + { + "position": [ + 45, + 46 + ], + "name": "BatCurrent", + "realname": "Battery Current", + "datatype": "float", + "factor": 0.1, + "unit": "A" + }, + { + "position": [ + 47, + 48 + ], + "name": "BatPower", + "realname": "Battery Power", + "datatype": "integer", + "openwbtopic": "setbatimpwh", + "unit": "W" + }, + { + "position": [ + 51, + 52 + ], + "name": "BatTemp", + "realname": "Battery Temperature", + "datatype": "integer", + "unit": "°C" + }, + { + "position": [ + 59, + 60 + ], + "name": "BatCapacity", + "realname": "Battery Capacity", + "datatype": "integer", + "openwbtopic": "setbatsoc", + "unit": "%" + }, + { + "position": [ + 575, + 576, + 573, + 574 + ], + "name": "BatDischargeableEnergy", + "realname": "Battery Dischargeable Energy", + "datatype": "integer", + "unit": "Wh" + }, + { + "position": [ + 571, + 572, + 569, + 570 + ], + "name": "BatChargeableEnergy", + "realname": "Battery Chargeable Energy", + "datatype": "integer", + "unit": "Wh" + }, + { + "position": [ + 388, + 389 + ], + "name": "BatUserSoC", + "realname": "Battery User SoC", + "datatype": "integer", + "unit": "%" + }, + { + "position": [ + 390, + 391 + ], + "name": "BatUserSoH", + "realname": "Battery User SoH", + "datatype": "integer", + "unit": "%" + }, + { + "position": [ + 67, + 68 + ], + "name": "ChargeEnergyOutputToday", + "realname": "Charge Energy Output Today", + "datatype": "float", + "factor": 0.1, + "unit": "KWh" + }, + { + "position": [ + 63, + 64, + 61, + 62 + ], + "name": "ChargeEnergyOutputTotal", + "realname": "Charge Energy Output Total", + "datatype": "float", + "factor": 0.1, + "unit": "KWh" + }, + { + "position": [ + 63, + 64, + 61, + 62 + ], + "name": "ChargeEnergyOutputTotalWh", + "realname": "Charge Energy Output Total(Wh)", + "datatype": "integer", + "openwbtopic": "setbatexpwh", + "factor": 100, + "unit": "Wh" + }, + { + "position": [ + 73, + 74 + ], + "name": "ChargeEnergyInputToday", + "realname": "Charge Energy Input Today", + "datatype": "float", + "factor": 0.1, + "unit": "kWh" + }, + { + "position": [ + 71, + 72, + 69, + 70 + ], + "name": "ChargeEnergyInputTotal", + "realname": "Charge Energy Input Total", + "datatype": "float", + "factor": 0.1, + "unit": "KWh" + }, + { + "position": [ + 71, + 72, + 69, + 70 + ], + "name": "ChargeEnergyInputTotalWh", + "realname": "Charge Energy Input Total (Wh)", + "datatype": "integer", + "openwbtopic": "setbatimpwh", + "factor": 100, + "unit": "Wh" + }, + { + "position": [ + 314, + 315, + 312, + 313 + ], + "name": "FeedInEnergyToday", + "realname": "FeedIn Energy Today", + "datatype": "float", + "factor": 0.01, + "unit": "kWh" + }, + { + "position": [ + 149, + 150, + 147, + 148 + ], + "name": "FeedInEnergyTotal", + "realname": "FeedIn Energy Total", + "datatype": "float", + "factor": 0.01, + "unit": "kWh" + }, + { + "position": [ + 318, + 319, + 316, + 317 + ], + "name": "ConsumedEnergyToday", + "realname": "Consumed Energy Today", + "datatype": "float", + "factor": 0.01, + "unit": "kWh" + }, + { + "position": [ + 153, + 154, + 151, + 152 + ], + "name": "ConsumedEnergyTotal", + "realname": "Consumed Energy Total", + "datatype": "float", + "factor": 0.01, + "unit": "kWh" + }, + { + "position": [ + 163, + 164 + ], + "name": "EnergyTodayToGrid", + "realname": "Today Energy to Grid", + "datatype": "float", + "factor": 0.1, + "unit": "kWh" + }, + { + "position": [ + 169, + 170, + 167, + 168 + ], + "name": "EnergyTotalToGrid", + "realname": "Total Energy to Grid in KWh", + "datatype": "float", + "factor": 0.1, + "unit": "kWh" + }, + { + "position": [ + 169, + 170, + 167, + 168 + ], + "name": "EnergyTotalToGridWh", + "realname": "Total Energy to Grid in Wh", + "datatype": "float", + "openwbtopic": "setpvwh", + "factor": 100, + "unit": "Wh" + }, + { + "position": [ + 215, + 216 + ], + "name": "GridVoltage_L1", + "realname": "Grid Voltage L1", + "datatype": "float", + "factor": 0.1, + "unit": "V" + }, + { + "position": [ + 223, + 224 + ], + "name": "GridVoltage_L2", + "realname": "Grid Voltage L2", + "datatype": "float", + "factor": 0.1, + "unit": "V" + }, + { + "position": [ + 231, + 232 + ], + "name": "GridVoltage_L3", + "realname": "Grid Voltage L3", + "datatype": "float", + "factor": 0.1, + "unit": "V" + }, + { + "position": [ + 217, + 218 + ], + "name": "GridCurrent_L1", + "realname": "Grid Current L1", + "datatype": "float", + "factor": 0.1, + "unit": "A" + }, + { + "position": [ + 225, + 226 + ], + "name": "GridCurrent_L2", + "realname": "Grid Current L2", + "datatype": "float", + "factor": 0.1, + "unit": "A" + }, + { + "position": [ + 233, + 234 + ], + "name": "GridCurrent_L3", + "realname": "Grid Current L3", + "datatype": "float", + "factor": 0.1, + "unit": "A" + }, + { + "position": [ + 219, + 220 + ], + "name": "GridPower_L1", + "realname": "Grid Power L1", + "datatype": "integer", + "unit": "W" + }, + { + "position": [ + 227, + 228 + ], + "name": "GridPower_L2", + "realname": "Grid Power L2", + "datatype": "integer", + "unit": "W" + }, + { + "position": [ + 235, + 236 + ], + "name": "GridPower_L3", + "realname": "Grid Power L3", + "datatype": "integer", + "unit": "W" + }, + { + "position": [ + 221, + 222 + ], + "name": "GridFrequency_L1", + "realname": "Grid Frequency L1", + "datatype": "float", + "factor": 0.01, + "unit": "Hz" + }, + { + "position": [ + 229, + 230 + ], + "name": "GridFrequency_L2", + "realname": "Grid Frequency L2", + "datatype": "float", + "factor": 0.01, + "unit": "Hz" + }, + { + "position": [ + 237, + 238 + ], + "name": "GridFrequency_L3", + "realname": "Grid Frequency L3", + "datatype": "float", + "factor": 0.01, + "unit": "Hz" + }, + { + "position": [ + 239, + 240 + ], + "name": "OffGridVoltage_L1", + "realname": "Off Grid Voltage L1", + "datatype": "float", + "factor": 0.1, + "unit": "V" + }, + { + "position": [ + 252, + 253 + ], + "name": "OffGridVoltage_L2", + "realname": "Off Grid Voltage L2", + "datatype": "float", + "factor": 0.1, + "unit": "V" + }, + { + "position": [ + 260, + 261 + ], + "name": "OffGridVoltage_L3", + "realname": "Off Grid Voltage L3", + "datatype": "float", + "factor": 0.1, + "unit": "V" + }, + { + "position": [ + 241, + 242 + ], + "name": "OffGridCurrent_L1", + "realname": "Off Grid Current L1", + "datatype": "float", + "factor": 0.1, + "unit": "A" + }, + { + "position": [ + 254, + 255 + ], + "name": "OffGridCurrent_L2", + "realname": "Off Grid Current L2", + "datatype": "float", + "factor": 0.1, + "unit": "A" + }, + { + "position": [ + 262, + 263 + ], + "name": "OffGridCurrent_L3", + "realname": "Off Grid Current L3", + "datatype": "float", + "factor": 0.1, + "unit": "A" + }, + { + "position": [ + 243, + 244 + ], + "name": "OffGridPowerActive_L1", + "realname": "Off Grid Power L1", + "datatype": "integer", + "unit": "W" + }, + { + "position": [ + 256, + 257 + ], + "name": "OffGridPowerActive_L2", + "realname": "Off Grid Power L2", + "datatype": "integer", + "unit": "W" + }, + { + "position": [ + 264, + 265 + ], + "name": "OffGridPowerActive_L3", + "realname": "Off Grid Power L3", + "datatype": "integer", + "unit": "W" + }, + { + "position": [ + 145, + 146, + 143, + 144 + ], + "name": "FeedInPower", + "realname": "FeedIn Power to Grid", + "datatype": "integer", + "unit": "W" + }, + { + "position": [ + 270, + 271, + 268, + 269 + ], + "name": "FeedInPower_L1", + "realname": "FeedIn Power L1", + "datatype": "integer", + "unit": "W" + }, + { + "position": [ + 274, + 275, + 272, + 273 + ], + "name": "FeedInPower_L2", + "realname": "FeedIn Power L2", + "datatype": "integer", + "unit": "W" + }, + { + "position": [ + 278, + 279, + 276, + 277 + ], + "name": "FeedInPower_L3", + "realname": "FeedIn Power L3", + "datatype": "integer", + "unit": "W" + }, + { + "position": [ + 282, + 283, + 280, + 281 + ], + "name": "OnGridRunTime", + "realname": "OnGrid RunTime", + "datatype": "float", + "factor": 0.1, + "unit": "h" + }, + { + "position": [ + 286, + 287, + 284, + 285 + ], + "name": "OffGridRunTime", + "realname": "OffGrid RunTime", + "datatype": "float", + "factor": 0.1, + "unit": "h" + }, + { + "position": [ + 296, + 297 + ], + "name": "OffGridYieldToday", + "realname": "OffGrid Yield Today", + "datatype": "float", + "factor": 0.1, + "unit": "kWh" + }, + { + "position": [ + 294, + 295, + 292, + 293 + ], + "name": "OffGridYieldTotal", + "realname": "OffGrid Yield Total", + "datatype": "float", + "factor": 0.1, + "unit": "kWh" + }, + { + "position": [ + 298, + 299 + ], + "name": "EChargeToday", + "realname": "ECharge Today", + "datatype": "float", + "factor": 0.1, + "unit": "kWh" + }, + { + "position": [ + 302, + 303, + 300, + 301 + ], + "name": "EChargeTotal", + "realname": "ECharge Total", + "datatype": "float", + "factor": 0.1, + "unit": "kWh" + }, + { + "position": [ + 308, + 309 + ], + "name": "SolarEnergyToday", + "realname": "SolarEnergy Today", + "datatype": "float", + "factor": 0.1, + "unit": "kWh" + }, + { + "position": [ + 306, + 307, + 304, + 305 + ], + "name": "SolarEnergyTotal", + "realname": "SolarEnergy Total", + "datatype": "float", + "factor": 0.1, + "unit": "kWh" + }, + { + "position": [ + 384, + 385 + ], + "name": "CellVoltageHigh", + "realname": "Cell Voltage High", + "datatype": "float", + "factor": 0.001, + "unit": "V" + }, + { + "position": [ + 386, + 387 + ], + "name": "CellVoltageLow", + "realname": "Cell Voltage Low", + "datatype": "float", + "factor": 0.001, + "unit": "V" + }, + { + "position": [ + 525, + 526 + ], + "name": "VPPControlMode", + "realname": "VPP Control Mode", + "datatype": "integer", + "mapping": [ + [ + 0, + "Disabled" + ], + [ + 1, + "PowerControl" + ], + [ + 2, + "ElectricQuantityControl" + ], + [ + 3, + "SoCTargetControl" + ] + ] + }, + { + "position": [ + 527, + 528 + ], + "name": "VPPControlStatus", + "realname": "VPP Control Status", + "datatype": "integer", + "mapping": [ + [ + 0, + "Unfinished" + ], + [ + 1, + "Finished" + ] + ] + }, + { + "position": [ + 531, + 532, + 529, + 530 + ], + "name": "VPPActivePowerTarget", + "realname": "VPP Active Power Target", + "datatype": "integer", + "unit": "W" + }, + { + "position": [ + 535, + 536, + 533, + 534 + ], + "name": "VPPReactivePowerTarget", + "realname": "VPP Reactive Power Target", + "datatype": "integer", + "unit": "VA" + }, + { + "position": [ + 563, + 564, + 561, + 562 + ], + "name": "VPPTargetEnergy", + "realname": "VPP Target Energy", + "datatype": "integer", + "unit": "Wh" + }, + { + "position": [ + 579, + 580 + ], + "name": "VPPTargetSoC", + "realname": "VPP Target SoC", + "datatype": "integer", + "unit": "%" + }, + { + "position": [ + 567, + 568, + 565, + 566 + ], + "name": "VPPChargeDischargePower", + "realname": "VPP Charge Discharge Power", + "datatype": "integer", + "unit": "W" + }, + { + "position": [ + 577, + 578 + ], + "name": "VPPDuration", + "realname": "VPP Duration", + "datatype": "integer", + "unit": "s" + }, + { + "position": [ + 585, + 586 + ], + "name": "VPPCtrlTimeout", + "realname": "VPP Ctrl Timeout", + "datatype": "integer", + "unit": "s" + } + ], + "id": [ + { + "position": [ + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 13, + 14, + 15, + 16 + ], + "name": "InverterSN", + "realname": "Inverter SerialNumber", + "datatype": "string" + }, + { + "position": [ + 17, + 18, + 19, + 20, + 21, + 22, + 23, + 24, + 25, + 26, + 27, + 28, + 29, + 30 + ], + "name": "InverterManufacturer", + "realname": "Inverter Manufacturer", + "datatype": "string" + }, + { + "position": [ + 535, + 536 + ], + "name": "InverterMachineType", + "realname": "Inverter Machine Type", + "datatype": "integer", + "mapping": [ + [ + 1, + "X1" + ], + [ + 3, + "X3" + ] + ] + }, + { + "position": [ + 539, + 540 + ], + "name": "InverterMachineStyle", + "realname": "Inverter Machine Style", + "datatype": "integer", + "mapping": [ + [ + 0, + "Hybrid" + ], + [ + 1, + "FIT" + ] + ] + }, + { + "position": [ + 380, + 381 + ], + "name": "InverterPowerType", + "realname": "Inverter Power Type", + "datatype": "integer", + "mapping": [ + [ + 15000, + "15k" + ], + [ + 12000, + "12k" + ], + [ + 10000, + "10k" + ], + [ + 8000, + "8k" + ], + [ + 6000, + "6k" + ], + [ + 5000, + "5k" + ] + ] + }, + { + "position": [ + 456, + 457 + ], + "name": "InverterUserPassword", + "realname": "Inverter User Password", + "datatype": "integer" + }, + { + "position": [ + 458, + 459 + ], + "name": "InverterAdminPassword", + "realname": "Inverter Admin Password", + "datatype": "integer" + }, + { + "position": [ + 286, + 287 + ], + "name": "OperationMode", + "realname": "Operation Mode", + "datatype": "integer", + "mapping": [ + [ + 0, + "SelfUseMode" + ], + [ + 1, + "FeedInPriority" + ], + [ + 2, + "BackupMode" + ], + [ + 3, + "ManualMode" + ], + [ + 4, + "PeakShaving" + ], + [ + 5, + "TOUMode" + ] + ] + }, + { + "position": [ + 288, + 289 + ], + "name": "ManualMode", + "realname": "Manual Mode", + "datatype": "integer", + "mapping": [ + [ + 0, + "StopCharge&Discharge" + ], + [ + 1, + "ForceCharge" + ], + [ + 2, + "ForceDischarge" + ] + ] + }, + { + "position": [ + 372, + 373 + ], + "name": "ExportLimit", + "realname": "Export Limit", + "datatype": "integer", + "factor": 10, + "unit": "W" + }, + { + "position": [ + 374, + 375 + ], + "name": "OffGridMute", + "realname": "Off-Grid Mute", + "datatype": "integer", + "mapping": [ + [ + 0, + "Off" + ], + [ + 1, + "On" + ] + ] + }, + { + "position": [ + 376, + 377 + ], + "name": "OffGridMinimumSoC", + "realname": "Off-Grid Minimum SoC", + "datatype": "integer", + "unit": "%" + }, + { + "position": [ + 384, + 385 + ], + "name": "MPPT", + "realname": "MPPT", + "datatype": "integer", + "mapping": [ + [ + 0, + "Off" + ], + [ + 1, + "On" + ] + ] + }, + { + "position": [ + 529, + 530 + ], + "name": "DRMFunction", + "realname": "DRM Function", + "datatype": "integer", + "mapping": [ + [ + 0, + "Off" + ], + [ + 1, + "On" + ] + ] + }, + { + "position": [ + 537, + 538 + ], + "name": "PhaseUnbalancedPowerFeed", + "realname": "Phase Unbalanced Power Feed", + "datatype": "integer", + "mapping": [ + [ + 0, + "Disabled" + ], + [ + 1, + "Enabled" + ] + ] + }, + { + "position": [ + 364, + 365 + ], + "name": "PgridBias", + "realname": "Pgrid Bias", + "datatype": "integer", + "mapping": [ + [ + 2, + "INV" + ], + [ + 1, + "Grid" + ], + [ + 0, + "Off" + ] + ] + }, + { + "position": [ + 296, + 297 + ], + "name": "BatChargeMaxCurrent", + "realname": "Battery Charge Max Current", + "datatype": "float", + "factor": 0.1, + "unit": "A" + }, + { + "position": [ + 298, + 299 + ], + "name": "BatDischargeMaxCurrent", + "realname": "Battery Discharge Max Current", + "datatype": "float", + "factor": 0.1, + "unit": "A" + }, + { + "position": [ + 302 + ], + "name": "SelfUseMinSoC", + "realname": "Self-Use Minimum SoC", + "datatype": "integer", + "unit": "%" + }, + { + "position": [ + 303 + ], + "name": "SelfUseChargeEnable", + "realname": "Self-Use Charge from Grid Enable", + "datatype": "integer", + "mapping": [ + [ + 0, + "Disabled" + ], + [ + 1, + "Enabled" + ] + ] + }, + { + "position": [ + 304, + 305 + ], + "name": "SelfUseMaxGridSoC", + "realname": "Self-Use Maximum Grid SoC", + "datatype": "integer", + "unit": "%" + }, + { + "position": [ + 306 + ], + "name": "FeedInNightChargeSoC", + "realname": "FeedIn NightCharge SoC", + "datatype": "integer", + "unit": "%" + }, + { + "position": [ + 307 + ], + "name": "FeedInMinSoC", + "realname": "FeedIn Minimum SoC", + "datatype": "integer", + "unit": "%" + }, + { + "position": [ + 308 + ], + "name": "BackupNightChargeSoC", + "realname": "Backup NightCharge SoC", + "datatype": "integer", + "unit": "%" + }, + { + "position": [ + 309 + ], + "name": "BackupMinSoC", + "realname": "Backup Minimum SoC", + "datatype": "integer", + "unit": "%" + }, + { + "position": [ + 311, + 310 + ], + "name": "ChargePeriod1_StartTime", + "realname": "Forced Charge Period Start Time", + "datatype": "integer", + "unit": "h:m" + }, + { + "position": [ + 313, + 312 + ], + "name": "ChargePeriod1_EndTime", + "realname": "Forced Charge Period End Time", + "datatype": "integer", + "unit": "h:m" + }, + { + "position": [ + 315, + 314 + ], + "name": "DischargePeriod1_StartTime", + "realname": "Allowed Disc Period Start Time", + "datatype": "integer", + "unit": "h:m" + }, + { + "position": [ + 317, + 316 + ], + "name": "DischargePeriod1_EndTime", + "realname": "Allowed Disc Period End Time", + "datatype": "integer", + "unit": "h:m" } - } + ] + }, + "set": [ + { + "name": "setUnlockSettings", + "realname": "Unlock Settings", + "info": "send the 4 digit advanced password", + "request": [ + "#ClientID", + "0x06", + "0x00", + "0x00" + ] + }, + { + "name": "setOperationMode", + "realname": "Operation Mode", + "info": "accepted values:", + "mapping": [ + [ + "SelfUseMode", + 0 + ], + [ + "FeedInPriority", + 1 + ], + [ + "BackupMode", + 2 + ], + [ + "ManualMode", + 3 + ], + [ + "PeakShaving", + 4 + ], + [ + "TOUMode", + 5 + ] + ], + "request": [ + "#ClientID", + "0x06", + "0x00", + "0x1f" + ] + }, + { + "name": "setManualMode", + "realname": "Manual Mode", + "info": "accepted values:", + "mapping": [ + [ + "StopCharge&Discharge", + 0 + ], + [ + "ForceCharge", + 1 + ], + [ + "ForceDischarge", + 2 + ] + ], + "request": [ + "#ClientID", + "0x06", + "0x00", + "0x20" + ] + }, + { + "name": "setExportLimit", + "realname": "Export Limit in Watt", + "info": "can be set in steps of 10, if you send 100 the limit is set to 1000 W", + "request": [ + "#ClientID", + "0x06", + "0x00", + "0x42" + ] + }, + { + "name": "setOffGridMute", + "realname": "Off-Grid Mute", + "info": "accepted values:", + "mapping": [ + [ + "Off", + 0 + ], + [ + "On", + 1 + ] + ], + "request": [ + "#ClientID", + "0x06", + "0x00", + "0x43" + ] + }, + { + "name": "setOffGridMinimumSoC", + "realname": "Off-Grid Minimum SoC", + "info": "can be set between 10 and 25 percent", + "request": [ + "#ClientID", + "0x06", + "0x00", + "0x44" + ] + }, + { + "name": "setPhaseUnbalancedPowerFeed", + "realname": "Phase Unbalanced Power Feed", + "info": "Allow unbalanced power feed to the grid", + "mapping": [ + [ + "Disable", + 0 + ], + [ + "Enable", + 1 + ] + ], + "request": [ + "#ClientID", + "0x06", + "0x00", + "0x9e" + ] + }, + { + "name": "setPgridBias", + "realname": "Pgrid Bias", + "info": "accepted values:", + "mapping": [ + [ + "Off", + 0 + ], + [ + "Grid", + 1 + ], + [ + "INV", + 2 + ] + ], + "request": [ + "#ClientID", + "0x06", + "0x00", + "0x8D" + ] + }, + { + "name": "setBatChargeMaxCurrent", + "realname": "Battery Charge Max Current", + "info": "can be set in steps of 0.1, if you send 300 the limit is set to 30A", + "request": [ + "#ClientID", + "0x06", + "0x00", + "0x24" + ] + }, + { + "name": "setBatDischargeMaxCurrent", + "realname": "Battery Charge Max Current", + "info": "can be set in steps of 0.1, if you send 300 the limit is set to 30A", + "request": [ + "#ClientID", + "0x06", + "0x00", + "0x25" + ] + }, + { + "name": "setSelfUseMinSoC", + "realname": "Self-Use Minimum SoC", + "info": "can be set between 10 - 100 percent", + "request": [ + "#ClientID", + "0x06", + "0x00", + "0x61" + ] + }, + { + "name": "setSelfUseChargeEnable", + "realname": "Self-Use Charge from Grid", + "info": "enable/disable charging from grid", + "mapping": [ + [ + "Disable", + 0 + ], + [ + "Enable", + 1 + ] + ], + "request": [ + "#ClientID", + "0x06", + "0x00", + "0x62" + ] + }, + { + "name": "setSelfUseMaxGridSoC", + "realname": "Self-Use Maximum Grid SoC", + "info": "can be set between 10 - 100 percent", + "request": [ + "#ClientID", + "0x06", + "0x00", + "0x63" + ] + }, + { + "name": "setFeedInMinSoC", + "realname": "FeedIn Minimum SoC", + "info": "can be set between 10 - 100 percent", + "request": [ + "#ClientID", + "0x06", + "0x00", + "0x65" + ] + }, + { + "name": "setFeedInNightChargeSoC", + "realname": "FeedIn NightCharge SoC", + "info": "can be set between 10 - 100 percent", + "request": [ + "#ClientID", + "0x06", + "0x00", + "0x64" + ] + }, + { + "name": "setBackupMinSoC", + "realname": "Backup Minimum SoC", + "info": "can be set between 15 - 100 percent", + "request": [ + "#ClientID", + "0x06", + "0x00", + "0x67" + ] + }, + { + "name": "setBackupNightChargeSoC", + "realname": "Backup NightCharge SoC", + "info": "can be set between 30 - 100 percent", + "request": [ + "#ClientID", + "0x06", + "0x00", + "0x66" + ] + }, + { + "name": "setVPPControl", + "realname": "VPP Control", + "info": "VPP Control Function, a comma separated List of Values. Check Solax Documentation for Details.", + "intsize": [ + "int16", + "int16", + "int32", + "int32", + "int16", + "int16", + "int32", + "int32", + "int16", + "int32" + ], + "request": [ + "#ClientID", + "0x10", + "0x00", + "0x7C", + "0x00", + "0x0F" + ] + }, + { + "name": "setChargePeriod1_StartTime", + "realname": "Forced Charge Period Start Time", + "info": "Forced Charge Period Start Time", + "request": [ + "#ClientID", + "0x06", + "0x00", + "0x68" + ] + }, + { + "name": "setChargePeriod1_EndTime", + "realname": "Forced Charge Period End Time", + "info": "Forced Charge Period End Time", + "request": [ + "#ClientID", + "0x06", + "0x00", + "0x69" + ] + }, + { + "name": "setDischargePeriod1_StartTime", + "realname": "Allowed Disc Period Start Time", + "info": "Allowed Discharge Period Start Time", + "request": [ + "#ClientID", + "0x06", + "0x00", + "0x6A" + ] + }, + { + "name": "setDischargePeriod1_EndTime", + "realname": "Allowed Disc Period End Time", + "info": "Allowed Discharge Period End Time", + "request": [ + "#ClientID", + "0x06", + "0x00", + "0x6B" + ] + } + ] + } } diff --git a/data/web/Javascript.js b/data/web/Javascript.js index 5474846d..fef10600 100644 --- a/data/web/Javascript.js +++ b/data/web/Javascript.js @@ -17,9 +17,9 @@ * Definition of constants *****************************************************************************************/ -const gpio_disabled = []; +export const gpio_disabled = []; -const gpio = [ {port: 1, name:'D1/TX0'}, +export const gpio = [ {port: 1, name:'D1/TX0'}, {port: 2 , name:'D2'}, {port: 3, name:'D3/RX0'}, {port: 4 , name:'D4'}, @@ -51,7 +51,7 @@ const gpio = [ {port: 1, name:'D1/TX0'}, {port: 39, name:'D39'} ]; -const gpioanalog = [ {port: 36, name:'ADC1_CH0 - GPIO36'}, +export const gpioanalog = [ {port: 36, name:'ADC1_CH0 - GPIO36'}, {port: 37, name:'ADC1_CH1 - GPIO37'}, {port: 38, name:'ADC1_CH2 - GPIO38'}, {port: 39, name:'ADC1_CH3 - GPIO39'}, @@ -71,14 +71,95 @@ const gpioanalog = [ {port: 36, name:'ADC1_CH0 - GPIO36'}, {port: 26, name:'ADC2_CH9 - GPIO26'} ]; +import { functionMap as statusFunctionMap } from './status.js'; +import { functionMap as baseconfigFunctionMap } from './baseconfig.js'; +import { functionMap as mbconfigFunctionMap } from './modbusconfig.js'; +import { functionMap as mbitemconfigFunctionMap } from './modbusitemconfig.js'; +import { functionMap as rawdataFunctionMap } from './rawdata.js'; +import { functionMap as filesFunctionMap } from './handlefiles.js'; + +const combinedFunctionMap = { + ...statusFunctionMap, + ...baseconfigFunctionMap, + ...mbconfigFunctionMap, + ...mbitemconfigFunctionMap, + ...rawdataFunctionMap, + ...filesFunctionMap +}; + +export let ws; // websocket handle +var datavalues; // form data values as string to check, if "needToSave" Dialog should be shown + var timer; // ID of setTimout Timer -> setResponse +let reconnectInterval = 5000; // 5 seconds interval to reconnect websocket connection + +/****************************************************************************************** + * Connect to WebSocket server + * *****************************************************************************************/ +export function connectWebSocket() { + window.addEventListener('beforeunload', function() { + if (ws) { + ws.close(); + } + }, false); + + document.addEventListener('visibilitychange', function() { + if (document.visibilityState === 'visible') { + if (!ws || ws.readyState === WebSocket.CLOSED) { + console.log('Reconnecting WebSocket due to visibility change'); + connectWebSocket(); + } + } else { + if (ws) { + console.log('Closing WebSocket due to visibility change'); + ws.close(); + } + } + }); + + if (document.visibilityState != 'visible') { + console.log('Not connecting WebSocket due to visibility state:', document.visibilityState); + return; + } + + ws = new WebSocket(location.origin.replace(/^http/, 'ws') + '/ajaxws'); + //ws = new WebSocket('ws://10.0.2.150/ajaxws'); + var wsStatus = document.getElementById('ws-status'); + + ws.onopen = function() { + console.log('WebSocket connection opened'); + if (wsStatus) wsStatus.style.backgroundColor = 'green'; + }; + + ws.onmessage = function(event) { + //try { + const json = JSON.parse(event.data); + console.log('Received JSON:', json); + handleJsonItems(json); + //} catch (e) { + // console.error('Invalid JSON received:', event.data); + //} + }; + + ws.onclose = function() { + console.log('WebSocket connection closed, attempting to reconnect in ' + reconnectInterval / 1000 + ' seconds'); + if (wsStatus) wsStatus.style.backgroundColor = 'yellow'; + setTimeout(connectWebSocket, reconnectInterval); + }; + + ws.onerror = function(error) { + console.error('WebSocket error:', error); + if (wsStatus) wsStatus.style.backgroundColor = 'red'; + ws.close(); + }; +} /****************************************************************************************** * activate all radioselections after pageload to hide unnecessary elements * Works for all checkbox and radio elements with onclick="radioselection(show, hide)" * ******************************************************************************************/ -function handleRadioSelections() { +export function handleRadioSelections() { var radios = document.querySelectorAll('input[type=radio][onclick*=radioselection]:checked'); for (var i = 0; i < radios.length; i++) { if (radios[i].onclick) { @@ -94,33 +175,57 @@ function handleRadioSelections() { for (var i = 0; i < checkboxes.length; i++) { if (checkboxes[i].onclick) { var onclickStr = checkboxes[i].getAttribute('onclick'); - var match = onclickStr.match(/onCheckboxSelection\((.*)\)/); + var match = onclickStr.match(/onCheckboxSelection\((.*)\)/); if (match) { - eval("onCheckboxSelection(" + match[1] + ")"); + // Entferne den ersten Parameter (->this) aus match[1] + // Beispiel: "this, ['EnableRelays_1','EnableRelays_2'],[]" + // Ergebnis: "['EnableRelays_1','EnableRelays_2'],[]" + let params = match[1].replace(/^\s*[^,]+,\s*/, ''); + // eval mit dem aktuellen Checkbox-Element als ersten Parameter + eval("onCheckboxSelection(checkboxes[i], " + params + ")"); } } } } /***************************************************************************************** - * central function to initiate data fetch + * central function to send data to server * @param {*} json -> json object to send * @param {*} highlight -> highlight on/off * @param {*} callbackFn -> callback function to call after data is fetched * @returns {*} void ******************************************************************************************/ +export function requestData(json) { + if (typeof ws !== 'undefined' && ws.readyState === WebSocket.OPEN) { + console.log('WebSocket is open, sending data:', json); + ws.send(JSON.stringify(json)); + } else { + console.log('WebSocket not open'); + setResponse(false, 'WebSocket not open, could not send data'); + } +} -function requestData(json, highlight, callbackFn) { - const data = new URLSearchParams(); - data.append('json', json); - - fetch('/ajax', { - method: 'POST', - headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, - body: data - }) - .then (response => response.json()) - .then (json => { handleJsonItems(json, highlight, callbackFn)}); +/***************************************************************************************** + * @description This function updated values according their data-id in DOM elements. + * @param {*} json: JSON object containing the data-id values to update + * @param {*} highlight: boolean value to highlight the updated elements + * @returns {*} void + * @example updateDataID({"data-id":{"InverterSN.value":"123456789"}}, true) + * *****************************************************************************************/ +function updateDataID(json, highlight) { + if (json["data-id"]) { + for (const key in json["data-id"]) { + const elements = document.querySelectorAll(`[data-id="${key}"]`); + elements.forEach(element => { + element.innerHTML = json["data-id"][key]; + if (highlight && element.classList.contains('ajaxchange')) { + element.classList.add('highlightOn'); + setTimeout(function() {document.getElementById(element.id).classList.remove('highlightOn')}, 1000); + } + + }); + } + } } /***************************************************************************************** @@ -140,13 +245,10 @@ function applyKey (_obj, _key, _val, counter, tplHierarchie, highlight) { if (['SPAN', 'DIV', 'TD', 'DFN'].includes(_obj.tagName)) { if (highlight && _obj.classList.contains('ajaxchange')) { _obj.classList.add('highlightOn'); - _obj.innerHTML = _val; - } if (!highlight && _obj.classList.contains('ajaxchange')) { - _obj.classList.remove('highlightOn'); - _obj.innerHTML = _val; - } else { - _obj.innerHTML = _val; - } + setTimeout(function() {document.getElementById(_obj.id).classList.remove('highlightOn')}, 1000); + } + _obj.innerHTML = _val; + } else if (_obj.tagName == 'INPUT' && ['checkbox','radio'].includes(_obj.type)) { if (_val == true) _obj.checked = true; } else if (_obj.tagName == 'OPTION') { @@ -155,8 +257,17 @@ function applyKey (_obj, _key, _val, counter, tplHierarchie, highlight) { _obj.value = _val; } } else { - // using parenet object - _obj[_key] = _val; + // using parent object + if (_key in _obj) { + if (highlight && _obj.classList.contains('ajaxchange')) { + _obj.classList.add('highlightOn'); + setTimeout(function() {document.getElementById(_obj.id).classList.remove('highlightOn')}, 1000); + } + + _obj[_key] = _val; + } else { + _obj.setAttribute(_key, _val); + } } } @@ -255,11 +366,37 @@ function applyTemplate(TemplateJson, templateID, doc, tplHierarchie, highlight) } } -function handleJsonItems(json, highlight, callbackFn) { +/***************************************************************************************** + * apply Javascript variables to the window object from a JSON object + * @param {*} json: JSON object containing the variables to apply + * @returns {*} void + * @example applyJS({"myVar": "myValue"}) + * *****************************************************************************************/ +function applyJS(json) { + for (var key in json) { + window[key] = json[key]; + } +} + +/***************************************************************************************** + * Main function to handle JSON response + * @param {*} json: JSON object containing the response data + * @returns {*} void + * @example handleJsonItems({"data-id": {"InverterSN": "123456789"}, "cmd": {"action": "GetInitData", "subaction": "status", "callbackFn": "MyCallback"}, " + * "response": {"status": 1, "text": "OK"}}) + * *****************************************************************************************/ +export function handleJsonItems(json) { + const callbackFn = (typeof json['cmd'] !== 'undefined' && typeof json['cmd']['callbackFn'] !== 'undefined') ? json['cmd']['callbackFn'] : undefined; + const highlight = (typeof json['cmd'] !== 'undefined' && typeof json['cmd']['highlight'] !== 'undefined') ? json['cmd']['highlight'] : false; + if ("data" in json) { applyKeys(json.data, document, undefined, undefined, '', highlight); } + if ('js' in json) { + applyJS(json.js); + } + if ('response' in json) { try { if (json.response.status == 1) {setResponse(true, json.response.text);} @@ -267,8 +404,14 @@ function handleJsonItems(json, highlight, callbackFn) { } catch(e) {setResponse(false, 'unknow error');} } + if ("data-id" in json) { + updateDataID(json, highlight); + } + // DOM objects now ready - if (callbackFn) {callbackFn();} + if (callbackFn && typeof combinedFunctionMap[callbackFn] === 'function') { + combinedFunctionMap[callbackFn](json); + } } /***************************************************************************************** @@ -276,7 +419,7 @@ function handleJsonItems(json, highlight, callbackFn) { * @param {*} b (bool): true = OK; false = Error * @param {*} s (String): text to show *****************************************************************************************/ -function setResponse(b, s) { +export function setResponse(b, s) { try { // clear if previous timer still run clearTimeout(timer); @@ -294,17 +437,14 @@ function setResponse(b, s) { } /****************************************************************************************** -# -# definition of creating selectionlists from input fields -# querySelector -> select input fields to convert -# jsonLists -> define multiple predefined lists to set as option as array -# blacklist -> simple list of ports (numbers) to set as disabled option -# -# example: -# CreateSelectionListFromInputField('input[type=number][id^=AllePorts], input[type=number][id^=GpioPin]', -# [gpio, gpio_analog], gpio_disabled); + * definition of creating selectionlists from input fields + * @param {*} querySelector -> select input fields to convert + * @param {*} jsonLists -> define multiple predefined lists to set as option as array + * @param {*}blacklist -> simple list of ports (numbers) to set as disabled option + * @example + * CreateSelectionListFromInputField('input[type=number][id^=AllePorts], input[type=number][id^=GpioPin]', [gpio, gpio_analog], gpio_disabled); ******************************************************************************************/ -function CreateSelectionListFromInputField(querySelector, jsonLists, blacklist) { +export function CreateSelectionListFromInputField(querySelector, jsonLists, blacklist) { var _parent, _select, _option, i, j, k; var objects = document.querySelectorAll(querySelector); for( j=0; j< objects.length; j++) { @@ -312,8 +452,8 @@ function CreateSelectionListFromInputField(querySelector, jsonLists, blacklist) _select = document.createElement('select'); _select.id = objects[j].id; _select.name = objects[j].name; - for ( k = 0; k < jsonLists.length; k += 1 ) { - for ( i = 0; i < jsonLists[k].length; i += 1 ) { + for ( k = 0; k < jsonLists.length; k++ ) { + for ( i = 0; i < jsonLists[k].length; i++ ) { _option = document.createElement( 'option' ); _option.value = jsonLists[k][i].port; _option.text = jsonLists[k][i].name; @@ -334,7 +474,7 @@ function CreateSelectionListFromInputField(querySelector, jsonLists, blacklist) ****************************************************************************************/ function isVisible(_obj) { var ret = true; - if (_obj && _obj.style.display == "none") { ret = false;} + if (_obj && (_obj.style.display == "none" || _obj.classList.contains("hide"))) { ret = false; } else if (_obj && _obj.parentNode && _obj.tagName != "HTML") ret = isVisible(_obj.parentNode); return ret; } @@ -345,7 +485,7 @@ regex of item ID to identify first element in row - if set, returned json is an array, all elements per row, example: "^myonoffswitch.*" - if emty, all elements at one level together, ONLY for small json´s (->memory issue) ****************************************************************************************/ -function onSubmit(DataForm, separator='') { +export function onSubmit(DataForm, separator='') { // init json Objects var JsonData, tempData; @@ -411,19 +551,20 @@ function onSubmit(DataForm, separator='') { }) .then (() => { var data = {}; - data['action'] = "ReloadConfig"; - data['subaction'] = filename; - requestData(JSON.stringify(data), false); + data['cmd'] = {}; + data['cmd']['action'] = "ReloadConfig"; + data['cmd']['subaction'] = filename; + requestData(data); }); } - /**************************************************************************************** -blendet Zeilen der Tabelle aus - show: Array of shown IDs return true; - hide: Array of hidden IDs + * blendet Zeilen der Tabelle aus + * @param {*} show: Array of shown IDs return true; + * @param {*} hide: Array of hidden IDs + * @example radioselection(["row1", "row2"], ["row3", "row4"]) ****************************************************************************************/ -function radioselection(show, hide) { +export function radioselection(show, hide) { for(var i = 0; i < show.length; i++){ if (document.getElementById(show[i])) {document.getElementById(show[i]).style.display = 'table-row';} } @@ -439,7 +580,7 @@ function radioselection(show, hide) { * @param {*} hide Array of hidden IDs if checkbox is checked * @returns {*} void ****************************************************************************************/ -function onCheckboxSelection(checkbox, show, hide) { +export function onCheckboxSelection(checkbox, show, hide) { if (checkbox.checked) { radioselection(show, hide); } else { @@ -454,7 +595,7 @@ function onCheckboxSelection(checkbox, show, hide) { * For each of these checkboxes, it creates a new div element with the class "onoffswitch", clones the checkbox into this div, * and adds a label with the necessary span elements for styling. Finally, it replaces the original checkbox with the new div element. ****************************************************************************************/ -function transformCheckboxes() { +export function transformCheckboxes() { // Alle Checkboxen im Dokument suchen deren elternelement kein div mit der Style class "onoffswitch" ist const checkboxes = document.querySelectorAll("input[type='checkbox']:not(.onoffswitch-checkbox)"); @@ -468,6 +609,7 @@ function transformCheckboxes() { // Checkbox in das neue Div-Element kopieren const newCheckbox = checkboxes[i].cloneNode(true) + newCheckbox.className = 'onoffswitch-checkbox'; div.appendChild(newCheckbox); @@ -495,3 +637,62 @@ function transformCheckboxes() { } } + +/**************************************************************************************** + * Get all form data values as a string + * @param {*} formElement: id of the form element + * + * @returns {*} string containing all form data values + * ****************************************************************************************/ +export function getFormData(formElement) { + const form = document.getElementById(formElement); + if (form) { + const formData = new FormData(form); + let dataString = ''; + formData.forEach((value, key) => { + dataString += `${key}=${value}|`; + }); + // Remove the last '|' character + dataString = dataString.slice(0, -1); + return dataString; + } +} + +/**************************************************************************************** + * Show a dialog if the values of items in formdata has been changed + * ****************************************************************************************/ +export function showMustSaveDialog() { + if (document.getElementById('needToSave') && datavalues !== getFormData("DataForm")) { + document.getElementById('needToSave').classList.remove('hide'); + } else { + document.getElementById('needToSave').classList.add('hide'); + } +} + +/**************************************************************************************** + * Save the current form data values in the variable "datavalues" to check on every change + * if the form data has been changed. If the form data has been changed, show a dialog. + * ****************************************************************************************/ +export function initDataValues() { + datavalues = getFormData("DataForm"); + + if (document.getElementById('needToSave')) { + document.getElementById('needToSave').classList.add('hide'); + } +} + +/**************************************************************************************** + * Show or hide an object + * @returns {*} void + * ****************************************************************************************/ +export function toggleView() { + const logView = document.getElementById('logView'); + if (logView.style.display === 'none') { + logView.style.display = 'block'; + } else { + logView.style.display = 'none'; + } +} + +/**************************************************************************************** +****************************************************************************************/ diff --git a/data/web/Style.css b/data/web/Style.css index 7fdbc505..dc43fbcb 100644 --- a/data/web/Style.css +++ b/data/web/Style.css @@ -18,7 +18,7 @@ body { display: none; } - input[type="submit"] { + input[type="submit"], button { padding: 4px 16px; margin: 4px; background-color: #07D; @@ -27,7 +27,10 @@ body { border-radius: 4px; border: none; } - input[type="submit"]:hover { background: #336699; } + + input[type="submit"]:hover, button:hover { + background: #336699; + } input, select, textarea { margin: 4px; @@ -233,4 +236,30 @@ body { to { transform: rotate(360deg); } +} + +@keyframes pulse { + 0% { + opacity: 1; + } + 50% { + opacity: 0.3; + } + 100% { + opacity: 1; + } +} + +.pulse { + animation: pulse 1.5s infinite; +} + +.needToSave { + width: 300px; + margin: 0 auto; + border: 1px solid red; + border-radius: 25px; + text-align: center; + margin-bottom: 5px; + padding-bottom: 3px; } \ No newline at end of file diff --git a/data/web/baseconfig.html b/data/web/baseconfig.html index cb204305..1c6443f8 100644 --- a/data/web/baseconfig.html +++ b/data/web/baseconfig.html @@ -3,13 +3,34 @@ - - - + + + + + + + Modbus MQTT Gateway
+ + +
+ + Änderungen nicht gespeichert +
+
@@ -118,9 +139,13 @@
- -
- - +

+
+
+ Connection Status: + +
+ x +
\ No newline at end of file diff --git a/data/web/baseconfig.js b/data/web/baseconfig.js index d3a815f2..6eaba900 100644 --- a/data/web/baseconfig.js +++ b/data/web/baseconfig.js @@ -1,24 +1,89 @@ +import * as global from './Javascript.js'; + // ************************************************ -window.addEventListener('DOMContentLoaded', init, false); -function init() { - GetInitData(); +export function init() { + // Initiale Verbindung aufbauen + global.connectWebSocket(); + + // Warte bis die WebSocket-Verbindung aufgebaut ist + let checkWebSocketInterval = setInterval(() => { + if (global.ws && global.ws.readyState === WebSocket.OPEN) { + clearInterval(checkWebSocketInterval); + GetInitData(); + } + }, 100); +} + +export function init1() { + var data = { + "data": { + "mqttroot": "exampleRoot", + "mqttserver": "exampleServer", + "mqttport": 1883, + "mqttuser": "exampleUser", + "mqttpass": "examplePass", + "mqttbasepath": "exampleBasePath", + "debuglevel": 2, + "sel_wifi": 1, + "sel_eth": 0, + "useRandomClientID": 1, + "sel_auth": 0, + "auth_user": "authUser", + "auth_pass": "authPass", + "GpioPin_serial_rx": 15, + "GpioPin_serial_tx": 16 + }, + "response": { + "status": 1, + "text": "successful" + } + , "cmd": { + "action": "GetInitData", + "subaction": "baseconfig" + ,"callbackFn": "baseconfig_Callback" + , "highlight": "true" + } + }; + + global.handleJsonItems(data); + + global.initDataValues(); } +export const functionMap = { + baseconfig_Callback: MyCallback +}; + // ************************************************ function GetInitData() { var data = {}; - data.action = "GetInitData"; - data.subaction = "baseconfig"; - requestData(JSON.stringify(data), true, MyCallback); + data['cmd'] = {}; + data['cmd']['action'] = "GetInitData"; + data['cmd']['subaction'] = "baseconfig"; + data['cmd']['callbackFn'] = "baseconfig_Callback"; + + global.requestData(data); } // ************************************************ -function MyCallback() { - transformCheckboxes(); - handleRadioSelections(); - CreateSelectionListFromInputField('input[type=number][id^=GpioPin]', [gpio]); +function MyCallback(json) { + global.transformCheckboxes(); + global.handleRadioSelections(); + global.CreateSelectionListFromInputField('input[type=number][id^=GpioPin]', [global.gpio], JSON.parse(gpio_disabled)); + + document.querySelectorAll('#DataForm input:not([type=checkbox]):not([type=radio]), #DataForm select').forEach(element => { + element.addEventListener('blur', global.showMustSaveDialog); + }); + + document.querySelectorAll('#DataForm input[type=checkbox], #DataForm input[type=radio]').forEach(element => { + element.addEventListener('click', global.showMustSaveDialog); + }); + + global.initDataValues(); + + // Hide loader and show body document.querySelector("#loader").style.visibility = "hidden"; document.querySelector("body").style.visibility = "visible"; } -// ************************************************ \ No newline at end of file +// ************************************************ diff --git a/data/web/handlefiles.html b/data/web/handlefiles.html deleted file mode 100644 index 75eb1710..00000000 --- a/data/web/handlefiles.html +++ /dev/null @@ -1,60 +0,0 @@ - - - - - - - - - - HandleFiles - - -
- - - - - - - - - - - - - - - - - - - -
DateienInhalt
- - - - - -
- path: - -
{path}
- -
-
- -
-
filename: - - - - - -
- - \ No newline at end of file diff --git a/data/web/handlefiles.js b/data/web/handlefiles.js deleted file mode 100644 index 9358a682..00000000 --- a/data/web/handlefiles.js +++ /dev/null @@ -1,245 +0,0 @@ -// https://jsfiddle.net/tobiasfaust/uc1jfpgb/ - -var DirJson; - -window.addEventListener('load', initHandleFS, false); -function initHandleFS() { - init("/"); - document.querySelector("#loader").style.visibility = "hidden"; - document.querySelector("body").style.visibility = "visible"; -} - -function init(startpath) { - requestListDir(startpath); - obj = document.getElementById('fullpath').innerHTML = ''; // div - obj = document.getElementById('filename').value = ''; // input field - obj = document.getElementById('content').value = ''; -} - -// *********************************** -// Ajax Request to update -// *********************************** -function requestListDir(startpath) { - var data = {}; - data['action'] = "handlefiles"; - data['subaction'] = "listDir" - //ajax_send(JSON.stringify(data)); - - var http = null; - if (window.XMLHttpRequest) { http =new XMLHttpRequest(); } - else { http =new ActiveXObject("Microsoft.XMLHTTP"); } - - if(!http){ alert("AJAX is not supported."); return; } - - var url = '/ajax'; - var params = 'json=' + JSON.stringify(data); - - http.open('POST', url, true); - http.setRequestHeader('Content-type', 'application/x-www-form-urlencoded'); - http.onreadystatechange = function() { //Call a function when the state changes. - if(http.readyState == 4 && http.status == 200) { - DirJson = JSON.parse(http.responseText); - listFiles(startpath); - } - } - http.send(params); -} - -// *********************************** -// show content of fetched file -// *********************************** -function setContent(string, file) { - obj = document.getElementById('fullpath').innerHTML = file; // div - obj = document.getElementById('filename').value = basename(file); // input field - - if (file.endsWith("json")) { - obj = document.getElementById('content').value = JSON.stringify(JSON.parse(string), null, 2); - } else { - obj = document.getElementById('content').value = string; - } -} - -// *********************************** -// fetch file from host -// *********************************** -function fetchFile(file) { - obj = document.getElementById('content').value = "loading "+file+"..."; - - fetch(file) - .then(response => response.text()) - .then(textString => setContent(textString, file)); -} - -// *********************************** -// show directory structure -// *********************************** -function listFiles(path) { - var table = document.querySelector('#files'), - row = document.querySelector('#NewRow'), - tr_tpl, DirJsonLocal; - - // cleanup table - table.replaceChildren(); - - // get the right part - for(let i = 0; i < DirJson.length; i++) { - if (DirJson[i].path == path) { - DirJsonLocal = DirJson[i] - } - } - - // show path information - document.getElementById('path').innerHTML = path; - - // set "back" item if not root - if (path != '/') { - tr_tpl = document.importNode(row.content, true); - cells = tr_tpl.querySelectorAll("td"); - cells.forEach(function (item, index) { - var text = item.innerHTML; - var oc = "listFiles('" + getParentPath(path) + "')" - text = text.replaceAll("{file}", '..'); - item.innerHTML = text; - item.setAttribute('onClick', oc); - }); - table.appendChild(tr_tpl); - } - - // show files - DirJsonLocal.content.forEach(function (file) { - // template "laden" (lies: klonen) - tr_tpl = document.importNode(row.content, true); - cells = tr_tpl.querySelectorAll("td"); - cells.forEach(function (item, index) { - var text = item.innerHTML; - var oc; - - if(file.isDir == 0) { - oc = item.getAttribute('onClick'); - var newPath = DirJsonLocal.path + "/" + file.name; - if (newPath.startsWith("//")) {newPath = newPath.substring(1)} - oc = oc.replaceAll("{fullpath}", newPath); - text = text.replaceAll("{file}", file.name); - } else if(file.isDir == 1) { - var newPath = DirJsonLocal.path + "/" + file.name; - if (newPath.startsWith("//")) {newPath = newPath.substring(1)} - oc = "listFiles('" + newPath + "')" - text = text.replaceAll("{file}", file.name + "/"); - } - - - item.innerHTML = text; - item.setAttribute('onClick', oc); - }); - table.appendChild(tr_tpl); - }) - } - -// *********************************** -// returns parent path: '/regs/web' -> '/regs' -// *********************************** -function getParentPath(path) { - var ParentPath, PathArray; - - PathArray = path.split('/') - PathArray.pop() - if (PathArray.length == 1) { ParentPath = '/' } - else { ParentPath = PathArray.join('/')} - return ParentPath -} - -// *********************************** -// extract the filename from path -// *********************************** -function basename(str) { - return str.split('\\').pop().split('/').pop(); -} - -// *********************************** -// return true if valid json, otherwise false -// *********************************** -function validateJson(json) { - try { - JSON.parse(json); - return true; - } catch { - return false; - } -} - -// *********************************** -// download content of textarea as filename on local pc -// *********************************** -function downloadFile() { - var textToSave = document.getElementById("content").value; - var textToSaveAsBlob = new Blob([textToSave], {type:"text/plain"}); - var textToSaveAsURL = window.URL.createObjectURL(textToSaveAsBlob); - var fileNameToSaveAs = document.getElementById("filename").value; - - if (fileNameToSaveAs != '') { - var downloadLink = document.createElement("a"); - downloadLink.download = fileNameToSaveAs; - downloadLink.innerHTML = "Download File"; - downloadLink.href = textToSaveAsURL; - - downloadLink.onclick = destroyClickedElement; - downloadLink.style.display = "none"; - document.body.appendChild(downloadLink); - downloadLink.click(); - } else { setResponse(false, 'Filename is empty, Please define it.');} -} - -function destroyClickedElement(event) -{ - document.body.removeChild(event.target); -} - -// *********************************** -// store content of textarea -// *********************************** -function uploadFile() { - var textToSave = document.getElementById("content").value; - var textToSaveAsBlob = new Blob([textToSave], {type:"text/plain"}); - var fileNameToSaveAs = document.getElementById("filename").value; - var pathOfFile = document.getElementById('path').innerHTML; - - if (fileNameToSaveAs != '') { - if (fileNameToSaveAs.toLowerCase().endsWith('.json')) { - if (!validateJson(textToSave)) { - setResponse(false, 'Json invalid') - return; - } - } - - setResponse(true, 'Please wait for saving ...'); - - const formData = new FormData(); - formData.append(fileNameToSaveAs, textToSaveAsBlob, pathOfFile + '/' + fileNameToSaveAs); - - fetch('/doUpload', { - method: 'POST', - body: formData, - }) - .then (response => response.json()) - .then (json => { - setResponse(true, json.text) - }); - - } else { setResponse(false, 'Filename is empty, Please define it.');} -} - -function deleteFile() { - var pathOfFile = document.getElementById('path').innerHTML; - var fileName = document.getElementById("filename").value; - - if (fileName != '') { - var data = {}; - data['action'] = 'handlefiles'; - data['subaction'] = "deleteFile"; - data['filename'] = pathOfFile + '/' + fileName; - - setResponse(true, 'Please wait for deleting ...'); - requestData(JSON.stringify(data)); - init(pathOfFile); - } else { setResponse(false, 'Filename is empty, Please define it.');} -} \ No newline at end of file diff --git a/data/web/index.html b/data/web/index.html index d66a03f6..35a68178 100644 --- a/data/web/index.html +++ b/data/web/index.html @@ -3,8 +3,8 @@ Modbus MQTT Gateway - - - + + + \ No newline at end of file diff --git a/data/web/modbusconfig.html b/data/web/modbusconfig.html index 6f46b960..73583cb3 100644 --- a/data/web/modbusconfig.html +++ b/data/web/modbusconfig.html @@ -3,13 +3,32 @@ - - - + + + + + + + ModbusConfig
+ + +
+ + Änderungen nicht gespeichert +
+
@@ -87,7 +106,7 @@ @@ -112,6 +131,11 @@ + + + + + - - + - + - +
Enable OpenWB support - +
smartmeter module ID
Enable Dataframe CRC Check @@ -155,7 +179,12 @@

- - +
+
+ Connection Status: + +
+ x +
\ No newline at end of file diff --git a/data/web/modbusconfig.js b/data/web/modbusconfig.js index f24e83c8..521dae45 100644 --- a/data/web/modbusconfig.js +++ b/data/web/modbusconfig.js @@ -1,24 +1,95 @@ +import * as global from './Javascript.js'; + // ************************************************ -window.addEventListener('DOMContentLoaded', init, false); -function init() { - GetInitData(); +export function init() { + // Initiale Verbindung aufbauen + global.connectWebSocket(); + + // Warte bis die WebSocket-Verbindung aufgebaut ist + let checkWebSocketInterval = setInterval(() => { + if (global.ws && global.ws.readyState === WebSocket.OPEN) { + clearInterval(checkWebSocketInterval); + GetInitData(); + } + }, 100); +} + +export function init1() { + // erstelle ein Beispiel json mit Beispielwerten welches die funktion modbus::GetInitData generieren würde und weise das json der variable data zu. + var data = { + "data": { + "pin_rx": 16, + "pin_tx": 17, + "pin_rts": 5, + "clientid": 1, + "baudrate": 19200, + "txintervallive": 10, + "txintervalid": 60, + "enableRelays": false, + "pin_RELAY1": 18, + "pin_RELAY2": 19, + "openwbversions": [{"openwbversion": {"text": "1.2.3"}}], + "openwbmodulid": 1, + "openwbbatteryid": 2, + "openwbmeterid": 3, + "enableOpenWb": true, + "enable_setters": false, + "enableCrcCheck": true, + "enableLengthCheck": false, + "inverters": [ [ { "inverter": {"value": "Kostal", "text": "Kostal"}}]], + }, + "response": { + "status": 1, + "text": "successful" + }, + "cmd": { + "action": "GetInitData", + "subaction": "status", + "callbackFn": "mbconfig_Callback" + }, + "js": { + "gpio_disabled": "[]" + } + }; + + global.handleJsonItems(data); + } +export const functionMap = { + mbconfig_Callback: MyCallback +}; + // ************************************************ function GetInitData() { var data = {}; - data.action = "GetInitData"; - data.subaction = "modbusconfig"; - requestData(JSON.stringify(data), false, MyCallback); + data['cmd'] = {}; + data['cmd']['action'] = "GetInitData"; + data['cmd']['subaction'] = "modbusconfig"; + data['cmd']['callbackFn'] = "mbconfig_Callback"; + + global.requestData(data); } // ************************************************ -function MyCallback() { - transformCheckboxes(); - handleRadioSelections(); - CreateSelectionListFromInputField('input[type=number][id^=GpioPin]', [gpio]); +function MyCallback(json) { + global.transformCheckboxes(); + global.handleRadioSelections(); + global.CreateSelectionListFromInputField('input[type=number][id^=GpioPin]', [global.gpio], JSON.parse(gpio_disabled)); + + document.querySelectorAll('#DataForm input:not([type=checkbox]):not([type=radio]), #DataForm select').forEach(element => { + element.addEventListener('blur', global.showMustSaveDialog); + }); + + document.querySelectorAll('#DataForm input[type=checkbox], #DataForm input[type=radio]').forEach(element => { + element.addEventListener('click', global.showMustSaveDialog); + }); + + global.initDataValues(); + + document.querySelector("#loader").style.visibility = "hidden"; document.querySelector("body").style.visibility = "visible"; } -// ************************************************ \ No newline at end of file +// ************************************************ diff --git a/data/web/modbusitemconfig.html b/data/web/modbusitemconfig.html index 240363c0..2fae4d63 100644 --- a/data/web/modbusitemconfig.html +++ b/data/web/modbusitemconfig.html @@ -3,18 +3,43 @@ - - - + + + + + + + status
+ + +
+ + Änderungen nicht gespeichert +
+
- +
- + @@ -43,10 +68,57 @@
Active + Active + + + Name OpenWB Wert
+
+ +
+ + Set commands deactivated +
+ +
+ + no Set commands available +
+ + + + + + + + + + + + +
+ Active + + + NameMQTT Topic


- - + +
+
+ Connection Status: + +
+ x +
- \ No newline at end of file + diff --git a/data/web/modbusitemconfig.js b/data/web/modbusitemconfig.js index d9122b3b..80ba32d2 100644 --- a/data/web/modbusitemconfig.js +++ b/data/web/modbusitemconfig.js @@ -1,41 +1,208 @@ +import * as global from './Javascript.js'; + // ************************************************ -window.addEventListener('DOMContentLoaded', init, false); -function init() { - GetInitData(); +export function init1() { + + var data = {"data": {"items": [ + {"name": "InverterIdData", "realname": "InverterIdData", + "value": {"innerHTML": "1234", "data-id": "InverterIdData.value"}, + "active": {"checked": 1, "name": "InverterIdData"}, + "mqtttopic": "InverterIdData" + }, + {"name": "Power", "realname": "Power", + "value": {"innerHTML": "1234", "data-id": "Power.value"}, + "active": {"checked": 0, "name": "Power"}, + "mqtttopic": "Power" + } + ]}, + "response": {"status": 1, "text": "successful"}, + "cmd": { + "callbackFn": "mbitemconfig_ItemCallback" + }} + + global.handleJsonItems(data); + + + data = {"globalEnabled": "1", "data": {"setitems": [{"name": "setUnlockSettings","realname": "Unlock Settings","active": {"checked": 0, "name": "setUnlockSettings"},"info": "send the 4 digit advanced password","subscription": "home/Solax/set/setUnlockSettings"},{"name": "setTargetBatSOC","realname": "Target SoC","active": {"checked": 0, "name": "setTargetBatSOC"},"info": "set battery SOC: 0 - 100 in percent","subscription": "home/Solax/set/setTargetBatSOC"},{"name": "setOperationMode","realname": "Operation Mode","active": {"checked": 0, "name": "setOperationMode"},"info": "setting of 6 possible operation modes","subscription": "home/Solax/set/setOperationMode","mapping": {"data-mapping": "[['SelfUse',0],['FeedInPriority',1],['BackupMode',2],['ManuelMode',3],['PeakShaving',4],['TUOMode',5]]"}} ]}, + "object_id": "home/Solax", + "cmd": { + "callbackFn": "mbitemconfig_SetterCallback" + }} + global.handleJsonItems(data); + + + data = {"data-id": { "InverterIdData.value" : "684453556"}, + "response": {"status": 1, "text": "successful"}, + "cmd": { + "highlight": "true" + }} + global.handleJsonItems(data); } // ************************************************ -var myInterval = setInterval(RefreshLiveData, 5000); +export function init() { + // Initiale Verbindung aufbauen + global.connectWebSocket(); -// ************************************************ -function GetInitData() { - var data = {}; - data.action = "RefreshLiveData"; - data.subaction = "all"; - requestData(JSON.stringify(data), false, MyCallback); + fetch('/getitems') + .then(response => response.json()) + .then(data => { + data['cmd'] = {}; + data['cmd']['callbackFn'] = "mbitemconfig_ItemCallback"; + global.handleJsonItems(data); + }) + .catch(error => console.error('Error fetching items:', error)); + + fetch('/getsetter') + .then(response => response.json()) + .then(data => { + data['cmd'] = {}; + data['cmd']['callbackFn'] = "mbitemconfig_SetterCallback"; + global.handleJsonItems(data); + }) + .catch(error => console.error('Error fetching setters:', error)); + + // Warte bis die WebSocket-Verbindung aufgebaut ist + let checkWebSocketInterval = setInterval(() => { + if (global.ws && global.ws.readyState === WebSocket.OPEN) { + clearInterval(checkWebSocketInterval); + RequestDataStream(); + } + }, 100); } // ************************************************ -function MyCallback() { - //transformCheckboxes() +export const functionMap = { + mbitemconfig_ItemCallback: MyItemCallback, + mbitemconfig_SetterCallback: MySetterCallback +}; + +// ************************************************ +function MyItemCallback(json) { + global.transformCheckboxes(); + + document.querySelectorAll('#DataForm input:not([type=checkbox]):not([type=radio]), #DataForm select').forEach(element => { + element.addEventListener('blur', global.showMustSaveDialog); + }); + + document.querySelectorAll('#DataForm input[type=checkbox], #DataForm input[type=radio]').forEach(element => { + element.addEventListener('click', global.showMustSaveDialog); + }); + + global.initDataValues(); + document.querySelector("#loader").style.visibility = "hidden"; document.querySelector("body").style.visibility = "visible"; } -function RefreshLiveData() { - var data = {}; - data.action = "RefreshLiveData"; - data.subaction = "all"; - requestData(JSON.stringify(data), true); +// ************************************************ +function MySetterCallback(json) { + MyItemCallback(json); + + if ("data" in json && "setitems" in json["data"] && json["data"]["setitems"].length == 0) { + document.getElementById("setterNa").classList.remove("hide"); + } + else if ("globalEnabled" in json && json["globalEnabled"] == 0) { + document.getElementById("setterDeactive").classList.remove("hide"); + } + else { + document.getElementById("settable").classList.remove("hide"); + } + + // handle data-mapping and add them to topic as tooltip + document.querySelectorAll('[data-mapping]').forEach(element => { + try { + const mapping = JSON.parse(element.getAttribute('data-mapping').replace(/'/g, '"')); + const obj = document.getElementById(element.id); + var info = ""; + + for (var i = 0; i < mapping.length; i++) { + if (info.length > 0) info += "
"; + info += "- " + mapping[i][0]; + } + + info = "possible values:

" + info; + createTooltip(obj, info); + + } catch (e) { + console.error('Invalid JSON:', e); + } + }); + + // handle data-info and add them to realname as tooltip + document.querySelectorAll('[data-info]').forEach(element => { + const info = element.getAttribute('data-info'); + const obj = document.getElementById(element.id); + + createTooltip(obj, info); + }); +} + +// ************************************************ +function createTooltip(obj, tooltip) { + var dfn = document.createElement('dfn'); + dfn.classList.add('tooltip_simple'); + obj.parentNode.replaceChild(dfn, obj); + dfn.appendChild(obj); + + var span = document.createElement('span'); + span.setAttribute('role', 'tooltip_simple'); + span.innerHTML = tooltip; + dfn.appendChild(span); } // ************************************************ -function ChangeActiveStatus(id) { - obj = document.getElementById(id); - //item = id.replace(/^activeswitch_(.*)$/g, "$1"); +function RequestDataStream() { var data = {}; - data.action = "SetActiveStatus"; - data.newState = (obj.checked?"true":"false"); - data.item = obj.name; - requestData(JSON.stringify(data)); + data.cmd = {}; + data.cmd.action = "subscribe"; + data.cmd.subaction = "modbus_data"; + data.cmd.highlight = "true"; + data.cmd.opts = ["+unit"]; + + global.requestData(data); +} + + +// ************************************************ +export function ChangeActiveStatus(id) { + var obj = document.getElementById(id); + + var data = {}; + data['cmd'] = {}; + data['cmd']['action'] = "SetActiveStatus"; + data['cmd']['newState'] = (obj.checked?"true":"false"); + data['cmd']["item"] = obj.name; + + global.requestData(data); +} + +// ************************************************ + +export function setToggleIcon(targetTable, element) { + if (element.classList.contains('fa-toggle-off')) { + element.classList.remove('fa-toggle-off'); + element.classList.add('fa-toggle-on'); + setCheckboxes(targetTable, "true"); + } else { + element.classList.remove('fa-toggle-on'); + element.classList.add('fa-toggle-off'); + setCheckboxes(targetTable, false); + } +} + +export function setCheckboxes(targetTable, state) { + document.querySelectorAll(`#${targetTable} input[type=checkbox]`).forEach(checkbox => { + if (typeof state == 'undefined') { + //checkbox.checked = !checkbox.checked; + checkbox.click(); + } else if ( typeof state !== 'undefined' && state && !checkbox.checked) { + //checkbox.checked = true; + checkbox.click(); + } else if (typeof state !== 'undefined' && !state && checkbox.checked) { + //checkbox.checked = false; + checkbox.click(); + } + }); + global.showMustSaveDialog(); } \ No newline at end of file diff --git a/data/web/navi.html b/data/web/navi.html index 41237263..d1a9d216 100644 --- a/data/web/navi.html +++ b/data/web/navi.html @@ -4,21 +4,32 @@ - - + + + + + + Modbus MQTT Gateway - - - - + - + - + - + - + - + @@ -52,5 +63,35 @@

Configuration

-

Configuration

+
+ Configuration of + - () - + + + + Release: @@ -29,17 +40,17 @@

Configuration

+ +
+ An update is available
+ You are using:
+ available version: +

+ + +
+ + + \ No newline at end of file diff --git a/data/web/navi.js b/data/web/navi.js index 17d7be61..f1133fec 100644 --- a/data/web/navi.js +++ b/data/web/navi.js @@ -1,20 +1,75 @@ +import * as global from './Javascript.js'; + +var currentVersion, newVersion = 0; + // ************************************************ -window.addEventListener('load', init, false); -function init() { - GetInitData(); +export function init() { + // Initiale Verbindung aufbauen + global.connectWebSocket(); + checkUpdate(); + + // Warte bis die WebSocket-Verbindung aufgebaut ist + let checkWebSocketInterval = setInterval(() => { + if (global.ws && global.ws.readyState === WebSocket.OPEN) { + clearInterval(checkWebSocketInterval); + GetInitData(); + } + }, 100); } // ************************************************ function GetInitData() { var data = {}; - data['action'] = "GetInitData"; - data['subaction'] = "navi"; - requestData(JSON.stringify(data)); + data['cmd'] = {}; + data['cmd']['action'] = "GetInitData"; + data['cmd']['subaction'] = "navi"; + + global.requestData(data); } // ************************************************ -function highlightNavi(item) { - collection = document.getElementsByName('navi') +function checkUpdate() { + // get deviceinfo the know where releases.json is + fetch("/getdeviceinfo") + .then(response => response.json()) + .then(data => { + fetch("https://"+ data.owner + ".github.io/" + data.repository + "/firmware/releases.json") + .then(response => response.json()) + .then(releases => { + var newBuild = 0; + for (let i = 0; i < releases.length; i++) { + if (releases[i].stage == "stable" && releases[i].chipFamilies.includes(data.chipfamily) && releases[i].build > newBuild) { + newBuild = releases[i].build; + newVersion = releases[i].version + " (@" + releases[i].stage + ") / Build: " + newBuild; + } + } + + currentVersion = data.FWVersion; + + if (newBuild > data.build && newBuild > 0) { + document.getElementById("updateInfoItem").classList.remove('hide'); + } + + }); + }); +} + +// ************************************************ +export function showUpdateInfoInMainFrame() { + document.getElementById("currentVersion").innerText = currentVersion; + document.getElementById("newVersion").innerText = newVersion; + const showUpdateInfo = document.getElementById("showUpdateInfo").cloneNode(true); + if (top.frames['frame_main'].document.getElementById("showUpdateInfo")) { + top.frames['frame_main'].document.getElementById("showUpdateInfo").classList.remove('hide'); + } else { + top.frames['frame_main'].document.body.appendChild(showUpdateInfo); + showUpdateInfo.classList.remove('hide'); + } +} + +// ************************************************ +export function highlightNavi(item) { + const collection = document.getElementsByName('navi') for (let i = 0; i < collection.length; i++) { if (item.id == collection[i].id ) { @@ -30,4 +85,36 @@ function highlightNavi(item) { } +/************************************************ + * init external site by filling predefined templates + * -> id = footer + ************************************************/ +export function initExternalSite(doc) { + fetch("navi.html") + .then(response => response.text()) + .then(html => { + const parser = new DOMParser(); + const naviDoc = parser.parseFromString(html, "text/html"); + const footerTemplate = naviDoc.getElementById("footer"); + + if (footerTemplate && doc) { + const targetFooter = doc.getElementById("footer"); + if (targetFooter) { + targetFooter.innerHTML = footerTemplate.innerHTML; + + if (doc.getElementById("ws-status")) { + if (global.ws.readyState === WebSocket.OPEN) { + doc.getElementById("ws-status").style.backgroundColor = 'green'; + } else { + doc.getElementById("ws-status").style.backgroundColor = 'yellow'; + } + } + } + } + }) + .catch(error => console.error("Error loading navi.html:", error)); + + document.defaultView.toggleView = global.toggleView; +} + // ************************************************ \ No newline at end of file diff --git a/data/web/rawdata.html b/data/web/rawdata.html index efe6a5ea..50c1b15d 100644 --- a/data/web/rawdata.html +++ b/data/web/rawdata.html @@ -3,10 +3,20 @@ - - - + + + + + + RawData @@ -19,10 +29,12 @@
Raw Data
RawData of ID-Data + RawData of ID-Data + + @@ -30,7 +42,10 @@
RawData of Live-Data + RawData of Live-Data + + @@ -40,9 +55,8 @@
-

+

- @@ -69,9 +83,14 @@
Insert your positions (comma separated) to test
- - - +

+
+
+ Connection Status: + +
+ x +
\ No newline at end of file diff --git a/data/web/rawdata.js b/data/web/rawdata.js index 08710fd8..3da30820 100644 --- a/data/web/rawdata.js +++ b/data/web/rawdata.js @@ -1,20 +1,72 @@ /* https://jsfiddle.net/tobiasfaust/p5q9hgsL/ */ +import * as global from './Javascript.js'; + // ************************************************ -window.addEventListener('DOMContentLoaded', init, false); -function init() { - GetInitData(); +export function init() { + // Initiale Verbindung aufbauen + global.connectWebSocket(); + + // Warte bis die WebSocket-Verbindung aufgebaut ist + let checkWebSocketInterval = setInterval(() => { + if (global.ws && global.ws.readyState === WebSocket.OPEN) { + clearInterval(checkWebSocketInterval); + GetInitData(); + } + }, 100); } -// ************************************************ +export function init1() { + var data = {"data": {"id_rawdata_org": "0103EEFF8A44130000281F0A0B0C0D0E0F", + "live_rawdata_org": "0102030405060708090a0b0c0d0e0f" + }, + "response": {"status": 1, "text": "successful"}, + "cmd": {"action": "GetInitData", "subaction": "rawdata", "callbackFn": "rawdata_Callback" + }}; + + global.handleJsonItems(data); +} + +/******************************* + * define all callback functions here to make them accessible from other modules by the global combinedFunctionMap +*******************************/ +export const functionMap = { + rawdata_Callback: MyCallback +}; + +/******************************* + * get initial data after page load +*******************************/ function GetInitData() { var data = {}; - data.action = "GetInitData"; - data.subaction = "rawdata"; - requestData(JSON.stringify(data), false, MyCallback); + data['cmd'] = {}; + data['cmd']['action'] = "GetInitData"; + data['cmd']['subaction'] = "rawdata"; + data['cmd']['callbackFn'] = "rawdata_Callback"; + + global.requestData(data); } -function MyCallback() { +export function cpRawdata2Clipboard(rawdatatype) { + const string_rawdata = document.getElementById(rawdatatype + '_org').innerHTML; + let bytes = chunk(string_rawdata,2) + + for( var i=0; i< bytes.length; i++) { + bytes[i] = "0x" + bytes[i]; + } + + // Copy the result to the clipboard + navigator.clipboard.writeText(bytes.join(' ')).then(() => { + alert('Raw data copied to clipboard. You can now paste it using CTRL-V.'); + }).catch(err => { + console.error('Failed to copy text: ', err); + }); +} + +/******************************* + * Callback function after receiving the data +*******************************/ +function MyCallback(json) { reset_rawdata('id_rawdata'); reset_rawdata('live_rawdata'); @@ -23,7 +75,7 @@ function MyCallback() { } /******************************* -split long byte-string into array + * split long byte-string into array *******************************/ function chunk(str, size) { return str.match(new RegExp('.{1,' + size + '}', 'g')) || []; @@ -45,7 +97,7 @@ insert Tooltips and linebreaks *******************************/ function prettyprint_rawdata(rawdatatype, bytearray, bytearray_org) { - for( i=0; i< bytearray.length; i++) { + for( var i=0; i< bytearray.length; i++) { const bstr = byte2string(bytearray_org[i]); const bint = byte2int(bytearray_org[i]); @@ -61,7 +113,7 @@ function prettyprint_rawdata(rawdatatype, bytearray, bytearray_org) { /******************************* take over the clicked byte position into posTextField *******************************/ -function cpRawDataPos(pos) { +export function cpRawDataPos(pos) { let posarray; const obj = document.getElementById('positions'); @@ -94,7 +146,7 @@ function byte2int(bytestring) { /******************************* compute result from selected positions *******************************/ -function check_rawdata() { +export function check_rawdata() { const datatype = document.querySelector('input[name="datatype"]:checked').value; const rawdatatype = document.querySelector('input[name="rawdatatype"]:checked').value; const string_positions = document.getElementById('positions').value; @@ -112,7 +164,7 @@ function check_rawdata() { if (datatype == 'int') { result = 0; } if (datatype == 'string') { result = "";} - for( j=0; j< pos.length; j++) { + for( var j=0; j< pos.length; j++) { if (datatype == 'int') { result = result << 8 | byte2int(bytes[Number(pos[j])]); } diff --git a/data/web/reboot.html b/data/web/reboot.html index bae87c4e..cc368db2 100644 --- a/data/web/reboot.html +++ b/data/web/reboot.html @@ -3,7 +3,7 @@ - + - + + + + + + status @@ -69,30 +78,34 @@ - - show logs via webserial -
+ + show current serial logs via web + + + Firmware Update -
+ + + Device Reboot -
+ + + Werkszustand herstellen (ohne WiFi) -
+ + + - - WiFi Zugangsdaten entfernen -
- @@ -113,5 +126,20 @@ + +
+

Are you sure you want to reset?

+ + +
+ +
+
+
+ Connection Status: + +
+ x +
\ No newline at end of file diff --git a/data/web/status.js b/data/web/status.js index e6a662ac..218769f2 100644 --- a/data/web/status.js +++ b/data/web/status.js @@ -1,31 +1,125 @@ +import * as global from './Javascript.js'; + // ************************************************ -window.addEventListener('DOMContentLoaded', init, false); -function init() { - GetInitData(); +export function init() { + // Initiale Verbindung aufbauen + global.connectWebSocket(); + + // Warte bis die WebSocket-Verbindung aufgebaut ist + let checkWebSocketInterval = setInterval(() => { + if (global.ws && global.ws.readyState === WebSocket.OPEN) { + clearInterval(checkWebSocketInterval); + GetInitData(); + } + }, 100); } -var myInterval = setInterval(RefreshLiveData, 5000); + +export function init1() { + + var data = { + "data": { + "ipaddress": "192.168.1.1", + "wifiname": "MyWiFi", + "macaddress": "00:1A:2B:3C:4D:5E", + "rssi": -50, + "bssid": "00:1A:2B:3C:4D:5F", + "mqtt_status": "Connected", + "inverter_type": "Solax-TypeA", + "inverter_serial": "SN123456789", + "uptime": "24h 15m", + "freeheapmem": 20480, + "tr_webserial": { + "className": "hide" + } + }, + "response": { + "status": 1, + "text": "successful" + } + , "cmd": { + "action": "GetInitData", + "subaction": "status" + ,"callbackFn": "status_Callback" + //, "highlight": "true" + } + }; + + global.handleJsonItems(data); +} + +export const functionMap = { + status_Callback: MyCallback, + status_CallRebootPage: CallRebootPage +}; // ************************************************ function GetInitData() { var data = {}; - data['action'] = "GetInitData"; - data['subaction'] = "status"; - requestData(JSON.stringify(data), false, MyCallback); + data['cmd'] = {}; + data['cmd']['action'] = "GetInitData"; + data['cmd']['subaction'] = "status"; + data['cmd']['callbackFn'] = "status_Callback"; + + global.requestData(data); +} + +// ************************************************ +function MyCallback(json) { + + fetch('/getitems') + .then(response => response.json()) + .then(data => { + global.handleJsonItems(ReduceJsonOnlyActiveItems(data)); + }) + .catch(error => console.error('Error fetching items:', error)); + + RefreshLiveData(); + document.querySelector("#loader").style.visibility = "hidden"; + document.querySelector("body").style.visibility = "visible"; +} + +// ************************************************ +function ReduceJsonOnlyActiveItems(json) { + if (json.data && json.data.items) { + json.data.items = json.data.items.filter(item => item.active && item.active.checked !== 0); + } + return json; } // ************************************************ function RefreshLiveData() { var data = {}; - data.action = "RefreshLiveData"; - data.subaction = "onlyactive"; - requestData(JSON.stringify(data), true); + data.cmd = {}; + data.cmd.action = "subscribe"; + data.cmd.subaction = "modbus_data"; + data.cmd.highlight = "true"; + data.cmd.opts = ["+unit", "onlyactive"]; + + global.requestData(data); } // ************************************************ -function MyCallback() { - document.querySelector("#loader").style.visibility = "hidden"; - document.querySelector("body").style.visibility = "visible"; +export function DoReboot() { + var data = {}; + data['cmd'] = {}; + data['cmd']['action'] = "reboot"; + data['cmd']['callbackFn'] = "status_CallRebootPage"; + global.requestData(data); } -// ************************************************ \ No newline at end of file +// ************************************************ +export function DoReset() { + var data = {}; + data['cmd'] = {}; + data['cmd']['action'] = "reset"; + data['cmd']['callbackFn'] = "status_CallRebootPage"; + global.requestData(data); +} + +// ************************************************ +export function CallRebootPage(json) { + window.location.href = "reboot.html"; +} + +// ************************************************ diff --git a/data/web/webserial_frame.html b/data/web/webserial_frame.html new file mode 100644 index 00000000..8e836282 --- /dev/null +++ b/data/web/webserial_frame.html @@ -0,0 +1,29 @@ + + + + + WebSerial Log Monitor + + + + + + + \ No newline at end of file diff --git a/docs/ModbusProtocolReferences/solax/Solax-Hybrid X1&X3-G4 Modbus RTU V3.47-English_250115.pdf b/docs/ModbusProtocolReferences/solax/Solax-Hybrid X1&X3-G4 Modbus RTU V3.47-English_250115.pdf new file mode 100644 index 00000000..dcf8cf1d Binary files /dev/null and b/docs/ModbusProtocolReferences/solax/Solax-Hybrid X1&X3-G4 Modbus RTU V3.47-English_250115.pdf differ diff --git a/esp_files/esp32-c3-devkitm-1/data/web/gpio.js b/esp_files/esp32-c3-devkitm-1/data/web/gpio.js new file mode 100644 index 00000000..45aa135e --- /dev/null +++ b/esp_files/esp32-c3-devkitm-1/data/web/gpio.js @@ -0,0 +1,22 @@ +gpio = [ {port: 0, name:'D0'}, + {port: 1, name:'D1'}, + {port: 2, name:'D2'}, + {port: 3, name:'D3'}, + {port: 4, name:'D4/SCK'}, + {port: 5, name:'D5/MISO'}, + {port: 6, name:'D6/MOSI'}, + {port: 7, name:'D7/SS'}, + {port: 8, name:'D8/SDA'}, + {port: 9, name:'D9/SCL'}, + {port: 10, name:'D10'}, + {port: 20, name:'D20/RX'}, + {port: 21, name:'D21/TX'} + ]; + +gpioanalog = [ {port: 0, name:'ADC0 - GPIO0'}, + {port: 1, name:'ADC1 - GPIO1'}, + {port: 2, name:'ADC2 - GPIO2'}, + {port: 3, name:'ADC3 - GPIO3'}, + {port: 4, name:'ADC4 - GPIO4'}, + {port: 5, name:'ADC5 - GPIO5'} + ]; diff --git a/esp_files/esp32-c3-devkitm-1/include/board.h b/esp_files/esp32-c3-devkitm-1/include/board.h new file mode 100644 index 00000000..e5ccfaf8 --- /dev/null +++ b/esp_files/esp32-c3-devkitm-1/include/board.h @@ -0,0 +1,9 @@ +#define DEFAULT_SERIAL_RX_PIN 20 +#define DEFAULT_SERIAL_TX_PIN 21 + +#define DEFAULT_MODBUS_RX_PIN 6 +#define DEFAULT_MODBUS_TX_PIN 7 + +// set TX Power only for ESP32-C3 to 8.5dBm to avoid WIFI issues +// https://forum.arduino.cc/t/no-wifi-connect-with-esp32-c3-super-mini/1324046/13 +#define WIFI_TX_POWER WIFI_POWER_8_5dBm \ No newline at end of file diff --git a/esp_files/esp32-s2-saola-1/data/web/gpio.js b/esp_files/esp32-s2-saola-1/data/web/gpio.js new file mode 100644 index 00000000..aaaf2713 --- /dev/null +++ b/esp_files/esp32-s2-saola-1/data/web/gpio.js @@ -0,0 +1,38 @@ +gpio = [ {port: 1, name:'D1/TX0'}, + {port: 3, name:'D3/RX0'}, + {port: 4, name:'D4'}, + {port: 5, name:'D5'}, + {port: 13, name:'D13'}, + {port: 16, name:'D16/RX2'}, + {port: 17, name:'D17/TX2'}, + {port: 18, name:'D18'}, + {port: 19, name:'D19'}, + {port: 21, name:'D21/SDA'}, + {port: 22, name:'D22/SCL'}, + {port: 23, name:'D23'}, + {port: 25, name:'D25'}, + {port: 26, name:'D26'}, + {port: 27, name:'D27'}, + {port: 32, name:'D32'}, + {port: 33, name:'D33'}, + ]; + +gpioanalog = [ {port: 36, name:'ADC1_CH0 - GPIO36'}, + {port: 37, name:'ADC1_CH1 - GPIO37'}, + {port: 38, name:'ADC1_CH2 - GPIO38'}, + {port: 39, name:'ADC1_CH3 - GPIO39'}, + {port: 32, name:'ADC1_CH4 - GPIO32'}, + {port: 33, name:'ADC1_CH5 - GPIO33'}, + {port: 34, name:'ADC1_CH6 - GPIO34'}, + {port: 35, name:'ADC1_CH7 - GPIO35'}, + {port: 4, name:'ADC2_CH0 - GPIO4'}, + {port: 0, name:'ADC2_CH1 - GPIO0'}, + {port: 2, name:'ADC2_CH2 - GPIO2'}, + {port: 15, name:'ADC2_CH3 - GPIO15'}, + {port: 13, name:'ADC2_CH4 - GPIO13'}, + {port: 12, name:'ADC2_CH5 - GPIO12'}, + {port: 14, name:'ADC2_CH6 - GPIO14'}, + {port: 27, name:'ADC2_CH7 - GPIO27'}, + {port: 25, name:'ADC2_CH8 - GPIO25'}, + {port: 26, name:'ADC2_CH9 - GPIO26'} + ]; diff --git a/esp_files/esp32-s2-saola-1/include/board.h b/esp_files/esp32-s2-saola-1/include/board.h new file mode 100644 index 00000000..f629a391 --- /dev/null +++ b/esp_files/esp32-s2-saola-1/include/board.h @@ -0,0 +1,5 @@ +#define DEFAULT_SERIAL_RX_PIN 3 +#define DEFAULT_SERIAL_TX_PIN 1 + +#define DEFAULT_MODBUS_RX_PIN 16 +#define DEFAULT_MODBUS_TX_PIN 17 \ No newline at end of file diff --git a/esp_files/esp32-s3-devkitc-1/data/web/gpio.js b/esp_files/esp32-s3-devkitc-1/data/web/gpio.js new file mode 100644 index 00000000..aaaf2713 --- /dev/null +++ b/esp_files/esp32-s3-devkitc-1/data/web/gpio.js @@ -0,0 +1,38 @@ +gpio = [ {port: 1, name:'D1/TX0'}, + {port: 3, name:'D3/RX0'}, + {port: 4, name:'D4'}, + {port: 5, name:'D5'}, + {port: 13, name:'D13'}, + {port: 16, name:'D16/RX2'}, + {port: 17, name:'D17/TX2'}, + {port: 18, name:'D18'}, + {port: 19, name:'D19'}, + {port: 21, name:'D21/SDA'}, + {port: 22, name:'D22/SCL'}, + {port: 23, name:'D23'}, + {port: 25, name:'D25'}, + {port: 26, name:'D26'}, + {port: 27, name:'D27'}, + {port: 32, name:'D32'}, + {port: 33, name:'D33'}, + ]; + +gpioanalog = [ {port: 36, name:'ADC1_CH0 - GPIO36'}, + {port: 37, name:'ADC1_CH1 - GPIO37'}, + {port: 38, name:'ADC1_CH2 - GPIO38'}, + {port: 39, name:'ADC1_CH3 - GPIO39'}, + {port: 32, name:'ADC1_CH4 - GPIO32'}, + {port: 33, name:'ADC1_CH5 - GPIO33'}, + {port: 34, name:'ADC1_CH6 - GPIO34'}, + {port: 35, name:'ADC1_CH7 - GPIO35'}, + {port: 4, name:'ADC2_CH0 - GPIO4'}, + {port: 0, name:'ADC2_CH1 - GPIO0'}, + {port: 2, name:'ADC2_CH2 - GPIO2'}, + {port: 15, name:'ADC2_CH3 - GPIO15'}, + {port: 13, name:'ADC2_CH4 - GPIO13'}, + {port: 12, name:'ADC2_CH5 - GPIO12'}, + {port: 14, name:'ADC2_CH6 - GPIO14'}, + {port: 27, name:'ADC2_CH7 - GPIO27'}, + {port: 25, name:'ADC2_CH8 - GPIO25'}, + {port: 26, name:'ADC2_CH9 - GPIO26'} + ]; diff --git a/esp_files/esp32-s3-devkitc-1/include/board.h b/esp_files/esp32-s3-devkitc-1/include/board.h new file mode 100644 index 00000000..f629a391 --- /dev/null +++ b/esp_files/esp32-s3-devkitc-1/include/board.h @@ -0,0 +1,5 @@ +#define DEFAULT_SERIAL_RX_PIN 3 +#define DEFAULT_SERIAL_TX_PIN 1 + +#define DEFAULT_MODBUS_RX_PIN 16 +#define DEFAULT_MODBUS_TX_PIN 17 \ No newline at end of file diff --git a/esp_files/esp32dev/data/web/gpio.js b/esp_files/esp32dev/data/web/gpio.js new file mode 100644 index 00000000..aaaf2713 --- /dev/null +++ b/esp_files/esp32dev/data/web/gpio.js @@ -0,0 +1,38 @@ +gpio = [ {port: 1, name:'D1/TX0'}, + {port: 3, name:'D3/RX0'}, + {port: 4, name:'D4'}, + {port: 5, name:'D5'}, + {port: 13, name:'D13'}, + {port: 16, name:'D16/RX2'}, + {port: 17, name:'D17/TX2'}, + {port: 18, name:'D18'}, + {port: 19, name:'D19'}, + {port: 21, name:'D21/SDA'}, + {port: 22, name:'D22/SCL'}, + {port: 23, name:'D23'}, + {port: 25, name:'D25'}, + {port: 26, name:'D26'}, + {port: 27, name:'D27'}, + {port: 32, name:'D32'}, + {port: 33, name:'D33'}, + ]; + +gpioanalog = [ {port: 36, name:'ADC1_CH0 - GPIO36'}, + {port: 37, name:'ADC1_CH1 - GPIO37'}, + {port: 38, name:'ADC1_CH2 - GPIO38'}, + {port: 39, name:'ADC1_CH3 - GPIO39'}, + {port: 32, name:'ADC1_CH4 - GPIO32'}, + {port: 33, name:'ADC1_CH5 - GPIO33'}, + {port: 34, name:'ADC1_CH6 - GPIO34'}, + {port: 35, name:'ADC1_CH7 - GPIO35'}, + {port: 4, name:'ADC2_CH0 - GPIO4'}, + {port: 0, name:'ADC2_CH1 - GPIO0'}, + {port: 2, name:'ADC2_CH2 - GPIO2'}, + {port: 15, name:'ADC2_CH3 - GPIO15'}, + {port: 13, name:'ADC2_CH4 - GPIO13'}, + {port: 12, name:'ADC2_CH5 - GPIO12'}, + {port: 14, name:'ADC2_CH6 - GPIO14'}, + {port: 27, name:'ADC2_CH7 - GPIO27'}, + {port: 25, name:'ADC2_CH8 - GPIO25'}, + {port: 26, name:'ADC2_CH9 - GPIO26'} + ]; diff --git a/esp_files/esp32dev/include/board.h b/esp_files/esp32dev/include/board.h new file mode 100644 index 00000000..f629a391 --- /dev/null +++ b/esp_files/esp32dev/include/board.h @@ -0,0 +1,5 @@ +#define DEFAULT_SERIAL_RX_PIN 3 +#define DEFAULT_SERIAL_TX_PIN 1 + +#define DEFAULT_MODBUS_RX_PIN 16 +#define DEFAULT_MODBUS_TX_PIN 17 \ No newline at end of file diff --git a/include/_Release.h b/include/_Release.h index d6f5edc4..fde85e99 100644 --- a/include/_Release.h +++ b/include/_Release.h @@ -1 +1 @@ -#define Release "3.3.1" +#define Release "3.4.3" diff --git a/include/helper.h b/include/helper.h new file mode 100644 index 00000000..02a689d5 --- /dev/null +++ b/include/helper.h @@ -0,0 +1,31 @@ +#include +#include + +// --------------------------------------------------------------------------- +// vector helper: push_back_unique +// Adds value to vector only if it does not already exist (using operator==) +// Returns true if inserted, false if value already present. +// Usage: +// std::vector v; +// push_back_unique(v, 42); // free function +// --------------------------------------------------------------------------- +template +inline bool push_back_unique(std::vector& vec, const T& value) { + if (std::find(vec.begin(), vec.end(), value) == vec.end()) { + vec.push_back(value); + return true; + } + return false; +} + +// Pointer overload: pass a pointer to a std::vector. Returns false +// if pointer is null or value already exists; true if inserted. +// Usage: +// std::vector* vp = &v; +// push_back_unique(vp, 7); +template +inline bool push_back_unique(std::vector* vecPtr, const T& value) { + if (!vecPtr) return false; // null: nothing to do + // Forward to reference overload to avoid code duplication + return push_back_unique(*vecPtr, value); +} \ No newline at end of file diff --git a/include/vectorlist.h b/include/vectorlist.h new file mode 100644 index 00000000..6aa1786d --- /dev/null +++ b/include/vectorlist.h @@ -0,0 +1,195 @@ +#ifndef VECTORLIST_H +#define VECTORLIST_H + +/* + vectorlist.h + Extended template container that stores pairs of (value, identifier) for arbitrary types. + + API (T = value type, I = identifier type): + - setOffset(uint8_t off) // sets a runtime offset that is applied to all numeric values in getArray* calls + - uint8_t getOffset() const // returns the currently set offset + - bool addValue(const T& value, const I& id) + - bool deleteValue(const T& value) // first match across all identifiers + - bool deleteValue(const T& value, const I& id) // match with specific identifier + - bool isInList(const T& value) const + - bool isInList(const T& value, const I& id) const + - bool deleteAll() // clears all entries + - bool deleteAll(const I& id) // clears only entries having this identifier + - int size() const // total entries + - int size(const I& id) const // entries with specific identifier + - String getArray() const // all values + - String getArrayForIdentifier(const I& id) const // only values with matching identifier + - String getArrayExcludeIdentifier(const I& id) const // all except those with identifier + - String getArray(const std::vector& excludeValues) const // exclude certain values (global) + - String getArray(std::initializer_list excludeValues) const + + Zusätzlich gewünschte Semantik: + * Wird bei Prüf-/Löschfunktionen kein Identifier angegeben wird global gesucht. + * Duplicate Policy: (value,id) Kombination muss eindeutig sein. Gleiches value mit anderem id ist erlaubt. + * bei Angabe eines Offsets wird dieser bei allen getArray* Aufrufen zu allen numerischen Werten addiert + + String Handling: + If T is Arduino String the element is quoted and internal double quotes are escaped so JS can parse it. +*/ + +#include +#include +#include +#include + +// Helper formatter: generic version +template +struct VectorListValueFormatter { + static String toString(const U& v) { return String(v); } +}; + +// Specialization for Arduino String +template +struct VectorListValueFormatter::value>::type> { + static String toString(const String& v) { + String esc = v; // escape double quotes + esc.replace("\"", "\\\""); + return String("\"") + esc + String("\""); + } +}; + +template +class vectorlist { +public: + struct Entry { T value; I id; }; + + // Set a runtime offset that is applied to values for all getArray* calls. + // The offset is only added for numeric types (integral except bool, or floating point types). + void setOffset(uint8_t off) { _offset = off; } + uint8_t getOffset() const { return _offset; } + + // Insert (value,id) if unique; duplicates allowed across different id values. + bool addValue(const T& value, const I& id) { + if (isInList(value, id)) { return false; } + _data.push_back({value, id}); + return true; + } + + // Bulk insert: add multiple values with the same identifier. + // Returns true if at least one new (value,id) pair was inserted. + bool addValues(const std::vector& values, const I& id) { + bool anyInserted = false; + if (!values.empty()) { _data.reserve(_data.size() + values.size()); } + for (const auto& v : values) { + if (!isInList(v, id)) { _data.push_back({v, id}); anyInserted = true; } + } + return anyInserted; + } + + // Convenience overload for initializer_list + bool addValues(std::initializer_list values, const I& id) { + return addValues(std::vector(values), id); + } + + // Delete first occurrence of value (across all identifiers) + bool deleteValue(const T& value) { + for (auto it = _data.begin(); it != _data.end(); ++it) { + if (it->value == value) { _data.erase(it); return true; } + } + return false; + } + + // Delete specific (value,id) + bool deleteValue(const T& value, const I& id) { + for (auto it = _data.begin(); it != _data.end(); ++it) { + if (it->value == value && it->id == id) { _data.erase(it); return true; } + } + return false; + } + + // Clear everything + bool deleteAll() { _data.clear(); return true; } + + // Clear only entries with identifier + bool deleteAll(const I& id) { + bool removed = false; + for (auto it = _data.begin(); it != _data.end();) { + if (it->id == id) { it = _data.erase(it); removed = true; } else { ++it; } + } + return removed; + } + + bool isInList(const T& value) const { + for (const auto& e : _data) { if (e.value == value) return true; } + return false; + } + bool isInList(const T& value, const I& id) const { + for (const auto& e : _data) { if (e.value == value && e.id == id) return true; } + return false; + } + + int size() const { return static_cast(_data.size()); } + int size(const I& id) const { + int c=0; for (const auto& e : _data) if (e.id == id) c++; return c; } + + // Build JS array from ALL values + String getArray() const { return buildArray(nullptr, nullptr, nullptr); } + + // Only values with given identifier + String getArrayForIdentifier(const I& id) const { return buildArray(&id, nullptr, nullptr); } + + // All values except those with given identifier + String getArrayExcludeIdentifier(const I& id) const { return buildArray(nullptr, &id, nullptr); } + + // Exclude given value list globally + String getArray(const std::vector& excludeValues) const { return buildArray(nullptr, nullptr, &excludeValues); } + + String getArray(std::initializer_list excludeValues) const { + std::vector tmp(excludeValues); + return getArray(tmp); + } + + // Raw value vectors (without identifiers) + std::vector raw() const { std::vector v; v.reserve(_data.size()); for (auto& e : _data) v.push_back(e.value); return v; } + std::vector raw(const I& id) const { std::vector v; for (auto& e : _data) if (e.id==id) v.push_back(e.value); return v; } + + // Direct access to entries if really needed + const std::vector& rawEntries() const { return _data; } + +private: + std::vector _data; + uint8_t _offset = 0; // runtime offset applied to numeric values in array output + + // Helper: determine at compile time if T supports addition with uint8_t safely (numeric, not bool) + template + struct CanAddOffset : std::integral_constant::value && !std::is_same::value) || std::is_floating_point::value )> {}; + + // Apply offset only if allowed + template + static typename std::enable_if::value, U>::type applyOffset(const U& v, uint8_t off) { + return static_cast(v + static_cast(off)); + } + template + static typename std::enable_if::value, U>::type applyOffset(const U& v, uint8_t) { + return v; // no change + } + + // Internal builder with optional filters: + // onlyId != nullptr -> include only entries with *onlyId + // excludeId != nullptr -> exclude entries with *excludeId + // excludeValues != nullptr -> exclude entries whose value is in *excludeValues + String buildArray(const I* onlyId, const I* excludeId, const std::vector* excludeValues) const { + String out("["); + bool first = true; + for (const auto& e : _data) { + if (onlyId && e.id != *onlyId) continue; + if (excludeId && e.id == *excludeId) continue; + if (excludeValues && std::find(excludeValues->begin(), excludeValues->end(), e.value) != excludeValues->end()) continue; + if (!first) out += ","; else first = false; + // Apply offset for eligible numeric types before formatting + auto adjusted = applyOffset(e.value, _offset); + out += VectorListValueFormatter::toString(adjusted); + } + out += "]"; + return out; + } + +}; + +#endif // VECTORLIST_H diff --git a/partitions.csv b/partitions.csv index 30f41a55..33a56b53 100644 --- a/partitions.csv +++ b/partitions.csv @@ -2,6 +2,7 @@ nvs, data, nvs, 0x9000, 0x5000, otadata, data, ota, 0xe000, 0x2000, app0, app, ota_0, 0x10000, 0x1A0000, -app1, app, ota_1, , 0x1A0000, -spiffs, data, spiffs, , 0x0A0000, +app1, app, ota_1, 0x1B0000, 0x1A0000, +config, data, spiffs, 0x350000, 0x10000, +webdata, data, spiffs, 0x360000, 0x90000, coredump, data, coredump,0x3F0000, 0x10000, \ No newline at end of file diff --git a/platformio.ini b/platformio.ini index 6125aa89..a7f34f97 100644 --- a/platformio.ini +++ b/platformio.ini @@ -25,8 +25,6 @@ build_flags = -D GITHUB_RUN=\"${sysenv.GITHUB_RUN}\" ; -D WIFISSID=\"gast\" ; use fixed WiFi credentials if Improv-WiFi-Library isn´t supported by your board ; -D WIFIPASSWORD=\"12345678\" ; use fixed WiFi credentials if Improv-WiFi-Library isn´t supported by your board -custom_build_flags_webserial = - -D USE_WEBSERIAL=1 -Wall -Wextra -D CONFIG_ARDUHAL_LOG_COLORS -D CORE_DEBUG_LEVEL=ARDUHAL_LOG_LEVEL_DEBUG @@ -42,19 +40,11 @@ lib_deps = https://github.com/YiannisBourkelis/Uptime-Library.git https://github.com/tobiasfaust/Improv-WiFi-Library.git https://github.com/tobiasfaust/ElegantOTA.git - ;https://github.com/mathieucarbou/AsyncTCP - ;https://github.com/mathieucarbou/ESPAsyncWebServer // installing by ElegantOTA - -custom_lib_webserial = - https://github.com/ayushsharma82/WebSerial.git - - -[env:firmware_ESP32-WebSerial] -board = esp32dev -build_flags = ${env.build_flags} - ${env.custom_build_flags_webserial} -lib_deps = ${env.lib_deps} - ${env.custom_lib_webserial} + https://github.com/tobiasfaust/esp-handlefiles.git + https://github.com/tobiasfaust/webserial.git + https://github.com/ESP32Async/ESPAsyncWebServer#v3.6.0 +extra_scripts = + pre:scripts/prepareDataDir.py [env:firmware_ESP32] board = esp32dev @@ -67,3 +57,6 @@ board = esp32-s3-devkitc-1 [env:firmware_ESP32-C3] board = esp32-c3-devkitm-1 +build_flags = ${env.build_flags} + -D ARDUINO_USB_MODE=1 + -D ARDUINO_USB_CDC_ON_BOOT=1 diff --git a/python.env b/python.env new file mode 100644 index 00000000..64772c40 --- /dev/null +++ b/python.env @@ -0,0 +1 @@ +HTML_DIR=data/web # spezifisches unterverzeichnis für HTML Dateien, gebraucht fuer handlefiles script diff --git a/scripts/prepareDataDir.py b/scripts/prepareDataDir.py new file mode 100644 index 00000000..92b71a74 --- /dev/null +++ b/scripts/prepareDataDir.py @@ -0,0 +1,34 @@ +Import("env"); +""" +PlatformIO build script that prepares the data directory by copying board-specific files. +This script is executed during the PlatformIO build process and performs the following: +- Checks if a board-specific directory exists in 'esp_files/{BOARD_NAME}/' +- If it exists: Copies all files from the board-specific directory to the project root, + overwriting existing files (dirs_exist_ok=True allows overwriting) +- If it doesn't exist: Prints an error message +Args: + env: PlatformIO environment object containing build configuration (e.g., env["BOARD"]) +copytree() function: + - Recursively copies an entire directory tree from source to destination + - In this case: copies from "esp_files/{BOARD}/" to project root (".") +dirs_exist_ok parameter (dirs_exist_ok=True): + - Allows the destination directory to already exist without raising an error + - Enables overwriting of existing files in the destination + - Without this parameter, copytree() would fail if the target directory already exists +""" +import sys, os, re; +from shutil import copytree; + +# Print environment variables for debugging +#print("Environment dump:") +#for key, value in env.items(): +# print(f" {key}: {value}") +#print() + + +data_master_dir = "esp_files"; +if (os.path.exists(data_master_dir +"/"+ env["BOARD"])): + copytree(data_master_dir +"/"+ env["BOARD"] + "/" , ".", dirs_exist_ok=True); + print("copy board specific files from:<" + data_master_dir +"/"+ env["BOARD"] + "> to "); +else: + print("path not exists: " + data_master_dir +"/"+ env["BOARD"] + "/"); \ No newline at end of file diff --git a/src/MyWebServer.cpp b/src/MyWebServer.cpp index 6ee3090d..10b03821 100644 --- a/src/MyWebServer.cpp +++ b/src/MyWebServer.cpp @@ -1,54 +1,202 @@ +/******************************************************** + * Copyright [2024] Tobias Faust registerLogCallback(std::bind(&BaseConfig::logN, Config, std::placeholders::_1, std::placeholders::_2)); + fsfiles->registerLittleFS(&sysFS, "/web"); + fsfiles->registerLittleFS(&configFS, "/config"); + + _wsclientRequests = new std::vector(); + + ws = new AsyncWebSocket("/ajaxws"); server->onNotFound(std::bind(&MyWebServer::handleNotFound, this, std::placeholders::_1)); - server->on("/", HTTP_GET, std::bind(&MyWebServer::handleRoot, this, std::placeholders::_1)); + server->on("/", HTTP_GET, std::bind(&MyWebServer::handleRoot, this, std::placeholders::_1), nullptr, nullptr); - server->on("/favicon.ico", HTTP_GET, std::bind(&MyWebServer::handleFavIcon, this, std::placeholders::_1)); - server->on("/reboot", HTTP_GET, std::bind(&MyWebServer::handleReboot, this, std::placeholders::_1)); - server->on("/reset", HTTP_GET, std::bind(&MyWebServer::handleReset, this, std::placeholders::_1)); - server->on("/wifireset", HTTP_GET, std::bind(&MyWebServer::handleWiFiReset, this, std::placeholders::_1)); + server->on("/favicon.ico", HTTP_GET, std::bind(&MyWebServer::handleFavIcon, this, std::placeholders::_1), nullptr, nullptr); + server->on("/getitems", HTTP_GET, [&](AsyncWebServerRequest *request){ mb->GetLiveDataAsJsonToWebServer(request); }); + server->on("/getsetter", HTTP_GET, [&](AsyncWebServerRequest *request){ mb->GetSettersAsJsonToWebServer(request); }); + - server->on("/ajax", HTTP_POST, std::bind(&MyWebServer::handleAjax, this, std::placeholders::_1)); - server->on("/getitems", HTTP_GET, std::bind(&MyWebServer::handleGetItemJson, this, std::placeholders::_1)); - server->on("/getregister", HTTP_GET, std::bind(&MyWebServer::handleGetRegisterJson, this, std::placeholders::_1)); + ws->onEvent(std::bind(&MyWebServer::onWsEvent, this, std::placeholders::_1, + std::placeholders::_2, + std::placeholders::_3, + std::placeholders::_4, + std::placeholders::_5, + std::placeholders::_6 )); + server->addHandler(ws); + ElegantOTA.begin(server); // Start ElegantOTA - ElegantOTA.setGitEnv(String(GIT_OWNER), String(GIT_REPO), String(GIT_BRANCH)); + ElegantOTA.setGitEnv(String(GIT_OWNER), String(GIT_REPO), String(GIT_BRANCH), String(GITHUB_RUN).toInt()); ElegantOTA.setFWVersion(String(Config->GetReleaseName() + " / Build: " + GITHUB_RUN )); - ElegantOTA.setBackupRestoreFS("/config"); + ElegantOTA.setTargetPartition("webdata"); // Set default partition for OTA updates ElegantOTA.setAutoReboot(true); - // ElegantOTA callbacks + //ElegantOTA callbacks //ElegantOTA.onStart(onOTAStart); //ElegantOTA.onProgress(onOTAProgress); //ElegantOTA.onEnd(std::bind(&MyWebServer::onOTAEnd, this, std::placeholders::_1)); if (Config->GetUseAuth()) { - server->serveStatic("/", LittleFS, "/", "max-age=3600") - .setDefaultFile("/web/index.html") + server->serveStatic("/web/", sysFS, "/", "max-age=3600") + .setDefaultFile("/web/web/index.html") .setAuthentication(Config->GetAuthUser().c_str(), Config->GetAuthPass().c_str()); } else { - server->serveStatic("/", LittleFS, "/", "max-age=3600") - .setDefaultFile("/web/index.html"); + server->serveStatic("/web/", sysFS, "/", "max-age=3600") + .setDefaultFile("/web/web/index.html"); } + + server->serveStatic("/config/", configFS, "/"); // try to start the server if wifi is connected, otherwise wait for wifi connection if (mqtt->GetConnectStatusWifi()) { server->begin(); - Config->log(1, "WebServer has been started ..."); + Config->logN(1, "WebServer has been started ..."); } else { mqtt->improvSerial.onImprovConnected(std::bind(&MyWebServer::onImprovWiFiConnectedCb, this, std::placeholders::_1, std::placeholders::_2)); } } +void MyWebServer::onWsEvent(AsyncWebSocket * server, AsyncWebSocketClient * client, AwsEventType type, void * arg, uint8_t *data, size_t len) { + if (type == WS_EVT_CONNECT) { + Config->logN(2, "[Client: %u] WebSocket client connected", client->id()); + + } else if (type == WS_EVT_DISCONNECT) { + Config->logN(2, "[Client: %u] WebSocket client disconnected", client->id()); + + // Remove client from WebSocket client requests if it exists + for (uint8_t i = 0; i < _wsclientRequests->size(); i++) { + if (_wsclientRequests->at(i).ws_id == client->id()) { + + if (_wsclientRequests->at(i).requestData == wsclient_t::MODBUS_DATA) { mb->onValues(nullptr, nullptr); } + if (_wsclientRequests->at(i).requestData == wsclient_t::LOG_DATA) { Config->onLogValues(nullptr); } + + _wsclientRequests->erase(_wsclientRequests->begin() + i); + } + } + _wsclientRequests->shrink_to_fit(); + + } else if (type == WS_EVT_DATA) { + String msg(""); msg.reserve(len + 1); + for (size_t i = 0; i < len; i++) { msg += (char)data[i]; } msg += '\0'; + Config->logN(2, "[Client: %u] WebSocket data received: %s", client->id(), msg.c_str()); + + // message json request format: {"cmd": {"action": "GetInitData", "subaction": "status"}} // subaction optional + // message json response format: die Antwort wird im json ergänzt, so weiß der Requestor zu welchem Command die Antwort gehört: + // Example: {"cmd": {"action": "GetInitData", "subaction": "status"}, "response": {"status": 1, "text": "successful"}, "data": {"ipaddress": "", "wifiname": "", "macaddress": "", "rssi": "", "bssid": "", "mqtt_status": "", "inverter_type": "", "inverter_serial": "", "uptime": "", "freeheapmem": ""}} + // Ausnahme: kontinuierliches Streaming der modbuswerte, hier wird kein response und nicht das ursprüngliche Command zurückgegeben + // example: {"data-id":{ "registername": "value", "registername": "value", ...}} + + String action(""), subaction(""), item(""); + std::list* options = nullptr; + bool newState = false; + JsonDocument json; + DeserializationError error = deserializeJson(json, msg.c_str()); + if (!error) { + if (json["cmd"]) { + if (json["cmd"]["action"]) {action = json["cmd"]["action"].as();} + if (json["cmd"]["subaction"]){subaction = json["cmd"]["subaction"].as();} + if (json["cmd"]["item"]) {item = json["cmd"]["item"].as();} + if (json["cmd"]["newState"]) {newState = (json["cmd"]["newState"].as() == "true"?true:false);} + + if (json["cmd"]["opts"]) { + // prüfe ob optionen übergeben wurden, validiere format [string1, string2, ...] und überführe die opts in -> std::list* options + JsonArray optsArray = json["cmd"]["opts"].as(); + options = new std::list(); + for (String opt : optsArray) { + opt.toLowerCase(); + options->push_back(opt); + } + } + } + + if (action && action == "subscribe") { + if (subaction && subaction == "log_data") { + push_back_unique(_wsclientRequests, {client->id(), wsclient_t::LOG_DATA}); + Config->onLogValues(std::bind(&MyWebServer::logGetValuesCallback, this, std::placeholders::_1, json, client->id())); + } else if (subaction && subaction == "modbus_data") { + push_back_unique(_wsclientRequests, {client->id(), wsclient_t::MODBUS_DATA}); + mb->onValues(std::bind(&MyWebServer::sendWebSocketMessage, this, std::placeholders::_1, msg, client->id()), options); + } + } + + if (action && action == "reset") { + if (handleReset()) { + json["response"]["status"] = 1; + json["response"]["text"] = "all config files deleted successfully"; + } else { + json["response"]["status"] = 0; + json["response"]["text"] = "deletion of config files failed"; + } + } + + if(action && action == "reboot") { + this->DoReboot = true; + json["response"]["status"] = 1; + json["response"]["text"] = "reboot after 5sec..."; + } + + if(action && action == "GetInitData") { + if (subaction && subaction == "status") { + this->GetInitDataStatus(json); + } else if (subaction && subaction == "navi") { + this->GetInitDataNavi(json); + } else if (subaction && subaction == "baseconfig") { + Config->GetInitData(json); + json["js"]["gpio_disabled"] = Config->disabledGPIO.getArrayExcludeIdentifier(BaseConfig::GpioIdentifier::BASECONFIG); + } else if (subaction && subaction == "modbusconfig") { + mb->GetInitData(json); + json["js"]["gpio_disabled"] = Config->disabledGPIO.getArrayExcludeIdentifier(BaseConfig::GpioIdentifier::MODBUS); + } else if (subaction && subaction == "rawdata") { + mb->GetInitRawData(json); + } + } + + if(action && action == "ReloadConfig") { + if (subaction && subaction == "baseconfig") { + Config->LoadJsonConfig(); + } else if (subaction && subaction == "modbusconfig") { + mb->LoadJsonConfig(false); + } else if (subaction && subaction == "modbusitemconfig") { + mb->LoadJsonItemConfig(); + } + + json["response"]["status"] = 1; + json["response"]["text"] = "new config reloaded sucessfully"; + } + + if (action && action == "SetActiveStatus") { + mb->SetItemActiveStatus(item, newState); + + json["response"]["status"] = 1; + json["response"]["text"] = String("item successfully set to " + String(newState ? "active" : "inactive")); + } + + if(action && action == "handlefiles") { + fsfiles->HandleRequest(json); + } + + } else { + Config->logN(1, "WebSocket data received but not a valid json string: %s -> %s", msg.c_str(), error.c_str()); + json["response"]["status"] = 0; + json["response"]["text"] = error.c_str(); + } + + ws->text(client->id(), json.as()); + + } +} void MyWebServer::onImprovWiFiConnectedCb(const char *ssid, const char *password) { server->begin(); - Config->log(1, "WebServer has been started now ..."); + Config->logN(1, "WebServer has been started now ..."); } void MyWebServer::loop() { @@ -56,14 +204,15 @@ void MyWebServer::loop() { if (this->DoReboot) { if (this->RequestRebootTime == 0) { this->RequestRebootTime = millis(); - Config->log(1, "Request to Reboot, wait 5sek ..."); + Config->logN(1, "Request to Reboot, wait 5sek ..."); } if (millis() - this->RequestRebootTime > 5000) { // wait 3sek until reboot - Config->log(1, "Rebooting..."); + Config->logN(1, "Rebooting..."); ESP.restart(); } } ElegantOTA.loop(); + ws->cleanupClients(); } void MyWebServer::handleNotFound(AsyncWebServerRequest *request) { @@ -71,7 +220,7 @@ void MyWebServer::handleNotFound(AsyncWebServerRequest *request) { } void MyWebServer::handleRoot(AsyncWebServerRequest *request) { - request->redirect("/web/index.html"); + request->redirect("/web/web/index.html"); } void MyWebServer::handleFavIcon(AsyncWebServerRequest *request) { @@ -80,165 +229,21 @@ void MyWebServer::handleFavIcon(AsyncWebServerRequest *request) { request->send(response); } -void MyWebServer::handleReboot(AsyncWebServerRequest *request) { - request->send(LittleFS, "/web/reboot.html", "text/html"); - this->DoReboot = true; -} - -void MyWebServer::handleReset(AsyncWebServerRequest *request) { - Config->log(3, "deletion of all config files was requested ...."); - //LittleFS.format(); // Werkszustand -> nur die config dateien loeschen, die register dateien muessen erhalten bleiben - File root = LittleFS.open("/config/"); - File file = root.openNextFile(); - while(file){ - String path("/config/"); path.concat(file.name()); - if (path.indexOf(".json") == -1) {file = root.openNextFile(); continue;} - file.close(); - bool f = LittleFS.remove(path); - Config->log(3, "deletion of configuration file '%s' %s", file.name(), (f?"was successful":"has failed")); - file = root.openNextFile(); - } - root.close(); - - this->handleReboot(request); -} - -void MyWebServer::handleWiFiReset(AsyncWebServerRequest *request) { - #ifdef ESP32 - WiFi.disconnect(true,true); - #elif defined(ESP8266) - ESP.eraseConfig(); - #endif - - this->handleReboot(request); -} - -void MyWebServer::handleGetItemJson(AsyncWebServerRequest *request) { - mb->GetLiveDataAsJson(request); -} - -void MyWebServer::handleGetRegisterJson(AsyncWebServerRequest *request) { - AsyncResponseStream *response = request->beginResponseStream("application/json"); - response->addHeader("Cache-Control", "no-cache, no-store, must-revalidate"); - response->addHeader("Pragma", "no-cache"); - response->addHeader("Expires", "-1"); - - mb->GetRegisterAsJson(response); - - request->send(response); -} - -void MyWebServer::handleAjax(AsyncWebServerRequest *request) { - char buffer[100] = {0}; - memset(buffer, 0, sizeof(buffer)); - String ret; - bool RaiseError = false; - String action, subaction, item, newState; - String json = "{}"; - - if(request->hasArg("json")) { - json = request->arg("json"); - } - - JsonDocument jsonGet; // TODO Use computed size?? - DeserializationError error = deserializeJson(jsonGet, json.c_str()); - - Config->log(4, "Ajax Json Empfangen: "); - if (!error) { - Config->log(4, jsonGet); - - if (jsonGet["action"]) {action = jsonGet["action"].as();} - if (jsonGet["subaction"]){subaction = jsonGet["subaction"].as();} - if (jsonGet["item"]) {item = jsonGet["item"].as();} - if (jsonGet["newState"]) {newState = jsonGet["newState"].as();} - - } else { - snprintf(buffer, sizeof(buffer), "Ajax Json Command not parseable: %s -> %s", json.c_str(), error.c_str()); - RaiseError = true; - } - - if (action && action == "RefreshLiveData") { - mb->GetLiveDataAsJson(request); - return; - } - - AsyncResponseStream *response = request->beginResponseStream("text/json"); - response->addHeader("Server","ESP Async Web Server"); - - JsonDocument jsonReturn; - jsonReturn["response"].to(); - - if (RaiseError) { - jsonReturn["response"]["status"] = 0; - jsonReturn["response"]["text"] = buffer; - serializeJson(jsonReturn, ret); - response->print(ret); - - Config->log(4, buffer); - - return; - - } else if(action && action == "GetInitData") { - if (subaction && subaction == "status") { - this->GetInitDataStatus(response); - } else if (subaction && subaction == "navi") { - this->GetInitDataNavi(response); - } else if (subaction && subaction == "baseconfig") { - Config->GetInitData(response); - } else if (subaction && subaction == "modbusconfig") { - mb->GetInitData(response); - } else if (subaction && subaction == "rawdata") { - mb->GetInitRawData(response); - } - - } else if(action && action == "ReloadConfig") { - if (subaction && subaction == "baseconfig") { - Config->LoadJsonConfig(); - } else if (subaction && subaction == "modbusconfig") { - mb->LoadJsonConfig(false); - } else if (subaction && subaction == "modbusitemconfig") { - mb->LoadJsonItemConfig(); - } - - jsonReturn["response"]["status"] = 1; - jsonReturn["response"]["text"] = "new config reloaded sucessfully"; - serializeJson(jsonReturn, ret); - response->print(ret); - - //} else if (action && action == "RefreshLiveData") { - //TODO - //mb->GetLiveDataAsJson(response, subaction); - - } else if (action && action == "SetActiveStatus") { - if (strcmp(newState.c_str(),"true")==0) mb->SetItemActiveStatus(item, true); - if (strcmp(newState.c_str(),"false")==0) mb->SetItemActiveStatus(item, false); - - jsonReturn["response"]["status"] = 1; - jsonReturn["response"]["text"] = "successful"; - serializeJson(jsonReturn, ret); - response->print(ret); - - } else if(action && action == "handlefiles") { - fsfiles->HandleAjaxRequest(jsonGet, response); +bool MyWebServer::handleReset() { + Config->logN(3, "deletion of all config files was requested ...."); + bool result = configFS.format(); + if (!result) { + Config->logN(2, "formatting of config Filesystem failed"); } else { - snprintf(buffer, sizeof(buffer), "Ajax Command unknown: %s - %s", action.c_str(), subaction.c_str()); - jsonReturn["response"]["status"] = 0; - jsonReturn["response"]["text"] = buffer; - serializeJson(jsonReturn, ret); - response->print(ret); - - Config->log(1, buffer); + Config->logN(4, "formatting of config Filesystem was successful"); } + this->DoReboot = true; - Config->log(4, "Ajax Json Antwort: ", ret); - - request->send(response); + return result; } -void MyWebServer::GetInitDataNavi(AsyncResponseStream *response){ - String ret; - JsonDocument json; +void MyWebServer::GetInitDataNavi(JsonDocument& json) { json["data"].to(); json["data"]["hostname"] = Config->GetMqttRoot(); json["data"]["releasename"] = Config->GetReleaseName(); @@ -248,13 +253,9 @@ void MyWebServer::GetInitDataNavi(AsyncResponseStream *response){ json["response"].to(); json["response"]["status"] = 1; json["response"]["text"] = "successful"; - serializeJson(json, ret); - response->print(ret); } -void MyWebServer::GetInitDataStatus(AsyncResponseStream *response) { - String ret; - JsonDocument json; +void MyWebServer::GetInitDataStatus(JsonDocument& json) { String rssi = (String)(Config->GetUseETH()?ETH.linkSpeed():WiFi.RSSI()); if (Config->GetUseETH()) rssi.concat(" Mbps"); @@ -270,14 +271,20 @@ void MyWebServer::GetInitDataStatus(AsyncResponseStream *response) { json["data"]["uptime"] = uptime_formatter::getUptime(); json["data"]["freeheapmem"] = ESP.getFreeHeap(); - #ifndef USE_WEBSERIAL - json["data"]["tr_webserial"]["className"] = "hide"; - #endif - json["response"].to(); json["response"]["status"] = 1; json["response"]["text"] = "successful"; +} + +void MyWebServer::logGetValuesCallback(const char* logline, JsonDocument& json, uint32_t wsclient_id) { + json["logline"] = logline; + this->ws->text(wsclient_id, json.as()); +} + +void MyWebServer::sendWebSocketMessage(String& message, String JsonRequest, uint32_t wsclient_id) { + // add original Request json to message + message = message.substring(0, message.length()-1) + "," + JsonRequest.substring(1, JsonRequest.length()-1); + Config->logN(4, "send WebSocket Message to client %u: %s", wsclient_id, message.c_str()); - serializeJson(json, ret); - response->print(ret); + this->ws->text(wsclient_id, message); } \ No newline at end of file diff --git a/src/MyWebServer.h b/src/MyWebServer.h index b297a18b..57c05e28 100644 --- a/src/MyWebServer.h +++ b/src/MyWebServer.h @@ -1,63 +1,66 @@ -// https://github.com/esp8266/Arduino/issues/3205 -// https://github.com/Hieromon/PageBuilder -// https://www.mediaevent.de/tutorial/sonderzeichen.html -// -// https://byte-style.de/2018/01/automatische-updates-fuer-microcontroller-mit-gitlab-und-platformio/ -// https://community.blynk.cc/t/self-updating-from-web-server-http-ota-firmware-for-esp8266-and-esp32/18544 -// https://forum.fhem.de/index.php?topic=50628.0 - -#ifndef MYWEBSERVER_H -#define MYWEBSERVER_H - -#include "commonlibs.h" +/******************************************************** + * Copyright [2024] Tobias Faust #include -#include "uptime.h" // https://github.com/YiannisBourkelis/Uptime-Library/ -#include "uptime_formatter.h" - -#include "baseconfig.h" -#include "modbus.h" -#include "handleFiles.h" -#include "mqtt.h" -#include "favicon.h" -//#include "html_update.h" +#include // https://github.com/YiannisBourkelis/Uptime-Library/ +#include + +#include +#include +#include +#include +#include #include -#include "_Release.h" +#include <_Release.h> class MyWebServer { - //enum page_t {ROOT, BASECONFIG, MODBUSCONFIG, MODBUSITEMCONFIG, MODBUSRAWDATA, FSFILES}; - - public: - MyWebServer(AsyncWebServer *server, DNSServer* dns); + struct wsclient_t { + uint32_t ws_id; + enum requestData_t {LOG_DATA, MODBUS_DATA} requestData; + // Equality operator needed for push_back_unique / std::find + bool operator==(const wsclient_t& other) const { + return ws_id == other.ws_id && requestData == other.requestData; + } + }; + + public: + MyWebServer(fs::LittleFSFS& sysFS, fs::LittleFSFS& configFS, AsyncWebServer *server, DNSServer* dns); void loop(); + void sendWebSocketMessage(String& message, String JsonRequest, uint32_t wsclient_id); + void logGetValuesCallback(const char* logline, JsonDocument& json, uint32_t wsclient_id); - private: + private: - bool DoReboot; - unsigned long RequestRebootTime; - + fs::LittleFSFS& sysFS; + fs::LittleFSFS& configFS; + bool DoReboot; + uint64_t RequestRebootTime; + + AsyncWebServer* server; DNSServer* dns; - + AsyncWebSocket* ws; handleFiles* fsfiles; -// void handle_update_page(AsyncWebServerRequest *request); -// void handle_update_progress(AsyncWebServerRequest *request, String filename, size_t index, uint8_t *data, size_t len, bool final); -// void handle_update_response(AsyncWebServerRequest *request); + std::vector* _wsclientRequests = nullptr; + void handleNotFound(AsyncWebServerRequest *request); - void handleReboot(AsyncWebServerRequest *request); - void handleReset(AsyncWebServerRequest *request); - void handleWiFiReset(AsyncWebServerRequest *request); + bool handleReset(); void handleRoot(AsyncWebServerRequest *request); void handleFavIcon(AsyncWebServerRequest *request); - void handleAjax(AsyncWebServerRequest *request); - void handleGetItemJson(AsyncWebServerRequest *request); - void handleGetRegisterJson(AsyncWebServerRequest *request); - void GetInitDataStatus(AsyncResponseStream *response); - void GetInitDataNavi(AsyncResponseStream *response); - + void GetInitDataStatus(JsonDocument& json); + void GetInitDataNavi(JsonDocument& json); + void onImprovWiFiConnectedCb(const char *ssid, const char *password); + void onWsEvent(AsyncWebSocket * server, AsyncWebSocketClient * client, AwsEventType type, void * arg, uint8_t *data, size_t len); }; -#endif +#endif // MYWEBSERVER_H_ + diff --git a/src/baseconfig.cpp b/src/baseconfig.cpp index 38724d58..5e16ff1a 100644 --- a/src/baseconfig.cpp +++ b/src/baseconfig.cpp @@ -1,44 +1,39 @@ -#include "baseconfig.h" - -BaseConfig::BaseConfig(): debuglevel(2), - serial_rx(3), - serial_tx(1), - mqtt_UseRandomClientID(true), - useAuth(false) { - #ifdef ESP8266 - LittleFS.begin(); - #elif defined(ESP32) - if (LittleFS.begin(true)) { // true: format LittleFS/NVS if mount fails - if (!LittleFS.exists("/config")) { - LittleFS.mkdir("/config"); - } - } else { - this->log(1, "LittleFS Mount Failed"); - } - #endif - - // Flash Write Issue - // https://github.com/esp8266/Arduino/issues/4061#issuecomment-428007580 - // LittleFS.format(); - +/******************************************************** + * Copyright [2024] Tobias Faust + +BaseConfig::BaseConfig(fs::LittleFSFS& configFS) + : configFS(configFS), + mqtt_UseRandomClientID(true), + keepalive(0), + debuglevel(3), + serial_rx(DEFAULT_SERIAL_RX_PIN), + serial_tx(DEFAULT_SERIAL_TX_PIN), + useAuth(false) { + // Partition wird im main.cpp gemountet LoadJsonConfig(); } + void BaseConfig::LoadJsonConfig() { bool loadDefaultConfig = false; - if (LittleFS.exists("/config/baseconfig.json")) { - //file exists, reading and loading - this->log(2, "reading config file"); - File configFile = LittleFS.open("/config/baseconfig.json", "r"); + this->disabledGPIO.deleteAll(GpioIdentifier::BASECONFIG); + + if (this->configFS.exists("/baseconfig.json")) { + // file exists, reading and loading + this->logN(2, "reading config file"); + File configFile = configFS.open("/baseconfig.json", "r"); if (configFile) { - this->log(2, "opened config file"); - + this->logN(2, "opened config file"); + JsonDocument doc; DeserializationError error = deserializeJson(doc, configFile); - + if (!error && doc["data"]) { this->log(1, doc); - + if (doc["data"]["mqttroot"]) { this->mqtt_root = doc["data"]["mqttroot"].as();} else {this->mqtt_root = "solax";} if (doc["data"]["mqttserver"]) { this->mqtt_server = doc["data"]["mqttserver"].as();} else {this->mqtt_server = "test.mosquitto.org";} if (doc["data"]["mqttport"]) { this->mqtt_port = doc["data"]["mqttport"].as();} else {this->mqtt_port = 1883;} @@ -48,8 +43,8 @@ void BaseConfig::LoadJsonConfig() { if (doc["data"]["SelectConnectivity"]){if (strcmp(doc["data"]["SelectConnectivity"], "wifi")==0) { this->useETH=false;} else {this->useETH=true;}} else {this->useETH = false;} if (doc["data"]["debuglevel"]) { this->debuglevel = _max(doc["data"]["debuglevel"].as(), 0);} else {this->debuglevel = 0; } if (doc["data"]["SelectLAN"]) { this->LANBoard = doc["data"]["SelectLAN"].as();} else {this->LANBoard = "";} - if (doc["data"]["serial_rx"]) { this->serial_rx = doc["data"]["serial_rx"].as(); } else {this->serial_rx = 3;} - if (doc["data"]["serial_tx"]) { this->serial_tx = doc["data"]["serial_tx"].as(); } else {this->serial_tx = 1;} + if (doc["data"]["serial_rx"]) { this->serial_rx = doc["data"]["serial_rx"].as(); } else {this->serial_rx = DEFAULT_SERIAL_RX_PIN;} + if (doc["data"]["serial_tx"]) { this->serial_tx = doc["data"]["serial_tx"].as(); } else {this->serial_tx = DEFAULT_SERIAL_TX_PIN;} if (doc["data"]["auth_user"]) { this->auth_user = doc["data"]["auth_user"].as();} else {this->auth_user = "admin";} if (doc["data"]["auth_pass"]) { this->auth_pass = doc["data"]["auth_pass"].as();} else {this->auth_pass = "password";} @@ -57,12 +52,12 @@ void BaseConfig::LoadJsonConfig() { this->mqtt_UseRandomClientID = doc["data"]["useRandomClientID"].as(); } else { - this->log(1, "failed to load json config, load default config"); + this->logN(1, "failed to load json config, load default config"); loadDefaultConfig = true; } } } else { - this->log(3, "baseconfig.json config File not exists, load default config"); + this->logN(3, "baseconfig.json config File not exists, load default config"); loadDefaultConfig = true; } @@ -86,15 +81,15 @@ void BaseConfig::LoadJsonConfig() { this->mqtt_basepath = this->mqtt_basepath.substring(0, this->mqtt_basepath.length()-1); } + this->disabledGPIO.addValue(this->serial_rx, GpioIdentifier::BASECONFIG); + this->disabledGPIO.addValue(this->serial_tx, GpioIdentifier::BASECONFIG); } const String BaseConfig::GetReleaseName() { return String(Release) + "(@" + String(GIT_BRANCH) + ")"; } -void BaseConfig::GetInitData(AsyncResponseStream *response) { - String ret; - JsonDocument json; +void BaseConfig::GetInitData(JsonDocument& json) { json["data"]["mqttroot"] = this->mqtt_root; json["data"]["mqttserver"] = this->mqtt_server; json["data"]["mqttport"] = this->mqtt_port; @@ -110,18 +105,28 @@ void BaseConfig::GetInitData(AsyncResponseStream *response) { json["data"]["auth_pass"] = this->auth_pass; - #ifdef USE_WEBSERIAL - json["data"]["tr_serial_rx"]["className"] = "hide"; - json["data"]["tr_serial_tx"]["className"] = "hide"; - #else - json["data"]["GpioPin_serial_rx"] = this->serial_rx; - json["data"]["GpioPin_serial_tx"] = this->serial_tx; - #endif + json["data"]["GpioPin_serial_rx"] = this->serial_rx; + json["data"]["GpioPin_serial_tx"] = this->serial_tx; json["response"]["status"] = 1; json["response"]["text"] = "successful"; - serializeJson(json, ret); - response->print(ret); +} + +void BaseConfig::logN(const int loglevel, const char* format, ...) { + if (this->GetDebugLevel() < loglevel) return; + + va_list args; + va_start(args, format); + char buffer[256]; + vsnprintf(buffer, sizeof(buffer), format, args); + Serial.printf("[Log %d] ", loglevel); + Serial.println(buffer); + + if (this->onLogValuesCallback) { + this->onLogValuesCallback(buffer); + } + + va_end(args); } void BaseConfig::log(const int loglevel, const char* format, ...) { @@ -131,24 +136,29 @@ void BaseConfig::log(const int loglevel, const char* format, ...) { va_start(args, format); char buffer[256]; vsnprintf(buffer, sizeof(buffer), format, args); - #ifdef USE_WEBSERIAL - WebSerial.printf("[Log %d] ", loglevel); - WebSerial.println(buffer); - #else - Serial.printf("[Log %d] ", loglevel); - Serial.println(buffer); - #endif + + Serial.printf("[Log %d] ", loglevel); + Serial.print(buffer); + + if (this->onLogValuesCallback) { + this->onLogValuesCallback(buffer); + } + va_end(args); } void BaseConfig::log(const int loglevel, const JsonDocument& json) { if (this->GetDebugLevel() < loglevel) return; - #ifdef USE_WEBSERIAL - serializeJsonPretty(json, WebSerial); - WebSerial.println(); - #else - serializeJsonPretty(json, Serial); - Serial.println(); - #endif + Serial.printf("[Log %d] ", loglevel); + serializeJsonPretty(json, Serial); + Serial.println(); + + if (this->onLogValuesCallback) { + this->onLogValuesCallback(json.as().c_str()); + } +} + +void BaseConfig::onLogValues(std::function callback) { + this->onLogValuesCallback = callback; } \ No newline at end of file diff --git a/src/baseconfig.h b/src/baseconfig.h index ebede46e..d428791a 100644 --- a/src/baseconfig.h +++ b/src/baseconfig.h @@ -1,17 +1,31 @@ -#ifndef BASECONFIG_H -#define BASECONFIG_H +/******************************************************** + * Copyright [2024] Tobias Faust +#include +#include +#include <_Release.h> class BaseConfig { public: - BaseConfig(); + + // Speaking identifiers + enum class GpioIdentifier : uint8_t { + BASECONFIG = 0, + MODBUS, + ETH, + OTHER + }; + + BaseConfig(fs::LittleFSFS& configFS); void LoadJsonConfig(); - void GetInitData(AsyncResponseStream *response); + void GetInitData(JsonDocument& json); /** * @brief Wrapper function for logging like Serial.printf @@ -19,8 +33,16 @@ class BaseConfig { * @param ... the arguments */ void log(const int loglevel, const char* format, ...); + void logN(const int loglevel, const char* format, ...); void log(const int loglevel, const JsonDocument& json); + // callbacks + /************************ + * @brief Callback for getting the values + * @param function(const char&) the callback function + ************************/ + void onLogValues(std::function callback); + const String& GetMqttServer() const {return mqtt_server;} const uint16_t& GetMqttPort() const {return mqtt_port;} const String& GetMqttUsername() const {return mqtt_username;} @@ -30,6 +52,7 @@ class BaseConfig { const bool& UseRandomMQTTClientID() const { return mqtt_UseRandomClientID; } const bool& GetUseETH() const { return useETH; } const String& GetLANBoard() const {return LANBoard;} + const uint16_t& GetKeepAlive() const {return keepalive;} const uint8_t& GetDebugLevel() const {return debuglevel;} const uint8_t& GetSerialRx() const {return serial_rx;} const uint8_t& GetSerialTx() const {return serial_tx;} @@ -37,8 +60,13 @@ class BaseConfig { const String& GetAuthUser() const {return auth_user;} const String& GetAuthPass() const {return auth_pass;} - const String GetReleaseName(); - private: + const String GetReleaseName(); + + // Maintains a list of currently 'reserved' GPIOs (SDA, SCL, 1Wire, Serial, etc.) + vectorlist disabledGPIO; + + private: + fs::LittleFSFS& configFS; String mqtt_server; String mqtt_username; String mqtt_password; @@ -48,6 +76,7 @@ class BaseConfig { bool mqtt_UseRandomClientID; bool useETH; // otherwise use WIFI String LANBoard; + uint16_t keepalive; uint8_t debuglevel; uint8_t serial_rx; uint8_t serial_tx; @@ -55,8 +84,9 @@ class BaseConfig { String auth_user; String auth_pass; + std::function onLogValuesCallback; // Callback function pointer }; extern BaseConfig* Config; -#endif +#endif // BASECONFIG_H_ diff --git a/src/commonlibs.h b/src/commonlibs.h index ecd3a2b8..12383857 100644 --- a/src/commonlibs.h +++ b/src/commonlibs.h @@ -1,3 +1,7 @@ +/******************************************************** + * Copyright [2024] Tobias Faust = 100 #include "Arduino.h" #else @@ -12,13 +16,9 @@ #include #include #include +#include +#include -#ifdef USE_WEBSERIAL - #include - #define dbg WebSerial -#else - #define dbg Serial -#endif #ifdef ESP8266 extern "C" { @@ -34,5 +34,4 @@ #include #include -//#include #include diff --git a/src/favicon.h b/src/favicon.h index 25971746..f8e5db90 100644 --- a/src/favicon.h +++ b/src/favicon.h @@ -1,3 +1,7 @@ +/******************************************************** + * Copyright [2024] Tobias Faust on("/doUpload", HTTP_POST, [](AsyncWebServerRequest *request) {}, - std::bind(&handleFiles::handleUpload, this, std::placeholders::_1, - std::placeholders::_2, - std::placeholders::_3, - std::placeholders::_4, - std::placeholders::_5, - std::placeholders::_6)); - -} - -//############################################################### -// returns the complete folder structure -//############################################################### -void handleFiles::getDirList(JsonArray* json, String path) { - JsonDocument doc; - JsonObject jsonRoot = doc.to(); - - jsonRoot["path"] = path; - JsonArray content = jsonRoot["content"].to(); - - File FSroot = LittleFS.open(path); - File file = FSroot.openNextFile(); - - while (file) { - JsonDocument doc1; - JsonObject jsonObj = doc1.to(); - String fname(file.name()); - jsonObj["name"] = fname; - - if(file.isDirectory()){ - jsonObj["isDir"] = 1; - String p = path + "/" + fname; - if (p.startsWith("//")) { p = p.substring(1); } - this->getDirList(json, p); // recursive call - } else { - jsonObj["isDir"] = 0; - } - - content.add(jsonObj); - file.close(); - file = FSroot.openNextFile(); - } - FSroot.close(); - json->add(jsonRoot); -} - -//############################################################### -// returns the requested data via AJAX from Webserver.cpp -//############################################################### -void handleFiles::HandleAjaxRequest(JsonDocument& jsonGet, AsyncResponseStream* response) { - String subaction = ""; - if (jsonGet["subaction"]) {subaction = jsonGet["subaction"].as();} - - Config->log(3, "handle Ajax Request in handleFiles.cpp: %s", subaction.c_str()); - - if (subaction == "listDir") { - JsonDocument doc; - JsonArray content = doc.add(); - - this->getDirList(&content, "/"); - String ret(""); - serializeJson(content, ret); - Config->log(5, content); - - response->print(ret); - } else if (subaction == "deleteFile") { - String filename(""), ret(""); - JsonDocument jsonReturn; - - Config->log(3, "Request to delete file %s", filename.c_str()); - - if (jsonGet["filename"]) {filename = jsonGet["filename"].as();} - - if (LittleFS.remove(filename)) { - jsonReturn["response_status"] = 1; - jsonReturn["response_text"] = "deletion successful"; - } else { - jsonReturn["response_status"] = 0; - jsonReturn["response_text"] = "deletion failed"; - } - Config->log(3, jsonReturn); - - serializeJson(jsonReturn, ret); - response->print(ret); - } -} - -//############################################################### -// store a file at Filesystem -//############################################################### -void handleFiles::handleUpload(AsyncWebServerRequest *request, String filename, size_t index, uint8_t *data, size_t len, bool final) { - - Config->log(5, "Client: %s %s", request->client()->remoteIP().toString().c_str(), request->url().c_str());; - - if (!index) { - // open the file on first call and store the file handle in the request object - request->_tempFile = LittleFS.open(filename, "w"); - Config->log(5, "Upload Start: %s", filename.c_str()); - } - - if (len) { - // stream the incoming chunk to the opened file - request->_tempFile.write(data, len); - Config->log(3, "Writing file: %s ,index=%d len=%d bytes, FreeMem: %d", filename.c_str(), index, len, ESP.getFreeHeap()); - } - - if (final) { - // close the file handle as the upload is now done - request->_tempFile.close(); - Config->log(3, "Upload Complete: %s ,size: %d Bytes", filename.c_str(), (index + len)); - - AsyncResponseStream *response = request->beginResponseStream("text/json"); - response->addHeader("Server","ESP Async Web Server"); - - JsonDocument jsonReturn; - String ret; - - jsonReturn["status"] = 1; - jsonReturn["text"] = "OK"; - - serializeJson(jsonReturn, ret); - response->print(ret); - request->send(response); - - if (Config->GetDebugLevel() >=5) { - serializeJson(jsonReturn, Serial); - } - } -} diff --git a/src/handleFiles.h b/src/handleFiles.h deleted file mode 100644 index b7001706..00000000 --- a/src/handleFiles.h +++ /dev/null @@ -1,18 +0,0 @@ -#ifndef HANDLEFILES_H -#define HANDLEFILES_H - -#include "commonlibs.h" -#include "baseconfig.h" - -class handleFiles { - public: - handleFiles(AsyncWebServer *server); - - void HandleAjaxRequest(JsonDocument& jsonGet, AsyncResponseStream* response); - void handleUpload(AsyncWebServerRequest *request, String filename, size_t index, uint8_t *data, size_t len, bool final); - - private: - void getDirList(JsonArray* json, String path); -}; - -#endif diff --git a/src/html_update.h b/src/html_update.h deleted file mode 100644 index b58a4ce3..00000000 --- a/src/html_update.h +++ /dev/null @@ -1,91 +0,0 @@ -#ifndef HTMLUPDATE_H -#define HTMLUPDATE_H - -// https://jsfiddle.net/tobiasfaust/Lc1earnz/ -const char HTML_UPDATEPAGE[] PROGMEM = R"=====( - - - - - - - - Solar Inverter Modbus MQTT Gateway - - Update firmware:

-

- - -
- - - -

please select 'data' directory: - -

- - - - - - -)====="; - -#endif \ No newline at end of file diff --git a/src/main.cpp b/src/main.cpp index e42bac8c..b5e1eae7 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -3,18 +3,23 @@ Solar Inverter Modbus-RTU Gateway to MQTT _________________________________________________________________ | | -| author : Tobias Faust +#include +#include +#include +#include + + +// Partitionen für System- und Konfigurationsdaten +fs::LittleFSFS sysFS; +fs::LittleFSFS configFS; AsyncWebServer server(80); DNSServer dns; @@ -26,59 +31,70 @@ MyWebServer* mywebserver = NULL; void myMQTTCallBack(char* topic, byte* payload, unsigned int length) { String msg; - Config->log(3, "Message arrived [%s]", topic); + Config->logN(3, "Message arrived [%s]", topic); for (unsigned int i = 0; i < length; i++) { - msg.concat((char)payload[i]); + msg.concat(static_cast(payload[i])); } - Config->log(3, "Message: %s", msg.c_str()); - - mb->ReceiveMQTT(topic, atoi(msg.c_str())); + Config->logN(3, "Message: %s", msg.c_str()); + + mb->ReceiveMQTT(topic, msg); } + void setup() { - Serial.begin(115200); - Config = new BaseConfig(); - #ifndef USE_WEBSERIAL - Serial.begin(115200, SERIAL_8N1, Config->GetSerialRx(), Config->GetSerialTx()); // RX, TX, zb.: 33, 32 - Serial.println(""); - Serial.println("ready"); - #endif + // Partitionen mounten + bool systemPartitionMounted = sysFS.begin(true, "/web", 5, "webdata"); + bool configPartitionMounted = configFS.begin(true, "/config", 5, "config"); - #ifdef USE_WEBSERIAL - WebSerial.onMessage([](const String& msg) { Serial.println(msg); }); - WebSerial.begin(&server); - WebSerial.setBuffer(100); - #endif + Config = new BaseConfig(configFS); - Config->log(1, "Start of Modbus-RTU MQTT Gateway"); - Config->log(1, "Starting BaseConfig"); + #ifdef ARDUINO_USB_CDC_ON_BOOT + Serial.begin(115200); + #else + Serial.begin(115200, + SERIAL_8N1, + Config->GetSerialRx(), + Config->GetSerialTx()); // RX, TX, zb.: 33, 32 + #endif - Config->log(1, "Starting Wifi and MQTT"); - mqtt = new MQTT(Config->GetMqttServer().c_str(), - Config->GetMqttPort(), - Config->GetMqttBasePath().c_str(), - Config->GetMqttRoot().c_str(), - (char*)"AP_ModbusGateway", - (char*)"MbMQTTGtw" - ); + Serial.println(""); + Serial.println("ready"); + + Config->logN(1, "Start of Modbus-RTU MQTT Gateway"); + Config->logN(1, "BaseConfig started"); + + Config->logN(1, "***** File System *****"); + Config->logN(1, "%s", systemPartitionMounted ? "System partition is mounted" : "System partition is not mounted"); + Config->logN(1, "Size: %d byte", systemPartitionMounted ? sysFS.totalBytes() : 0); + Config->logN(1, "Used: %d byte", systemPartitionMounted ? sysFS.usedBytes() : 0); + Config->logN(1, "***** ********** *****"); + Config->logN(1, "%s", configPartitionMounted ? "User partition is mounted" : "Config partition is not mounted"); + Config->logN(1, "Size: %d byte", configPartitionMounted ? configFS.totalBytes() : 0); + Config->logN(1, "Used: %d byte", configPartitionMounted ? configFS.usedBytes() : 0); + Config->logN(1, "***** ********** *****\n\n"); + + Config->logN(1, "Starting Wifi and MQTT"); + #ifdef WIFI_TX_POWER + WiFi.setTxPower(WIFI_TX_POWER); + #endif + mqtt = new MQTT(Config->GetMqttServer().c_str(), + Config->GetMqttPort(), + Config->GetMqttBasePath().c_str(), + Config->GetMqttRoot().c_str()); mqtt->setCallback(myMQTTCallBack); - mb = new modbus(); + mb = new modbus(sysFS, configFS); mb->enableMqtt(mqtt); - - Config->log(1, "attempting to start WebServer"); - mywebserver = new MyWebServer(&server, &dns); + + Config->logN(1, "attempting to start WebServer"); + mywebserver = new MyWebServer(sysFS, configFS, &server, &dns); } void loop() { mqtt->loop(); mywebserver->loop(); - mb->loop(); - - #ifdef USE_WEBSERIAL - WebSerial.loop(); - #endif + mb->loop(); } diff --git a/src/modbus.cpp b/src/modbus.cpp index 9ab71947..6bc4f51d 100644 --- a/src/modbus.cpp +++ b/src/modbus.cpp @@ -1,17 +1,28 @@ +/******************************************************** + * Copyright [2024] Tobias Faust {}; SaveIdDataframe = new std::vector{}; SaveLiveDataframe = new std::vector{}; @@ -19,8 +30,8 @@ modbus::modbus(): enableRelays(false), InverterLiveData = new std::vector{}; InverterIdData = new std::vector{}; AvailableInverters = new std::vector{}; - Setters = new std::vector{}; - OpenWB = new openwb(); + Setters = new std::vector{}; + OpenWB = new openwb(sysFS); Conf_RequestLiveData= new std::vector>{}; Conf_RequestIdData = new std::vector>{}; @@ -37,14 +48,14 @@ modbus::modbus(): enableRelays(false), this->pin_Relay1 = this->default_pin_Relay1 = 12; this->pin_Relay2 = this->default_pin_Relay2 = 14; } else { - this->pin_RX = this->default_pin_RX = 16; - this->pin_TX = this->default_pin_TX = 17; + this->pin_RX = this->default_pin_RX = DEFAULT_MODBUS_RX_PIN; + this->pin_TX = this->default_pin_TX = DEFAULT_MODBUS_TX_PIN; this->pin_RTS = this->default_pin_RTS = 5; this->pin_Relay1 = this->default_pin_Relay1 = 18; this->pin_Relay2 = this->default_pin_Relay2 = 19; } - this->LoadInvertersFromJson(); //needed for selecting default inverter + this->LoadInvertersFromJson(); //needed for selecting default inverter in LoadJsonConfig this->LoadJsonConfig(true); this->OpenWB->begin(this->Conf_OpenWBVersion); this->init(true); @@ -54,8 +65,8 @@ modbus::modbus(): enableRelays(false), * initialize transmission *******************************************************/ void modbus::init(bool firstrun) { - Config->log(3, "Start Hardwareserial 1 on RX (%d), TX(%d), RTS(%d)", this->pin_RX, this->pin_TX, this->pin_RTS); - Config->log(3, "Init Modbus to Client 0x%02X with %d Baud", this->ClientID, this->Baudrate); + Config->logN(3, "Start Hardwareserial 1 on RX (%d), TX(%d), RTS(%d)", this->pin_RX, this->pin_TX, this->pin_RTS); + Config->logN(3, "Init Modbus to Client 0x%02X with %d Baud", this->ClientID, this->Baudrate); // Configure Direction Control pin pinMode(this->pin_RTS, OUTPUT); @@ -64,11 +75,11 @@ void modbus::init(bool firstrun) { pinMode(this->pin_Relay2, INPUT_PULLUP); } - this->LoadInvertersFromJson(); this->LoadInverterConfigFromJson(); - this->LoadRegItems(this->InverterIdData, "id"); - this->LoadRegItems(this->InverterLiveData, "livedata"); - this->LoadJsonItemConfig(); // loads InverterLiveData Items too + this->LoadRegItems(this->InverterIdData, "id"); // load item definitions from register file + this->LoadRegItems(this->InverterLiveData, "livedata"); // load item definitions from register file + this->LoadSettersFromRegFile(); // loads setter config from config file and updates Setters + this->LoadJsonItemConfig(); // loads item config (active/inactive) from config file and updates InverterIdData, InverterLiveData and Setters // https://forum.arduino.cc/t/creating-serial-objects-within-a-library/697780/11 @@ -79,6 +90,14 @@ void modbus::init(bool firstrun) { this->QueryIdData(); } +/******************************************************* +* set websocket callback +********************************************************/ +void modbus::onValues(std::function callback, std::list* options) { + this->onValuesCallback = callback; + this->onValuesOptions = options; +} + /******************************************************* * Read configured pin states *******************************************************/ @@ -88,6 +107,11 @@ void modbus::ReadRelays() { this->state_Relay2 = digitalRead(this->pin_Relay2); this->mqtt->Publish_Int("relay2", this->state_Relay2, false); + + if (this->onValuesCallback) { + String message = "{\"data-id\": {\"relay1.value\":\"" + String(this->state_Relay1?"On":"Off") + "\",\"relay2.value\":\"" + String(this->state_Relay2?"On":"Off") + "\"}}"; + onValuesCallback(message); + } } /******************************************************* @@ -102,41 +126,33 @@ String modbus::GetMqttSetTopic(String command) { } /******************************************************* - * subscribe to all possible "set" register (register.h) + * load all "set" register from regfile if setters are enabled globally *******************************************************/ -void modbus::GenerateMqttSubscriptions() { +void modbus::LoadSettersFromRegFile() { // clear vector this->Setters->clear(); - File regfile = LittleFS.open("/regs/"+this->InverterType.filename); + File regfile = _sysFS.open("/regs/"+this->InverterType.filename); String streamString = ""; streamString = "\""+ this->InverterType.name +"\": {"; regfile.find(streamString.c_str()); streamString = "\"set\": ["; regfile.find(streamString.c_str()); + do { JsonDocument elem; DeserializationError error = deserializeJson(elem, regfile); if (!error) { // Print the result - Config->log(4, "parsing JSON for data ok"); + Config->logN(4, "parsing JSON for data ok"); Config->log(5, elem); if(!elem["name"].isNull() && elem["request"].is()) { - subscription_t s = {}; - s.command = elem["name"].as(); - - JsonArray arr = elem["request"].as(); - std::vector t = {}; - for (String x : arr) { - byte e = this->String2Byte(x); - t.push_back(e); - } - s.request = t; + setter_t s = {}; + s.Name = elem["name"].as(); - this->mqtt->Subscribe(this->GetMqttSetTopic(s.command)); - Config->log(4, "Set command successfully parsed from JSON: %s with %s", s.command.c_str(), (this->PrintDataFrame(&(s.request))).c_str()); + Config->logN(4, "Set command successfully parsed from JSON: %s", s.Name.c_str()); this->Setters->push_back(s); } else { @@ -144,45 +160,155 @@ void modbus::GenerateMqttSubscriptions() { } } else { - Config->log(1, "Failed to parse JSON Register Data: %s", error.c_str()); + Config->logN(1, "Failed to parse JSON Register Data: %s", error.c_str()); } } while (regfile.findUntil(",","]")); regfile.close(); } - /******************************************************* * act on received mqtt command *******************************************************/ -void modbus::ReceiveMQTT(String topic, int msg) { +void modbus::ReceiveMQTT(String topic, String msg) { if (!this->Conf_EnableSetters) { - Config->log(2, "Set command <%s> received, but setters over mqtt are currently disabled", topic.c_str()); + Config->logN(2, "Set command <%s> received, but setters over mqtt are currently disabled globally", topic.c_str()); return; } for (uint8_t i = 0; i < this->Setters->size(); i++ ) { - if (topic == this->GetMqttSetTopic(this->Setters->at(i).command)) { - std::vector request = this->Setters->at(i).request; - byte bytes[4]; + if (topic == this->GetMqttSetTopic(this->Setters->at(i).Name)) { + if (!this->Setters->at(i).active) { + Config->logN(2, "Set command <%s> received, but setter %s is not active", topic.c_str(), this->Setters->at(i).Name.c_str()); + return; + } + + JsonDocument elem = this->GetSetterByName(this->Setters->at(i).Name); + if (elem.isNull()) { + Config->logN(1, "Setter %s not found in JSON", this->Setters->at(i).Name.c_str()); + return; + } - bytes[0] = (msg >> 24) & 0xFF; - bytes[1] = (msg >> 16) & 0xFF; - bytes[2] = (msg >> 8) & 0xFF; - bytes[3] = (msg >> 0) & 0xFF; + JsonArray arr = elem["request"].as(); + std::vector request = {}; - // 16bit number - request.push_back(bytes[2]); - request.push_back(bytes[3]); + for (String x : arr) { + byte e = this->String2Byte(x); + request.push_back(e); + } + //added by Lazgar + if (!elem["intsize"].isNull()) { // wenn "intsize" gefunden wird, muss es als multiregister beschrieben werden + + byte bn = this->String2Byte(arr[5]); // der letzte Value aus "arr" auslesen und in Byte umwandeln (byte number) + request.push_back(bn*2); //Byte verdoppeln und dem "request" anhängen + + JsonArray sizearr = elem["intsize"].as(); //intsize als Array verfügbar machen + + std::vector mparts = splitStringToVector(msg); // Spliten der "msg" in einzelne Strings und in einen Vector laden + + if (mparts.size() != sizearr.size()) { + Config->logN(1, "The correct number of values ​​was not passed (%s)", sizearr.size()); + return; + } + + for (uint8_t z = 0; z < mparts.size(); z++ ) { //Schleife zum umwandeln der Strings und anhängen an den "request" + + int msgInt = mparts.at(z).toInt(); // atoi(msg.c_str()) + byte bytes[4]; + + bytes[0] = (msgInt >> 24) & 0xFF; + bytes[1] = (msgInt >> 16) & 0xFF; + bytes[2] = (msgInt >> 8) & 0xFF; + bytes[3] = (msgInt >> 0) & 0xFF; + + if (sizearr[z] == "int32") { // bei int32 werden 4 byte dem "request" angehängt + + request.push_back(bytes[2]); // LSB zuerst + request.push_back(bytes[3]); + request.push_back(bytes[0]); // MSB danach + request.push_back(bytes[1]); + + } else { // bei int16 werden 2 byte dem "request" angehängt + + request.push_back(bytes[2]); + request.push_back(bytes[3]); + + } + + } + + } else { // übliche abarbeitung normaler Set Befehle + // map values if a mapping is specified + if(!elem["mapping"].isNull() && elem["mapping"].is() && msg != "") { + Config->logN(4, "Map values for item %s", msg.c_str()); + + JsonArray map = elem["mapping"].as(); + msg = this->MapItem(map, msg, true); + } - Config->log(3, "MQTT Setter found: %s" ,this->Setters->at(i).command.c_str()); - Config->log(3, "Initiate Set Request to queue: %s" ,(this->PrintDataFrame(&request)).c_str()); + int msgInt = msg.toInt(); // atoi(msg.c_str()) + byte bytes[4]; + + bytes[0] = (msgInt >> 24) & 0xFF; + bytes[1] = (msgInt >> 16) & 0xFF; + bytes[2] = (msgInt >> 8) & 0xFF; + bytes[3] = (msgInt >> 0) & 0xFF; + + // 32bit number + request.push_back(bytes[2]); + request.push_back(bytes[3]); + } + + Config->logN(3, "MQTT Setter found: %s" ,this->Setters->at(i).Name.c_str()); + Config->logN(3, "Initiate Set Request to queue: %s" ,(this->PrintDataFrame(&request)).c_str()); this->SetQueue->enqueue(request); } } } +/******************************************************* + * @brief get setter by name, read json file and return the json object for the setter + * @param name: name of the setter + * @return JsonDocument: json object for the setter + * ******************************************************/ +JsonDocument modbus::GetSetterByName(String name) { + File regfile = _sysFS.open("/regs/" + this->InverterType.filename); + if (!regfile) { + Config->logN(1, "failed to open %s file", this->InverterType.filename.c_str()); + return JsonDocument(); + } + + String streamString = ""; + streamString = "\""+ this->InverterType.name +"\": {"; + regfile.find(streamString.c_str()); + + streamString = "\"set\": ["; + regfile.find(streamString.c_str()); + do { + JsonDocument elem; + DeserializationError error = deserializeJson(elem, regfile); + + if (!error) { + // Print the result + Config->logN(4, "parsing JSON ok"); + Config->log(5, elem); + } else { + Config->logN(1, "(Function GetSetterByName) Failed to parse JSON Register Data: %s", error.c_str()); + } + + if (elem["name"] == name) { + regfile.close(); + return elem; + } + + } while (regfile.findUntil(",","]")); + + if (regfile) { regfile.close(); } + + return JsonDocument(); +} + /******************************************************* * get all defined inverters from json (register.h) *******************************************************/ @@ -193,17 +319,17 @@ void modbus::LoadInvertersFromJson() { AvailableInverters->clear(); filter["*"]["config"]["ClientIdPos"] = true; - File root = LittleFS.open("/regs/"); + File root = _sysFS.open("/regs/"); File file = root.openNextFile(); while(file){ - Config->log(3, "open register file from Filesystem: %s", file.name()); + Config->logN(3, "open register file from Filesystem: %s", file.name()); DeserializationError error = deserializeJson(regjson, file, DeserializationOption::Filter(filter)); if (!error && regjson.size() > 0) { // https://arduinojson.org/v6/api/jsonobject/begin_end/ JsonObject root = regjson.as(); for (JsonPair kv : root) { - Config->log(3, "Inverter found: %s", kv.key().c_str()); + Config->logN(3, "Inverter found: %s", kv.key().c_str()); regfiles_t wr = {}; wr.filename = file.name(); @@ -211,7 +337,7 @@ void modbus::LoadInvertersFromJson() { AvailableInverters->push_back(wr); } } else{ - Config->log(1, "Error: unable to load inverters from File %s: %s", file.name(), error.c_str()); + Config->logN(1, "Error: unable to load inverters from File %s: %s", file.name(), error.c_str()); } file.close(); file = root.openNextFile(); @@ -219,8 +345,8 @@ void modbus::LoadInvertersFromJson() { root.close(); if (this->AvailableInverters->size() == 0) { - Config->log(1, "ALERT: No register definitions found. ESP cannot work properly"); - Config->log(1, "Please flash filesystem Image!"); + Config->logN(1, "ALERT: No register definitions found. ESP cannot work properly"); + Config->logN(1, "Please flash filesystem Image!"); } } @@ -231,9 +357,9 @@ void modbus::LoadInverterConfigFromJson() { JsonDocument doc; JsonDocument filter; - File regfile = LittleFS.open("/regs/"+this->InverterType.filename); + File regfile = _sysFS.open("/regs/"+this->InverterType.filename); if (!regfile) { - Config->log(1, "failed to open %s file", this->InverterType.filename.c_str()); + Config->logN(1, "failed to open %s file", this->InverterType.filename.c_str()); } filter[this->InverterType.name]["config"] = true; @@ -241,9 +367,9 @@ void modbus::LoadInverterConfigFromJson() { DeserializationError error = deserializeJson(doc, regfile, DeserializationOption::Filter(filter)); if (error) { - Config->log(1, "Error: unable to read configdata for inverter %s: %s", this->InverterType.name.c_str(), error.c_str()); + Config->logN(1, "Error: unable to read configdata for inverter %s: %s", this->InverterType.name.c_str(), error.c_str()); } else { - Config->log(4, "Read config data for inverter %s", this->InverterType.name.c_str()); + Config->logN(4, "Read config data for inverter %s", this->InverterType.name.c_str()); Config->log(4, doc); } @@ -304,19 +430,41 @@ byte modbus::String2Byte(String s){ return ret; } +/******************************************************* + * split a String into Vector with comma as separator +*******************************************************/ +std::vector modbus::splitStringToVector(String msg){ + std::vector subStrings; + int j=0; + for(int i =0; i < msg.length(); i++){ + if(msg.charAt(i) == ','){ + subStrings.push_back(msg.substring(j,i)); + j = i+1; + } + } + subStrings.push_back(msg.substring(j,msg.length())); //to grab the last value of the string + return subStrings; +} + /******************************************************* * Enable MQTT Transmission *******************************************************/ void modbus::enableMqtt(MQTT* object) { this->mqtt = object; - this->GenerateMqttSubscriptions(); + + // subscribe to all active setters + for (uint8_t i = 0; i < this->Setters->size(); i++) { + if (this->Setters->at(i).active) { + this->mqtt->Subscribe(this->GetMqttSetTopic(this->Setters->at(i).Name)); + } + } } /******************************************************* * Query ID Data to Inverter *******************************************************/ void modbus::QueryIdData() { - Config->log(4, "Query ID Data into Queue:"); + Config->logN(4, "Query ID Data into Queue:"); /* byte message[] = {this->ClientID, 0x03, // FunctionCode @@ -331,19 +479,18 @@ void modbus::QueryIdData() { if (this->ReadQueue->isEmpty()) { for (uint8_t i = 0; i < this->Conf_RequestIdData->size(); i++) { - Config->log(4, this->PrintDataFrame(&this->Conf_RequestIdData->at(i)).c_str()); + Config->logN(4, this->PrintDataFrame(&this->Conf_RequestIdData->at(i)).c_str()); this->ReadQueue->enqueue(this->Conf_RequestIdData->at(i)); } this->LastTxIdData = millis(); //erst setzen wenn erfolgreich in die Queue geschickt wurde } } - /******************************************************* * Query Live Data to Inverter *******************************************************/ void modbus::QueryLiveData() { - Config->log(4, "Query Live Data into Queue:"); + Config->logN(4, "Query Live Data into Queue:"); /* byte message[] = {this->ClientID, 0x04, // FunctionCode @@ -358,7 +505,7 @@ void modbus::QueryLiveData() { if (this->ReadQueue->isEmpty()) { for (uint8_t i = 0; i < this->Conf_RequestLiveData->size(); i++) { - Config->log(4, this->PrintDataFrame(&this->Conf_RequestLiveData->at(i)).c_str()); + Config->logN(4, this->PrintDataFrame(&this->Conf_RequestLiveData->at(i)).c_str()); this->ReadQueue->enqueue(this->Conf_RequestLiveData->at(i)); } this->LastTxLiveData = millis(); //erst setzen wenn erfolgreich in die Queue geschickt wurde @@ -387,7 +534,7 @@ void modbus::QueryQueueToInverter() { else { rwtype = NUL; } if (rwtype != NUL) { - Config->log(3, "Request queue data to inverter: "); + Config->logN(3, "Request queue data to inverter: "); digitalWrite(this->pin_RTS, RS485Receive); // init Receive while (RS485Serial->available() > 0) { // read serial if any old data is available @@ -406,7 +553,7 @@ void modbus::QueryQueueToInverter() { m.push_back(lowByte(crc)); m.push_back(highByte(crc)); - Config->log(3, this->PrintDataFrame(message, sizeof(message)).c_str()); + Config->logN(3, this->PrintDataFrame(message, sizeof(message)).c_str()); digitalWrite(this->pin_RTS, RS485Transmit); // init Transmit RS485Serial->write(message, sizeof(message)); @@ -439,7 +586,7 @@ bool modbus::ReceiveSetData(std::vector* SendHexFrame) { std::vector RecvHexframe = {}; bool ret = false; - Config->log(3, "Read Data from Queue: "); + Config->logN(3, "Read Data from Queue: "); digitalWrite(this->pin_RTS, RS485Receive); // init Receive if (RS485Serial->available()) { @@ -447,10 +594,10 @@ bool modbus::ReceiveSetData(std::vector* SendHexFrame) { while(RS485Serial->available()) { byte d = RS485Serial->read(); RecvHexframe.push_back(d); - if (Config->GetDebugLevel() >=4) {dbg.print(PrintHex(d)); dbg.print(" ");} + Config->log(4, "%s ", PrintHex(d)); delay(1); // keep this! Loosing bytes possible if too fast } - if (Config->GetDebugLevel() >=4) {dbg.println();} + Config->logN(4, ""); // TODO // compare Set and Received Answer, should be equal @@ -468,7 +615,7 @@ bool modbus::ReceiveSetData(std::vector* SendHexFrame) { void modbus::ReceiveReadData() { size_t dataFrameStartPos = this->DataFrame->size(); - Config->log(3, "Read Data from Queue: "); + Config->logN(3, "Read Data from Queue: "); digitalWrite(this->pin_RTS, RS485Receive); // init Receive if (RS485Serial->available()) { @@ -476,10 +623,10 @@ void modbus::ReceiveReadData() { while(RS485Serial->available()) { byte d = RS485Serial->read(); this->DataFrame->push_back(d); - if (Config->GetDebugLevel() >=4) {dbg.print(PrintHex(d)); dbg.print(" ");} + Config->log(4, "%s", PrintHex(d).c_str()); delay(1); // keep this! Loosing bytes possible if too fast } - if (Config->GetDebugLevel() >=4) {dbg.println();} + Config->logN(4, ""); bool valid = true; @@ -488,39 +635,39 @@ void modbus::ReceiveReadData() { this->DataFrame->at(dataFrameStartPos+this->Conf_IdDataErrorPos) != this->Conf_IdDataErrorCode && this->DataFrame->at(dataFrameStartPos+this->Conf_LiveDataErrorPos) != this->Conf_LiveDataErrorCode) { - Config->log(4, "ErrorCode passed, OK"); + Config->logN(4, "ErrorCode passed, OK"); if (this->enableCrcCheck) { //CRC Check uint16_t crc = this->Calc_CRC(this->DataFrame, dataFrameStartPos, this->DataFrame->size()-2); - Config->log(4, "Received CRC: 0x%02X 0x%02X", this->DataFrame->at(this->DataFrame->size()-2), this->DataFrame->at(this->DataFrame->size()-1)); - Config->log(4, "Calculated CRC: 0x%02X 0x%02X", lowByte(crc), highByte(crc)); + Config->logN(4, "Received CRC: 0x%02X 0x%02X", this->DataFrame->at(this->DataFrame->size()-2), this->DataFrame->at(this->DataFrame->size()-1)); + Config->logN(4, "Calculated CRC: 0x%02X 0x%02X", lowByte(crc), highByte(crc)); if (this->DataFrame->at(this->DataFrame->size()-2) != lowByte(crc) || this->DataFrame->at(this->DataFrame->size()-1) != highByte(crc)) { valid = false; - Config->log(2, "CRC check failed!"); + Config->logN(2, "CRC check failed!"); } } if (this->enableLengthCheck) { // Check datalength - Config->log(4, "Dataframe length should be: %d, is: %d bytes", this->DataFrame->at(dataFrameStartPos+2), this->DataFrame->size()-dataFrameStartPos-5); + Config->logN(4, "Dataframe length should be: %d, is: %d bytes", this->DataFrame->at(dataFrameStartPos+2), this->DataFrame->size()-dataFrameStartPos-5); if (this->DataFrame->at(dataFrameStartPos+2) != this->DataFrame->size()-dataFrameStartPos-5) { valid = false; - Config->log(2, "data length check failed, should be %d but is %d bytes", this->DataFrame->at(dataFrameStartPos+2), this->DataFrame->size()-dataFrameStartPos-5); + Config->logN(2, "data length check failed, should be %d but is %d bytes", this->DataFrame->at(dataFrameStartPos+2), this->DataFrame->size()-dataFrameStartPos-5); } } } else { valid = false; } if (valid) { // Dataframe valid - Config->log(3, "Dataframe valid, Dateframe size: %d bytes", this->DataFrame->size()); + Config->logN(3, "Dataframe valid, Dateframe size: %d bytes", this->DataFrame->size()); } else { - Config->log(2, "Dataframe invalid"); + Config->logN(2, "Dataframe invalid"); // clear dataframe, clear ReadQueue to start from fresh this->DataFrame->clear(); for (unsigned int n = 0; n < this->ReadQueue->itemCount(); n++) { @@ -529,7 +676,7 @@ void modbus::ReceiveReadData() { } } else { - Config->log(2, "no response from client"); + Config->logN(2, "no response from client"); } } @@ -548,7 +695,7 @@ int modbus::JsonPosArrayToInt(JsonArray posArray, JsonArray posArray2) { if (v < this->DataFrame->size()) { val_i = (val_i << 8) | this->DataFrame->at(v); val_max = (val_max << 8) | 0xFF; - } else Config->log(1, "Error: position %d out of dataframe range", v); + } else Config->logN(1, "Error: position %d out of dataframe range", v); } } @@ -557,7 +704,7 @@ int modbus::JsonPosArrayToInt(JsonArray posArray, JsonArray posArray2) { for(uint16_t w : posArray2) { if (w < this->DataFrame->size()) { val2_i = (val2_i << 8) | this->DataFrame->at(w); - } else Config->log(1, "Error: position %d out of dataframe range", w); + } else Config->logN(1, "Error: position %d out of dataframe range", w); } } @@ -596,7 +743,7 @@ void modbus::ParseData() { // *********************************************** #ifdef DEBUGMODE this->DataFrame->clear(); - Config->log(3, "Start parsing in testmode, use some testdata instead real live data :)"); + Config->logN(3, "Start parsing in testmode, use some testdata instead real live data :)"); // Solar-KTL //byte ReadBuffer[] = {0x01, 0x03, 0x60, 0x00, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x05, 0x34, 0x01, 0xF3, 0x00, 0x00, 0x00, 0x00, 0x00, 0x42, 0x00, 0x00, 0x00, 0x3F, 0x00, 0x00, 0x13, 0x86, 0x09, 0x11, 0x01, 0x12, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x4C, 0x00, 0x00, 0x02, 0x5F, 0x00, 0x8A, 0x01, 0x7D, 0x00, 0x28, 0x00, 0x33, 0x0E, 0x62, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0x60, 0x01, 0x42, 0x0A, 0x3B, 0x00, 0x0E, 0x00, 0x05, 0x00, 0x00, 0x00, 0x09, 0x00, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x39, 0x25,0x01}; @@ -619,10 +766,13 @@ void modbus::ParseData() { // K8H //byte ReadBuffer[] = {0xF7,0x02,0x1E,0x0B,0x3E,0x00,0x39,0x05,0xE1,0x0A,0x68,0x00,0x39,0x05,0xFA,0x00,0x08,0x00,0x00,0x00,0x00,0x00,0x00,0x08,0xFC,0x00,0x7C,0x00,0x00,0x13,0x87,0x00,0x00,0x24,0x27, 0x02}; + // Solax-X3 + //byte ReadBuffer[] = {0x01,0x04,0x42,0x17,0xB4,0x12,0x21,0x00,0x02,0x00,0x02,0x13,0x88,0x00,0x27,0x00,0x02,0x00,0x7A,0x00,0x7A,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x02,0x00,0x00,0x00,0x02,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0xAA,0xDB,0x01,0x04,0x2C,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0xFE,0x48,0xFF,0xFF,0x05,0x2F,0x00,0x02,0x7E,0xDD,0x00,0x02,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x1B,0x00,0x00,0x4D,0x6E,0x00,0x00,0x00,0x01,0x00,0x00,0xD3,0x0A,0x01,0x04,0x64,0x08,0x4A,0x00,0x09,0x00,0x64,0x13,0x88,0x08,0x81,0x00,0x09,0x00,0x3D,0x13,0x88,0x08,0x20,0x00,0x09,0x00,0x61,0x13,0x89,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0xFF,0x95,0xFF,0xFF,0x00,0x00,0x00,0x00,0xFF,0x2D,0xFF,0xFF,0x31,0xA8,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x02,0x00,0x00,0x4A,0xC0,0x00,0x00,0x00,0x1B,0x00,0x00,0x00,0x47,0x00,0x00,0x01,0xD9,0x00,0x00,0x55,0x5B,0x01,0x04,0x04,0x00,0x00,0x00,0x00,0xFB,0x84,0x02}; + for (uint16_t i = 0; iDataFrame->push_back(ReadBuffer[i]); } - Config->log(4, "%s", this->PrintDataFrame(this->DataFrame).c_str()); + Config->logN(4, "%s", this->PrintDataFrame(this->DataFrame).c_str()); #endif // *********************************************** } @@ -639,12 +789,12 @@ void modbus::ParseData() { RequestType = "livedata"; } - Config->log(3, "parse %d bytes of data", this->DataFrame->size()); - Config->log(4, "identified datatype: %s", RequestType.c_str()); - - File regfile = LittleFS.open("/regs/"+this->InverterType.filename); + Config->logN(3, "parse %d bytes of data", this->DataFrame->size()); + Config->logN(4, "identified datatype: %s", RequestType.c_str()); + + File regfile = this->_sysFS.open("/regs/"+this->InverterType.filename); if (!regfile) { - Config->log(1, "failed to open %s file", this->InverterType.filename.c_str()); + Config->logN(1, "failed to open %s file", this->InverterType.filename.c_str()); } String streamString = ""; streamString = "\""+ this->InverterType.name +"\": {"; @@ -658,10 +808,10 @@ void modbus::ParseData() { if (!error) { // Print the result - Config->log(4, "parsing JSON ok"); + Config->logN(4, "parsing JSON ok"); Config->log(5, elem); } else { - Config->log(1, "(Function ParseData) Failed to parse JSON Register Data: %s", error.c_str()); + Config->logN(1, "(Function ParseData) Failed to parse JSON Register Data: %s", error.c_str()); } // setUp local variables @@ -691,7 +841,7 @@ void modbus::ParseData() { if (elem["position"].is()) { posArray = elem["position"].as(); } else { - Config->log(1, "Error: for Name '%s' no position array found", d.Name.c_str()); + Config->logN(1, "Error: for Name '%s' no position array found", d.Name.c_str()); continue; } @@ -708,12 +858,12 @@ void modbus::ParseData() { // optional field if (elem["factor"]) { - factor = elem["factor"]; + factor = elem["factor"].as(); } // optional field if (elem["valueAdd"]) { - valueAdd = elem["valueAdd"]; + valueAdd = elem["valueAdd"].as(); } // optional field @@ -769,7 +919,7 @@ void modbus::ParseData() { if (v < DataFrame->size()) { buffer[i] = static_cast(DataFrame->at(v)); i++; - } else Config->log(1, "Error: for item '%s' position %d out of dataframe range", d.Name.c_str(), v); + } else Config->logN(1, "Error: for item '%s' position %d out of dataframe range", d.Name.c_str(), v); } buffer[i] = '\0'; d.value = String(buffer); @@ -777,23 +927,24 @@ void modbus::ParseData() { } else { //****************** sonst, leer *******************// d.value = ""; - Config->log(2, "Error: for Name '%s' no valid datatype found", d.Name.c_str()); + Config->logN(2, "Error: for Name '%s' no valid datatype found", d.Name.c_str()); } // map values if a mapping is specified if(!elem["mapping"].isNull() && elem["mapping"].is() && d.value != "") { - Config->log(4, "Map values for item %s", d.Name.c_str()); + Config->logN(4, "Map values for item %s", d.Name.c_str()); JsonArray map = elem["mapping"].as(); if (datatype == "binary") d.value = this->MapBitwise(map, d.value); - else d.value = this->MapItem(map, d.value); + else d.value = this->MapItem(map, d.value, false); } - Config->log(4, "Data: %s -> %s %s", d.Name.c_str(), d.value.c_str(), d.unit.c_str()); + Config->logN(4, "Data: %s -> %s %s", d.Name.c_str(), d.value.c_str(), d.unit.c_str()); if (this->mqtt && IsActiveItem && d.Name != "") { this->mqtt->Publish_String(d.Name.c_str(), d.value, false); + if (openwbtopic.length() > 0) { const String newTopic(OpenWB->getOpenWbTopic(openwbtopic)); if (newTopic.length() > 0) { @@ -807,9 +958,15 @@ void modbus::ParseData() { } else if(RequestType == "id") { this->ChangeRegItem(this->InverterIdData, d); - Config->log(3, "Inverter ID Data found -> %s: %s ", d.Name.c_str(), d.value.c_str()); + Config->logN(3, "Inverter ID Data found -> %s: %s ", d.Name.c_str(), d.value.c_str()); } + + //if (this->onValuesCallback) { + //const String ws("{\"data-id\":{\"" + d.Name + ".value\":\"" + d.value + " "+ d.unit +"\"}}"); + //const String ws("{\"" + d.Name + ".value\":\"" + d.value + " "+ d.unit +"\"}"); + //onValuesCallback(ws); + //} } while (regfile.findUntil(",","]")); @@ -827,9 +984,36 @@ void modbus::ParseData() { this->SaveIdDataframe->assign(this->DataFrame->begin(), this->DataFrame->end()); } + if (this->onValuesCallback) { + this->SendDataToWebSocket(RequestType == "livedata" ? this->InverterLiveData : this->InverterIdData); + } + this->DataFrame->clear(); } +void modbus::SendDataToWebSocket(std::vector* vector) { + if (this->onValuesCallback) { + uint16_t counter = 0; + String msg("{\"data-id\":{"); msg.reserve(vector->size() * 25); + bool onlyactive = this->onValuesOptions && std::find(this->onValuesOptions->begin(), this->onValuesOptions->end(), "onlyactive") != this->onValuesOptions->end(); + + for (uint8_t i=0; i < vector->size(); i++) { + if ((onlyactive && vector->at(i).active) || !onlyactive) { + if (counter > 0) msg += ","; + msg += "\"" + vector->at(i).Name + ".value\":\"" + vector->at(i).value; + if (this->onValuesOptions && std::find(this->onValuesOptions->begin(), this->onValuesOptions->end(), "+unit") != this->onValuesOptions->end()) { + msg += " "+ vector->at(i).unit; + } + msg += "\""; + counter++; + + } + } + msg += "}}"; + this->onValuesCallback(msg); + } +} + String modbus::ConvertIntToBinaryString(int n, int numBits) { String binaryString = ""; binaryString.reserve(numBits); @@ -849,9 +1033,9 @@ String modbus::MapBitwise(JsonArray map, String value) { for (uint8_t i=0; ilog(4, "Mapped value: %s -> %s\n", String(value[i]).c_str(), map[map.size() -2 -i].as().c_str()); + Config->logN(4, "Mapped value: %s -> %s\n", String(value[i]).c_str(), map[map.size() -2 -i].as().c_str()); if (ret.length() > 0) ret += ", "; - if ((map.size() -2 -i) >=0 && map[map.size() -2 -i]) ret += map[map.size() -2 -i].as(); + if (((signed)map.size() -2 -i) >=0 && map[map.size() -2 -i]) ret += map[map.size() -2 -i].as(); else ret += "undefined"; } } @@ -864,16 +1048,22 @@ String modbus::MapBitwise(JsonArray map, String value) { /******************************************************* * Map a value to a predefined constant string *******************************************************/ -String modbus::MapItem(JsonArray map, String value) { +String modbus::MapItem(JsonArray map, String value, bool isSetter) { String ret = value; for (JsonArray mapItem : map) { String v1 = mapItem[0].as(); String v2 = mapItem[1].as(); + if (isSetter) { + v1.toLowerCase(); + v2.toLowerCase(); + value.toLowerCase(); + } + if (value == v1) { ret = v2; - Config->log(4, "Mapped value: %s -> %s", v1.c_str(), v2.c_str()); + Config->logN(4, "Mapped value: %s -> %s", v1.c_str(), v2.c_str()); } } return ret; @@ -977,9 +1167,11 @@ String modbus::GetInverterSN() { * Return all LiveData as jsonArray * {data: [{"name": "xx", "value": "xx", ...}, ...] } *******************************************************/ -void modbus::GetLiveDataAsJson(AsyncWebServerRequest *request) { +void modbus::GetLiveDataAsJsonToWebServer(AsyncWebServerRequest *request) { std::shared_ptr counter = std::make_shared(0); - String subaction(""), json("{}"); + std::shared_ptr firstRow = std::make_shared(true); + + String json("{}"); if(request->hasArg("json")) { json = request->arg("json"); @@ -987,46 +1179,44 @@ void modbus::GetLiveDataAsJson(AsyncWebServerRequest *request) { JsonDocument jsonGet; DeserializationError error = deserializeJson(jsonGet, json.c_str()); - Config->log(4, "[GetLiveDataAsJson] Json command empfangen: "); + Config->logN(4, "[GetLiveDataAsJsonToWebServer] Json command empfangen: "); if (!error) { Config->log(4, jsonGet); - - if (jsonGet["subaction"]){subaction = jsonGet["subaction"].as();} - } else { - Config->log(2, "[GetLiveDataAsJson] Json Command not parseable: %s -> %s", json.c_str(), error.c_str()); + Config->logN(2, "[GetLiveDataAsJsonToWebServer] Json Command not parseable: %s -> %s", json.c_str(), error.c_str()); } - AsyncWebServerResponse *response = request->beginChunkedResponse("application/json", [this, counter, subaction](uint8_t *buffer, size_t maxLen, size_t index) { + AsyncWebServerResponse *response = request->beginChunkedResponse("application/json", [this, firstRow, counter](uint8_t *buffer, size_t maxLen, size_t index) { String ret(""); ret.reserve(maxLen); maxLen -= 500; // use a puffer of 500 bytes, every item is assumed to be 200 bytes + if (*counter == 0) { // send start of JSON ret += "{\"data\": {\"items\": ["; (*counter)++; } + if (*counter <= this->InverterIdData->size() && ret.length() < maxLen) { // send IdData uint16_t i = *counter - 1; // jedes JsonObject wird mit 200 bytes angenommen, + 100 bytes puffer am Ende while (i < this->InverterIdData->size() && ret.length() < maxLen) { - if (!(subaction == "onlyactive" && !this->InverterIdData->at(i).active)) { - if(*counter > 1) ret += ","; - ret += "{\"name\": \"" + this->InverterIdData->at(i).Name + "\","; - ret += "\"realname\": \"" + this->InverterIdData->at(i).RealName + "\","; - ret += "\"value\": \"" + this->InverterIdData->at(i).value + " " + this->InverterIdData->at(i).unit + "\","; - ret += "\"active\": {\"checked\": " + String(this->InverterIdData->at(i).active ? 1 : 0) + ", \"name\": \"" + this->InverterIdData->at(i).Name + "\"},"; - ret += "\"mqtttopic\": \"" + this->mqtt->getTopic(this->InverterIdData->at(i).Name, false) + "\""; + if(!(*firstRow)) ret += ","; + ret += "{\"name\": \"" + this->InverterIdData->at(i).Name + "\","; + ret += "\"realname\": \"" + this->InverterIdData->at(i).RealName + "\","; + ret += "\"value\": {\"innerHTML\": \"" + this->InverterIdData->at(i).value + " " + this->InverterIdData->at(i).unit + "\", \"data-id\": \"" + this->InverterIdData->at(i).Name + ".value" + "\"},"; + ret += "\"active\": {\"checked\": " + String(this->InverterIdData->at(i).active ? 1 : 0) + ", \"name\": \"" + this->InverterIdData->at(i).Name + "\"},"; + ret += "\"mqtttopic\": \"" + this->mqtt->getTopic(this->InverterIdData->at(i).Name, false) + "\""; - if (this->Conf_EnableOpenWB && this->InverterIdData->at(i).openwb.length() > 0) { - ret += ",\"openwb\": [{\"openwbtopic\": \"" + OpenWB->getOpenWbTopic(this->InverterIdData->at(i).openwb) + "\"}]"; - } - ret += "}"; + if (this->Conf_EnableOpenWB && this->InverterIdData->at(i).openwb.length() > 0) { + ret += ",\"openwb\": [{\"openwbtopic\": \"" + OpenWB->getOpenWbTopic(this->InverterIdData->at(i).openwb) + "\"}]"; } - + ret += "}"; + (*firstRow) = false; + (*counter)++; i++; } @@ -1037,20 +1227,19 @@ void modbus::GetLiveDataAsJson(AsyncWebServerRequest *request) { uint16_t i = *counter - this->InverterIdData->size() - 1; while (i < this->InverterLiveData->size() && ret.length() < maxLen) { - if (!(subaction == "onlyactive" && !this->InverterLiveData->at(i).active)) { - if(*counter > 1) ret += ","; - ret += "{\"name\": \"" + this->InverterLiveData->at(i).Name + "\","; - ret += "\"realname\": \"" + this->InverterLiveData->at(i).RealName + "\","; - ret += "\"value\": \"" + this->InverterLiveData->at(i).value + " " + this->InverterLiveData->at(i).unit + "\","; - ret += "\"active\": {\"checked\": " + String(this->InverterLiveData->at(i).active ? 1 : 0) + ", \"name\": \"" + this->InverterLiveData->at(i).Name + "\"},"; - ret += "\"mqtttopic\": \"" + this->mqtt->getTopic(this->InverterLiveData->at(i).Name, false) + "\""; + if(!(*firstRow)) ret += ","; + ret += "{\"name\": \"" + this->InverterLiveData->at(i).Name + "\","; + ret += "\"realname\": \"" + this->InverterLiveData->at(i).RealName + "\","; + ret += "\"value\": {\"innerHTML\": \"" + this->InverterLiveData->at(i).value + " " + this->InverterLiveData->at(i).unit + "\", \"data-id\": \"" + this->InverterLiveData->at(i).Name + ".value" + "\"},"; + ret += "\"active\": {\"checked\": " + String(this->InverterLiveData->at(i).active ? 1 : 0) + ", \"name\": \"" + this->InverterLiveData->at(i).Name + "\"},"; + ret += "\"mqtttopic\": \"" + this->mqtt->getTopic(this->InverterLiveData->at(i).Name, false) + "\""; - if (this->Conf_EnableOpenWB && this->InverterLiveData->at(i).openwb.length() > 0) { - ret += ",\"openwb\": [{\"openwbtopic\": \"" + OpenWB->getOpenWbTopic(this->InverterLiveData->at(i).openwb) + "\"}]"; - } - ret += "}"; - } - + if (this->Conf_EnableOpenWB && this->InverterLiveData->at(i).openwb.length() > 0) { + ret += ",\"openwb\": [{\"openwbtopic\": \"" + OpenWB->getOpenWbTopic(this->InverterLiveData->at(i).openwb) + "\"}]"; + } + ret += "}"; + (*firstRow) = false; + (*counter)++; i++; } @@ -1070,79 +1259,107 @@ void modbus::GetLiveDataAsJson(AsyncWebServerRequest *request) { request->send(response); } - /******************************************************* * Return all LiveData as jsonArray - * {data: [{"name": "xx", "value": "xx"}], } - * {"GridVoltage_R":"0.00 V","GridCurrent_R":"0.00 A","GridPower_R":"0 W","GridFrequency_R":"0.00 Hz","GridVoltage_S":"0.90 V","GridCurrent_S":"1715.40 A","GridPower_S":"-28671 W","GridFrequency_S":"174.08 Hz","GridVoltage_T":"0.00 V","GridCurrent_T":"0.00 A","GridPower_T":"0 W","GridFrequency_T":"1.30 Hz","PvVoltage1":"259.80 V","PvVoltage2":"0.00 V","PvCurrent1":"1.00 A","PvCurrent2":"0.00 A","Temperature":"28 °C","PowerPv1":"283 W","PowerPv2":"0 W","BatVoltage":"0.00 V","BatCurrent":"0.00 A","BatPower":"0 W","BatTemp":"0 °C","BatCapacity":"0 %","OutputEnergyChargeWh":"0 Wh","OutputEnergyChargeKWh":"0.00 KWh","OutputEnergyChargeToday":"0.00 KWh","InputEnergyChargeWh":"0 Wh","InputEnergyChargeKWh":"0.00 KWh"} + * {data: [{"name": "xx", "value": "xx", ...}, ...] } *******************************************************/ -void modbus::GetRegisterAsJson(AsyncResponseStream *response) { - int count = 0; +void modbus::GetSettersAsJsonToWebServer(AsyncWebServerRequest *request) { + std::shared_ptr counter = std::make_shared(0); - File regfile = LittleFS.open("/regs/"+this->InverterType.filename); - if (!regfile) { - Config->log(1, "failed to open %s file", this->InverterType.filename.c_str()); - return; + if(request->hasArg("json")) { + const String json = request->arg("json"); + Config->logN(4, "[GetSetterAsJson] Json command empfangen: %s", json.c_str()); + + JsonDocument jsonGet; + DeserializationError error = deserializeJson(jsonGet, json.c_str()); + + if (error) { + Config->logN(2, "[GetSetterAsJson] Json Command not parseable: %s -> %s", json.c_str(), error.c_str()); + } } - response->print("{\"data\": ["); - - String streamString = ""; - streamString = "\""+ this->InverterType.name +"\": {"; - regfile.find(streamString.c_str()); + AsyncWebServerResponse *response = request->beginChunkedResponse("application/json", [this, counter](uint8_t *buffer, size_t maxLen, size_t index) { + String ret(""); + ret.reserve(maxLen); + maxLen -= 500; // use a puffer of 500 bytes, every item is assumed to be 200 bytes - streamString = "\"livedata\": ["; - regfile.find(streamString.c_str()); + if (*counter == 0) { + // send start of JSON + ret += "{\"globalEnabled\": \""+ String(this->Conf_EnableSetters) +"\", \"data\": {\"setitems\": ["; + (*counter)++; + } - do { - JsonDocument elem; - DeserializationError error = deserializeJson(elem, regfile); - - if (!error) { - // Print the result - Config->log(4, "parsing JSON ok"); - Config->log(5, elem); - } else { - Config->log(4, "(Function GetRegisterAsJson) Failed to parse JSON Register Data: %s", error.c_str()); - } - - String s = ""; - serializeJson(elem, s); - if(count>0) response->print(", "); - response->print(s); - count++; + File regfile = this->_sysFS.open("/regs/"+this->InverterType.filename); + if (!regfile) { + Config->logN(1, "failed to open %s file", this->InverterType.filename.c_str()); + return 0; + } - } while (regfile.findUntil(",","]")); - //Lazgar - streamString = ""; - streamString = "\""+ this->InverterType.name +"\": {"; - regfile.find(streamString.c_str()); + String streamString = ""; + uint16_t itemIterator = 0; - streamString = "\"id\": ["; - regfile.find(streamString.c_str()); + streamString = "\""+ this->InverterType.name +"\": {"; + regfile.find(streamString.c_str()); + + streamString = "\"set\": ["; + regfile.find(streamString.c_str()); - do { - JsonDocument elem; - DeserializationError error = deserializeJson(elem, regfile); + do { + JsonDocument elem; + DeserializationError error = deserializeJson(elem, regfile); - if (!error) { - // Print the result - Config->log(4, "parsing JSON ok"); - Config->log(5, elem); - } else { - Config->log(1, "(Function GetRegisterAsJson) Failed to parse JSON Register Data: %s", error.c_str()); - } + if (error) { + Config->logN(1, "(Function GetSettersAsJsonToWebServer) Failed to parse JSON Register Data: %s", error.c_str()); + break; + } - String s = ""; - serializeJson(elem, s); - if(count>0) response->print(", "); - response->print(s); - count++; + Config->logN(4, "parsing JSON ok"); + Config->log(5, elem); - } while (regfile.findUntil(",","]")); - //Lazgar - if (regfile) { regfile.close(); } - response->print("]}"); + if (itemIterator == (*counter - 1) && ret.length() < maxLen) { + bool isActive = false; // default + + //check if setter is active + for (uint8_t i = 0; i < this->Setters->size(); i++) { + if (this->Setters->at(i).Name == elem["name"].as()) { + isActive = this->Setters->at(i).active; + break; + } + } + + if(*counter > 1) ret += ","; + String mapping = elem["mapping"].as(); mapping.replace("\"", "'"); + + ret += "{\"name\": \"" + elem["name"].as() + "\","; + ret += "\"realname\": {\"innerHTML\": \"" + elem["realname"].as() + "\""; + if (elem["info"]) ret += ", \"data-info\": \"" + elem["info"].as() + "\""; + ret += "},"; + ret += "\"active\": {\"checked\": " + String(isActive ? 1 : 0) + ", \"name\": \"" + elem["name"].as() + "\"},"; + ret += "\"subscription\": {\"innerHTML\": \"" + this->GetMqttSetTopic(elem["name"].as()) + "\""; + if (elem["mapping"]) ret += ", \"data-mapping\": \""+ mapping + "\""; + ret += "}}"; + + (*counter)++; + } + + itemIterator++; + + } while (regfile.findUntil(",","]")); + + if (regfile) { regfile.close(); } + + if (itemIterator == (*counter - 1)) { + // send end of JSON + ret += " ]}, \"object_id\": \"" + Config->GetMqttBasePath() + "/" + Config->GetMqttRoot() + "\"}"; + (*counter)++; + } + + int len = sprintf((char*)buffer, ret.c_str()); + return len; + + }); + + request->send(response); } /******************************************************* @@ -1152,18 +1369,35 @@ void modbus::GetRegisterAsJson(AsyncResponseStream *response) { void modbus::SetItemActiveStatus(String item, bool newstate) { for (uint16_t j=0; j < this->InverterLiveData->size(); j++) { if (this->InverterLiveData->at(j).Name == item) { - Config->log(3, "Set Item <%s> ActiveState to %s", item.c_str(), (newstate?"true":"false")); + Config->logN(3, "Set Item <%s> ActiveState to %s", item.c_str(), (newstate?"true":"false")); this->InverterLiveData->at(j).active = newstate; + return; } } - //Lazgar + for (uint16_t j=0; j < this->InverterIdData->size(); j++) { if (this->InverterIdData->at(j).Name == item) { - Config->log(3, "Set Item <%s> ActiveState to %s", item.c_str(), (newstate?"true":"false")); + Config->logN(3, "Set Item <%s> ActiveState to %s", item.c_str(), (newstate?"true":"false")); this->InverterIdData->at(j).active = newstate; + return; } } - //Lazgar + + for (uint16_t j=0; j < this->Setters->size(); j++) { + if (this->Setters->at(j).Name == item) { + Config->logN(3, "Set Item <%s> ActiveState to %s", item.c_str(), (newstate?"true":"false")); + if (this->mqtt && this->Setters->at(j).active != newstate) { + if (!newstate) { + this->mqtt->UnSubscribe(this->GetMqttSetTopic(this->Setters->at(j).Name)); + } else { + this->mqtt->Subscribe(this->GetMqttSetTopic(this->Setters->at(j).Name)); + } + } + this->Setters->at(j).active = newstate; + return; + } + } + } /******************************************************* @@ -1201,11 +1435,11 @@ void modbus::loop() { void modbus::LoadRegItems(std::vector* vector, String type) { vector->clear(); - Config->log(4, "Load RegItems for Inverter %s and type <%s>", this->InverterType.name.c_str(), type.c_str()); + Config->logN(4, "Load RegItems for Inverter %s and type <%s>", this->InverterType.name.c_str(), type.c_str()); - File regfile = LittleFS.open("/regs/"+this->InverterType.filename); + File regfile = this->_sysFS.open("/regs/"+this->InverterType.filename); if (!regfile) { - Config->log(1, "failed to open %s file", this->InverterType.filename.c_str()); + Config->logN(1, "failed to open %s file", this->InverterType.filename.c_str()); return; } @@ -1221,10 +1455,10 @@ void modbus::LoadRegItems(std::vector* vector, String type) { if (!error) { // Print the result - Config->log(4, "parsing JSON ok"); + Config->logN(4, "parsing JSON ok"); Config->log(5, elem); } else { - Config->log(1, "(Function LoadRegItems) Failed to parse JSON Register Data for Inverter <%s> and type <%s>: %s", this->InverterType.name.c_str(), type.c_str(), error.c_str()); + Config->logN(1, "(Function LoadRegItems) Failed to parse JSON Register Data for Inverter <%s> and type <%s>: %s", this->InverterType.name.c_str(), type.c_str(), error.c_str()); } reg_t d = {}; @@ -1251,7 +1485,7 @@ void modbus::LoadRegItems(std::vector* vector, String type) { d.active = false; // set initial vector->push_back(d); - Config->log(4, "processed RegItem: %s", d.Name.c_str()); + Config->logN(4, "processed RegItem: %s", d.Name.c_str()); } while (regfile.findUntil(",","]")); @@ -1271,13 +1505,16 @@ void modbus::LoadJsonConfig(bool firstrun) { uint8_t pin_Relay1_old = this->pin_Relay1; uint8_t pin_Relay2_old = this->pin_Relay2; bool enableRelays_old = this->enableRelays; + bool enableSetters_old = this->Conf_EnableSetters; + + Config->disabledGPIO.deleteAll(BaseConfig::GpioIdentifier::MODBUS); - if (LittleFS.exists("/config/modbusconfig.json")) { + if (this->_configFS.exists("/modbusconfig.json")) { //file exists, reading and loading - Config->log(3, "reading config file...."); - File configFile = LittleFS.open("/config/modbusconfig.json", "r"); + Config->logN(3, "reading config file...."); + File configFile = this->_configFS.open("/modbusconfig.json", "r"); if (configFile) { - Config->log(3, "config file is open:"); + Config->logN(3, "config file is open:"); //size_t size = configFile.size(); JsonDocument doc; @@ -1299,6 +1536,7 @@ void modbus::LoadJsonConfig(bool firstrun) { if (doc["data"]["openwbversion"]) { this->Conf_OpenWBVersion = doc["data"]["openwbversion"].as(); this->OpenWB->setVersion(this->Conf_OpenWBVersion); } if (doc["data"]["openwbmodulid"]) { this->Conf_OpenWBModulID = doc["data"]["openwbmodulid"].as(); this->OpenWB->addMapping("InverterID", String(this->Conf_OpenWBModulID)); } if (doc["data"]["openwbbatteryid"]) { this->Conf_OpenWBBatteryID = doc["data"]["openwbbatteryid"].as(); this->OpenWB->addMapping("BatteryID", String(this->Conf_OpenWBBatteryID)); } + if (doc["data"]["openwbmeterid"]) { this->Conf_OpenWBMeterID = doc["data"]["openwbmeterid"].as(); this->OpenWB->addMapping("SmartMeterID", String(this->Conf_OpenWBMeterID)); } this->Conf_EnableOpenWB = doc["data"]["enableOpenWb"].as(); this->Conf_EnableSetters = doc["data"]["enable_setters"].as(); @@ -1311,7 +1549,8 @@ void modbus::LoadJsonConfig(bool firstrun) { for (uint8_t i=0; iAvailableInverters->size(); i++) { if (this->AvailableInverters->at(i).name == (doc["data"]["invertertype"]).as()) { this->InverterType = this->AvailableInverters->at(i); - Config->log(3, "Invertertyp '%s' was found in register file '%s', set it as selected active Inverter", this->InverterType.name.c_str(), this->InverterType.filename.c_str()); + + Config->logN(3, "Invertertyp '%s' was found in register file '%s', set it as selected active Inverter", this->InverterType.name.c_str(), this->InverterType.filename.c_str()); found = true; } } @@ -1319,18 +1558,18 @@ void modbus::LoadJsonConfig(bool firstrun) { if (this->AvailableInverters->size()>0) { this->InverterType = this->AvailableInverters->at(0); } - Config->log(3, "Invertertyp '%s' was not found, use default '%s' instead", (doc["data"]["invertertype"]).as().c_str(), this->InverterType.name.c_str()); + Config->logN(3, "Invertertyp '%s' was not found, use default '%s' instead", (doc["data"]["invertertype"]).as().c_str(), this->InverterType.name.c_str()); } } } else { - Config->log(1, "failed to load modbus json config, load default config"); + Config->logN(1, "failed to load modbus json config, load default config"); loadDefaultConfig = true; } configFile.close(); } } else { - Config->log(3, "modbusconfig.json config File not exists, load default config"); + Config->logN(3, "modbusconfig.json config File not exists, load default config"); loadDefaultConfig = true; } @@ -1356,7 +1595,7 @@ void modbus::LoadJsonConfig(bool firstrun) { loadDefaultConfig = false; //set back } - // ReInit if Baudrate was changed, not at firstrun! + // ReInit if basics has been changed, not at firstrun! if(!firstrun && ( (Baudrate_old != this->Baudrate) || (pin_RX_old != this->pin_RX) || @@ -1364,31 +1603,38 @@ void modbus::LoadJsonConfig(bool firstrun) { (pin_RTS_old != this->pin_RTS) || (enableRelays_old != this->enableRelays) || (pin_Relay1_old != this->pin_Relay1) || - (pin_Relay2_old != this->pin_Relay2))) { + (pin_Relay2_old != this->pin_Relay2) || + (InverterType_old.name != this->InverterType.name))) { this->init(false); } - // ReInit if Invertertype was changed - if(!firstrun && ( - InverterType_old.name != this->InverterType.name) ) { - - this->init(false); + if (enableSetters_old != this->Conf_EnableSetters) { + this->LoadSettersFromRegFile(); + this->LoadJsonItemConfig(false, false, true); // load only Setters } + Config->disabledGPIO.addValue(this->pin_RX, BaseConfig::GpioIdentifier::MODBUS); + Config->disabledGPIO.addValue(this->pin_TX, BaseConfig::GpioIdentifier::MODBUS); + Config->disabledGPIO.addValue(this->pin_RTS, BaseConfig::GpioIdentifier::MODBUS); + } /******************************************************* * load Modbus Item configuration from file *******************************************************/ -void modbus::LoadJsonItemConfig() { - - if (LittleFS.exists("/config/modbusitemconfig.json")) { +void modbus::LoadJsonItemConfig() { + this->LoadJsonItemConfig(true, true, true); +} + +void modbus::LoadJsonItemConfig(bool loadLiveData, bool loadIdData, bool loadSetters) { + + if (this->_configFS.exists("/modbusitemconfig.json")) { //file exists, reading and loading - Config->log(3, "reading modbus item config file...."); - File configFile = LittleFS.open("/config/modbusitemconfig.json", "r"); + Config->logN(3, "reading modbus item config file...."); + File configFile = this->_configFS.open("/modbusitemconfig.json", "r"); if (configFile) { - Config->log(3, "modbus item config file is open:"); + Config->logN(3, "modbus item config file is open:"); ReadBufferingStream stream{configFile, 64}; stream.find("\"data\":["); @@ -1398,57 +1644,72 @@ void modbus::LoadJsonItemConfig() { if (!error) { // Print the result - Config->log(4, "parsing JSON ok"); + Config->logN(4, "parsing JSON ok"); Config->log(5, elem); } else { - Config->log(1, "(Function LoadJsonItemConfig) Failed to parse JSON Register Data: %s", error.c_str()); + Config->logN(1, "(Function LoadJsonItemConfig) Failed to parse JSON Register Data: %s", error.c_str()); } for (JsonPair kv : elem.as()) { const char* ItemName = kv.key().c_str(); + + /* handle LiveData */ + if (loadLiveData) { + for(uint16_t i=0; iInverterLiveData->size(); i++) { + if (this->InverterLiveData->at(i).Name == ItemName ) { + this->InverterLiveData->at(i).active = kv.value().as(); - for(uint16_t i=0; iInverterLiveData->size(); i++) { - if (this->InverterLiveData->at(i).Name == ItemName ) { - this->InverterLiveData->at(i).active = kv.value().as(); + Config->logN(3, "item %s -> %s", ItemName, (this->InverterLiveData->at(i).active?"enabled":"disabled")); - Config->log(3, "item %s -> %s", ItemName, (this->InverterLiveData->at(i).active?"enabled":"disabled")); + break; + } + } + } - break; + if (loadIdData) { + /* handle IdData */ + for(uint16_t i=0; iInverterIdData->size(); i++) { + if (this->InverterIdData->at(i).Name == ItemName ) { + this->InverterIdData->at(i).active = kv.value().as(); + + Config->logN(3, "item %s -> %s", ItemName, (this->InverterIdData->at(i).active?"enabled":"disabled")); + break; + } } } - //Lazgar - for(uint16_t i=0; iInverterIdData->size(); i++) { - if (this->InverterIdData->at(i).Name == ItemName ) { - this->InverterIdData->at(i).active = kv.value().as(); - Config->log(3, "item %s -> %s", ItemName, (this->InverterIdData->at(i).active?"enabled":"disabled")); - break; + if (loadSetters) { + /* handle Setters */ + for(uint16_t i=0; iSetters->size(); i++) { + if (this->Setters->at(i).Name == ItemName ) { + this->Setters->at(i).active = kv.value().as(); + Config->logN(3, "setter %s -> %s", ItemName, (this->Setters->at(i).active?"enabled":"disabled")); + break; + } } } - //Lazgar + } } while (stream.findUntil(",","]")); configFile.close(); } else { - Config->log(1, "failed to load modbusitemconfig.json, load default item config"); + Config->logN(1, "failed to load modbusitemconfig.json, load default item config"); } } else { - Config->log(3, "modbusitemconfig.json config File not exists, all items are inactive as default"); + Config->logN(3, "modbusitemconfig.json config File not exists, all items are inactive as default"); } } /******************************************************************************************************* * WebContent *******************************************************************************************************/ -void modbus::GetInitData(AsyncResponseStream *response) { - String ret; - JsonDocument json; +void modbus::GetInitData(JsonDocument &json){ json["data"].to(); json["data"]["GpioPin_RX"] = this->pin_RX; json["data"]["GpioPin_TX"] = this->pin_TX; json["data"]["GpioPin_RTS"] = this->pin_RTS; - json["data"]["clientid"] = this->ClientID; + json["data"]["clientid"] = String(this->ClientID, HEX); json["data"]["baudrate"] = this->Baudrate; json["data"]["txintervallive"] = this->TxIntervalLiveData; json["data"]["txintervalid"] = this->TxIntervalIdData; @@ -1459,6 +1720,7 @@ void modbus::GetInitData(AsyncResponseStream *response) { json["data"]["enableOpenWb"] = ((this->Conf_EnableOpenWB)?1:0); json["data"]["openwbmodulid"] = this->Conf_OpenWBModulID; json["data"]["openwbbatteryid"] = this->Conf_OpenWBBatteryID; + json["data"]["openwbmeterid"] = this->Conf_OpenWBMeterID; json["data"]["enableCrcCheck"] = ((this->enableCrcCheck)?1:0); json["data"]["enableLengthCheck"] = ((this->enableLengthCheck)?1:0); @@ -1483,15 +1745,11 @@ void modbus::GetInitData(AsyncResponseStream *response) { json["response"].to(); json["response"]["status"] = 1; json["response"]["text"] = "successful"; - serializeJson(json, ret); - response->print(ret); } -void modbus::GetInitRawData(AsyncResponseStream *response) { - String ret = ""; +void modbus::GetInitRawData(JsonDocument& json) { std::ostringstream id, live; - JsonDocument json; - + live << std::hex << std::uppercase; id << std::hex << std::uppercase; @@ -1510,7 +1768,4 @@ void modbus::GetInitRawData(AsyncResponseStream *response) { json["response"].to(); json["response"]["status"] = 1; json["response"]["text"] = "successful"; - serializeJson(json, ret); - - response->print(ret); } diff --git a/src/modbus.h b/src/modbus.h index 438911c2..ee281391 100644 --- a/src/modbus.h +++ b/src/modbus.h @@ -1,9 +1,13 @@ +/******************************************************** + * Copyright [2024] Tobias Faust +#include +#include #include #include #include @@ -13,7 +17,7 @@ #include #include -//#define DEBUGMODE +#define DEBUGMODE class modbus { @@ -27,9 +31,9 @@ class modbus { } reg_t; typedef struct { - String command = ""; - std::vector request; - } subscription_t; + String Name; + bool active = false; + } setter_t; // available inverter register json files typedef struct { @@ -43,25 +47,32 @@ class modbus { #define DATAISLIVE (byte) 0x02 public: - modbus(); + modbus(fs::LittleFSFS& sysFS, fs::LittleFSFS& configFS); void init(bool firstrun); void LoadJsonConfig(bool firstrun); void LoadJsonItemConfig(); + void LoadJsonItemConfig(bool loadLiveData, bool loadIdData, bool loadSetters); void loop(); const String& GetInverterType() const {return InverterType.name;} const String GetOpenWbVersion() const {return Conf_OpenWBVersion;} - void enableMqtt(MQTT* object); - void GetInitData(AsyncResponseStream *response); - void GetInitRawData(AsyncResponseStream *response); + void GetInitData(JsonDocument& json); + void GetInitRawData(JsonDocument& json); String GetInverterSN(); - - void GetLiveDataAsJson(AsyncWebServerRequest *request); - void GetRegisterAsJson(AsyncResponseStream *response); + void GetLiveDataAsJsonToWebServer(AsyncWebServerRequest *request); + void GetSettersAsJsonToWebServer(AsyncWebServerRequest *request); void SetItemActiveStatus(String item, bool newstate); - void ReceiveMQTT(String topic, int msg); + void ReceiveMQTT(String topic, String msg); + JsonDocument GetSetterByName(String name); + + // callbacks + /************************ + * @brief Callback for getting the values + * @param function(JsonDocument&) the callback function + ************************/ + void onValues(std::function callback, std::list* options); private: uint8_t pin_RX; // Serial Receive pin @@ -94,11 +105,13 @@ class modbus { std::vector* DataFrame; // storing read results as hexdata to parse std::vector* InverterIdData; // storing readable results std::vector* InverterLiveData; // storing readable results - std::vector*AvailableInverters; // available inverters from JSON - std::vector* Setters; // available set Options from JSON register + std::vector* AvailableInverters; // available inverters from JSON + std::vector* Setters; // available set Options from JSON register MQTT* mqtt = NULL; openwb* OpenWB = NULL; + fs::LittleFSFS& _sysFS; + fs::LittleFSFS& _configFS; String PrintHex(byte num); String PrintDataFrame(std::vector* frame); @@ -114,14 +127,16 @@ class modbus { void ParseData(); void LoadInvertersFromJson(); void LoadInverterConfigFromJson(); - void GenerateMqttSubscriptions(); + void LoadSettersFromRegFile(); String GetMqttSetTopic(String command); void ChangeRegItem(std::vector* vector, reg_t item); void LoadRegItems(std::vector* vector, String type); - String MapItem(JsonArray map, String value); + String MapItem(JsonArray map, String value, bool isSetter); String MapBitwise(JsonArray map, String value); String ConvertIntToBinaryString(int n, int numBits); void ReadRelays(); + void SendDataToWebSocket(std::vector* vector); + std::vector splitStringToVector(String msg); // inverter config, in sync with register.h ->config ArduinoQueue>* ReadQueue; @@ -129,6 +144,7 @@ class modbus { std::vector>* Conf_RequestLiveData; std::vector>* Conf_RequestIdData; + uint8_t Conf_ClientIdPos; //uint8_t Conf_LiveDataStartsAtPos; //uint8_t Conf_IdDataStartsAtPos; @@ -149,6 +165,7 @@ class modbus { String Conf_OpenWBVersion; uint8_t Conf_OpenWBModulID; uint8_t Conf_OpenWBBatteryID; + uint8_t Conf_OpenWBMeterID; bool Conf_EnableSetters; byte String2Byte(String s); @@ -158,6 +175,9 @@ class modbus { HardwareSerial* RS485Serial; + std::function onValuesCallback; // Callback function pointer + std::list* onValuesOptions = nullptr; + }; extern modbus* mb; diff --git a/src/mqtt.cpp b/src/mqtt.cpp index 2f1cc479..d7f02437 100644 --- a/src/mqtt.cpp +++ b/src/mqtt.cpp @@ -1,25 +1,27 @@ -#include "mqtt.h" - -MQTT::MQTT(const char* MqttServer, uint16_t MqttPort, String MqttBasepath, String MqttRoot, char* APName, char* APpassword): - improvSerial(&Serial), - mqtt_root(MqttRoot), - mqtt_basepath(MqttBasepath), - ConnectStatusWifi(false), - ConnectStatusMqtt(false) -{ - +/******************************************************** + * Copyright [2024] Tobias Faust + +MQTT::MQTT(const char* MqttServer, uint16_t MqttPort, String MqttBasepath, String MqttRoot): + improvSerial(&Serial), + mqtt_root(MqttRoot), + mqtt_basepath(MqttBasepath), + ConnectStatusWifi(false), + ConnectStatusMqtt(false) { this->subscriptions = new std::vector{}; - + WiFi.setHostname(this->mqtt_root.c_str()); -#ifdef ESP32 +#ifdef ESP32 WiFi.onEvent(std::bind(&MQTT::WifiOnEvent, this, std::placeholders::_1)); #endif - Config->log(3, "Go into %s Mode", (Config->GetUseETH()?"ETH":"Wifi")); + Config->logN(3, "Go into %s Mode", (Config->GetUseETH()?"ETH":"Wifi")); ImprovTypes::ChipFamily variant; - + #ifdef ESP32 String variantString = ARDUINO_VARIANT; #else @@ -38,44 +40,52 @@ MQTT::MQTT(const char* MqttServer, uint16_t MqttPort, String MqttBasepath, Strin variant = ImprovTypes::ChipFamily::CF_ESP32; } - improvSerial.setDeviceInfo(variant, String(GIT_REPO).c_str(), Config->GetReleaseName().c_str(), Config->GetMqttRoot().c_str()); - improvSerial.onImprovError(std::bind(&MQTT::onImprovWiFiErrorCb, this, std::placeholders::_1)); - + improvSerial.setDeviceInfo(variant, + String(GIT_REPO).c_str(), + Config->GetReleaseName().c_str(), + Config->GetMqttRoot().c_str()); + improvSerial.onImprovError(std::bind(&MQTT::onImprovWiFiErrorCb, + this, + std::placeholders::_1)); + if (Config->GetUseETH()) { #ifdef ESP32 eth_shield_t* shield = this->GetEthShield(Config->GetLANBoard()); - - //ETH.begin(1, 16, 23, 18, ETH_PHY_LAN8720, ETH_CLOCK_GPIO0_IN); + + // reserve all ETH pins + Config->disabledGPIO.addValues(shield->blockedGpio, BaseConfig::GpioIdentifier::ETH); + + // ETH.begin(1, 16, 23, 18, ETH_PHY_LAN8720, ETH_CLOCK_GPIO0_IN); ETH.begin(shield->PHY_ADDR, shield->PHY_POWER, shield->PHY_MDC, shield->PHY_MDIO, shield->PHY_TYPE, shield->CLK_MODE); - + this->WaitForConnect(); #endif } else { // use Wifi + Config->disabledGPIO.deleteAll(BaseConfig::GpioIdentifier::ETH); // free all ETH pins improvSerial.ConnectToWifi(); } - if (Config->GetDebugLevel() >=4) WiFi.printDiag(dbg); + //if (Config->GetDebugLevel() >=4) WiFi.printDiag(Serial); - Config->log(1, "Initializing MQTT (%s:%d)", Config->GetMqttServer().c_str(), Config->GetMqttPort()); + Config->logN(1, "Initializing MQTT (%s:%d)", Config->GetMqttServer().c_str(), Config->GetMqttPort()); espClient = WiFiClient(); - + PubSubClient::setClient(espClient); PubSubClient::setServer(Config->GetMqttServer().c_str(), Config->GetMqttPort()); } -void MQTT::onImprovWiFiErrorCb(ImprovTypes::Error err) -{ - if(err == ImprovTypes::Error::ERROR_WIFI_DISCONNECTED) { +void MQTT::onImprovWiFiErrorCb(ImprovTypes::Error err) { + if (err == ImprovTypes::Error::ERROR_WIFI_DISCONNECTED) { this->disconnect(); } - if(err == ImprovTypes::Error::ERROR_WIFI_CONNECT_GIVEUP) { + if (err == ImprovTypes::Error::ERROR_WIFI_CONNECT_GIVEUP) { Serial.println("Giving up on connecting to WiFi, restart the device"); ESP.restart(); } @@ -83,102 +93,101 @@ void MQTT::onImprovWiFiErrorCb(ImprovTypes::Error err) #ifdef ESP32 void MQTT::WifiOnEvent(WiFiEvent_t event) { - Config->log(4, "[WiFi-event] event: %d", event); + Config->logN(4, "[WiFi-event] event: %d", event); switch (event) { - case ARDUINO_EVENT_WIFI_READY: - Config->log(1, "WiFi interface ready"); + case ARDUINO_EVENT_WIFI_READY: + Config->logN(1, "WiFi interface ready"); break; case ARDUINO_EVENT_WIFI_SCAN_DONE: - Config->log(1, "Completed scan for access points"); + Config->logN(1, "Completed scan for access points"); break; case ARDUINO_EVENT_WIFI_STA_START: - Config->log(1, "WiFi client started"); + Config->logN(1, "WiFi client started"); break; case ARDUINO_EVENT_WIFI_STA_STOP: - Config->log(1, "WiFi clients stopped"); + Config->logN(1, "WiFi clients stopped"); break; case ARDUINO_EVENT_WIFI_STA_CONNECTED: - Config->log(1, "Connected to access point"); + Config->logN(1, "Connected to access point"); break; case ARDUINO_EVENT_WIFI_STA_DISCONNECTED: - Config->log(1, "Disconnected from WiFi access point"); + Config->logN(1, "Disconnected from WiFi access point"); this->ConnectStatusWifi = false; break; case ARDUINO_EVENT_WIFI_STA_AUTHMODE_CHANGE: - Config->log(1, "Authentication mode of access point has changed"); + Config->logN(1, "Authentication mode of access point has changed"); break; case ARDUINO_EVENT_WIFI_STA_GOT_IP: - Config->log(1, "WiFi connected with local IP: %s", WiFi.localIP().toString().c_str()); + Config->logN(1, "WiFi connected with local IP: %s", WiFi.localIP().toString().c_str()); this->ipadresse = WiFi.localIP(); this->ConnectStatusWifi = true; break; case ARDUINO_EVENT_WIFI_STA_LOST_IP: - Config->log(1, "Lost IP address and IP address is reset to 0"); + Config->logN(1, "Lost IP address and IP address is reset to 0"); this->ConnectStatusWifi = false; - this->ipadresse = (0,0,0,0); + this->ipadresse = IPAddress(0, 0, 0, 0); break; case ARDUINO_EVENT_WPS_ER_SUCCESS: - Config->log(1, "WiFi Protected Setup (WPS): succeeded in enrollee mode"); + Config->logN(1, "WiFi Protected Setup (WPS): succeeded in enrollee mode"); break; case ARDUINO_EVENT_WPS_ER_FAILED: - Config->log(1, "WiFi Protected Setup (WPS): failed in enrollee mode"); + Config->logN(1, "WiFi Protected Setup (WPS): failed in enrollee mode"); break; case ARDUINO_EVENT_WPS_ER_TIMEOUT: - Config->log(1, "WiFi Protected Setup (WPS): timeout in enrollee mode"); + Config->logN(1, "WiFi Protected Setup (WPS): timeout in enrollee mode"); break; case ARDUINO_EVENT_WPS_ER_PIN: - Config->log(1, "WiFi Protected Setup (WPS): pin code in enrollee mode"); + Config->logN(1, "WiFi Protected Setup (WPS): pin code in enrollee mode"); break; case ARDUINO_EVENT_WIFI_AP_START: - Config->log(1, "WiFi access point started"); + Config->logN(1, "WiFi access point started"); break; case ARDUINO_EVENT_WIFI_AP_STOP: - Config->log(1, "WiFi access point stopped"); + Config->logN(1, "WiFi access point stopped"); break; case ARDUINO_EVENT_WIFI_AP_STACONNECTED: - Config->log(1, "Client connected"); + Config->logN(1, "Client connected"); break; case ARDUINO_EVENT_WIFI_AP_STADISCONNECTED: - Config->log(1, "Client disconnected"); + Config->logN(1, "Client disconnected"); break; case ARDUINO_EVENT_WIFI_AP_STAIPASSIGNED: - Config->log(1, "Assigned IP address to client"); + Config->logN(1, "Assigned IP address to client"); break; case ARDUINO_EVENT_WIFI_AP_PROBEREQRECVED: - Config->log(1, "Received probe request"); + Config->logN(1, "Received probe request"); break; case ARDUINO_EVENT_WIFI_AP_GOT_IP6: - Config->log(1, "AP IPv6 is preferred"); + Config->logN(1, "AP IPv6 is preferred"); break; case ARDUINO_EVENT_WIFI_STA_GOT_IP6: - Config->log(1, "STA IPv6 is preferred"); + Config->logN(1, "STA IPv6 is preferred"); break; case ARDUINO_EVENT_ETH_GOT_IP6: - Config->log(1, "Ethernet IPv6 is preferred"); + Config->logN(1, "Ethernet IPv6 is preferred"); break; case ARDUINO_EVENT_ETH_START: - Config->log(1, "Ethernet started"); + Config->logN(1, "Ethernet started"); break; case ARDUINO_EVENT_ETH_STOP: - Config->log(1, "Ethernet stopped"); + Config->logN(1, "Ethernet stopped"); break; case ARDUINO_EVENT_ETH_CONNECTED: - Config->log(1, "Ethernet connected"); + Config->logN(1, "Ethernet connected"); break; case ARDUINO_EVENT_ETH_DISCONNECTED: - Config->log(1, "Ethernet disconnected"); + Config->logN(1, "Ethernet disconnected"); this->ConnectStatusWifi = false; - this->ipadresse = (0,0,0,0); + this->ipadresse = IPAddress(0, 0, 0, 0); break; case ARDUINO_EVENT_ETH_GOT_IP: if (!this->ConnectStatusWifi) { - Config->log(1, "ETH MAC: %s, IPv4: %s, %s, Mbps: %d", - ETH.macAddress().c_str(), + Config->logN(1, "ETH MAC: %s, IPv4: %s, %s, Mbps: %d", + ETH.macAddress().c_str(), ETH.localIP().toString().c_str(), (ETH.fullDuplex()?"FULL_DUPLEX":"HALF_DUPLEX"), - ETH.linkSpeed() - ); + ETH.linkSpeed()); this->ipadresse = ETH.localIP(); this->ConnectStatusWifi = true; } @@ -192,8 +201,8 @@ void MQTT::WifiOnEvent(WiFiEvent_t event) { return LanShield parameter tuple ########################################*/ eth_shield_t* MQTT::GetEthShield(String ShieldName) { - for(uint8_t i=0; ilan_shields.size(); i++) { - if(this->lan_shields.at(i).name == ShieldName) { + for (uint8_t i = 0; i < this->lan_shields.size(); i++) { + if (this->lan_shields.at(i).name == ShieldName) { return &this->lan_shields.at(i); break; } @@ -202,10 +211,10 @@ eth_shield_t* MQTT::GetEthShield(String ShieldName) { } void MQTT::WaitForConnect() { - while (!this->ConnectStatusWifi) + while (!this->ConnectStatusWifi) { delay(100); - Config->log(1, "Wait for connect"); - //yield(); + Config->logN(1, "Wait for connect"); + } } void MQTT::reconnect() { @@ -213,32 +222,43 @@ void MQTT::reconnect() { char LWT[50]; memset(&LWT[0], 0, sizeof(LWT)); memset(&topic[0], 0, sizeof(topic)); - - if (Config->UseRandomMQTTClientID()) { + + if (Config->UseRandomMQTTClientID()) { + Config->logN(1, "Using random MQTT ClientID"); snprintf (topic, sizeof(topic), "%s-%s", this->mqtt_root.c_str(), String(random(0xffff)).c_str()); } else { + Config->logN(1, "Using fixed MQTT ClientID"); snprintf (topic, sizeof(topic), "%s-%08X", this->mqtt_root.c_str(), ESP_getChipId()); } snprintf(LWT, sizeof(LWT), "%s/state", this->mqtt_root.c_str()); - - Config->log(1, "Attempting MQTT connection as %s ", topic); - - if (PubSubClient::connect(topic, Config->GetMqttUsername().c_str(), Config->GetMqttPassword().c_str(), LWT, true, false, "Offline")) { - Config->log(1, "connected... "); + + Config->logN(1, "Attempting MQTT connection as %s ", topic); + + if (PubSubClient::connect(topic, + Config->GetMqttUsername().c_str(), + Config->GetMqttPassword().c_str(), + LWT, + true, + false, + "Offline")) { + Config->logN(1, "connected... "); // Once connected, publish basics ... this->Publish_IP(); this->Publish_String("ssid", WiFi.SSID(), false); this->Publish_String("version", Config->GetReleaseName(), false); - this->Publish_String("state", "Online", false); //LWT reset - + this->Publish_String("state", "Online", false); // LWT reset + // ... and resubscribe if needed for (uint8_t i=0; i< this->subscriptions->size(); i++) { - PubSubClient::subscribe(this->subscriptions->at(i).c_str()); - Config->log(1, "MQTT resubscribed to: %s", this->subscriptions->at(i).c_str()); + String topic = this->subscriptions->at(i); + if (topic.endsWith("/")) topic += "#"; + else topic += "/#"; + PubSubClient::subscribe(topic.c_str()); + Config->logN(1, "MQTT resubscribed to: %s", topic.c_str()); } } else { - Config->log(1, "failed, rc=%d - Trying again in 5 seconds", PubSubClient::state()); + Config->logN(1, "failed, rc=%d - Trying again in 5 seconds", PubSubClient::state()); } } @@ -247,13 +267,17 @@ void MQTT::disconnect() { } void MQTT::Publish_Bool(const char* subtopic, bool b, bool fulltopic) { - String s; - if(b) {s = "1";} else {s = "0";}; + String s(""); + if (b) { + s = "1"; + } else { + s = "0"; + } Publish_String(subtopic, s, fulltopic); } void MQTT::Publish_Int(const char* subtopic, int number, bool fulltopic) { - char buffer[20] = {0}; + char buffer[20] = {0}; memset(buffer, 0, sizeof(buffer)); snprintf(buffer, sizeof(buffer), "%d", number); Publish_String(subtopic, (String)buffer, fulltopic); @@ -271,18 +295,20 @@ void MQTT::Publish_String(const char* subtopic, String value, bool fulltopic) { if (PubSubClient::connected()) { PubSubClient::publish((const char*)topic.c_str(), value.c_str(), true); - Config->log(3, "Publish %s: %s ", topic.c_str(), value.c_str()); - } else Config->log(2, "Request for MQTT Publish, but not connected to Broker"); + Config->logN(3, "Publish %s: %s ", topic.c_str(), value.c_str()); + } else { + Config->logN(2, "Request for MQTT Publish, but not connected to Broker"); + } } String MQTT::getTopic(String subtopic, bool fulltopic) { if (!fulltopic) { - return std::move(this->mqtt_basepath + "/" + this->mqtt_root + "/" + subtopic); + return std::move((this->mqtt_basepath.length()>0?this->mqtt_basepath + "/":"") + this->mqtt_root + "/" + subtopic); } return std::move(subtopic); } -void MQTT::Publish_IP() { +void MQTT::Publish_IP() { char buffer[16] = {0}; memset(&buffer[0], 0, sizeof(buffer)); snprintf(buffer, sizeof(buffer), "%s", this->ipadresse.toString().c_str()); @@ -295,8 +321,10 @@ void MQTT::Publish_IP() { void MQTT::Subscribe(String topic) { this->subscriptions->push_back(topic); if (PubSubClient::connected()) { + if (topic.endsWith("/")) topic += "#"; + else topic += "/#"; PubSubClient::subscribe(topic.c_str()); - Config->log(3, "MQTT now subscribed to: %s", topic.c_str()); + Config->logN(3, "MQTT now subscribed to: %s", topic.c_str()); } } @@ -305,9 +333,9 @@ bool MQTT::UnSubscribe(String topic) { for (uint8_t i=0; i< this->subscriptions->size(); i++) { if (topic == this->subscriptions->at(i)) { if (PubSubClient::connected()) { - PubSubClient::unsubscribe(this->subscriptions->at(i).c_str()); + PubSubClient::unsubscribe(this->subscriptions->at(i).c_str()); } - Config->log(3, "MQTT unsubscribed from: %s", this->subscriptions->at(i).c_str()); + Config->logN(3, "MQTT unsubscribed from: %s", this->subscriptions->at(i).c_str()); this->subscriptions->erase(this->subscriptions->begin()+i); ret = true; break; @@ -317,77 +345,94 @@ bool MQTT::UnSubscribe(String topic) { } void MQTT::ClearSubscriptions() { - for ( uint8_t i=0; i< this->subscriptions->size(); i++) { - if (PubSubClient::connected()) { - PubSubClient::unsubscribe(this->subscriptions->at(i).c_str()); + for (uint8_t i = 0; i < this->subscriptions->size(); i++) { + if (PubSubClient::connected()) { + PubSubClient::unsubscribe(this->subscriptions->at(i).c_str()); } } this->subscriptions->clear(); this->subscriptions->shrink_to_fit(); } -void MQTT::loop() { +void MQTT::loop() { improvSerial.loop(); - + #ifdef ESP8266 - if (WiFi.status() == WL_CONNECTED) { + if (WiFi.status() == WL_CONNECTED && !this->ConnectStatusWifi) { this->ConnectStatusWifi = true; this->ipadresse = WiFi.localIP(); - } else { + } + if (WiFi.status() != WL_CONNECTED && this->ConnectStatusWifi) { this->ConnectStatusWifi = false; - this->ipadresse = (0,0,0,0); + this->ipadresse = (0, 0, 0, 0); } #endif if (this->mqtt_root != Config->GetMqttRoot()) { - Config->log(3, "MQTT DeviceName has changed via Web Configuration from %s to %s ", this->mqtt_root.c_str(), Config->GetMqttRoot().c_str()); - Config->log(3, "Initiate Reconnect"); + Config->logN(3, "MQTT DeviceName has changed via Web Configuration from %s to %s ", + this->mqtt_root.c_str(), + Config->GetMqttRoot().c_str()); + Config->logN(3, "Initiate Reconnect"); this->mqtt_root = Config->GetMqttRoot(); if (PubSubClient::connected()) PubSubClient::disconnect(); } if (this->mqtt_basepath != Config->GetMqttBasePath()) { - Config->log(3, "MQTT Basepath has changed via Web Configuration from %s to %s ", this->mqtt_basepath.c_str(), Config->GetMqttBasePath().c_str()); - Config->log(3, "Initiate Reconnect"); + Config->logN(3, "MQTT Basepath has changed via Web Configuration from %s to %s ", + this->mqtt_basepath.c_str(), + Config->GetMqttBasePath().c_str()); + Config->logN(3, "Initiate Reconnect"); this->mqtt_basepath = Config->GetMqttBasePath(); if (PubSubClient::connected()) PubSubClient::disconnect(); } // WIFI ok, MQTT lost - if (!PubSubClient::connected() && this->ConnectStatusWifi) { + if (!PubSubClient::connected() && this->ConnectStatusWifi) { if (millis() - mqttreconnect_lasttry > 10000) { - this->reconnect(); + this->reconnect(); this->mqttreconnect_lasttry = millis(); } - } else if (this->ConnectStatusWifi) { + } else if (this->ConnectStatusWifi) { PubSubClient::loop(); } - if (PubSubClient::connected()) { + if (PubSubClient::connected() && !this->ConnectStatusMqtt) { this->ConnectStatusMqtt = true; - } else { + } + if (!PubSubClient::connected() && this->ConnectStatusMqtt) { this->ConnectStatusMqtt = false; } - if (Config->GetDebugLevel() >=4 && millis() - this->last_keepalive > (30 * 1000)) { + if (Config->GetKeepAlive() > 0 && millis() - this->last_keepalivemsg > (Config->GetKeepAlive() * 1000)) { + this->last_keepalivemsg = millis(); + this->Publish_String("state", "Online", false); + Config->logN(4, "KeepAlive: Publish state Online"); + } + + if (Config->GetDebugLevel() >=4 && millis() - this->last_debugmsg > (30 * 1000)) { // send messages for debugging every 30 seconds - this->last_keepalive = millis(); - + this->last_debugmsg = millis(); + if (Config->GetDebugLevel() >=4) { char buffer[100] = {0}; memset(buffer, 0, sizeof(buffer)); - snprintf(buffer, sizeof(buffer), "%d", ESP.getFreeHeap() / 1024); + snprintf(buffer, sizeof(buffer), "%d kb", ESP.getFreeHeap() / 1024); this->Publish_String("memory", buffer, false); snprintf(buffer, sizeof(buffer), "%d", WiFi.RSSI()); this->Publish_String("rssi", buffer, false); - - uint64_t uptimeMicroSeconds = esp_timer_get_time(); - uint64_t uptimeSeconds = uptimeMicroSeconds / 1000000; - this->Publish_Int("uptime",uptimeSeconds,false); + + unsigned long uptime = millis() / 1000; + unsigned int hours = uptime / 3600; + unsigned int minutes = (uptime % 3600) / 60; + unsigned int seconds = uptime % 60; + char uptimeStr[20]; + snprintf(uptimeStr, sizeof(uptimeStr), "%02d:%02d:%02d", hours, minutes, seconds); + + this->Publish_String("uptime", uptimeStr, false); } } } diff --git a/src/mqtt.h b/src/mqtt.h index ed991a99..0cd97b0e 100644 --- a/src/mqtt.h +++ b/src/mqtt.h @@ -1,53 +1,86 @@ -#ifndef MQTT_H -#define MQTT_H +/******************************************************** + * Copyright [2024] Tobias Faust #include #include -#include #include +#include +#include +#include + #ifdef ESP8266 - //#define SetHostName(x) wifi_station_set_hostname(x); - #define ESP_getChipId() ESP.getChipId() + // #define SetHostName(x) wifi_station_set_hostname(x); + #define ESP_getChipId() ESP.getChipId() #endif #ifdef ESP32 #include - //#define SetHostName(x) WiFi.getHostname(x); --> MQTT.cpp TODO - #define ESP_getChipId() (uint32_t)ESP.getEfuseMac() // Unterschied zu ESP.getFlashChipId() ??? + // #define SetHostName(x) WiFi.getHostname(x); --> MQTT.cpp TODO + #define ESP_getChipId() static_cast(ESP.getEfuseMac()) // Unterschied zu ESP.getFlashChipId() ??? #endif #ifdef ESP32 - typedef struct { - String name; - uint8_t PHY_ADDR; - int PHY_POWER; - int PHY_MDC; - int PHY_MDIO; - eth_phy_type_t PHY_TYPE; - eth_clock_mode_t CLK_MODE; - } eth_shield_t; + + struct eth_shield_t { + String name; + uint8_t PHY_ADDR; + int PHY_POWER; + int PHY_MDC; + int PHY_MDIO; + eth_phy_type_t PHY_TYPE; + eth_clock_mode_t CLK_MODE; + std::vector blockedGpio; // gpios blocked by eth device, DO not use them for other tasks! + + eth_shield_t(const String& n, + uint8_t addr, + int power, + int mdc, + int mdio, + eth_phy_type_t phy, + eth_clock_mode_t clk, + std::initializer_list blk = {}) + : name(n), + PHY_ADDR(addr), + PHY_POWER(power), + PHY_MDC(mdc), + PHY_MDIO(mdio), + PHY_TYPE(phy), + CLK_MODE(clk), + blockedGpio(blk) {} +}; + #elif defined(ESP8266) typedef struct { - String name; + String name; } eth_shield_t; #endif class MQTT: PubSubClient { - #ifdef ESP32 - std::vector lan_shields = {{"WT32-ETH01", 1, 16, 23, 18, ETH_PHY_LAN8720, ETH_CLOCK_GPIO0_IN}, - {"test", 1, 16, 23, 18, ETH_PHY_LAN8720, ETH_CLOCK_GPIO0_IN}}; + std::vector lan_shields = { + {"WT32-ETH01", 1, 16, 23, 18, ETH_PHY_LAN8720, ETH_CLOCK_GPIO0_IN, {16, 23, 18, 19, 21, 22}} + //{"TTGO T-ETH", 0, 0, 23, 18, ETH_PHY_LAN8720, ETH_CLOCK_GPIO0_IN, {2, 4, 15}}, + //{"AI-Thinker", 0, -1, 23, 18, ETH_PHY_LAN8720, ETH_CLOCK_GPIO0_IN, {2, 4, 15}}, + //{"M5Stack", 0, -1, 23, 18, ETH_PHY_LAN8720, ETH_CLOCK_GPIO0_IN, {2, 4, 15}} + }; #elif defined(ESP8266) - std::vector lan_shields = {{"test1"}, - {"test2"}}; + std::vector lan_shields = { + {"test1"}, + {"test2"}}; #endif - public: - - MQTT(const char* MqttServer, uint16_t MqttPort, String MqttBasepath, String MqttRoot, char* APName, char* APpassword); + public: + MQTT(const char* MqttServer, + uint16_t MqttPort, + String MqttBasepath, + String MqttRoot); void loop(); void Publish_Bool(const char* subtopic, bool b, bool fulltopic); void Publish_Int(const char* subtopic, int number, bool fulltopic); @@ -56,12 +89,12 @@ class MQTT: PubSubClient { void Publish_IP(); String getTopic(String subtopic, bool fulltopic); void disconnect(); - const String& GetRoot() const {return mqtt_root;}; - const String& GetBasePath() const {return mqtt_basepath;}; + const String& GetRoot() const {return mqtt_root;} + const String& GetBasePath() const {return mqtt_basepath;} void Subscribe(String topic); bool UnSubscribe(String topic); void ClearSubscriptions(); - + const bool& GetConnectStatusWifi() const {return ConnectStatusWifi;} const bool& GetConnectStatusMqtt() const {return ConnectStatusMqtt;} const IPAddress& GetIPAddress() const {return ipadresse;} @@ -70,22 +103,23 @@ class MQTT: PubSubClient { ImprovWiFi improvSerial; - protected: + protected: void reconnect(); - private: + private: WiFiClient espClient; std::vector* subscriptions = NULL; String mqtt_root = ""; String mqtt_basepath = ""; - unsigned long mqttreconnect_lasttry = 0; - unsigned long last_keepalive = 0; + uint64_t mqttreconnect_lasttry = 0; + uint64_t last_debugmsg = 0; + uint64_t last_keepalivemsg = 0; bool ConnectStatusWifi; bool ConnectStatusMqtt; IPAddress ipadresse; - + #ifdef ESP32 void WifiOnEvent(WiFiEvent_t event); #endif @@ -97,4 +131,4 @@ class MQTT: PubSubClient { extern MQTT* mqtt; -#endif +#endif // MQTT_H_ diff --git a/src/openwb.cpp b/src/openwb.cpp index 21afd554..55ec5c09 100644 --- a/src/openwb.cpp +++ b/src/openwb.cpp @@ -1,6 +1,12 @@ -#include "openwb.h" +/******************************************************** + * Copyright [2024] Tobias Faust + + +openwb::openwb(fs::LittleFSFS& fs): _fs(fs), _version("") { OpenWBTopics = new std::vector(); OpenWBVersions = new std::vector(); OpenWBMappings = new std::vector(); @@ -18,34 +24,34 @@ void openwb::setVersion(String version) { } void openwb::LoadAvailableOpenWbVersions() { - File file = LittleFS.open("/misc/openwb.json", "r"); + File file = _fs.open("/misc/openwb.json", "r"); if (!file) { - Config->log(1, "Failed to open /misc/openwb.json"); + Config->logN(1, "Failed to open /misc/openwb.json"); return; } OpenWBVersions->clear(); - + JsonDocument doc; DeserializationError error = deserializeJson(doc, file); if (error) { - Config->log(1, "Failed to parse /misc/openwb.json: %s", error.c_str()); + Config->logN(1, "Failed to parse /misc/openwb.json: %s", error.c_str()); file.close(); return; } for (JsonObject v : doc.as()) { OpenWBVersions->push_back(v["version"].as()); - Config->log(3, "OpenWB Version found: %s", v["version"].as().c_str()); + Config->logN(3, "OpenWB Version found: %s", v["version"].as().c_str()); } file.close(); } void openwb::LoadOpenWBTopicsFromJson() { - File file = LittleFS.open("/misc/openwb.json", "r"); + File file = _fs.open("/misc/openwb.json", "r"); if (!file) { - Config->log(1, "Failed to open /misc/openwb.json"); + Config->logN(1, "Failed to open /misc/openwb.json"); return; } @@ -53,7 +59,7 @@ void openwb::LoadOpenWBTopicsFromJson() { DeserializationError error = deserializeJson(doc, file); if (error) { - Config->log(1, "Failed to parse /misc/openwb.json: %s", error.c_str()); + Config->logN(1, "Failed to parse /misc/openwb.json: %s", error.c_str()); file.close(); return; } @@ -69,8 +75,7 @@ void openwb::LoadOpenWBTopicsFromJson() { t.value = kv.value().as(); this->OpenWBTopics->push_back(t); - Config->log(3, "openWB topic loaded: %s", kv.value().as().c_str()); - + Config->logN(3, "openWB topic loaded: %s", kv.value().as().c_str()); } } break; @@ -80,7 +85,7 @@ void openwb::LoadOpenWBTopicsFromJson() { file.close(); } -String openwb::getOpenWbTopic(String& key) { +const String openwb::getOpenWbTopic(const String& key) { for (uint8_t i = 0; i < this->OpenWBTopics->size(); i++) { if (this->OpenWBTopics->at(i).key == key) { String topic = this->OpenWBTopics->at(i).value; @@ -91,7 +96,7 @@ String openwb::getOpenWbTopic(String& key) { } } return ""; -} +} void openwb::addMapping(String key, String value) { for (uint8_t i = 0; i < this->OpenWBMappings->size(); i++) { @@ -105,4 +110,4 @@ void openwb::addMapping(String key, String value) { t.key = key; t.value = value; this->OpenWBMappings->push_back(t); -} \ No newline at end of file +} diff --git a/src/openwb.h b/src/openwb.h index 42bcbce4..eb77fb5b 100644 --- a/src/openwb.h +++ b/src/openwb.h @@ -1,19 +1,24 @@ -#ifndef OPENWB_H -#define SOLAXMODBUS_H +/******************************************************** + * Copyright [2024] Tobias Faust +#include +#include class openwb { - //openwb mqtt topics + + // openwb mqtt topics typedef struct { String key; String value; } openwb_t; - public: - openwb(); + public: + openwb(fs::LittleFSFS& fs); /******************************************************* * @brief initialize openWB @@ -24,21 +29,21 @@ class openwb { * @brief set the needed OpenWB API Version, used from getOpenWbVersions *******************************************************/ void setVersion(String version); - + /******************************************************* * get openWB topic from key * * @param key: key from openWB JSON * @return string: topic *******************************************************/ - String getOpenWbTopic(String& key); + const String getOpenWbTopic(const String& key); /******************************************************* * @brief Get all available openWB API Versions * * @return std::vector* ******************************************************/ - const std::vector* getOpenWbVersions() { return OpenWBVersions; } + const std::vector* getOpenWbVersions() { return OpenWBVersions; } /******************************************************* * @brief add a new Mapping for Topic Keys like #key#, @@ -56,13 +61,13 @@ class openwb { /******************************************************* * @brief clear all mappings ******************************************************/ - void clearMappings() { OpenWBMappings->clear(); } - - private: + void clearMappings() { OpenWBMappings->clear(); } - std::vector* OpenWBTopics; // openWB mqtt topics from JSON - std::vector* OpenWBVersions; // openWB available versions from JSON - std::vector* OpenWBMappings; // openWB mappings from JSON + private: + fs::LittleFSFS& _fs; + std::vector* OpenWBTopics = nullptr; // openWB mqtt topics from JSON + std::vector* OpenWBVersions = nullptr; // openWB available versions from JSON + std::vector* OpenWBMappings = nullptr; // openWB mappings from JSON String _version; @@ -70,4 +75,4 @@ class openwb { void LoadAvailableOpenWbVersions(); }; -#endif \ No newline at end of file +#endif // OPENWB_H_