diff --git a/.github/scripts/createMergedFirmware.py b/.github/scripts/createMergedFirmware.py index 301c66eb..b89ac3ad 100644 --- a/.github/scripts/createMergedFirmware.py +++ b/.github/scripts/createMergedFirmware.py @@ -31,18 +31,33 @@ 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: + bootloader_offset = bootloader_offsets[args.ChipFamily] + result = f'esptool.py --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 \ + {bootloader_offset} {args.BuildDir}/bootloader.bin \ 0x8000 {args.BuildDir}/partitions.bin \ {readOffsetFromPartitionCSV("partitions.csv", "app0")} {args.BuildDir}/firmware.bin' diff --git a/.github/scripts/myUtils.py b/.github/scripts/myUtils.py index 5f80746a..445eebc9 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 diff --git a/.github/workflows/BuildAndDeploy.yml b/.github/workflows/BuildAndDeploy.yml index aa6cdc1c..74417466 100644 --- a/.github/workflows/BuildAndDeploy.yml +++ b/.github/workflows/BuildAndDeploy.yml @@ -17,6 +17,10 @@ on: - '**.yml' - '**.sh' - '**.py' + - '**.json' + - '**.js' + - '**.css' + - '**.html' jobs: build: 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..8c91a1e1 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -1,9 +1,18 @@ +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/data/regs/Growatt-SPH-V124.json b/data/regs/Growatt-SPH-V124.json new file mode 100644 index 00000000..8fbe97fb --- /dev/null +++ b/data/regs/Growatt-SPH-V124.json @@ -0,0 +1,659 @@ +{ + "Growatt-SPH-V124": { + "config": { + "author": "StefanNouza", + "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", + "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", + "datatype": "float", + "factor": 0.01, + "unit": "Hz" + }, + { + "position": [109, 110, 111, 112], + "name": "Energy_Generated_today_kWh", + "realname": "generated Energy today", + "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": "integer", + "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", + "datatype": "float", + "factor": 0.1, + "unit": "W" + }, + { + "position": [250, 251, 252, 253], + "name": "Power_Battery_Charge_W", + "realname": "Charge Power", + "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", + "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": [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", + "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", + "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/Solax-X1.json b/data/regs/Solax-X1.json index 65a8f3fc..70ae2b47 100644 --- a/data/regs/Solax-X1.json +++ b/data/regs/Solax-X1.json @@ -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..0a9cb884 100644 --- a/data/regs/Solax-X3.json +++ b/data/regs/Solax-X3.json @@ -1,874 +1,1751 @@ { - "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", + "settopic": "UnlockSettings", + "mapping": [ + [ + 0, + "Locked" + ], + [ + 1, + "Unlocked" + ] + ] + }, + { + "position": [ + 525, + 526 + ], + "name": "ModbusPowerControl", + "realname": "Modbus Power Control", + "datatype": "integer", + "settopic": "ModbusPowerControl", + "mapping": [ + [ + 0, + "Off" + ], + [ + 1, + "PowerCtrl" + ], + [ + 2, + "ElectricQuantityCtrl" + ], + [ + 3, + "SoCTargetCtrl" + ] + ] + }, + { + "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": [ + 388, + 389 + ], + "name": "BatUserSoC", + "realname": "Battery User SoC", + "datatype": "integer", + "unit": "%" + }, + { + "position": [ + 390, + 391 + ], + "name": "BatUserSoH", + "realname": "Battery User SoH", + "datatype": "integer", + "unit": "%" + }, + { + "position": [ + 579, + 580 + ], + "name": "BatTargetSoC", + "realname": "Battery Target SoC", + "datatype": "integer", + "settopic": "TargetSoC", + "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": "setcounterwh", + "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, + 306 + ], + "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" + } + ], + "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, + "ManuelMode" + ], + [ + 4, + "PeakShaving" + ], + [ + 5, + "TOUMode" + ] + ] + }, + { + "position": [ + 288, + 289 + ], + "name": "ManuelMode", + "realname": "Manuel 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": "PhasePowerBalance", + "realname": "Phase Power Balance", + "datatype": "integer", + "mapping": [ + [ + 1, + "On" + ], + [ + 0, + "Off" + ] + ] + }, + { + "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": [ + 307 + ], + "name": "FeedInMinSoC", + "realname": "FeedIn Minimum SoC", + "datatype": "integer", + "unit": "%" + }, + { + "position": [ + 306 + ], + "name": "FeedInNightChargeSoC", + "realname": "FeedIn NightCharge SoC", + "datatype": "integer", + "unit": "%" + }, + { + "position": [ + 309 + ], + "name": "BackupMinSoC", + "realname": "Backup Minimum SoC", + "datatype": "integer", + "unit": "%" + }, + { + "position": [ + 309 + ], + "name": "BackupNightChargeSoC", + "realname": "Backup NightCharge SoC", + "datatype": "integer", + "unit": "%" } - } + ] + }, + "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 + ], + [ + "ManuelMode", + 3 + ], + [ + "PeakShaving", + 4 + ], + [ + "TUOMode", + 5 + ] + ], + "request": [ + "#ClientID", + "0x06", + "0x00", + "0x1f" + ] + }, + { + "name": "setManuelMode", + "realname": "Manuel 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": "setPhasePowerBalance", + "realname": "Phase Power Balance", + "info": "has to be investigated", + "mapping": [ + [ + "Off", + 0 + ], + [ + "On", + 1 + ] + ], + "request": [ + "#ClientID", + "0x06", + "0x00", + "0x9e" + ] + }, + { + "name": "setModbusPowerControl", + "realname": "Modbus Power Control", + "info": "accepted values:", + "mapping": [ + [ + "Off", + 0 + ], + [ + "PowerCtrl", + 1 + ], + [ + "ElectricQuantityCtrl", + 2 + ], + [ + "SoCTargetCtrl", + 3 + ] + ], + "request": [ + "#ClientID", + "0x10", + "0x00", + "0x7c" + ] + }, + { + "name": "setTargetSetType", + "realname": "Target Set Type", + "info": "accepted values:", + "mapping": [ + [ + "Set", + 1 + ], + [ + "Update", + 2 + ] + ], + "request": [ + "#ClientID", + "0x10", + "0x00", + "0x7d" + ] + }, + { + "name": "setTargetSoC", + "realname": "Target SoC", + "info": "set 0 - 100 in percent", + "request": [ + "#ClientID", + "0x10", + "0x00", + "0x83" + ] + }, + { + "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": "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" + ] + } + ] + } } diff --git a/data/web/Javascript.js b/data/web/Javascript.js index 5474846d..b5297ec8 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) { @@ -103,24 +184,43 @@ function handleRadioSelections() { } /***************************************************************************************** - * 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 +240,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 +252,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 +361,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 +399,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 +414,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 +432,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 +447,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; @@ -345,7 +480,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 +546,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 +575,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 +590,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 +604,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 +632,48 @@ 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'); + } +} +/**************************************************************************************** +****************************************************************************************/ 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..7bb5b7cc 100644 --- a/data/web/baseconfig.html +++ b/data/web/baseconfig.html @@ -4,12 +4,33 @@ - - + + + + + + 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..12603507 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]); + + 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 index 75eb1710..2bc657ca 100644 --- a/data/web/handlefiles.html +++ b/data/web/handlefiles.html @@ -4,9 +4,21 @@ - - + + + + + HandleFiles @@ -48,13 +60,25 @@ filename: - - - + + + +
+

