diff --git a/.github/workflows/compile_development.yml b/.github/workflows/compile_development.yml new file mode 100644 index 00000000..838293b4 --- /dev/null +++ b/.github/workflows/compile_development.yml @@ -0,0 +1,58 @@ +name: Ahoy Dev-Build for ESP8266/ESP32 + +on: + push: + branches: development* + paths-ignore: + - '**.md' # Do no build on *.md changes +jobs: + build: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v3 + with: + ref: development02 + - uses: benjlevesque/short-sha@v1.2 + id: short-sha + with: + length: 7 + - name: cache-pip + uses: actions/cache@v3 + with: + path: ~/.cache/pip + key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements.txt') }} + restore-keys: | + ${{ runner.os }}-pip- + - name: cache-platformio + uses: actions/cache@v3 + with: + path: ~/.platformio + key: ${{ runner.os }}-${{ hashFiles('**/lockfiles') }} + - name: setup-python + uses: actions/setup-python@v3 + - name: install-platformio + run: | + python -m pip install --upgrade pip + pip install --upgrade platformio + - name: update-html + working-directory: tools/esp8266/html + run: python convert.py + - name: Run PlatformIO + run: pio run -d tools/esp8266 --environment esp8266-release --environment esp32-wroom32-release + - name: rename-binary-files + id: rename-binary-files + working-directory: tools/esp8266/scripts + run: python getVersion.py + - name: set-version + uses: cschleiden/replace-tokens@v1 + with: + files: tools/esp8266/User_Manual.md + env: + VERSION: ${{ steps.rename-binary-files.outputs.name }} + - name: create-artifact + run: zip --junk-paths ${{ steps.rename-binary-files.outputs.name }}.zip tools/esp8266/.pio/build/out/* tools/esp8266/User_Manual.md + - uses: actions/upload-artifact@v3 + with: + name: ${{ steps.rename-binary-files.outputs.name }}_dev_build + path: ./${{ steps.rename-binary-files.outputs.name }}.zip diff --git a/.github/workflows/compile_esp8266.yml b/.github/workflows/compile_esp8266.yml index 245a5aac..787465d3 100644 --- a/.github/workflows/compile_esp8266.yml +++ b/.github/workflows/compile_esp8266.yml @@ -39,7 +39,7 @@ jobs: working-directory: tools/esp8266/html run: python convert.py - name: Run PlatformIO - run: pio run -d tools/esp8266 --environment esp8266-release + run: pio run -d tools/esp8266 --environment esp8266-release --environment esp32-wroom32-release - name: rename-binary-files id: rename-binary-files working-directory: tools/esp8266/scripts @@ -72,4 +72,4 @@ jobs: upload_url: ${{ steps.create-release.outputs.upload_url }} asset_path: ./${{ steps.rename-binary-files.outputs.name }}.zip asset_name: ${{ steps.rename-binary-files.outputs.name }}.zip - asset_content_type: application/zip \ No newline at end of file + asset_content_type: application/zip diff --git a/README.md b/README.md index 4e287236..82e79d47 100644 --- a/README.md +++ b/README.md @@ -1,22 +1,28 @@ +![actions/workflows/compile_esp8266.yml](../../actions/workflows/compile_esp8266.yml/badge.svg) ![actions/workflows/compile_development.yml](../../actions/workflows/compile_development.yml/badge.svg) + ![Logo](https://github.com/grindylow/ahoy/blob/main/doc/logo1_small.png?raw=true) # ahoy -Various tools, examples, and documentation for communicating with Hoymiles microinverters. +Ahoi is a project to bypass the original Hoymiles cloud solution. +In order to use this project, it is important what the area of ​​​​application looks like. +With each version it is necessary to have an NRF24L01+. -In particular: +Click on the link below you are interested in. +There you will find further explanations on how to proceed. (*Note: It is still under construction!*) -* `doc/hoymiles-format-description.txt` is a [detailed description of the communications format](doc/hoymiles-format-description.md) and the history of this project -* `doc/getting-started-ESP8266.md` shows the [hardware setup for an ESP8266-based system](doc/getting-started-ESP8266.md) -* The `tools` folder contains various software tools for RaspberryPi, Arduino and ESP8266/ESP32: - * A [version for ESP8266](tools/esp8266/) that includes an web interface ![](../../actions/workflows/compile_esp8266.yml/badge.svg) - * A [version for Arduino Nano](tools/nano/NRF24_SendRcv/) - * An [alternative Version of the above](tools/NRF24_SendRcv/) - * A [different implementation](tools/HoyDtuSim/) - * An [implementation for Raspberry Pi](tools/rpi/) that polls an inverter and archives results as log files/stdout as well as posting them to an MQTT broker. +##### Most updated section +- [ESP8266](tools/esp8266/) that includes an web interface -Contributors are always welcome! +##### will be updated as needed +- [Arduino Nano](tools/nano/NRF24_SendRcv/) +- [Raspberry Pi](tools/rpi/) +- [others](tools/nano/NRF24_SendRcv/) + +If errors occur or you have suggestions for ideas, please feel free to contact us [here](https://github.com/grindylow/ahoy/issues). ## Contact We run a Discord Server that can be used to get in touch with the Developers and Users. https://discord.gg/WzhxEY62mB + +**Contributors are always welcome!** diff --git a/tools/esp8266/.gitignore b/tools/esp8266/.gitignore index 89cc49cb..3e881135 100644 --- a/tools/esp8266/.gitignore +++ b/tools/esp8266/.gitignore @@ -3,3 +3,4 @@ .vscode/c_cpp_properties.json .vscode/launch.json .vscode/ipch +config_override.h diff --git a/tools/esp8266/README.md b/tools/esp8266/README.md index fcf4f512..fdebe916 100644 --- a/tools/esp8266/README.md +++ b/tools/esp8266/README.md @@ -13,7 +13,7 @@ ## Overview -This code is intended to run on a Wemos D1mini or similar. The code is based on 'Hubi's code, which can be found here: +This page describes how the module of a Wemos D1 mini and ESP8266 is wired to the radio module, flashed and how the further steps are to communicate with the WR HM series. The NRF24L01+ radio module is connected to the standard SPI pins: @@ -28,52 +28,8 @@ Additional there are 3 pins, which can be set individual: These pins can be changed from the /setup URL - -## Compile - -This code can be compiled using Visual Studio Code and **PlatformIO** Addon. The settings were: - -- Board: Generic ESP8266 Module -- Flash-Size: 4MB -- Install libraries (not included in the Arduino IDE 1.8.19): - - Time Arduino Time library (TimeLib.h) - - RF24 Optimized high speed nRF24L01+ driver class documentation - - PubSubClient A client library for MQTT messaging. By Nick O'Leary - - ArduinoJson Arduino Json library - -### Optional Configuration before compilation - -- number of supported inverters (set to 3 by default) `defines.h` -- DTU radio id `hmRadio.h` -- unformated list in webbrowser `/livedata` `config.h`, `LIVEDATA_VISUALIZED` - - -## Flash ESP with Firmware - -1. flash the ESP with the compiled firmware using the UART pins or any preinstalled firmware with OTA capabilities -2. repower the ESP -3. the ESP will start as access point (AP) if there is no network config stored in its eeprom -4. connect to the AP, you will be forwarded to the setup page -5. configure your WiFi settings, save, repower -6. check your router or serial console for the IP address of the module. You can try ping the configured device name as well. - - -## Usage - -Connect the ESP to power and to your serial console (optional). The webinterface has the following abilities: - -- OTA Update (over the air update) -- Configuration (Wifi, inverter(s), Pinout, MQTT) -- visual display of the connected inverters / modules -- some statistics about communication (debug) - -The serial console will print the converted values which were read out of the inverter(s) - - -## Compatiblity - +#### Compatiblity For now the following inverters should work out of the box: - - HM300 - HM350 - HM400 @@ -83,6 +39,25 @@ For now the following inverters should work out of the box: - HM1200 - HM1500 +The NRF24L01+ radio module is connected to the standard SPI pins. +Additional there are 3 pins, which can be set individual: CS, CE and IRQ +These pins can be changed in the http:///setup URL or with a click on the Setup link. + +## ESP8266 electr. associate + + +## Compile + +This code can be compiled using Visual Studio Code and **PlatformIO** Addon. The settings were: + +- Board: Generic ESP8266 Module +- Flash-Size: 4MB +- Install libraries (not included in the Arduino IDE 1.8.19): + - `Time` 1.6.1 + - `RF24` 1.4.5 + - `PubSubClient` 2.8 + - `ArduinoJson` 6.19.4 + ## Used Libraries - `ESP8266WiFi` 1.0 @@ -94,6 +69,80 @@ For now the following inverters should work out of the box: - `PubSubClient` 2.8 - `ArduinoJson` 6.19.4 +### Optional Configuration before compilation + +- number of supported inverters (set to 3 by default) `config.h` +- DTU radio id `config.h` (default = 1234567801) +- unformated list in webbrowser `/livedata` `config.h`, `LIVEDATA_VISUALIZED` + +Alternativly, instead of modifying `config.h`, `config_override_example.h` can be copied to `config_override.h` and customized. +config_override.h is excluded from version control and stays local. + +## Flash ESP with Firmware + +#### nodemcu-pyflasher (easy way) +1. download the flash-tool [nodemcu-pyflasher](https://github.com/marcelstoer/nodemcu-pyflasher) +2. download latest release bin-file from [ahoy_](https://github.com/grindylow/ahoy/releases) +3. connect the target device with your pc. +4. Set the correct serial port and select the correct *.bin file +5. click now on "Flash NodeMCU" + +1. flash the ESP with the compiled firmware using the UART pins or any preinstalled firmware with OTA capabilities +2. repower the ESP +3. the ESP will start as access point (AP) if there is no network config stored in its eeprom +4. connect to the AP, you will be forwarded to the setup page + +X. configure your WiFi settings, save, repower +Y. check your router or serial console for the IP address of the module. You can try ping the configured device name as well. + +! ATTENTION: If you update from a very low version to the newest, please make sure to wipe all flash data! + +## pages +| page | output | +| ---- | ------ | +| /uptime | 0 Days, 01:37:34; now: 2022-08-21 11:13:53 | +| /reboot | reboot dtu device | +| /erase | | +| /factory | | +| /setup | | +| /save | open the setup site | +| /cmdstat | show stat from the home site | +| /visualization | | +| /livedata | | +| /json | json output from the livedata | +| /api | | + +## Usage + +The webinterface has the following abilities: +- OTA Update (over the air update) +- Configuration (Wifi, inverter(s), NTP Server, Pinout, MQTT, Amplifier Power Level, Debug) +- visual display of the connected inverters / modules +- some statistics about communication (debug) + +The serial console will print the converted values which were read out of the inverter(s) + +### MQTT command to set the DTU without webinterface + [Read here](https://github.com/grindylow/ahoy/blob/development02/tools/esp8266/User_Manual.md) + + ## Todo's [See this post](https://github.com/grindylow/ahoy/issues/142) + +- [ ] Wechsel zu AsyncWebServer und ElegantOTA für Stabilität +- [x] klarer Scheduler / Task manager, der ggf. den Receive Task priorisieren kann +- [x] Device Info Kommandos (Firmware Version, etc.) über das Dashboard anzeigen [Device Information ( `0x15` `REQ_ARW_DAT_ALL` ) SubCmd Kommandos #145](https://github.com/grindylow/ahoy/issues/145) +- [ ] AlarmData & AlarmUpdate Parsen und auf eigener Seite darstellen +------------------ SWIM LANE --------------------------- +- [ ] Device Control Kommandos aus dem Setup ermöglichen (TurnOn, TurnOff, Restart, ActivePower Limit, ReactivePower Limit, SetPowerFactor, etc.) +- [ ] Settings exportieren / importieren (API/UI) +- [ ] Settings in settings.ini speichern (LittleFS statt EEPROM) [Settings in settings.ini speichern (LittleFS statt EEPROM) #164](https://github.com/grindylow/ahoy/issues/164) +- [ ] Homepage aufräumen nur ein Status (aktuell drei AJAX Calls /uptime, /time, /cmdstat) +- [ ] app.cpp aufräumen und in hmRadio / hmProtokollGen3 auslagern +- [ ] MI Wechselrichter unterstützen (miSystem, miInverter, miDefines, miProtokollGen2 etc.) +- [ ] nRF24 Interrupt Handling sinnvoll oder warum macht die nRF24 Bibliothek ständig `0x07` Statusabfragen [NRF24 polling trotz aktiviertem IRQ #83](https://github.com/grindylow/ahoy/issues/83) +- [ ] Debug Level im Setup änderbar -auch Livedata Visualisierung abschalten ? +- [ ] MQTT Discovery (HomeAssistant) im Setup optional machen +- [x] MQTT Subscribe nur beim Reconnect [Das subscribe in der Reconnect Procedure sollte doch nur nach einem conect ausgeführt werden und nicht bei jedem Duchlauf #139](https://github.com/grindylow/ahoy/issues/139) + ## Contact We run a Discord Server that can be used to get in touch with the Developers and Users. diff --git a/tools/esp8266/app.cpp b/tools/esp8266/app.cpp index 75fe77df..be78d6a5 100644 --- a/tools/esp8266/app.cpp +++ b/tools/esp8266/app.cpp @@ -20,7 +20,7 @@ app::app() { mWifi = new ahoywifi(this, &mSysConfig, &mConfig); resetSystem(); - loadDefaultConfig(); + loadDefaultConfig(); mSys = new HmSystemType(); } @@ -53,18 +53,22 @@ void app::loop(void) { mWebInst->loop(); if(checkTicker(&mUptimeTicker, mUptimeInterval)) { - mUptimeSecs++; - if(0 != mTimestamp) - mTimestamp++; - else { - if(!apActive) { - mTimestamp = mWifi->getNtpTime(); - DPRINTLN(DBG_INFO, "[NTP]: " + getDateTimeStr(mTimestamp)); - } + if(millis() - mPrevMillis >= 1000) { + mPrevMillis += 1000; + mUptimeSecs++; + if(0 != mTimestamp) + mTimestamp++; } } - + if(checkTicker(&mNtpRefreshTicker, mNtpRefreshInterval)) { + if(!apActive) { + mTimestamp = mWifi->getNtpTime(); + DPRINTLN(DBG_INFO, "[NTP]: " + getDateTimeStr(mTimestamp)); + } + } + + mSys->Radio.loop(); yield(); @@ -114,31 +118,31 @@ void app::loop(void) { } if(NULL != iv && p->packet[0] == (TX_REQ_DEVCONTROL + 0x80)) { // response from dev control command DPRINTLN(DBG_DEBUG, F("Response from devcontrol request received")); - iv->devControlRequest = false; - switch (p->packet[12]){ - case ActivePowerContr: - if (iv->devControlCmd >= ActivePowerContr && iv->devControlCmd <= PFSet){ // ok inverter accepted the set point copy it to dtu eeprom - if ((iv->powerLimit[1] & 0xff00) >0){ // User want to have it persistent - mEep->write(ADDR_INV_PWR_LIM + iv->id * 2,iv->powerLimit[0]); - mEep->write(ADDR_INV_PWR_LIM_CON + iv->id * 2,iv->powerLimit[1]); - updateCrc(); - mEep->commit(); - DPRINTLN(DBG_INFO, F("Inverter ") + String(iv->id) + F(" has accepted power limit set point ") + String(iv->powerLimit[0]) + F(" with PowerLimitControl ") + String(iv->powerLimit[1]) + F(", written to dtu eeprom")); - } else { - DPRINTLN(DBG_INFO, F("Inverter ") + String(iv->id) + F(" has accepted power limit set point ") + String(iv->powerLimit[0]) + F(" with PowerLimitControl ") + String(iv->powerLimit[1])); + iv->devControlRequest = false; + switch (p->packet[12]) { + case ActivePowerContr: + if (iv->devControlCmd >= ActivePowerContr && iv->devControlCmd <= PFSet) { // ok inverter accepted the set point copy it to dtu eeprom + if ((iv->powerLimit[1] & 0xff00) > 0) { // User want to have it persistent + mEep->write(ADDR_INV_PWR_LIM + iv->id * 2, iv->powerLimit[0]); + mEep->write(ADDR_INV_PWR_LIM_CON + iv->id * 2, iv->powerLimit[1]); + updateCrc(); + mEep->commit(); + DPRINTLN(DBG_INFO, F("Inverter ") + String(iv->id) + F(" has accepted power limit set point ") + String(iv->powerLimit[0]) + F(" with PowerLimitControl ") + String(iv->powerLimit[1]) + F(", written to dtu eeprom")); + } else + DPRINTLN(DBG_INFO, F("Inverter ") + String(iv->id) + F(" has accepted power limit set point ") + String(iv->powerLimit[0]) + F(" with PowerLimitControl ") + String(iv->powerLimit[1])); + iv->devControlCmd = Init; + } + break; + + default: + if (iv->devControlCmd == ActivePowerContr) { + //case inverter did not accept the sent limit; set back to last stored limit + mEep->read(ADDR_INV_PWR_LIM + iv->id * 2, (uint16_t *)&(iv->powerLimit[0])); + mEep->read(ADDR_INV_PWR_LIM_CON + iv->id * 2, (uint16_t *)&(iv->powerLimit[1])); + DPRINTLN(DBG_INFO, F("Inverter has not accepted power limit set point")); } iv->devControlCmd = Init; - } - break; - default: - if (iv->devControlCmd == ActivePowerContr){ - //case inverter did not accept the sent limit; set back to last stored limit - mEep->read(ADDR_INV_PWR_LIM + iv->id * 2, (uint16_t *)&(iv->powerLimit[0])); - mEep->read(ADDR_INV_PWR_LIM_CON + iv->id * 2, (uint16_t *)&(iv->powerLimit[1])); - DPRINTLN(DBG_INFO, F("Inverter has not accepted power limit set point")); - } - iv->devControlCmd = Init; - break; + break; } } } @@ -160,20 +164,7 @@ void app::loop(void) { if((++mMqttTicker >= mMqttInterval) && (mMqttInterval != 0xffff) && mMqttActive) { mMqttTicker = 0; mMqtt.isConnected(true); // really needed? See comment from HorstG-57 #176 - char topic[30], val[10]; - for(uint8_t id = 0; id < mSys->getNumInverters(); id++) { - Inverter<> *iv = mSys->getInverterByPos(id); - if(NULL != iv) { - if(iv->isAvailable(mTimestamp)) { - for(uint8_t i = 0; i < iv->listLen; i++) { - snprintf(topic, 30, "%s/ch%d/%s", iv->name, iv->assign[i].ch, fields[iv->assign[i].fieldId]); - snprintf(val, 10, "%.3f", iv->getValue(i)); - mMqtt.sendMsg(topic, val); - yield(); - } - } - } - } + char val[10]; snprintf(val, 10, "%ld", millis()/1000); #ifndef __MQTT_NO_DISCOVERCONFIG__ @@ -239,6 +230,10 @@ void app::loop(void) { if(!mPayload[iv->id].complete) { mRxFailed++; + iv->setQueuedCmdFinished(); // command failed + if(mConfig.serialDebug) { + DPRINTLN(DBG_INFO, F("enqueued cmd failed/timeout")); + } if(mConfig.serialDebug) { DPRINT(DBG_INFO, F("Inverter #") + String(iv->id) + " "); DPRINTLN(DBG_INFO, F("no Payload received! (retransmits: ") + String(mPayload[iv->id].retransmits) + ")"); @@ -249,12 +244,12 @@ void app::loop(void) { yield(); if(mConfig.serialDebug) - DPRINTLN(DBG_DEBUG, F("app:loop WiFi WiFi.status ") + String(WiFi.status()) ); - DPRINTLN(DBG_INFO, F("Requesting Inverter SN ") + String(iv->serial.u64, HEX)); - if(iv->devControlRequest && iv->powerLimit[0] > 0){ // prevent to "switch off" + DPRINTLN(DBG_DEBUG, F("app:loop WiFi WiFi.status ") + String(WiFi.status())); + DPRINTLN(DBG_INFO, F("Requesting Inverter SN ") + String(iv->serial.u64, HEX)); + if(iv->devControlRequest && (iv->powerLimit[0] > 0) && (NoPowerLimit != iv->powerLimit[1])) { // prevent to "switch off" if(mConfig.serialDebug) DPRINTLN(DBG_INFO, F("Devcontrol request ") + String(iv->devControlCmd) + F(" power limit ") + String(iv->powerLimit[0])); - mSys->Radio.sendControlPacket(iv->radioId.u64,iv->devControlCmd ,iv->powerLimit); + mSys->Radio.sendControlPacket(iv->radioId.u64, iv->devControlCmd ,iv->powerLimit); iv->enqueCommand(SystemConfigPara); } else { mSys->Radio.sendTimePacket(iv->radioId.u64,iv->getQueuedCmd(), mPayload[iv->id].ts,iv->alarmMesIndex); @@ -346,8 +341,11 @@ void app::processPayload(bool retransmit) { else { mPayload[iv->id].complete = true; iv->ts = mPayload[iv->id].ts; - uint8_t payload[128] = {0}; + uint8_t payload[128]; uint8_t offs = 0; + + memset(payload, 0, 128); + for(uint8_t i = 0; i < (mPayload[iv->id].maxPackId); i ++) { memcpy(&payload[offs], mPayload[iv->id].data[i], (mPayload[iv->id].len[i])); offs += (mPayload[iv->id].len[i]); @@ -366,11 +364,34 @@ void app::processPayload(bool retransmit) { yield(); } iv->doCalculations(); // cmd value decides which parser is used to decode payload + + iv->setQueuedCmdFinished(); + + // MQTT send out + if(mMqttActive) { + char topic[30], val[10]; + for (uint8_t id = 0; id < mSys->getNumInverters(); id++) + { + Inverter<> *iv = mSys->getInverterByPos(id); + if (NULL != iv) + { + if (iv->isAvailable(mTimestamp)) + { + for (uint8_t i = 0; i < iv->listLen; i++) + { + snprintf(topic, 30, "%s/ch%d/%s", iv->name, iv->assign[i].ch, fields[iv->assign[i].fieldId]); + snprintf(val, 10, "%.3f", iv->getValue(i)); + mMqtt.sendMsg(topic, val); + yield(); + } + } + } + } + } #ifdef __MQTT_AFTER_RX__ doMQTT = true; #endif - iv->setQueuedCmdFinished(); } } yield(); @@ -483,9 +504,10 @@ String app::getStatistics(void) { Inverter<> *iv; for(uint8_t i = 0; i < MAX_NUM_INVERTERS; i++) { iv = mSys->getInverterByPos(i); + content += F("Inverter #") + String(i) + F(": "); if(NULL != iv) { bool avail = true; - content += F("Inverter '") + String(iv->name) + F(" (FW-Version: ") + String(iv->fwVersion) +F(")") + F("' is "); + content += String(iv->name) + F(" (v") + String(iv->fwVersion) +F(")") + F(" is "); if(!iv->isAvailable(mTimestamp)) { content += F("not "); avail = false; @@ -500,9 +522,8 @@ String app::getStatistics(void) { content += F("-> last successful transmission: ") + getDateTimeStr(iv->getLastTs()) + "\n"; } } - else { - content += F("Inverter ") + String(i) + F(" not (correctly) configured\n"); - } + else + content += F("n/a\n"); } if(!mSys->Radio.isChipConnected()) @@ -523,119 +544,6 @@ String app::getStatistics(void) { } - -//----------------------------------------------------------------------------- -String app::getLiveData(void) -{ - String modHtml; - for (uint8_t id = 0; id < mSys->getNumInverters(); id++) - { - Inverter<> *iv = mSys->getInverterByPos(id); - if (NULL != iv) - { -#ifdef LIVEDATA_VISUALIZED - uint8_t modNum, pos; - switch (iv->type) - { - default: - case INV_TYPE_1CH: - modNum = 1; - break; - case INV_TYPE_2CH: - modNum = 2; - break; - case INV_TYPE_4CH: - modNum = 4; - break; - } - - modHtml += F("
" - "
") + - String(iv->name) + F(" Limit ") + String(iv->actPowerLimit); - if (true) - { // live Power Limit from inverter is always in % - modHtml += F(" %"); - } - else - { - modHtml += F(" W"); - } - uint8_t list[] = {FLD_UAC, FLD_IAC, FLD_PAC, FLD_F, FLD_PCT, FLD_T, FLD_YT, FLD_YD, FLD_PDC, FLD_EFF, FLD_PRA, FLD_ALARM_MES_ID}; - - for (uint8_t fld = 0; fld < 12; fld++) - { - pos = (iv->getPosByChFld(CH0, list[fld])); - if (0xff != pos) - { - modHtml += F("
"); - modHtml += F("") + String(iv->getValue(pos)); - modHtml += F("") + String(iv->getUnit(pos)) + F(""); - modHtml += F("") + String(iv->getFieldName(pos)) + F(""); - modHtml += F("
"); - } - } - modHtml += "
"; - - for (uint8_t ch = 1; ch <= modNum; ch++) - { - modHtml += F("
"); - if (iv->chName[ch - 1][0] == 0) - modHtml += F("CHANNEL ") + String(ch); - else - modHtml += String(iv->chName[ch - 1]); - modHtml += F(""); - for (uint8_t j = 0; j < 6; j++) - { - switch (j) - { - default: - pos = (iv->getPosByChFld(ch, FLD_UDC)); - break; - case 1: - pos = (iv->getPosByChFld(ch, FLD_IDC)); - break; - case 2: - pos = (iv->getPosByChFld(ch, FLD_PDC)); - break; - case 3: - pos = (iv->getPosByChFld(ch, FLD_YD)); - break; - case 4: - pos = (iv->getPosByChFld(ch, FLD_YT)); - break; - case 5: - pos = (iv->getPosByChFld(ch, FLD_IRR)); - break; - } - if (0xff != pos) - { - modHtml += F("") + String(iv->getValue(pos)); - modHtml += F("") + String(iv->getUnit(pos)) + F(""); - modHtml += F("") + String(iv->getFieldName(pos)) + F(""); - } - } - modHtml += "
"; - yield(); - } - modHtml += F("
Last received data requested at: ") + getDateTimeStr(iv->ts) + F("
"); - modHtml += F("
"); -#else - // dump all data to web frontend - modHtml = F("
");
-            char topic[30], val[10];
-            for (uint8_t i = 0; i < iv->listLen; i++)
-            {
-                snprintf(topic, 30, "%s/ch%d/%s", iv->name, iv->assign[i].ch, iv->getFieldName(i));
-                snprintf(val, 10, "%.3f %s", iv->getValue(i), iv->getUnit(i));
-                modHtml += String(topic) + ": " + String(val) + "\n";
-            }
-            modHtml += F("
"); -#endif - } - } - return modHtml; -} - //----------------------------------------------------------------------------- String app::getJson(void) { DPRINTLN(DBG_VERBOSE, F("app::showJson")); @@ -751,7 +659,11 @@ const char* app::getFieldStateClass(uint8_t fieldId) { void app::resetSystem(void) { mUptimeSecs = 0; mUptimeTicker = 0xffffffff; - mUptimeInterval = 1000; + mUptimeInterval = 500; // [ms] + mPrevMillis = 0; + + mNtpRefreshTicker = 0; + mNtpRefreshInterval = NTP_REFRESH_INTERVAL; // [ms] #ifdef AP_ONLY mTimestamp = 1; @@ -852,11 +764,16 @@ void app::loadEEpconfig(void) { // it is "doppelt-gemoppelt" because the inverter shall remember the setting if the dtu makes a power cycle / reboot if (iv->powerLimit[0] != 0xffff) { iv->devControlCmd = ActivePowerContr; // set active power limit - if (iv->powerLimit[1] & 0x0001){ - DPRINTLN(DBG_INFO, F("add inverter: ") + String(name) + ", SN: " + String(invSerial, HEX) + ", Power Limit: " + String(iv->powerLimit[0]) + " in %"); - } else { - DPRINTLN(DBG_INFO, F("add inverter: ") + String(name) + ", SN: " + String(invSerial, HEX) + ", Power Limit: " + String(iv->powerLimit[0]) + " in Watt"); + DPRINT(DBG_INFO, F("add inverter: ") + String(name) + ", SN: " + String(invSerial, HEX)); + if(iv->powerLimit[1] != NoPowerLimit) { + DBGPRINT(F(", Power Limit: ") + String(iv->powerLimit[0])); + if ((iv->powerLimit[1] & 0x0001) == 0x0001) + DBGPRINTLN(F(" in %")); + else + DBGPRINTLN(F(" in Watt")); } + else + DBGPRINTLN(F(" ")); } for(uint8_t j = 0; j < 4; j++) { mEep->read(ADDR_INV_CH_NAME + (i * 4 * MAX_NAME_LENGTH) + j * MAX_NAME_LENGTH, iv->chName[j], MAX_NAME_LENGTH); @@ -879,17 +796,15 @@ void app::saveValues(void) { mEep->write(ADDR_CFG, (uint8_t*)&mConfig, CFG_LEN); Inverter<> *iv; for(uint8_t i = 0; i < MAX_NUM_INVERTERS; i ++) { - iv = mSys->getInverterByPos(i); - if(NULL != iv) { - mEep->write(ADDR_INV_ADDR + (i * 8), iv->serial.u64); - mEep->write(ADDR_INV_PWR_LIM + i * 2, iv->powerLimit[0]); - mEep->write(ADDR_INV_PWR_LIM_CON + i * 2, iv->powerLimit[1]); - mEep->write(ADDR_INV_NAME + (i * MAX_NAME_LENGTH), iv->name, MAX_NAME_LENGTH); - // max channel power / name - for(uint8_t j = 0; j < 4; j++) { - mEep->write(ADDR_INV_CH_PWR + (i * 2 * 4) + (j*2), iv->chMaxPwr[j]); - mEep->write(ADDR_INV_CH_NAME + (i * 4 * MAX_NAME_LENGTH) + j * MAX_NAME_LENGTH, iv->chName[j], MAX_NAME_LENGTH); - } + iv = mSys->getInverterByPos(i, false); + mEep->write(ADDR_INV_ADDR + (i * 8), iv->serial.u64); + mEep->write(ADDR_INV_PWR_LIM + i * 2, iv->powerLimit[0]); + mEep->write(ADDR_INV_PWR_LIM_CON + i * 2, iv->powerLimit[1]); + mEep->write(ADDR_INV_NAME + (i * MAX_NAME_LENGTH), iv->name, MAX_NAME_LENGTH); + // max channel power / name + for(uint8_t j = 0; j < 4; j++) { + mEep->write(ADDR_INV_CH_PWR + (i * 2 * 4) + (j*2), iv->chMaxPwr[j]); + mEep->write(ADDR_INV_CH_NAME + (i * 4 * MAX_NAME_LENGTH) + j * MAX_NAME_LENGTH, iv->chName[j], MAX_NAME_LENGTH); } } @@ -946,4 +861,4 @@ void app::resetPayload(Inverter<>* iv) mPayload[iv->id].complete = false; mPayload[iv->id].requested = true; mPayload[iv->id].ts = mTimestamp; -} \ No newline at end of file +} diff --git a/tools/esp8266/app.h b/tools/esp8266/app.h index 0e09701a..b7fd462b 100644 --- a/tools/esp8266/app.h +++ b/tools/esp8266/app.h @@ -71,7 +71,6 @@ class app { void saveValues(void); void resetPayload(Inverter<>* iv); String getStatistics(void); - String getLiveData(void); String getJson(void); bool getWifiApActive(void); @@ -80,9 +79,10 @@ class app { } uint64_t Serial2u64(const char *val) { - char tmp[3] = {0}; + char tmp[3]; uint64_t ret = 0ULL; uint64_t u64; + memset(tmp, 0, 3); for(uint8_t i = 0; i < 6; i++) { tmp[0] = val[i*2]; tmp[1] = val[i*2 + 1]; @@ -95,7 +95,7 @@ class app { } String getDateTimeStr(time_t t) { - char str[20] = {0}; + char str[20]; if(0 == t) sprintf(str, "n/a"); else @@ -113,9 +113,11 @@ class app { void eraseSettings(bool all = false) { //DPRINTLN(DBG_VERBOSE, F("main.h:eraseSettings")); - uint8_t buf[64] = {0}; + uint8_t buf[64]; uint16_t addr = (all) ? ADDR_START : ADDR_START_SETTINGS; uint16_t end; + + memset(buf, 0xff, 64); do { end = addr + 64; if(end > (ADDR_SETTINGS_CRC + 2)) @@ -222,7 +224,10 @@ class app { uint32_t mUptimeTicker; uint16_t mUptimeInterval; uint32_t mUptimeSecs; + uint32_t mPrevMillis; uint8_t mHeapStatCnt; + uint32_t mNtpRefreshTicker; + uint32_t mNtpRefreshInterval; bool mWifiSettingsValid; diff --git a/tools/esp8266/config.h b/tools/esp8266/config.h index 8331c273..ab633f9a 100644 --- a/tools/esp8266/config.h +++ b/tools/esp8266/config.h @@ -36,6 +36,9 @@ #define DEF_RF24_CE_PIN 2 #define DEF_RF24_IRQ_PIN 0 +// default radio ID +#define DTU_RADIO_ID ((uint64_t)0x1234567801ULL) + // default NRF24 power, possible values (0 - 3) #define DEF_AMPLIFIERPOWER 2 @@ -69,12 +72,15 @@ // threshold of minimum power on which the inverter is marked as inactive #define INACT_PWR_THRESH 3 -// default ntp server uri +// default NTP server uri #define DEF_NTP_SERVER_NAME "pool.ntp.org" -// default ntp server port +// default NTP server port #define DEF_NTP_PORT 8888 +// NTP refresh interval in ms (default 12h) +#define NTP_REFRESH_INTERVAL 12 * 3600 * 1000 + // default mqtt interval #define MQTT_INTERVAL 60 @@ -96,4 +102,8 @@ // changes the style of "/setup" page, visualized = nicer #define LIVEDATA_VISUALIZED +#if __has_include("config_override.h") + #include "config_override.h" +#endif + #endif /*__CONFIG_H__*/ diff --git a/tools/esp8266/config_override_example.h b/tools/esp8266/config_override_example.h new file mode 100644 index 00000000..dbaf54df --- /dev/null +++ b/tools/esp8266/config_override_example.h @@ -0,0 +1,30 @@ +//----------------------------------------------------------------------------- +// 2022 Ahoy, https://www.mikrocontroller.net/topic/525778 +// Creative Commons - http://creativecommons.org/licenses/by-nc-sa/3.0/de/ +//----------------------------------------------------------------------------- + +#ifndef __CONFIG_OVERRIDE_H__ +#define __CONFIG_OVERRIDE_H__ + +// override fallback WiFi info + +// each ovveride must be preceeded with an #undef statement +#undef FB_WIFI_SSID +#define FB_WIFI_SSID "MY_SSID" + +// each ovveride must be preceeded with an #undef statement +#undef FB_WIFI_PWD +#define FB_WIFI_PWD "MY_WIFI_KEY" + +// ESP32 default pinout +#undef DEF_RF24_CS_PIN +#define DEF_RF24_CS_PIN 5 +#undef DEF_RF24_CE_PIN +#define DEF_RF24_CE_PIN 4 +#undef DEF_RF24_IRQ_PIN +#define DEF_RF24_IRQ_PIN 16 + +#undef DTU_RADIO_ID +#define DTU_RADIO_ID ((uint64_t)0x1234567802ULL) + +#endif /*__CONFIG_OVERRIDE_H__*/ diff --git a/tools/esp8266/defines.h b/tools/esp8266/defines.h index 31b65b77..33732ff5 100644 --- a/tools/esp8266/defines.h +++ b/tools/esp8266/defines.h @@ -13,7 +13,7 @@ //------------------------------------- #define VERSION_MAJOR 0 #define VERSION_MINOR 5 -#define VERSION_PATCH 15 +#define VERSION_PATCH 16 //------------------------------------- @@ -58,6 +58,7 @@ typedef enum { } DevControlCmdType; typedef enum { // ToDo: to be verified by field tests + NoPowerLimit = 0xffff, // ahoy internal value, no hoymiles value! AbsolutNonPersistent = 0UL, // 0x0000 RelativNonPersistent = 1UL, // 0x0001 AbsolutPersistent = 256UL, // 0x0100 @@ -104,6 +105,7 @@ typedef enum { // ToDo: to be verified by field tests #define MQTT_PORT_LEN 2 // uint16_t #define MQTT_DISCOVERY_PREFIX "homeassistant" #define MQTT_MAX_PACKET_SIZE 384 +#define MQTT_RECONNECT_DELAY 5000 #define SER_ENABLE_LEN 1 // uint8_t #define SER_DEBUG_LEN 1 // uint8_t diff --git a/tools/esp8266/hmDefines.h b/tools/esp8266/hmDefines.h index 36139bb2..6330db98 100644 --- a/tools/esp8266/hmDefines.h +++ b/tools/esp8266/hmDefines.h @@ -23,9 +23,13 @@ const char* const units[] = {"V", "A", "W", "Wh", "kWh", "Hz", "°C", "%","VAr", // field types enum {FLD_UDC = 0, FLD_IDC, FLD_PDC, FLD_YD, FLD_YW, FLD_YT, - FLD_UAC, FLD_IAC, FLD_PAC, FLD_F, FLD_T, FLD_PCT, FLD_EFF, FLD_IRR, FLD_PRA,FLD_ALARM_MES_ID,FLD_FW_VERSION,FLD_FW_BUILD_YEAR,FLD_FW_BUILD_MONTH_DAY,FLD_HW_ID,FLD_ACT_PWR_LIMIT}; + FLD_UAC, FLD_IAC, FLD_PAC, FLD_F, FLD_T, FLD_PCT, FLD_EFF, + FLD_IRR, FLD_PRA,FLD_ALARM_MES_ID,FLD_FW_VERSION,FLD_FW_BUILD_YEAR, + FLD_FW_BUILD_MONTH_DAY,FLD_HW_ID,FLD_ACT_PWR_LIMIT,FLD_LAST_ALARM_CODE}; + const char* const fields[] = {"U_DC", "I_DC", "P_DC", "YieldDay", "YieldWeek", "YieldTotal", - "U_AC", "I_AC", "P_AC", "Freq", "Temp", "Pct", "Efficiency", "Irradiation","P_ACr","ALARM_MES_ID","FWVersion","FWBuildYear","FWBuildMonthDay","HWPartId","PowerLimit"}; + "U_AC", "I_AC", "P_AC", "Freq", "Temp", "Pct", "Efficiency", "Irradiation","P_ACr", + "ALARM_MES_ID","FWVersion","FWBuildYear","FWBuildMonthDay","HWPartId","PowerLimit","LastAlarmCode"}; // mqtt discovery device classes enum {DEVICE_CLS_NONE = 0, DEVICE_CLS_CURRENT, DEVICE_CLS_ENERGY, DEVICE_CLS_PWR, DEVICE_CLS_VOLTAGE, DEVICE_CLS_FREQ, DEVICE_CLS_TEMP}; @@ -97,6 +101,10 @@ const byteAssign_t SystemConfigParaAssignment[] = { }; #define HMSYSTEM_LIST_LEN (sizeof(SystemConfigParaAssignment) / sizeof(byteAssign_t)) +const byteAssign_t AlarmDataAssignment[] = { + { FLD_LAST_ALARM_CODE, UNIT_NONE, CH0, 0, 2, 1 } +}; +#define HMALARMDATA_LIST_LEN (sizeof(AlarmDataAssignment) / sizeof(byteAssign_t)) diff --git a/tools/esp8266/hmInverter.h b/tools/esp8266/hmInverter.h index 774fd809..fd2eafee 100644 --- a/tools/esp8266/hmInverter.h +++ b/tools/esp8266/hmInverter.h @@ -115,17 +115,20 @@ class Inverter { RECORDTYPE *record; // pointer for values uint16_t chMaxPwr[4]; // maximum power of the modules (Wp) char chName[4][MAX_NAME_LENGTH]; // human readable name for channel + String lastAlarmMsg; bool initialized; // needed to check if the inverter was correctly added (ESP32 specific - union types are never null) Inverter() { ts = 0; powerLimit[0] = 0xffff; // 65535 W Limit -> unlimited - powerLimit[1] = 0x0000; // + powerLimit[1] = NoPowerLimit; // actPowerLimit = 0xffff; // init feedback from inverter to -1 devControlRequest = false; - devControlCmd = 0xff; + devControlCmd = InitDataState; initialized = false; fwVersion = 0; + lastAlarmMsg = "nothing"; + alarmMesIndex = 0; } ~Inverter() { @@ -141,7 +144,8 @@ class Inverter { void setQueuedCmdFinished(){ if (!_commandQueue.empty()){ - _commandQueue.pop(); // Will destroy CommandAbstract Class Object (?) + // Will destroy CommandAbstract Class Object (?) + _commandQueue.pop(); } } @@ -150,7 +154,14 @@ class Inverter { if (_commandQueue.empty()){ // Fill with default commands enqueCommand(RealTimeRunData_Debug); - //enqueCommand(SystemConfigPara); + if (fwVersion == 0) + { // info needed maybe after "one night" (=> DC>0 to DC=0 and to DC>0) or reboot + enqueCommand(InverterDevInform_All); + } + if (actPowerLimit == 0xffff) + { // info needed maybe after "one nigth" (=> DC>0 to DC=0 and to DC>0) or reboot + enqueCommand(SystemConfigPara); + } } return _commandQueue.front().get()->getCmd(); } @@ -164,8 +175,6 @@ class Inverter { memset(name, 0, MAX_NAME_LENGTH); memset(chName, 0, MAX_NAME_LENGTH * 4); memset(record, 0, sizeof(RECORDTYPE) * listLen); - enqueCommand(InverterDevInform_All); - enqueCommand(SystemConfigPara); initialized = true; } @@ -206,12 +215,25 @@ class Inverter { val <<= 8; val |= buf[ptr]; } while(++ptr != end); - record[pos] = (RECORDTYPE)(val) / (RECORDTYPE)(div); + if ((RECORDTYPE)(div) > 1){ + record[pos] = (RECORDTYPE)(val) / (RECORDTYPE)(div); + } + else { + record[pos] = (RECORDTYPE)(val); + } + } if (cmd == RealTimeRunData_Debug) { // get last alarm message index and save it in the inverter object if (getPosByChFld(0, FLD_ALARM_MES_ID) == pos){ - alarmMesIndex = record[pos]; + if (alarmMesIndex < record[pos]){ + alarmMesIndex = record[pos]; + //enqueCommand(AlarmUpdate); // What is the function of AlarmUpdate? + enqueCommand(AlarmData); + } + else { + alarmMesIndex = record[pos]; // no change + } } } if (cmd == InverterDevInform_All) { @@ -228,6 +250,11 @@ class Inverter { DPRINT(DBG_DEBUG, F("Inverter actual power limit: ") + String(actPowerLimit)); } } + if (cmd == AlarmData){ + if (getPosByChFld(0, FLD_LAST_ALARM_CODE) == pos){ + lastAlarmMsg = getAlarmStr(record[pos]); + } + } } RECORDTYPE getValue(uint8_t pos) { @@ -263,52 +290,273 @@ class Inverter { return false; } - uint32_t getLastTs(void) { + uint32_t getLastTs(void) + { DPRINTLN(DBG_VERBOSE, F("hmInverter.h:getLastTs")); return ts; } - void getAssignment() { + void getAssignment() + { DPRINTLN(DBG_DEBUG, F("hmInverter.h:getAssignment")); - uint8_t cmd = getQueuedCmd(); - switch (cmd) + // Default assignment; + if (INV_TYPE_1CH == type) { - case RealTimeRunData_Debug: - if (INV_TYPE_1CH == type) - { - listLen = (uint8_t)(HM1CH_LIST_LEN); - assign = (byteAssign_t *)hm1chAssignment; - channels = 1; - } - else if (INV_TYPE_2CH == type) - { - listLen = (uint8_t)(HM2CH_LIST_LEN); - assign = (byteAssign_t *)hm2chAssignment; - channels = 2; - } - else if (INV_TYPE_4CH == type) - { - listLen = (uint8_t)(HM4CH_LIST_LEN); - assign = (byteAssign_t *)hm4chAssignment; - channels = 4; - } - else - { - listLen = 0; - channels = 0; - assign = NULL; - } + listLen = (uint8_t)(HM1CH_LIST_LEN); + assign = (byteAssign_t *)hm1chAssignment; + channels = 1; + } + else if (INV_TYPE_2CH == type) + { + listLen = (uint8_t)(HM2CH_LIST_LEN); + assign = (byteAssign_t *)hm2chAssignment; + channels = 2; + } + else if (INV_TYPE_4CH == type) + { + listLen = (uint8_t)(HM4CH_LIST_LEN); + assign = (byteAssign_t *)hm4chAssignment; + channels = 4; + } + else + { + listLen = 0; + channels = 0; + assign = NULL; + } + + switch (getQueuedCmd()) { + case RealTimeRunData_Debug: + // Do nothing will use default + break; + case InverterDevInform_All: + listLen = (uint8_t)(HMINFO_LIST_LEN); + assign = (byteAssign_t *)InfoAssignment; + break; + case SystemConfigPara: + listLen = (uint8_t)(HMSYSTEM_LIST_LEN); + assign = (byteAssign_t *)SystemConfigParaAssignment; + break; + case AlarmData: + listLen = (uint8_t)(HMALARMDATA_LIST_LEN); + assign = (byteAssign_t *)AlarmDataAssignment; + break; + default: + DPRINTLN(DBG_INFO, "Parser not implemented"); + break; + } + } + String getAlarmStr(u_int16_t alarmCode) + { + switch (alarmCode) + { + case 1: + return String(F("Inverter start")); break; - case InverterDevInform_All: - listLen = (uint8_t)(HMINFO_LIST_LEN); - assign = (byteAssign_t *)InfoAssignment; + case 2: + return String(F("DTU command failed")); break; - case SystemConfigPara: - listLen = (uint8_t)(HMSYSTEM_LIST_LEN); - assign = (byteAssign_t *)SystemConfigParaAssignment; + case 121: + return String(F("Over temperature protection")); + break; + case 125: + return String(F("Grid configuration parameter error")); + break; + case 126: + return String(F("Software error code 126")); + break; + case 127: + return String(F("Firmware error")); + break; + case 128: + return String(F("Software error code 128")); + break; + case 129: + return String(F("Software error code 129")); + break; + case 130: + return String(F("Offline")); + break; + case 141: + return String(F("Grid overvoltage")); + break; + case 142: + return String(F("Average grid overvoltage")); + break; + case 143: + return String(F("Grid undervoltage")); + break; + case 144: + return String(F("Grid overfrequency")); + break; + case 145: + return String(F("Grid underfrequency")); + break; + case 146: + return String(F("Rapid grid frequency change")); + break; + case 147: + return String(F("Power grid outage")); + break; + case 148: + return String(F("Grid disconnection")); + break; + case 149: + return String(F("Island detected")); + break; + case 205: + return String(F("Input port 1 & 2 overvoltage")); + break; + case 206: + return String(F("Input port 3 & 4 overvoltage")); + break; + case 207: + return String(F("Input port 1 & 2 undervoltage")); + break; + case 208: + return String(F("Input port 3 & 4 undervoltage")); + break; + case 209: + return String(F("Port 1 no input")); + break; + case 210: + return String(F("Port 2 no input")); + break; + case 211: + return String(F("Port 3 no input")); + break; + case 212: + return String(F("Port 4 no input")); + break; + case 213: + return String(F("PV-1 & PV-2 abnormal wiring")); + break; + case 214: + return String(F("PV-3 & PV-4 abnormal wiring")); + break; + case 215: + return String(F("PV-1 Input overvoltage")); + break; + case 216: + return String(F("PV-1 Input undervoltage")); + break; + case 217: + return String(F("PV-2 Input overvoltage")); + break; + case 218: + return String(F("PV-2 Input undervoltage")); + break; + case 219: + return String(F("PV-3 Input overvoltage")); + break; + case 220: + return String(F("PV-3 Input undervoltage")); + break; + case 221: + return String(F("PV-4 Input overvoltage")); + break; + case 222: + return String(F("PV-4 Input undervoltage")); + break; + case 301: + return String(F("Hardware error code 301")); + break; + case 302: + return String(F("Hardware error code 302")); + break; + case 303: + return String(F("Hardware error code 303")); + break; + case 304: + return String(F("Hardware error code 304")); + break; + case 305: + return String(F("Hardware error code 305")); + break; + case 306: + return String(F("Hardware error code 306")); + break; + case 307: + return String(F("Hardware error code 307")); + break; + case 308: + return String(F("Hardware error code 308")); + break; + case 309: + return String(F("Hardware error code 309")); + break; + case 310: + return String(F("Hardware error code 310")); + break; + case 311: + return String(F("Hardware error code 311")); + break; + case 312: + return String(F("Hardware error code 312")); + break; + case 313: + return String(F("Hardware error code 313")); + break; + case 314: + return String(F("Hardware error code 314")); + break; + case 5041: + return String(F("Error code-04 Port 1")); + break; + case 5042: + return String(F("Error code-04 Port 2")); + break; + case 5043: + return String(F("Error code-04 Port 3")); + break; + case 5044: + return String(F("Error code-04 Port 4")); + break; + case 5051: + return String(F("PV Input 1 Overvoltage/Undervoltage")); + break; + case 5052: + return String(F("PV Input 2 Overvoltage/Undervoltage")); + break; + case 5053: + return String(F("PV Input 3 Overvoltage/Undervoltage")); + break; + case 5054: + return String(F("PV Input 4 Overvoltage/Undervoltage")); + break; + case 5060: + return String(F("Abnormal bias")); + break; + case 5070: + return String(F("Over temperature protection")); + break; + case 5080: + return String(F("Grid Overvoltage/Undervoltage")); + break; + case 5090: + return String(F("Grid Overfrequency/Underfrequency")); + break; + case 5100: + return String(F("Island detected")); + break; + case 5120: + return String(F("EEPROM reading and writing error")); + break; + case 5150: + return String(F("10 min value grid overvoltage")); + break; + case 5200: + return String(F("Firmware error")); + break; + case 8310: + return String(F("Shut down")); + break; + case 9000: + return String(F("Microinverter is suspected of being stolen")); break; default: - DPRINTLN(DBG_INFO, "Parser not implemented"); + return String(F("Unknown")); + break; } } diff --git a/tools/esp8266/hmRadio.h b/tools/esp8266/hmRadio.h index 7decd2c2..c7fcf0e0 100644 --- a/tools/esp8266/hmRadio.h +++ b/tools/esp8266/hmRadio.h @@ -16,7 +16,6 @@ #define DEFAULT_RECV_CHANNEL 3 #define SPI_SPEED 1000000 -#define DTU_RADIO_ID ((uint64_t)0x1234567801ULL) #define DUMMY_RADIO_ID ((uint64_t)0xDEADBEEF01ULL) #define RF_CHANNELS 5 @@ -192,10 +191,9 @@ class HmRadio { mTxBuf[10] = cmd; // cid mTxBuf[11] = 0x00; CP_U32_LittleEndian(&mTxBuf[12], ts); - if (cmd == RealTimeRunData_Debug || cmd == AlarmData || cmd == AlarmUpdate ){ + if (cmd == RealTimeRunData_Debug || cmd == AlarmData ){ mTxBuf[18] = (alarmMesId >> 8) & 0xff; mTxBuf[19] = (alarmMesId ) & 0xff; - //mTxBuf[19] = 0x05; // ToDo: Shall be the last received Alarm Index Number } else { mTxBuf[18] = 0x00; mTxBuf[19] = 0x00; diff --git a/tools/esp8266/html/convert.py b/tools/esp8266/html/convert.py index 98027b2f..dadaf92d 100755 --- a/tools/esp8266/html/convert.py +++ b/tools/esp8266/html/convert.py @@ -2,6 +2,8 @@ import re import sys import os +from pathlib import Path + def convert2Header(inFile): fileType = inFile.split(".")[1] define = inFile.split(".")[0].upper() @@ -12,8 +14,10 @@ def convert2Header(inFile): print("ok") outName = "html/" + "h/" + inFileVarName + ".h" inFile = "html/" + inFile + Path("html/h").mkdir(exist_ok=True) else: outName = "h/" + inFileVarName + ".h" + Path("h").mkdir(exist_ok=True) f = open(inFile, "r") data = f.read().replace('\n', '') diff --git a/tools/esp8266/html/h/index_html.h b/tools/esp8266/html/h/index_html.h deleted file mode 100644 index c6f769ce..00000000 --- a/tools/esp8266/html/h/index_html.h +++ /dev/null @@ -1,4 +0,0 @@ -#ifndef __INDEX_HTML_H__ -#define __INDEX_HTML_H__ -const char index_html[] PROGMEM = "Index - {DEVICE}

AHOY - {DEVICE}

Visualization

Setup

Uptime:

Statistics:

Every {TS}seconds the values are updated

This project was started from this discussion. (Mikrocontroller.net)
New updates can be found on Github: https://github.com/grindylow/ahoy

Please report issues using the feature provided by Github

Discuss with us on Discord

Creative Commons - https://creativecommons.org/licenses/by-nc-sa/3.0/de/
Check the licenses which are published on https://github.com/grindylow/ahoyas well

© 2022

Update Firmware

AHOY :: {VERSION}

Reboot

Git SHA: {BUILD}

"; -#endif /*__INDEX_HTML_H__*/ diff --git a/tools/esp8266/html/h/setup_html.h b/tools/esp8266/html/h/setup_html.h deleted file mode 100644 index 5ec006e5..00000000 --- a/tools/esp8266/html/h/setup_html.h +++ /dev/null @@ -1,4 +0,0 @@ -#ifndef __SETUP_HTML_H__ -#define __SETUP_HTML_H__ -const char setup_html[] PROGMEM = "Setup - {DEVICE}

Setup

ERASE SETTINGS (not WiFi)
Device Host Name
WiFi

Enter the credentials to your prefered WiFi station. After rebooting the device tries to connect with this information.

Inverter{INVERTERS}

General

NTP Server
MQTT
System Config

Pinout (Wemos)

{PINOUT}

Radio (NRF24L01+)

Serial Console



"; -#endif /*__SETUP_HTML_H__*/ diff --git a/tools/esp8266/html/h/style_css.h b/tools/esp8266/html/h/style_css.h deleted file mode 100644 index 24f50e79..00000000 --- a/tools/esp8266/html/h/style_css.h +++ /dev/null @@ -1,4 +0,0 @@ -#ifndef __STYLE_CSS_H__ -#define __STYLE_CSS_H__ -const char style_css[] PROGMEM = "h1 {margin:0;padding:20pt;font-size:22pt;color:#fff;background-color:#006ec0;display:block;text-transform:uppercase;}html, body {font-family:Arial;margin:0;padding:0;}p {text-align:justify;font-size:13pt;}p.lic, p.lic a {font-size:8pt;color:#999;}.des {margin-top:20px;font-size:13pt;color:#006ec0;}.s_active, .s_collapsible:hover {background-color:#006ec0;}.s_content {display:none;overflow:hidden;}.s_collapsible {background-color:#044e86;color:white;cursor:pointer;padding:18px;width:100%;border:none;text-align:left;outline:none;font-size:15px;margin-bottom:4px;}.subdes {font-size:12pt;color:#006ec0;margin-left:7px;}.subsubdes {font-size:12pt;color:#006ec0;margin:0 0 7px 12px;}.hide {display:none;}a:link, a:visited {text-decoration:none;font-size:13pt;color:#006ec0;}a:hover, a:focus {color:#f00;}a.erase {background-color:#006ec0;color:#fff;padding:7px;display:inline-block;margin-top:30px;}#content {padding:15px 15px 60px 15px;}#footer {position:fixed;bottom:0px;height:45px;background-color:#006ec0;width:100%;border-top:5px solid #fff;}#footer p, #footer a {color:#fff;padding:0 7px 0 7px;font-size:10pt !important;}div.content {background-color:#fff;padding-bottom:65px;overflow:auto;}input, select {padding:7px;font-size:13pt;}input.text, select {width:70%;box-sizing:border-box;margin-bottom:10px;border:1px solid #ccc;}input.sh {max-width:150px !important;margin-right:10px;}input.btn {background-color:#006ec0;color:#fff;border:0px;float:right;margin:10px 0 30px;text-transform:uppercase;}input.cb {margin-bottom:20px;}label {width:20%;display:inline-block;font-size:12pt;padding-right:10px;margin:10px 0px 0px 15px;vertical-align:top;}fieldset {margin-bottom:15px;}.left {float:left;}.right {float:right;}div.ch-iv {width:100%;background-color:#32b004;display:inline-block;margin-bottom:15px;padding-bottom:20px;overflow:auto;}div.ch {width:220px;min-height:350px;background-color:#006ec0;display:inline-block;margin:0 10px 15px 10px;overflow:auto;padding-bottom:20px;}div.ch .value, div.ch .info, div.ch .head, div.ch-iv .value, div.ch-iv .info, div.ch-iv .head {color:#fff;display:block;width:100%;text-align:center;}.subgrp {float:left;width:220px;}div.ch .unit, div.ch-iv .unit {font-size:19px;margin-left:10px;}div.ch .value, div.ch-iv .value {margin-top:20px;font-size:24px;}div.ch .info, div.ch-iv .info {margin-top:3px;font-size:10px;}div.ch .head {background-color:#003c80;padding:10px 0 10px 0;}div.ch-iv .head {background-color:#1c6800;padding:10px 0 10px 0;}div.iv {max-width:960px;margin-bottom:40px;}div.ts {font-size:13px;background-color:#ddd;border-top:7px solid #999;padding:7px;}div.modpwr, div.modname {width:70%;display:inline-block;}#note {margin:50px 10px 10px 10px;padding-top:10px;width:100%;border-top:1px solid #bbb;}@media(max-width:500px) {div.ch .unit, div.ch-iv .unit {font-size:18px;}div.ch {width:170px;min-height:100px }.subgrp {width:180px;}}"; -#endif /*__STYLE_CSS_H__*/ diff --git a/tools/esp8266/html/h/visualization_html.h b/tools/esp8266/html/h/visualization_html.h deleted file mode 100644 index b70bc5b2..00000000 --- a/tools/esp8266/html/h/visualization_html.h +++ /dev/null @@ -1,4 +0,0 @@ -#ifndef __VISUALIZATION_HTML_H__ -#define __VISUALIZATION_HTML_H__ -const char visualization_html[] PROGMEM = "Index - {DEVICE}

AHOY - {DEVICE}

Every {TS}seconds the values are updated

© 2022

Home

AHOY :: {VERSION}

"; -#endif /*__VISUALIZATION_HTML_H__*/ diff --git a/tools/esp8266/html/setup.html b/tools/esp8266/html/setup.html index 98a30680..5be9e9d7 100644 --- a/tools/esp8266/html/setup.html +++ b/tools/esp8266/html/setup.html @@ -102,7 +102,7 @@
MQTT - + @@ -128,7 +128,7 @@

Serial Console


- +
diff --git a/tools/esp8266/mqtt.h b/tools/esp8266/mqtt.h index 74193afa..28ca2c1c 100644 --- a/tools/esp8266/mqtt.h +++ b/tools/esp8266/mqtt.h @@ -85,7 +85,8 @@ class mqtt { #endif boolean resub = false; - if(!mClient->connected()) { + if(!mClient->connected() && (millis() - lastReconnect) > MQTT_RECONNECT_DELAY ) { + lastReconnect = millis(); if(strlen(mDevName) > 0) { // der Server und der Port müssen neu gesetzt werden, // da ein MQTT_CONNECTION_LOST -3 die Werte zerstört hat. @@ -95,14 +96,14 @@ class mqtt { resub = mClient->connect(mDevName, mCfg->user, mCfg->pwd); else resub = mClient->connect(mDevName); - } - // ein Subscribe ist nur nach einem connect notwendig - if(resub) { - char topic[MQTT_TOPIC_LEN + 13 ]; // "/devcontrol/#" --> + 6 byte - // ToDo: "/devcontrol/#" is hardcoded - snprintf(topic, MQTT_TOPIC_LEN + 13, "%s/devcontrol/#", mCfg->topic); - DPRINTLN(DBG_INFO, F("subscribe to ") + String(topic)); - mClient->subscribe(topic); // subscribe to mTopic + "/devcontrol/#" + // ein Subscribe ist nur nach einem connect notwendig + if(resub) { + char topic[MQTT_TOPIC_LEN + 13 ]; // "/devcontrol/#" --> + 6 byte + // ToDo: "/devcontrol/#" is hardcoded + snprintf(topic, MQTT_TOPIC_LEN + 13, "%s/devcontrol/#", mCfg->topic); + DPRINTLN(DBG_INFO, F("subscribe to ") + String(topic)); + mClient->subscribe(topic); // subscribe to mTopic + "/devcontrol/#" + } } } } @@ -113,6 +114,7 @@ class mqtt { bool mAddressSet; mqttConfig_t *mCfg; char mDevName[DEVNAME_LEN]; + unsigned long lastReconnect = 0; }; #endif /*__MQTT_H_*/ diff --git a/tools/esp8266/platformio.ini b/tools/esp8266/platformio.ini index ee0ad242..48b292c7 100644 --- a/tools/esp8266/platformio.ini +++ b/tools/esp8266/platformio.ini @@ -14,9 +14,11 @@ src_dir = . [env] framework = arduino +build_flags = + -include "config.h" ; ;;;;; Possible Debug options ;;;;;; ; https://docs.platformio.org/en/latest/platforms/espressif8266.html#debug-level -;build_flags = -DDEBUG_ESP_PORT=Serial + ;-DDEBUG_ESP_PORT=Serial ;-DDEBUG_ESP_CORE ;-DDEBUG_ESP_WIFI ;-DDEBUG_ESP_HTTP_CLIENT diff --git a/tools/esp8266/scripts/getVersion.py b/tools/esp8266/scripts/getVersion.py index 9907eb55..6ebe71b0 100644 --- a/tools/esp8266/scripts/getVersion.py +++ b/tools/esp8266/scripts/getVersion.py @@ -24,6 +24,12 @@ def readVersion(path, infile): src = path + ".pio/build/esp8266-release/firmware.bin" dst = path + ".pio/build/out/" + versionout os.rename(src, dst) + + versionout = version[:-1] + "_esp32_" + sha + ".bin" + src = path + ".pio/build/esp32-wroom32-release/firmware.bin" + dst = path + ".pio/build/out/" + versionout + os.rename(src, dst) + print("::set-output name=name::" + versionnumber[:-1] ) diff --git a/tools/esp8266/web.cpp b/tools/esp8266/web.cpp index 9e4e9802..1bd12af5 100644 --- a/tools/esp8266/web.cpp +++ b/tools/esp8266/web.cpp @@ -16,6 +16,23 @@ #include "html/h/setup_html.h" #include "html/h/visualization_html.h" + +const uint16_t pwrLimitOptionValues[] { + NoPowerLimit, + AbsolutNonPersistent, + AbsolutPersistent, + RelativNonPersistent, + RelativPersistent +}; + +const char* const pwrLimitOptions[] { + "no power limit", + "absolute in Watt non persistent", + "absolute in Watt persistent", + "relativ in percent non persistent", + "relativ in percent persistent" +}; + //----------------------------------------------------------------------------- web::web(app *main, sysConfig_t *sysCfg, config_t *config, char version[]) { mMain = main; @@ -185,7 +202,7 @@ void web::showSetup(void) { inv += F("serial.u64, HEX); - inv += F("\"/ maxlength=\"12\" onkeyup=\"checkSerial()\">"); + inv += F("\"/ maxlength=\"12\">"); inv += F(""); inv += F(""; inv += F(""); - inv += F(""); - if(iv->powerLimit[1] == RelativNonPersistent) - inv += F("PowerLimitControl\">"); - if(iv->powerLimit[1] == AbsolutPersistent) - inv += F("PowerLimitControl\">"); - if(iv->powerLimit[1] == RelativPersistent) - inv += F("PowerLimitControl\">"); - } else - inv += F("PowerLimitControl\">"); - // UGLY! But I do not know it a better way --// + inv += F(""); inv += F("
"); @@ -320,13 +332,14 @@ void web::showSave(void) { iv->powerLimit[1] = actPwrLimitControl; iv->devControlCmd = ActivePowerContr; iv->devControlRequest = true; - if (iv->powerLimit[1] & 0x0001) + if ((iv->powerLimit[1] & 0x0001) == 0x0001) DPRINTLN(DBG_INFO, F("Power limit for inverter ") + String(iv->id) + F(" set to ") + String(iv->powerLimit[0]) + F("%") ); - else + else { DPRINTLN(DBG_INFO, F("Power limit for inverter ") + String(iv->id) + F(" set to ") + String(iv->powerLimit[0]) + F("W") ); DPRINTLN(DBG_INFO, F("Power Limit Control Setting ") + String(iv->powerLimit[1])); + } } - if (actPwrLimit == 0xffff){ // set to 100% + if (actPwrLimit == 0xffff) { // set to 100% iv->powerLimit[0] = 100; iv->powerLimit[1] = RelativPersistent; iv->devControlCmd = ActivePowerContr; @@ -371,7 +384,9 @@ void web::showSave(void) { // mqtt if(mWeb->arg("mqttAddr") != "") { - mWeb->arg("mqttAddr").toCharArray(mConfig->mqtt.broker, MQTT_ADDR_LEN); + String addr = mWeb->arg("mqttAddr"); + addr.trim(); + addr.toCharArray(mConfig->mqtt.broker, MQTT_ADDR_LEN); mWeb->arg("mqttUser").toCharArray(mConfig->mqtt.user, MQTT_USER_LEN); mWeb->arg("mqttPwd").toCharArray(mConfig->mqtt.pwd, MQTT_PWD_LEN); mWeb->arg("mqttTopic").toCharArray(mConfig->mqtt.topic, MQTT_TOPIC_LEN); @@ -421,7 +436,84 @@ void web::showVisualization(void) { //----------------------------------------------------------------------------- void web::showLiveData(void) { DPRINTLN(DBG_VERBOSE, F("web::showLiveData")); - mWeb->send(200, F("text/html"), mMain->getLiveData()); + + String modHtml; + for (uint8_t id = 0; id < mMain->mSys->getNumInverters(); id++) { + Inverter<> *iv = mMain->mSys->getInverterByPos(id); + if (NULL != iv) { +#ifdef LIVEDATA_VISUALIZED + uint8_t modNum, pos; + switch (iv->type) { + default: + case INV_TYPE_1CH: modNum = 1; break; + case INV_TYPE_2CH: modNum = 2; break; + case INV_TYPE_4CH: modNum = 4; break; + } + + modHtml += F("
" + "
") + + String(iv->name) + F(" Limit ") + + String(iv->actPowerLimit) + F("%"); + if(NoPowerLimit == iv->powerLimit[1]) + modHtml += F(" (not controlled)"); + modHtml += F(" | last Alarm: ") + iv->lastAlarmMsg + F(""); + + uint8_t list[] = {FLD_UAC, FLD_IAC, FLD_PAC, FLD_F, FLD_PCT, FLD_T, FLD_YT, FLD_YD, FLD_PDC, FLD_EFF, FLD_PRA, FLD_ALARM_MES_ID}; + + for (uint8_t fld = 0; fld < 11; fld++) { + pos = (iv->getPosByChFld(CH0, list[fld])); + if (0xff != pos) { + modHtml += F("
"); + modHtml += F("") + String(iv->getValue(pos)); + modHtml += F("") + String(iv->getUnit(pos)) + F(""); + modHtml += F("") + String(iv->getFieldName(pos)) + F(""); + modHtml += F("
"); + } + } + modHtml += "
"; + + for (uint8_t ch = 1; ch <= modNum; ch++) { + modHtml += F("
"); + if (iv->chName[ch - 1][0] == 0) + modHtml += F("CHANNEL ") + String(ch); + else + modHtml += String(iv->chName[ch - 1]); + modHtml += F(""); + for (uint8_t j = 0; j < 6; j++) { + switch (j) { + default: pos = (iv->getPosByChFld(ch, FLD_UDC)); break; + case 1: pos = (iv->getPosByChFld(ch, FLD_IDC)); break; + case 2: pos = (iv->getPosByChFld(ch, FLD_PDC)); break; + case 3: pos = (iv->getPosByChFld(ch, FLD_YD)); break; + case 4: pos = (iv->getPosByChFld(ch, FLD_YT)); break; + case 5: pos = (iv->getPosByChFld(ch, FLD_IRR)); break; + } + if (0xff != pos) { + modHtml += F("") + String(iv->getValue(pos)); + modHtml += F("") + String(iv->getUnit(pos)) + F(""); + modHtml += F("") + String(iv->getFieldName(pos)) + F(""); + } + } + modHtml += "
"; + yield(); + } + modHtml += F("
Last received data requested at: ") + mMain->getDateTimeStr(iv->ts) + F("
"); + modHtml += F("
"); +#else + // dump all data to web frontend + modHtml = F("
");
+            char topic[30], val[10];
+            for (uint8_t i = 0; i < iv->listLen; i++) {
+                snprintf(topic, 30, "%s/ch%d/%s", iv->name, iv->assign[i].ch, iv->getFieldName(i));
+                snprintf(val, 10, "%.3f %s", iv->getValue(i), iv->getUnit(i));
+                modHtml += String(topic) + ": " + String(val) + "\n";
+            }
+            modHtml += F("
"); +#endif + } + } + + mWeb->send(200, F("text/html"), modHtml); } @@ -450,10 +542,11 @@ void web::showWebApi(void) if (response["tx_request"] == (uint8_t)TX_REQ_INFO) { // if the AlarmData is requested set the Alarm Index to the requested one - if (cmd == AlarmData){ - iv->alarmMesIndex = response["payload"]; + if (cmd == AlarmData || cmd == AlarmUpdate){ + // set the AlarmMesIndex for the request from user input + iv->alarmMesIndex = response["payload"]; } - DPRINTLN(DBG_INFO, F("Will make tx-request 0x15 with subcmd ") + String(cmd) + F(" and payload ") + String(response["payload"])); + DPRINTLN(DBG_INFO, F("Will make tx-request 0x15 with subcmd ") + String(cmd) + F(" and payload ") + String((uint16_t) response["payload"])); // process payload from web request corresponding to the cmd iv->enqueCommand(cmd); } @@ -488,6 +581,14 @@ void web::showWebApi(void) iv->devControlRequest = true; // queue it in the request loop } } + if (response["cmd"] == (uint8_t)TurnOff){ + iv->devControlCmd = TurnOff; + iv->devControlRequest = true; // queue it in the request loop + } + if (response["cmd"] == (uint8_t)TurnOn){ + iv->devControlCmd = TurnOn; + iv->devControlRequest = true; // queue it in the request loop + } } } mWeb->send(200, "text/json", "{success:true}");