Are you sure you want to delete this file?

+ + +
+

+
+
+ Connection Status: + +
+
\ No newline at end of file diff --git a/data/web/handlefiles.js b/data/web/handlefiles.js index 9358a682..e60fbe93 100644 --- a/data/web/handlefiles.js +++ b/data/web/handlefiles.js @@ -1,69 +1,91 @@ // https://jsfiddle.net/tobiasfaust/uc1jfpgb/ +import * as global from './Javascript.js'; + var DirJson; -window.addEventListener('load', initHandleFS, false); -function initHandleFS() { - init("/"); +// ************************************************ +export const functionMap = { + files_Callback: MyCallback +}; + +// ************************************************ +export function init1() { + var data = {"JS": {"listdir": [ + {"path": "/", "content": [{"name": "file1.txt", "isDir": 0}, {"name": "file2.txt", "isDir": 0}, {"name": "dir1", "isDir": 1}]}, + {"path": "/dir1", "content": [{"name": "file3.txt", "isDir": 0}, {"name": "file4.txt", "isDir": 0}]} + ]}, + "response": {"status": 1, "text": "successful"}, + "cmd": {"callbackFn": "files_Callback", "startpath": "/"} + }; + + global.handleJsonItems(data); + + document.getElementById('fullpath').innerHTML = ''; // div + document.getElementById('filename').value = ''; // input field + document.getElementById('content').value = ''; + 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 = ''; +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); } -// *********************************** -// Ajax Request to update -// *********************************** -function requestListDir(startpath) { +// ************************************************ +export function GetInitData(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); + data['cmd'] = {}; + data['cmd']['action'] = "handlefiles"; + data['cmd']['subaction'] = "listDir" + data['cmd']['startpath'] = startpath; + data['cmd']['callbackFn'] = "files_Callback"; + + global.requestData(data); + + document.getElementById('fullpath').innerHTML = ''; // div + document.getElementById('filename').value = ''; // input field + document.getElementById('content').value = ''; + + document.querySelector("#loader").style.visibility = "hidden"; + document.querySelector("body").style.visibility = "visible"; } -// *********************************** +// ************************************************ +function MyCallback(json) { + DirJson = json["JS"].listdir; + listFiles(json["cmd"]["startpath"]); +} + +// ************************************************ // 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 + document.getElementById('fullpath').innerHTML = file; // div + document.getElementById('filename').value = basename(file); // input field if (file.endsWith("json")) { - obj = document.getElementById('content').value = JSON.stringify(JSON.parse(string), null, 2); + document.getElementById('content').value = JSON.stringify(JSON.parse(string), null, 2); } else { - obj = document.getElementById('content').value = string; + document.getElementById('content').value = string; } } // *********************************** // fetch file from host // *********************************** -function fetchFile(file) { - obj = document.getElementById('content').value = "loading "+file+"..."; +export function fetchFile(file) { + document.getElementById('content').value = "loading "+file+"..."; fetch(file) .then(response => response.text()) @@ -73,10 +95,10 @@ function fetchFile(file) { // *********************************** // show directory structure // *********************************** -function listFiles(path) { +export function listFiles(path) { var table = document.querySelector('#files'), row = document.querySelector('#NewRow'), - tr_tpl, DirJsonLocal; + cells, tr_tpl, DirJsonLocal; // cleanup table table.replaceChildren(); @@ -87,7 +109,7 @@ function listFiles(path) { DirJsonLocal = DirJson[i] } } - + // show path information document.getElementById('path').innerHTML = path; @@ -170,7 +192,7 @@ function validateJson(json) { // *********************************** // download content of textarea as filename on local pc // *********************************** -function downloadFile() { +export function downloadFile() { var textToSave = document.getElementById("content").value; var textToSaveAsBlob = new Blob([textToSave], {type:"text/plain"}); var textToSaveAsURL = window.URL.createObjectURL(textToSaveAsBlob); @@ -182,11 +204,11 @@ function downloadFile() { downloadLink.innerHTML = "Download File"; downloadLink.href = textToSaveAsURL; - downloadLink.onclick = destroyClickedElement; + downloadLink.onclick = destroyClickedElement; downloadLink.style.display = "none"; document.body.appendChild(downloadLink); downloadLink.click(); - } else { setResponse(false, 'Filename is empty, Please define it.');} + } else { global.setResponse(false, 'Filename is empty, Please define it.');} } function destroyClickedElement(event) @@ -197,7 +219,7 @@ function destroyClickedElement(event) // *********************************** // store content of textarea // *********************************** -function uploadFile() { +export function uploadFile() { var textToSave = document.getElementById("content").value; var textToSaveAsBlob = new Blob([textToSave], {type:"text/plain"}); var fileNameToSaveAs = document.getElementById("filename").value; @@ -206,12 +228,12 @@ function uploadFile() { if (fileNameToSaveAs != '') { if (fileNameToSaveAs.toLowerCase().endsWith('.json')) { if (!validateJson(textToSave)) { - setResponse(false, 'Json invalid') + global.setResponse(false, 'Json invalid') return; } } - setResponse(true, 'Please wait for saving ...'); + global.setResponse(true, 'Please wait for saving ...'); const formData = new FormData(); formData.append(fileNameToSaveAs, textToSaveAsBlob, pathOfFile + '/' + fileNameToSaveAs); @@ -222,24 +244,26 @@ function uploadFile() { }) .then (response => response.json()) .then (json => { - setResponse(true, json.text) + global.setResponse(true, json.text) }); - } else { setResponse(false, 'Filename is empty, Please define it.');} + } else { global.setResponse(false, 'Filename is empty, Please define it.');} } -function deleteFile() { +// ************************************************ +export 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.');} + data['cmd'] = {}; + data['cmd']['action'] = 'handlefiles'; + data['cmd']['subaction'] = "deleteFile"; + data['cmd']['filename'] = pathOfFile + '/' + fileName; + + global.setResponse(true, 'Please wait for deleting ...'); + global.requestData(data); + GetInitData(pathOfFile); + } else { global.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..3955fdcc 100644 --- a/data/web/index.html +++ b/data/web/index.html @@ -3,7 +3,7 @@ Modbus MQTT Gateway - + diff --git a/data/web/modbusconfig.html b/data/web/modbusconfig.html index 6f46b960..fada88a8 100644 --- a/data/web/modbusconfig.html +++ b/data/web/modbusconfig.html @@ -4,12 +4,31 @@ - - + + + + + + ModbusConfig
+ + +
+ + Änderungen nicht gespeichert +
+
@@ -155,7 +174,12 @@

- - +
+
+ Connection Status: + +
+ x +
\ No newline at end of file diff --git a/data/web/modbusconfig.js b/data/web/modbusconfig.js index f24e83c8..d1d6e956 100644 --- a/data/web/modbusconfig.js +++ b/data/web/modbusconfig.js @@ -1,24 +1,87 @@ +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, + "pin_RELAY1": 18, + "pin_RELAY2": 19, + "openwbversion": "1.2.3", + "openwbmodulid": 1, + "openwbbatteryid": 2, + "inverters": [ [ { "inverter": {"value": "Kostal", "text": "Kostal"}}]], + }, + "response": { + "status": 1, + "text": "successful" + }, + "cmd": { + "action": "GetInitData", + "subaction": "status", + "callbackFn": "mbconfig_Callback" + } + } + + global.handleJsonItems(data); + + datavalues = global.getFormData("DataForm"); } +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]); + + 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..a3386211 100644 --- a/data/web/modbusitemconfig.html +++ b/data/web/modbusitemconfig.html @@ -4,17 +4,42 @@ - - + + + + + + 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..56eace1d 100644 --- a/data/web/modbusitemconfig.js +++ b/data/web/modbusitemconfig.js @@ -1,41 +1,206 @@ +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); + RefreshLiveData(); + } + }, 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 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 RefreshLiveData() { var data = {}; - data.action = "RefreshLiveData"; - data.subaction = "all"; - requestData(JSON.stringify(data), true); + data['cmd'] = {}; + data['cmd']['action'] = "GetItemsAsStream"; + data['cmd']['highlight'] = "true"; + + global.requestData(data); } - + + // ************************************************ -function ChangeActiveStatus(id) { - obj = document.getElementById(id); - //item = id.replace(/^activeswitch_(.*)$/g, "$1"); +export function ChangeActiveStatus(id) { + var obj = document.getElementById(id); + var data = {}; - data.action = "SetActiveStatus"; - data.newState = (obj.checked?"true":"false"); - data.item = obj.name; - requestData(JSON.stringify(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..95cec900 100644 --- a/data/web/navi.html +++ b/data/web/navi.html @@ -4,21 +4,32 @@ - - + + + + + + Modbus MQTT Gateway - - -
-

Configuration

+
+ Configuration of + - () - + + + + Release: @@ -52,5 +63,15 @@

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..a2b1abe2 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 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'); + } } // ************************************************ -function highlightNavi(item) { - collection = document.getElementsByName('navi') +export function highlightNavi(item) { + const collection = document.getElementsByName('navi') for (let i = 0; i < collection.length; i++) { if (item.id == collection[i].id ) { diff --git a/data/web/rawdata.html b/data/web/rawdata.html index efe6a5ea..64b351c3 100644 --- a/data/web/rawdata.html +++ b/data/web/rawdata.html @@ -4,9 +4,18 @@ - - + + + + + RawData @@ -19,8 +28,7 @@ Raw Data - - + RawData of ID-Data @@ -40,9 +48,8 @@ -

+

- @@ -69,9 +76,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..1746c4ed 100644 --- a/data/web/rawdata.js +++ b/data/web/rawdata.js @@ -1,20 +1,56 @@ /* 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() { +/******************************* + * Callback function after receiving the data +*******************************/ +function MyCallback(json) { reset_rawdata('id_rawdata'); reset_rawdata('live_rawdata'); @@ -23,7 +59,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 +81,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 +97,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 +130,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 +148,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/status.html b/data/web/status.html index 33e70726..ac57f33c 100644 --- a/data/web/status.html +++ b/data/web/status.html @@ -4,8 +4,17 @@ - - + + + + + status @@ -71,28 +80,32 @@ show logs via webserial -
+ + + 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..07548536 100644 --- a/data/web/status.js +++ b/data/web/status.js @@ -1,31 +1,124 @@ +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'] = "GetItemsAsStream"; + data['cmd']['subaction'] = "onlyactive"; + data['cmd']['highlight'] = "true"; + + 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/include/_Release.h b/include/_Release.h index d6f5edc4..8881175d 100644 --- a/include/_Release.h +++ b/include/_Release.h @@ -1 +1 @@ -#define Release "3.3.1" +#define Release "3.3.2" diff --git a/src/MyWebServer.cpp b/src/MyWebServer.cpp index 6ee3090d..347f2f83 100644 --- a/src/MyWebServer.cpp +++ b/src/MyWebServer.cpp @@ -1,28 +1,43 @@ +/******************************************************** + * Copyright [2024] Tobias Faust onNotFound(std::bind(&MyWebServer::handleNotFound, this, std::placeholders::_1)); server->on("/", HTTP_GET, std::bind(&MyWebServer::handleRoot, this, std::placeholders::_1)); 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("/getitems", HTTP_GET, [&](AsyncWebServerRequest *request){ mb->GetLiveDataAsJsonToWebServer(request); }); + //server->on("/getregister", HTTP_GET, std::bind(&MyWebServer::handleGetRegisterJson, this, std::placeholders::_1)); // deprecated, not longer in use + 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.setAutoReboot(true); - // ElegantOTA callbacks + //ElegantOTA callbacks //ElegantOTA.onStart(onOTAStart); //ElegantOTA.onProgress(onOTAProgress); //ElegantOTA.onEnd(std::bind(&MyWebServer::onOTAEnd, this, std::placeholders::_1)); @@ -45,12 +60,150 @@ MyWebServer::MyWebServer(AsyncWebServer *server, DNSServer* dns): DoReboot(false } } +void MyWebServer::onWsEvent(AsyncWebSocket * server, AsyncWebSocketClient * client, AwsEventType type, void * arg, uint8_t *data, size_t len) { + if (type == WS_EVT_CONNECT) { + Config->log(2, "[Client: %u] WebSocket client connected", client->id()); + + } else if (type == WS_EVT_DISCONNECT) { + Config->log(2, "[Client: %u] WebSocket client disconnected", client->id()); + + // wenn client->id() in der Liste WsConnectedClientsForBroadcast vorhanden ist, dann entfernen + + auto it = std::find_if(WsConnectedClientsForBroadcast.begin(), WsConnectedClientsForBroadcast.end(), + [client](const WsConnClient_t& c) { return c.id == client->id(); }); + if (it != WsConnectedClientsForBroadcast.end()) { + WsConnectedClientsForBroadcast.erase(it); + } + + // wenn keine clients mehr in der Liste sind, dann den Callback für die modbuswerte entfernen + if (this->WsConnectedClientsForBroadcast.size() == 0) { + mb->setWebSocketCallback(nullptr); + } + + } 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->log(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(""); + 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 (action == "GetItemsAsStream") { + // add client id to the list of clients to broadcast if not already in the list + auto it = std::find_if(WsConnectedClientsForBroadcast.begin(), WsConnectedClientsForBroadcast.end(), + [client](const WsConnClient_t& c) { return c.id == client->id(); }); + if (it == WsConnectedClientsForBroadcast.end()) { + const WsConnClient_t w = {client->id(), msg}; + WsConnectedClientsForBroadcast.push_back(w); + } + // if this is the first client in the list, then set the callback for the modbus values + if (this->WsConnectedClientsForBroadcast.size() == 1) { + mb->setWebSocketCallback([this](String& message) { + this->sendWebSocketMessage(message); + }); + } + return; + } + + 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); + } else if (subaction && subaction == "modbusconfig") { + mb->GetInitData(json); + } 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->log(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 ..."); } +void MyWebServer::sendWebSocketMessage(String& message) { + // send message to all connected clients in the list WsConnectedClientsForBroadcast + for (auto client : WsConnectedClientsForBroadcast) { + if (ws->client(client.id)) { + message = message.substring(0, message.length()-1) + "," + client.json.substring(1, client.json.length()-1); + + Config->log(4, "send WebSocket Message to client %u: %s", client.id, message.c_str()); + ws->text(client.id, message); + } + } + +} + void MyWebServer::loop() { //delay(1); // slow response Issue: https://github.com/espressif/arduino-esp32/issues/4348#issuecomment-695115885 if (this->DoReboot) { @@ -64,6 +217,7 @@ void MyWebServer::loop() { } } ElegantOTA.loop(); + ws->cleanupClients(); } void MyWebServer::handleNotFound(AsyncWebServerRequest *request) { @@ -80,12 +234,8 @@ 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) { +bool MyWebServer::handleReset() { + bool ret = true; 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/"); @@ -94,151 +244,34 @@ void MyWebServer::handleReset(AsyncWebServerRequest *request) { 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")); + + if (LittleFS.remove(path)) { + Config->log(4, "deletion of configuration file '%s' was successful", file.name()); + } else { + Config->log(2, "deletion of configuration file '%s' has failed", file.name()); + ret = false; + } file = root.openNextFile(); } root.close(); + this->DoReboot = true; - 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); + return ret; } +/* 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); - + mb->GetRegisterAsJsonToWebServer(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); - - } 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->log(4, "Ajax Json Antwort: ", ret); - - request->send(response); -} - -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 +281,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"); @@ -277,7 +306,4 @@ void MyWebServer::GetInitDataStatus(AsyncResponseStream *response) { json["response"].to(); json["response"]["status"] = 1; json["response"]["text"] = "successful"; - - serializeJson(json, ret); - response->print(ret); } \ No newline at end of file diff --git a/src/MyWebServer.h b/src/MyWebServer.h index b297a18b..149f30ee 100644 --- a/src/MyWebServer.h +++ b/src/MyWebServer.h @@ -1,63 +1,63 @@ -// 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: + typedef struct { + uint32_t id; + String json; + } WsConnClient_t; + + public: MyWebServer(AsyncWebServer *server, DNSServer* dns); void loop(); + void sendWebSocketMessage(String& message); - private: + private: bool DoReboot; - unsigned long RequestRebootTime; + uint64_t RequestRebootTime; + std::vector WsConnectedClientsForBroadcast = {}; + 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); 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 handleGetItemJson(AsyncWebServerRequest *request); void handleGetRegisterJson(AsyncWebServerRequest *request); - void GetInitDataStatus(AsyncResponseStream *response); - void GetInitDataNavi(AsyncResponseStream *response); - +// void handleGetSetterJson(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..b1bf5c5c 100644 --- a/src/baseconfig.cpp +++ b/src/baseconfig.cpp @@ -1,14 +1,18 @@ -#include "baseconfig.h" +/******************************************************** + * Copyright [2024] Tobias Faust + +BaseConfig::BaseConfig(): debuglevel(2), + serial_rx(3), serial_tx(1), mqtt_UseRandomClientID(true), - useAuth(false) { + useAuth(false) { #ifdef ESP8266 LittleFS.begin(); #elif defined(ESP32) - if (LittleFS.begin(true)) { // true: format LittleFS/NVS if mount fails + if (LittleFS.begin(true)) { // true: format LittleFS/NVS if mount fails if (!LittleFS.exists("/config")) { LittleFS.mkdir("/config"); } @@ -16,29 +20,29 @@ BaseConfig::BaseConfig(): debuglevel(2), this->log(1, "LittleFS Mount Failed"); } #endif - + // Flash Write Issue // https://github.com/esp8266/Arduino/issues/4061#issuecomment-428007580 // LittleFS.format(); - + LoadJsonConfig(); } void BaseConfig::LoadJsonConfig() { bool loadDefaultConfig = false; if (LittleFS.exists("/config/baseconfig.json")) { - //file exists, reading and loading + // file exists, reading and loading this->log(2, "reading config file"); File configFile = LittleFS.open("/config/baseconfig.json", "r"); if (configFile) { this->log(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;} @@ -92,9 +96,7 @@ 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; @@ -120,8 +122,6 @@ void BaseConfig::GetInitData(AsyncResponseStream *response) { json["response"]["status"] = 1; json["response"]["text"] = "successful"; - serializeJson(json, ret); - response->print(ret); } void BaseConfig::log(const int loglevel, const char* format, ...) { diff --git a/src/baseconfig.h b/src/baseconfig.h index ebede46e..68aeb59d 100644 --- a/src/baseconfig.h +++ b/src/baseconfig.h @@ -1,17 +1,20 @@ -#ifndef BASECONFIG_H -#define BASECONFIG_H +/******************************************************** + * Copyright [2024] Tobias Faust +#include +#include <_Release.h> -class BaseConfig { - public: +class BaseConfig { + public: BaseConfig(); void LoadJsonConfig(); - void GetInitData(AsyncResponseStream *response); + void GetInitData(JsonDocument& json); /** * @brief Wrapper function for logging like Serial.printf @@ -37,8 +40,9 @@ class BaseConfig { const String& GetAuthUser() const {return auth_user;} const String& GetAuthPass() const {return auth_pass;} - const String GetReleaseName(); - private: + const String GetReleaseName(); + + private: String mqtt_server; String mqtt_username; String mqtt_password; @@ -54,9 +58,8 @@ class BaseConfig { bool useAuth; String auth_user; String auth_pass; - }; extern BaseConfig* Config; -#endif +#endif // BASECONFIG_H_ diff --git a/src/commonlibs.h b/src/commonlibs.h index ecd3a2b8..18147a93 100644 --- a/src/commonlibs.h +++ b/src/commonlibs.h @@ -1,3 +1,7 @@ +/******************************************************** + * Copyright [2024] Tobias Faust = 100 #include "Arduino.h" #else @@ -34,5 +38,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 (); @@ -45,47 +49,39 @@ void handleFiles::getDirList(JsonArray* json, String path) { file = FSroot.openNextFile(); } FSroot.close(); - json->add(jsonRoot); + json.add(jsonRoot); } //############################################################### -// returns the requested data via AJAX from Webserver.cpp +// returns the requested data from Webserver.cpp //############################################################### -void handleFiles::HandleAjaxRequest(JsonDocument& jsonGet, AsyncResponseStream* response) { +void handleFiles::HandleRequest(JsonDocument& json) { String subaction = ""; - if (jsonGet["subaction"]) {subaction = jsonGet["subaction"].as();} + if (json["cmd"]["subaction"]) {subaction = json["cmd"]["subaction"].as();} - Config->log(3, "handle Ajax Request in handleFiles.cpp: %s", subaction.c_str()); + Config->log(3, "handle Request in handleFiles.cpp: %s", subaction.c_str()); if (subaction == "listDir") { - JsonDocument doc; - JsonArray content = doc.add(); + JsonArray content = json["JS"]["listdir"].to(); - this->getDirList(&content, "/"); - String ret(""); - serializeJson(content, ret); - Config->log(5, content); + this->getDirList(content, "/"); + Config->log(5, json["content"].as().c_str()); - response->print(ret); } else if (subaction == "deleteFile") { - String filename(""), ret(""); - JsonDocument jsonReturn; + String filename(""); Config->log(3, "Request to delete file %s", filename.c_str()); - if (jsonGet["filename"]) {filename = jsonGet["filename"].as();} + if (json["cmd"]["filename"]) {filename = json["cmd"]["filename"].as();} if (LittleFS.remove(filename)) { - jsonReturn["response_status"] = 1; - jsonReturn["response_text"] = "deletion successful"; + json["response"]["status"] = 1; + json["response"]["text"] = "deletion successful"; } else { - jsonReturn["response_status"] = 0; - jsonReturn["response_text"] = "deletion failed"; + json["response"]["status"] = 0; + json["response"]["text"] = "deletion failed"; } - Config->log(3, jsonReturn); - - serializeJson(jsonReturn, ret); - response->print(ret); + Config->log(3, json.as().c_str()); } } diff --git a/src/handleFiles.h b/src/handleFiles.h index b7001706..75a8b133 100644 --- a/src/handleFiles.h +++ b/src/handleFiles.h @@ -1,18 +1,23 @@ +/******************************************************** + * Copyright [2024] Tobias Faust class handleFiles { public: handleFiles(AsyncWebServer *server); - void HandleAjaxRequest(JsonDocument& jsonGet, AsyncResponseStream* response); + void HandleRequest(JsonDocument& json); void handleUpload(AsyncWebServerRequest *request, String filename, size_t index, uint8_t *data, size_t len, bool final); private: - void getDirList(JsonArray* json, String path); + 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..0d0cd9c1 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -3,18 +3,18 @@ Solar Inverter Modbus-RTU Gateway to MQTT _________________________________________________________________ | | -| author : Tobias Faust +#include +#include +#include +#include AsyncWebServer server(80); DNSServer dns; @@ -29,12 +29,12 @@ void myMQTTCallBack(char* topic, byte* payload, unsigned int length) { Config->log(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())); + + mb->ReceiveMQTT(topic, msg); } void setup() { @@ -42,7 +42,10 @@ void setup() { Config = new BaseConfig(); #ifndef USE_WEBSERIAL - Serial.begin(115200, SERIAL_8N1, Config->GetSerialRx(), Config->GetSerialTx()); // RX, TX, zb.: 33, 32 + Serial.begin(115200, + SERIAL_8N1, + Config->GetSerialRx(), + Config->GetSerialTx()); // RX, TX, zb.: 33, 32 Serial.println(""); Serial.println("ready"); #endif @@ -53,22 +56,19 @@ void setup() { WebSerial.setBuffer(100); #endif - Config->log(1, "Start of Modbus-RTU MQTT Gateway"); + Config->log(1, "Start of Modbus-RTU MQTT Gateway"); Config->log(1, "Starting BaseConfig"); - + 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" - ); + mqtt = new MQTT(Config->GetMqttServer().c_str(), + Config->GetMqttPort(), + Config->GetMqttBasePath().c_str(), + Config->GetMqttRoot().c_str()); mqtt->setCallback(myMQTTCallBack); mb = new modbus(); mb->enableMqtt(mqtt); - + Config->log(1, "attempting to start WebServer"); mywebserver = new MyWebServer(&server, &dns); } @@ -76,8 +76,8 @@ void setup() { void loop() { mqtt->loop(); mywebserver->loop(); - mb->loop(); - + mb->loop(); + #ifdef USE_WEBSERIAL WebSerial.loop(); #endif diff --git a/src/modbus.cpp b/src/modbus.cpp index 9ab71947..091dd46e 100644 --- a/src/modbus.cpp +++ b/src/modbus.cpp @@ -1,3 +1,7 @@ +/******************************************************** + * Copyright [2024] Tobias Faust {}; SaveIdDataframe = new std::vector{}; SaveLiveDataframe = new std::vector{}; @@ -19,7 +24,7 @@ modbus::modbus(): enableRelays(false), InverterLiveData = new std::vector{}; InverterIdData = new std::vector{}; AvailableInverters = new std::vector{}; - Setters = new std::vector{}; + Setters = new std::vector{}; OpenWB = new openwb(); Conf_RequestLiveData= new std::vector>{}; @@ -44,7 +49,7 @@ modbus::modbus(): enableRelays(false), 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); @@ -64,11 +69,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 +84,13 @@ void modbus::init(bool firstrun) { this->QueryIdData(); } +/******************************************************* +* set websocket callback +********************************************************/ +void modbus::setWebSocketCallback(std::function callback) { + webSocketCallback = callback; +} + /******************************************************* * Read configured pin states *******************************************************/ @@ -88,6 +100,11 @@ void modbus::ReadRelays() { this->state_Relay2 = digitalRead(this->pin_Relay2); this->mqtt->Publish_Int("relay2", this->state_Relay2, false); + + if (webSocketCallback) { + String message = "{\"data-id\": {\"relay1.value\":\"" + String(this->state_Relay1?"On":"Off") + "\",\"relay2.value\":\"" + String(this->state_Relay2?"On":"Off") + "\"}}"; + webSocketCallback(message); + } } /******************************************************* @@ -102,9 +119,9 @@ 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(); @@ -115,6 +132,7 @@ void modbus::GenerateMqttSubscriptions() { regfile.find(streamString.c_str()); streamString = "\"set\": ["; regfile.find(streamString.c_str()); + do { JsonDocument elem; DeserializationError error = deserializeJson(elem, regfile); @@ -124,19 +142,10 @@ void modbus::GenerateMqttSubscriptions() { 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->log(4, "Set command successfully parsed from JSON: %s", s.Name.c_str()); this->Setters->push_back(s); } else { @@ -151,31 +160,57 @@ void modbus::GenerateMqttSubscriptions() { 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->log(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; + if (topic == this->GetMqttSetTopic(this->Setters->at(i).Name)) { + if (!this->Setters->at(i).active) { + Config->log(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->log(1, "Setter %s not found in JSON", this->Setters->at(i).Name.c_str()); + return; + } + + JsonArray arr = elem["request"].as(); + std::vector request = {}; + + for (String x : arr) { + byte e = this->String2Byte(x); + request.push_back(e); + } + + // map values if a mapping is specified + if(!elem["mapping"].isNull() && elem["mapping"].is() && msg != "") { + Config->log(4, "Map values for item %s", msg.c_str()); + + JsonArray map = elem["mapping"].as(); + msg = this->MapItem(map, msg); + } + + int msgInt = msg.toInt(); // atoi(msg.c_str()) byte bytes[4]; - bytes[0] = (msg >> 24) & 0xFF; - bytes[1] = (msg >> 16) & 0xFF; - bytes[2] = (msg >> 8) & 0xFF; - bytes[3] = (msg >> 0) & 0xFF; + bytes[0] = (msgInt >> 24) & 0xFF; + bytes[1] = (msgInt >> 16) & 0xFF; + bytes[2] = (msgInt >> 8) & 0xFF; + bytes[3] = (msgInt >> 0) & 0xFF; - // 16bit number + // 32bit number request.push_back(bytes[2]); request.push_back(bytes[3]); - Config->log(3, "MQTT Setter found: %s" ,this->Setters->at(i).command.c_str()); + Config->log(3, "MQTT Setter found: %s" ,this->Setters->at(i).Name.c_str()); Config->log(3, "Initiate Set Request to queue: %s" ,(this->PrintDataFrame(&request)).c_str()); this->SetQueue->enqueue(request); @@ -183,6 +218,48 @@ void modbus::ReceiveMQTT(String topic, int msg) { } } +/******************************************************* + * @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 = LittleFS.open("/regs/" + this->InverterType.filename); + if (!regfile) { + Config->log(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->log(4, "parsing JSON ok"); + Config->log(5, elem); + } else { + Config->log(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) *******************************************************/ @@ -309,7 +386,13 @@ byte modbus::String2Byte(String s){ *******************************************************/ 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)); + } + } } /******************************************************* @@ -338,7 +421,6 @@ void modbus::QueryIdData() { } } - /******************************************************* * Query Live Data to Inverter *******************************************************/ @@ -619,6 +701,9 @@ 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]); } @@ -794,6 +879,7 @@ void modbus::ParseData() { 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) { @@ -810,6 +896,12 @@ void modbus::ParseData() { Config->log(3, "Inverter ID Data found -> %s: %s ", d.Name.c_str(), d.value.c_str()); } + + //if (webSocketCallback) { + //const String ws("{\"data-id\":{\"" + d.Name + ".value\":\"" + d.value + " "+ d.unit +"\"}}"); + //const String ws("{\"" + d.Name + ".value\":\"" + d.value + " "+ d.unit +"\"}"); + //webSocketCallback(ws); + //} } while (regfile.findUntil(",","]")); @@ -827,9 +919,26 @@ void modbus::ParseData() { this->SaveIdDataframe->assign(this->DataFrame->begin(), this->DataFrame->end()); } + if (webSocketCallback) { + this->SendDataToWebSocket(RequestType == "livedata" ? this->InverterLiveData : this->InverterIdData); + } + this->DataFrame->clear(); } +void modbus::SendDataToWebSocket(std::vector* vector) { + if (webSocketCallback) { + String msg("{\"data-id\":{"); msg.reserve(vector->size() * 25); + + for (uint8_t i=0; i < vector->size(); i++) { + if (i > 0) msg += ","; + msg += "\"" + vector->at(i).Name + ".value\":\"" + vector->at(i).value + " "+ vector->at(i).unit +"\""; + } + msg += "}}"; + webSocketCallback(msg); + } +} + String modbus::ConvertIntToBinaryString(int n, int numBits) { String binaryString = ""; binaryString.reserve(numBits); @@ -871,6 +980,10 @@ String modbus::MapItem(JsonArray map, String value) { String v1 = mapItem[0].as(); String v2 = mapItem[1].as(); + v1.toLowerCase(); + v2.toLowerCase(); + value.toLowerCase(); + if (value == v1) { ret = v2; Config->log(4, "Mapped value: %s -> %s", v1.c_str(), v2.c_str()); @@ -977,8 +1090,10 @@ 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); + std::shared_ptr firstRow = std::make_shared(true); + String subaction(""), json("{}"); if(request->hasArg("json")) { @@ -987,26 +1102,28 @@ void modbus::GetLiveDataAsJson(AsyncWebServerRequest *request) { JsonDocument jsonGet; DeserializationError error = deserializeJson(jsonGet, json.c_str()); - Config->log(4, "[GetLiveDataAsJson] Json command empfangen: "); + Config->log(4, "[GetLiveDataAsJsonToWebServer] Json command empfangen: "); if (!error) { Config->log(4, jsonGet); - if (jsonGet["subaction"]){subaction = jsonGet["subaction"].as();} + if (jsonGet["cmd"]["subaction"]) subaction = jsonGet["cmd"]["subaction"].as(); } else { - Config->log(2, "[GetLiveDataAsJson] Json Command not parseable: %s -> %s", json.c_str(), error.c_str()); + Config->log(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, subaction](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; @@ -1014,10 +1131,10 @@ void modbus::GetLiveDataAsJson(AsyncWebServerRequest *request) { // 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 += ","; + if(!(*firstRow)) 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 += "\"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) + "\""; @@ -1025,6 +1142,7 @@ void modbus::GetLiveDataAsJson(AsyncWebServerRequest *request) { ret += ",\"openwb\": [{\"openwbtopic\": \"" + OpenWB->getOpenWbTopic(this->InverterIdData->at(i).openwb) + "\"}]"; } ret += "}"; + (*firstRow) = false; } (*counter)++; @@ -1038,10 +1156,10 @@ void modbus::GetLiveDataAsJson(AsyncWebServerRequest *request) { while (i < this->InverterLiveData->size() && ret.length() < maxLen) { if (!(subaction == "onlyactive" && !this->InverterLiveData->at(i).active)) { - if(*counter > 1) ret += ","; + if(!(*firstRow)) 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 += "\"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) + "\""; @@ -1049,6 +1167,7 @@ void modbus::GetLiveDataAsJson(AsyncWebServerRequest *request) { ret += ",\"openwb\": [{\"openwbtopic\": \"" + OpenWB->getOpenWbTopic(this->InverterLiveData->at(i).openwb) + "\"}]"; } ret += "}"; + (*firstRow) = false; } (*counter)++; @@ -1070,13 +1189,121 @@ void modbus::GetLiveDataAsJson(AsyncWebServerRequest *request) { request->send(response); } +/******************************************************* + * Return all LiveData as jsonArray + * {data: [{"name": "xx", "value": "xx", ...}, ...] } +*******************************************************/ +void modbus::GetSettersAsJsonToWebServer(AsyncWebServerRequest *request) { + std::shared_ptr counter = std::make_shared(0); + String subaction(""); + + if(request->hasArg("json")) { + const String json = request->arg("json"); + Config->log(4, "[GetSetterAsJson] Json command empfangen: %s", json.c_str()); + + JsonDocument jsonGet; + DeserializationError error = deserializeJson(jsonGet, json.c_str()); + + if (!error) { + if (jsonGet["cmd"]["subaction"]) subaction = jsonGet["cmd"]["subaction"].as(); + } else { + Config->log(2, "[GetSetterAsJson] 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) { + 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 += "{\"globalEnabled\": \""+ String(this->Conf_EnableSetters) +"\", \"data\": {\"setitems\": ["; + (*counter)++; + } + + File regfile = LittleFS.open("/regs/"+this->InverterType.filename); + if (!regfile) { + Config->log(1, "failed to open %s file", this->InverterType.filename.c_str()); + return 0; + } + + String streamString = ""; + uint16_t itemIterator = 0; + + streamString = "\""+ this->InverterType.name +"\": {"; + regfile.find(streamString.c_str()); + + streamString = "\"set\": ["; + regfile.find(streamString.c_str()); + do { + if (itemIterator == (*counter - 1)) { + bool isActive = false; // default + JsonDocument elem; + DeserializationError error = deserializeJson(elem, regfile); + + if (error) { + Config->log(1, "(Function GetSettersAsJsonToWebServer) Failed to parse JSON Register Data: %s", error.c_str()); + break; + } + + Config->log(4, "parsing JSON ok"); + Config->log(5, elem); + + //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 ((subaction == "onlyactive" && isActive) || subaction != "onlyactive") { + 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 (ret.length() > 0) { + // 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); +} /******************************************************* * 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"} *******************************************************/ -void modbus::GetRegisterAsJson(AsyncResponseStream *response) { +/* +void modbus::GetRegisterAsJsonToWebServer(AsyncResponseStream *response) { int count = 0; File regfile = LittleFS.open("/regs/"+this->InverterType.filename); @@ -1144,6 +1371,7 @@ void modbus::GetRegisterAsJson(AsyncResponseStream *response) { if (regfile) { regfile.close(); } response->print("]}"); } +*/ /******************************************************* * request for changing active Status of a certain item @@ -1154,16 +1382,33 @@ void modbus::SetItemActiveStatus(String item, bool newstate) { if (this->InverterLiveData->at(j).Name == item) { Config->log(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")); 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->log(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; + } + } + } /******************************************************* @@ -1271,6 +1516,7 @@ 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; if (LittleFS.exists("/config/modbusconfig.json")) { //file exists, reading and loading @@ -1311,6 +1557,7 @@ 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()); found = true; } @@ -1356,7 +1603,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,16 +1611,15 @@ 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 } } @@ -1381,7 +1627,11 @@ void modbus::LoadJsonConfig(bool firstrun) { /******************************************************* * load Modbus Item configuration from file *******************************************************/ -void modbus::LoadJsonItemConfig() { +void modbus::LoadJsonItemConfig() { + this->LoadJsonItemConfig(true, true, true); +} + +void modbus::LoadJsonItemConfig(bool loadLiveData, bool loadIdData, bool loadSetters) { if (LittleFS.exists("/config/modbusitemconfig.json")) { //file exists, reading and loading @@ -1406,26 +1656,43 @@ void modbus::LoadJsonItemConfig() { 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->log(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->log(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->log(3, "setter %s -> %s", ItemName, (this->Setters->at(i).active?"enabled":"disabled")); + break; + } } } - //Lazgar + } } while (stream.findUntil(",","]")); @@ -1441,14 +1708,12 @@ void modbus::LoadJsonItemConfig() { /******************************************************************************************************* * 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; @@ -1483,15 +1748,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 +1771,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..4f5f850d 100644 --- a/src/modbus.h +++ b/src/modbus.h @@ -1,9 +1,13 @@ +/******************************************************** + * Copyright [2024] Tobias Faust +#include +#include #include #include #include @@ -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 { @@ -47,21 +51,26 @@ class modbus { 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 GetRegisterAsJsonToWebServer(AsyncResponseStream *response); void SetItemActiveStatus(String item, bool newstate); - void ReceiveMQTT(String topic, int msg); + void ReceiveMQTT(String topic, String msg); + JsonDocument GetSetterByName(String name); + + // Callback setzen + void setWebSocketCallback(std::function callback); + void deleteWebSocketCallback() { webSocketCallback = nullptr; } private: uint8_t pin_RX; // Serial Receive pin @@ -94,8 +103,8 @@ 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; @@ -114,7 +123,7 @@ 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); @@ -122,6 +131,7 @@ class modbus { String MapBitwise(JsonArray map, String value); String ConvertIntToBinaryString(int n, int numBits); void ReadRelays(); + void SendDataToWebSocket(std::vector* vector); // inverter config, in sync with register.h ->config ArduinoQueue>* ReadQueue; @@ -129,6 +139,9 @@ class modbus { std::vector>* Conf_RequestLiveData; std::vector>* Conf_RequestIdData; + + std::function webSocketCallback; // Callback-Funktion + uint8_t Conf_ClientIdPos; //uint8_t Conf_LiveDataStartsAtPos; //uint8_t Conf_IdDataStartsAtPos; diff --git a/src/mqtt.cpp b/src/mqtt.cpp index 2f1cc479..9d677d98 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")); ImprovTypes::ChipFamily variant; - + #ifdef ESP32 String variantString = ARDUINO_VARIANT; #else @@ -38,21 +40,26 @@ 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); + + // 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 @@ -65,17 +72,16 @@ MQTT::MQTT(const char* MqttServer, uint16_t MqttPort, String MqttBasepath, Strin Config->log(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(); } @@ -86,7 +92,7 @@ void MQTT::WifiOnEvent(WiFiEvent_t event) { Config->log(4, "[WiFi-event] event: %d", event); switch (event) { - case ARDUINO_EVENT_WIFI_READY: + case ARDUINO_EVENT_WIFI_READY: Config->log(1, "WiFi interface ready"); break; case ARDUINO_EVENT_WIFI_SCAN_DONE: @@ -116,7 +122,7 @@ void MQTT::WifiOnEvent(WiFiEvent_t event) { case ARDUINO_EVENT_WIFI_STA_LOST_IP: Config->log(1, "Lost IP address and IP address is reset to 0"); this->ConnectStatusWifi = false; - this->ipadresse = (0,0,0,0); + this->ipadresse = (0, 0, 0, 0); break; case ARDUINO_EVENT_WPS_ER_SUCCESS: Config->log(1, "WiFi Protected Setup (WPS): succeeded in enrollee mode"); @@ -169,16 +175,15 @@ void MQTT::WifiOnEvent(WiFiEvent_t event) { case ARDUINO_EVENT_ETH_DISCONNECTED: Config->log(1, "Ethernet disconnected"); this->ConnectStatusWifi = false; - this->ipadresse = (0,0,0,0); + this->ipadresse = (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->log(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 +197,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; } @@ -204,8 +209,7 @@ eth_shield_t* MQTT::GetEthShield(String ShieldName) { void MQTT::WaitForConnect() { while (!this->ConnectStatusWifi) delay(100); - Config->log(1, "Wait for connect"); - //yield(); + Config->log(1, "Wait for connect"); } void MQTT::reconnect() { @@ -213,27 +217,33 @@ void MQTT::reconnect() { char LWT[50]; memset(&LWT[0], 0, sizeof(LWT)); memset(&topic[0], 0, sizeof(topic)); - - if (Config->UseRandomMQTTClientID()) { + + if (Config->UseRandomMQTTClientID()) { snprintf (topic, sizeof(topic), "%s-%s", this->mqtt_root.c_str(), String(random(0xffff)).c_str()); } else { 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")) { + + if (PubSubClient::connect(topic, + Config->GetMqttUsername().c_str(), + Config->GetMqttPassword().c_str(), + LWT, + true, + false, + "Offline")) { Config->log(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()); + PubSubClient::subscribe(this->subscriptions->at(i).c_str()); Config->log(1, "MQTT resubscribed to: %s", this->subscriptions->at(i).c_str()); } @@ -247,13 +257,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); @@ -272,7 +286,9 @@ 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"); + } else { + Config->log(2, "Request for MQTT Publish, but not connected to Broker"); + } } String MQTT::getTopic(String subtopic, bool fulltopic) { @@ -282,7 +298,7 @@ String MQTT::getTopic(String subtopic, bool fulltopic) { 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()); @@ -305,7 +321,7 @@ 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()); this->subscriptions->erase(this->subscriptions->begin()+i); @@ -317,16 +333,16 @@ 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 @@ -335,12 +351,14 @@ void MQTT::loop() { this->ipadresse = WiFi.localIP(); } else { 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, "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"); this->mqtt_root = Config->GetMqttRoot(); @@ -348,7 +366,9 @@ void MQTT::loop() { } 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, "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"); this->mqtt_basepath = Config->GetMqttBasePath(); @@ -356,12 +376,12 @@ void MQTT::loop() { } // 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(); } @@ -374,7 +394,7 @@ void MQTT::loop() { if (Config->GetDebugLevel() >=4 && millis() - this->last_keepalive > (30 * 1000)) { // send messages for debugging every 30 seconds this->last_keepalive = millis(); - + if (Config->GetDebugLevel() >=4) { char buffer[100] = {0}; memset(buffer, 0, sizeof(buffer)); @@ -384,10 +404,10 @@ void MQTT::loop() { 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); + this->Publish_Int("uptime", uptimeSeconds, false); } } } diff --git a/src/mqtt.h b/src/mqtt.h index ed991a99..41fefa86 100644 --- a/src/mqtt.h +++ b/src/mqtt.h @@ -1,53 +1,63 @@ -#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; + String name; uint8_t PHY_ADDR; - int PHY_POWER; + int PHY_POWER; int PHY_MDC; - int PHY_MDIO; + int PHY_MDIO; eth_phy_type_t PHY_TYPE; eth_clock_mode_t CLK_MODE; } eth_shield_t; #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}, + {"test", 1, 16, 23, 18, ETH_PHY_LAN8720, ETH_CLOCK_GPIO0_IN}}; #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 +66,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 +80,22 @@ 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_keepalive = 0; bool ConnectStatusWifi; bool ConnectStatusMqtt; IPAddress ipadresse; - + #ifdef ESP32 void WifiOnEvent(WiFiEvent_t event); #endif @@ -97,4 +107,4 @@ class MQTT: PubSubClient { extern MQTT* mqtt; -#endif +#endif // MQTT_H_ diff --git a/src/openwb.cpp b/src/openwb.cpp index 21afd554..b1f51cbf 100644 --- a/src/openwb.cpp +++ b/src/openwb.cpp @@ -1,4 +1,8 @@ -#include "openwb.h" +/******************************************************** + * Copyright [2024] Tobias Faust openwb::openwb(): _version("") { OpenWBTopics = new std::vector(); @@ -25,7 +29,7 @@ void openwb::LoadAvailableOpenWbVersions() { } OpenWBVersions->clear(); - + JsonDocument doc; DeserializationError error = deserializeJson(doc, file); if (error) { @@ -70,7 +74,6 @@ void openwb::LoadOpenWBTopicsFromJson() { this->OpenWBTopics->push_back(t); Config->log(3, "openWB topic loaded: %s", kv.value().as().c_str()); - } } break; @@ -80,7 +83,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 +94,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 +108,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..b8a07c3c 100644 --- a/src/openwb.h +++ b/src/openwb.h @@ -1,18 +1,22 @@ -#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: + public: openwb(); /******************************************************* @@ -24,21 +28,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,10 +60,9 @@ class openwb { /******************************************************* * @brief clear all mappings ******************************************************/ - void clearMappings() { OpenWBMappings->clear(); } - - private: + void clearMappings() { OpenWBMappings->clear(); } + private: std::vector* OpenWBTopics; // openWB mqtt topics from JSON std::vector* OpenWBVersions; // openWB available versions from JSON std::vector* OpenWBMappings; // openWB mappings from JSON @@ -70,4 +73,4 @@ class openwb { void LoadAvailableOpenWbVersions(); }; -#endif \ No newline at end of file +#endif // OPENWB_H_