Browse Source

merge development03 to PR #1155

pull/1475/head
lumapu 8 months ago
parent
commit
020f8cadfa
  1. 179
      .github/workflows/compile_zero-export.yml
  2. 3
      ahoy.code-workspace
  3. 64
      src/app.cpp
  4. 15
      src/app.h
  5. 3
      src/config/config.h
  6. 75
      src/config/settings.h
  7. 3
      src/defines.h
  8. 405
      src/plugins/zeroExport/SML.cpp
  9. 106
      src/plugins/zeroExport/SML.h
  10. 50
      src/plugins/zeroExport/SMLCRCTable.h
  11. 0
      src/plugins/zeroExport/zeroExport.cpp
  12. 122
      src/plugins/zeroExport/zeroExport.h
  13. 20
      src/web/RestApi.h
  14. 101
      src/web/html/setup.html
  15. 4
      src/web/html/style.css
  16. 2
      src/web/html/visualization.html
  17. 33
      src/web/web.h

179
.github/workflows/compile_zero-export.yml

@ -0,0 +1,179 @@
name: Ahoy Zero-Export
on:
push:
branches: zero-export
paths-ignore:
- '**.md' # Do no build on *.md changes
jobs:
check:
name: Check Repository
runs-on: ubuntu-latest
if: github.repository == 'lumapu/ahoy' && github.ref_name == 'zero-export'
continue-on-error: true
steps:
- uses: actions/checkout@v4
build-en:
name: Build Environments (English)
needs: check
runs-on: ubuntu-latest
continue-on-error: true
strategy:
matrix:
variant:
- esp32-wroom32
- esp32-wroom32-prometheus
- esp32-wroom32-ethernet
- esp32-s2-mini
- esp32-c3-mini
- opendtufusion
- opendtufusion-ethernet
steps:
- uses: actions/checkout@v4
- uses: benjlevesque/short-sha@v3.0
id: short-sha
with:
length: 7
- name: Cache Pip
uses: actions/cache@v4
with:
path: ~/.cache/pip
key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements.txt') }}
restore-keys: |
${{ runner.os }}-pip-
- name: Cache PlatformIO
uses: actions/cache@v4
with:
path: ~/.platformio
key: ${{ runner.os }}-${{ hashFiles('**/lockfiles') }}
- name: Setup Python
uses: actions/setup-python@v5
with:
python-version: "3.x"
- name: Install PlatformIO
run: |
python -m pip install setuptools --upgrade pip
pip install --upgrade platformio
- name: Run PlatformIO
run: pio run -d src -e ${{ matrix.variant }}
- name: Rename Firmware
run: python scripts/getVersion.py ${{ matrix.variant }} >> $GITHUB_OUTPUT
- name: Create Artifact
uses: actions/upload-artifact@v4
with:
name: zero-${{ matrix.variant }}
path: firmware/*
build-de:
name: Build Environments (German)
needs: check
runs-on: ubuntu-latest
continue-on-error: true
strategy:
matrix:
variant:
- esp32-wroom32-de
- esp32-wroom32-prometheus-de
- esp32-wroom32-ethernet-de
- esp32-s2-mini-de
- esp32-c3-mini-de
- opendtufusion-de
- opendtufusion-ethernet-de
steps:
- uses: actions/checkout@v4
- uses: benjlevesque/short-sha@v3.0
id: short-sha
with:
length: 7
- name: Cache Pip
uses: actions/cache@v4
with:
path: ~/.cache/pip
key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements.txt') }}
restore-keys: |
${{ runner.os }}-pip-
- name: Cache PlatformIO
uses: actions/cache@v4
with:
path: ~/.platformio
key: ${{ runner.os }}-${{ hashFiles('**/lockfiles') }}
- name: Setup Python
uses: actions/setup-python@v5
with:
python-version: "3.x"
- name: Install PlatformIO
run: |
python -m pip install setuptools --upgrade pip
pip install --upgrade platformio
- name: Run PlatformIO
run: pio run -d src -e ${{ matrix.variant }}
- name: Rename Firmware
run: python scripts/getVersion.py ${{ matrix.variant }} >> $GITHUB_OUTPUT
- name: Create Artifact
uses: actions/upload-artifact@v4
with:
name: zero-${{ matrix.variant }}
path: firmware/*
combine:
name: Combine Artifacts
needs: [build-en, build-de]
runs-on: ubuntu-latest
continue-on-error: false
steps:
- uses: actions/checkout@v4
#- name: Copy boot_app0.bin
# run: cp ~/.platformio/packages/framework-arduinoespressif32/tools/partitions/boot_app0.bin src/.pio/build/opendtufusion/ota.bin
- name: Get Artifacts
uses: actions/download-artifact@v4
with:
merge-multiple: true
path: firmware
- name: Get Version from code
id: version_name
run: python scripts/getVersion.py ${{ matrix.variant }} >> $GITHUB_OUTPUT
- name: Set Version
uses: cschleiden/replace-tokens@v1
with:
files: manual/User_Manual.md
env:
VERSION: ${{ steps.version_name.outputs.name }}
- name: Rename firmware directory
run: mv firmware ${{ steps.version_name.outputs.name }}
- name: Zip Artifacts
uses: vimtor/action-zip@v1.2
with:
files: ${{ steps.version_name.outputs.name }} manual/User_Manual.md manual/Getting_Started.md
dest: '${{ steps.version_name.outputs.name }}.zip'
- name: delete environment Artifacts
uses: geekyeggo/delete-artifact@v4
with:
name: zero-*
- name: Create Artifact
uses: actions/upload-artifact@v4
with:
name: zero-${{ steps.version_name.outputs.name }}
path: '${{ steps.version_name.outputs.name }}.zip'

3
ahoy.code-workspace

@ -2,9 +2,6 @@
"folders": [ "folders": [
{ {
"path": "." "path": "."
},
{
"path": "src"
} }
], ],
"settings": { "settings": {

64
src/app.cpp

@ -14,7 +14,7 @@
//----------------------------------------------------------------------------- //-----------------------------------------------------------------------------
app::app() : ah::Scheduler {} { app::app() : ah::Scheduler {} {
memset(mVersion, 0, sizeof(char) * 12); memset(mVersion, 0, sizeof(char) * 17);
memset(mVersionModules, 0, sizeof(char) * 12); memset(mVersionModules, 0, sizeof(char) * 12);
} }
@ -124,6 +124,11 @@ void app::setup() {
mPubSerial.setup(mConfig, &mSys, &mTimestamp); mPubSerial.setup(mConfig, &mSys, &mTimestamp);
// ZeroExport
if (mConfig->plugin.zexport.enabled) {
mzExport.setup(&mConfig->plugin.zexport, &mSys, mConfig);
}
#if !defined(ETHERNET) #if !defined(ETHERNET)
//mImprov.setup(this, mConfig->sys.deviceName, mVersion); //mImprov.setup(this, mConfig->sys.deviceName, mVersion);
#endif #endif
@ -191,6 +196,13 @@ void app::regularTickers(void) {
if (DISP_TYPE_T0_NONE != mConfig->plugin.display.type) if (DISP_TYPE_T0_NONE != mConfig->plugin.display.type)
everySec(std::bind(&DisplayType::tickerSecond, &mDisplay), "disp"); everySec(std::bind(&DisplayType::tickerSecond, &mDisplay), "disp");
#endif #endif
// ZeroExport
#if defined(PLUGIN_ZEROEXPORT)
if (mConfig->plugin.zexport.enabled)
everySec(std::bind(&ZeroExportType::tickerSecond, &mzExport), "zExport");
#endif
every(std::bind(&PubSerialType::tick, &mPubSerial), 5, "uart"); every(std::bind(&PubSerialType::tick, &mPubSerial), 5, "uart");
#if !defined(ETHERNET) #if !defined(ETHERNET)
//everySec([this]() { mImprov.tickSerial(); }, "impro"); //everySec([this]() { mImprov.tickSerial(); }, "impro");
@ -445,6 +457,10 @@ void app::tickSend(void) {
else else
mCommunication.add(iv, cmd); mCommunication.add(iv, cmd);
}); });
#if defined(ESP32)
if(mConfig->nrf.enabled || mConfig->cmt.enabled) zeroexport();
#endif
} }
} }
@ -506,7 +522,7 @@ void app:: zeroIvValues(bool checkAvail, bool skipYieldDay) {
//----------------------------------------------------------------------------- //-----------------------------------------------------------------------------
void app::resetSystem(void) { void app::resetSystem(void) {
snprintf(mVersion, sizeof(mVersion), "%d.%d.%d", VERSION_MAJOR, VERSION_MINOR, VERSION_PATCH); snprintf(mVersion, sizeof(mVersion), "zero-%d.%d.%d", VERSION_MAJOR, VERSION_MINOR, VERSION_PATCH);
snprintf(mVersionModules, sizeof(mVersionModules), "%s", snprintf(mVersionModules, sizeof(mVersionModules), "%s",
#ifdef ENABLE_PROMETHEUS_EP #ifdef ENABLE_PROMETHEUS_EP
"P" "P"
@ -619,3 +635,47 @@ void app::updateLed(void) {
analogWrite(mConfig->led.led[2], led_off); analogWrite(mConfig->led.led[2], led_off);
} }
} }
//-----------------------------------------------------------------------------
#if defined(ESP32)
void app::zeroexport() {
if (!mConfig->plugin.zexport.enabled ||
!mSys.getInverterByPos(mConfig->plugin.zexport.Iv)->isProducing()) { // check if plugin is enabled && indicate to send new value
mConfig->plugin.zexport.lastTime = millis(); // set last timestamp
return;
}
if (millis() - mConfig->plugin.zexport.lastTime > mConfig->plugin.zexport.count_avg * 1000UL)
{
Inverter<> *iv = mSys.getInverterByPos(mConfig->plugin.zexport.Iv);
DynamicJsonDocument doc(512);
JsonObject object = doc.to<JsonObject>();
double nValue = round(mzExport.getPowertoSetnewValue());
double twoPerVal = nValue <= (iv->getMaxPower() / 100 * 2 );
if(mConfig->plugin.zexport.two_percent && (nValue <= twoPerVal))
nValue = twoPerVal;
if(mConfig->plugin.zexport.max_power <= nValue)
nValue = mConfig->plugin.zexport.max_power;
if(iv->actPowerLimit == nValue) {
mConfig->plugin.zexport.lastTime = millis(); // set last timestamp
return; // if PowerLimit same as befor, then skip
}
object["val"] = nValue;
object["id"] = mConfig->plugin.zexport.Iv;
object["path"] = "ctrl";
object["cmd"] = "limit_nonpersistent_absolute";
String data;
serializeJsonPretty(object, data);
DPRINTLN(DBG_INFO, data);
mApi.ctrlRequest(object);
mConfig->plugin.zexport.lastTime = millis(); // set last timestamp
}
}
#endif

15
src/app.h

@ -77,6 +77,11 @@ typedef Simulator<HmSystemType> SimulatorType;
typedef Display<HmSystemType, Radio> DisplayType; typedef Display<HmSystemType, Radio> DisplayType;
#endif #endif
#if defined(PLUGIN_ZEROEXPORT)
#include "plugins/zeroExport/zeroExport.h"
typedef ZeroExport<HmSystemType> ZeroExportType;
#endif
class app : public IApp, public ah::Scheduler { class app : public IApp, public ah::Scheduler {
public: public:
app(); app();
@ -348,6 +353,10 @@ class app : public IApp, public ah::Scheduler {
void setupLed(); void setupLed();
void updateLed(); void updateLed();
#if defined(ESP32)
void zeroexport();
#endif
void tickReboot(void) { void tickReboot(void) {
DPRINTLN(DBG_INFO, F("Rebooting...")); DPRINTLN(DBG_INFO, F("Rebooting..."));
ah::Scheduler::resetTicker(); ah::Scheduler::resetTicker();
@ -414,7 +423,7 @@ class app : public IApp, public ah::Scheduler {
CmtRadio<> mCmtRadio; CmtRadio<> mCmtRadio;
#endif #endif
char mVersion[12]; char mVersion[17];
char mVersionModules[12]; char mVersionModules[12];
settings mSettings; settings mSettings;
settings_t *mConfig = nullptr; settings_t *mConfig = nullptr;
@ -450,6 +459,10 @@ class app : public IApp, public ah::Scheduler {
#if defined(ENABLE_SIMULATOR) #if defined(ENABLE_SIMULATOR)
SimulatorType mSimulator; SimulatorType mSimulator;
#endif /*ENABLE_SIMULATOR*/ #endif /*ENABLE_SIMULATOR*/
#if defined(PLUGIN_ZEROEXPORT)
ZeroExportType mzExport;
#endif
}; };
#endif /*__APP_H__*/ #endif /*__APP_H__*/

3
src/config/config.h

@ -235,6 +235,9 @@
// default MQTT broker uri // default MQTT broker uri
#define DEF_MQTT_BROKER "\0" #define DEF_MQTT_BROKER "\0"
// default zero-export uri
#define DEF_ZEXPORT "\0"
// default MQTT port // default MQTT port
#define DEF_MQTT_PORT 1883 #define DEF_MQTT_PORT 1883

75
src/config/settings.h

@ -139,6 +139,24 @@ typedef struct {
uint16_t interval; uint16_t interval;
} cfgMqtt_t; } cfgMqtt_t;
/* Zero Export section */
#if defined(ESP32)
typedef struct {
char monitor_url[ZEXPORT_ADDR_LEN];
char json_path[ZEXPORT_ADDR_LEN];
uint8_t query_device; // 0 - Tibber, 1 - Shelly, 2 - other (rs232?)
uint8_t Iv; // saves the inverter that is used for regulation
bool enabled;
float power_avg;
uint8_t count_avg;
double total_power;
unsigned long lastTime; // tic toc
double max_power;
bool two_percent; // ask if not go lower then 2%
char tibber_pw[10]; // needed for tibber QWGH-ED12
} cfgzeroExport_t;
#endif
typedef struct { typedef struct {
bool enabled; bool enabled;
char name[MAX_NAME_LENGTH]; char name[MAX_NAME_LENGTH];
@ -188,6 +206,9 @@ typedef struct {
display_t display; display_t display;
char customLink[MAX_CUSTOM_LINK_LEN]; char customLink[MAX_CUSTOM_LINK_LEN];
char customLinkText[MAX_CUSTOM_LINK_TEXT_LEN]; char customLinkText[MAX_CUSTOM_LINK_TEXT_LEN];
#if defined(ESP32)
cfgzeroExport_t zexport;
#endif
} plugins_t; } plugins_t;
typedef struct { typedef struct {
@ -291,6 +312,7 @@ class settings {
if(root.containsKey(F("nrf"))) jsonNrf(root[F("nrf")]); if(root.containsKey(F("nrf"))) jsonNrf(root[F("nrf")]);
#if defined(ESP32) #if defined(ESP32)
if(root.containsKey(F("cmt"))) jsonCmt(root[F("cmt")]); if(root.containsKey(F("cmt"))) jsonCmt(root[F("cmt")]);
if(root.containsKey(F("zeroExport"))) jsonzeroExport(root[F("zeroExport")]);
#endif #endif
if(root.containsKey(F("ntp"))) jsonNtp(root[F("ntp")]); if(root.containsKey(F("ntp"))) jsonNtp(root[F("ntp")]);
if(root.containsKey(F("sun"))) jsonSun(root[F("sun")]); if(root.containsKey(F("sun"))) jsonSun(root[F("sun")]);
@ -320,6 +342,7 @@ class settings {
jsonNrf(root[F("nrf")].to<JsonObject>(), true); jsonNrf(root[F("nrf")].to<JsonObject>(), true);
#if defined(ESP32) #if defined(ESP32)
jsonCmt(root[F("cmt")].to<JsonObject>(), true); jsonCmt(root[F("cmt")].to<JsonObject>(), true);
jsonzeroExport(root.createNestedObject(F("zeroExport")), true);
#endif #endif
jsonNtp(root[F("ntp")].to<JsonObject>(), true); jsonNtp(root[F("ntp")].to<JsonObject>(), true);
jsonSun(root[F("sun")].to<JsonObject>(), true); jsonSun(root[F("sun")].to<JsonObject>(), true);
@ -329,6 +352,7 @@ class settings {
jsonPlugin(root[F("plugin")].to<JsonObject>(), true); jsonPlugin(root[F("plugin")].to<JsonObject>(), true);
jsonInst(root[F("inst")].to<JsonObject>(), true); jsonInst(root[F("inst")].to<JsonObject>(), true);
DPRINT(DBG_INFO, F("memory usage: ")); DPRINT(DBG_INFO, F("memory usage: "));
DBGPRINTLN(String(json.memoryUsage())); DBGPRINTLN(String(json.memoryUsage()));
DPRINT(DBG_INFO, F("capacity: ")); DPRINT(DBG_INFO, F("capacity: "));
@ -446,7 +470,22 @@ class settings {
snprintf(mCfg.mqtt.topic, MQTT_TOPIC_LEN, "%s", DEF_MQTT_TOPIC); snprintf(mCfg.mqtt.topic, MQTT_TOPIC_LEN, "%s", DEF_MQTT_TOPIC);
mCfg.mqtt.interval = 0; // off mCfg.mqtt.interval = 0; // off
mCfg.inst.sendInterval = SEND_INTERVAL; // Zero-Export
#if defined(ESP32)
snprintf(mCfg.plugin.zexport.monitor_url, ZEXPORT_ADDR_LEN, "%s", DEF_ZEXPORT);
snprintf(mCfg.plugin.zexport.tibber_pw, ZEXPORT_ADDR_LEN, "%s", DEF_ZEXPORT);
snprintf(mCfg.plugin.zexport.json_path, ZEXPORT_ADDR_LEN, "%s", DEF_ZEXPORT);
mCfg.plugin.zexport.enabled = false;
mCfg.plugin.zexport.count_avg = 10;
mCfg.plugin.zexport.lastTime = millis(); // do not change!
mCfg.plugin.zexport.query_device = 1; // Standard shelly
mCfg.plugin.zexport.power_avg = 10;
mCfg.plugin.zexport.Iv = 0;
mCfg.plugin.zexport.max_power = 600; // Max 600W to stay safe
mCfg.plugin.zexport.two_percent = true;
#endif
mCfg.inst.rstYieldMidNight = false; mCfg.inst.rstYieldMidNight = false;
mCfg.inst.rstValsNotAvail = false; mCfg.inst.rstValsNotAvail = false;
mCfg.inst.rstValsCommStop = false; mCfg.inst.rstValsCommStop = false;
@ -694,6 +733,40 @@ class settings {
} }
} }
#if defined(ESP32)
void jsonzeroExport(JsonObject obj, bool set = false) {
if(set) {
obj[F("en_zeroexport")] = (bool) mCfg.plugin.zexport.enabled;
obj[F("monitor_url")] = mCfg.plugin.zexport.monitor_url;
obj[F("json_path")] = mCfg.plugin.zexport.json_path;
obj[F("Iv")] = mCfg.plugin.zexport.Iv;
obj[F("power_avg")] = mCfg.plugin.zexport.power_avg;
obj[F("query_device")] = mCfg.plugin.zexport.query_device;
obj[F("count_avg")] = mCfg.plugin.zexport.count_avg;
obj[F("max_power")] = mCfg.plugin.zexport.max_power;
obj[F("total_power")] = mCfg.plugin.zexport.total_power;
obj[F("two_percent")] = (bool)mCfg.plugin.zexport.two_percent;
}
else
{
getVal<bool>(obj, F("en_zeroexport"), &mCfg.plugin.zexport.enabled);
getVal<bool>(obj, F("two_percent"), &mCfg.plugin.zexport.two_percent);
getChar(obj, F("monitor_url"), mCfg.plugin.zexport.monitor_url, ZEXPORT_ADDR_LEN);
getChar(obj, F("json_path"), mCfg.plugin.zexport.json_path, ZEXPORT_ADDR_LEN);
getVal<uint8_t>(obj, F("Iv"), &mCfg.plugin.zexport.Iv);
getVal<uint8_t>(obj, F("count_avg"), &mCfg.plugin.zexport.count_avg);
getVal<double>(obj, F("max_power"), &mCfg.plugin.zexport.max_power);
getVal<float>(obj, F("power_avg"), &mCfg.plugin.zexport.power_avg);
getVal<uint8_t>(obj, F("query_device"), &mCfg.plugin.zexport.query_device);
getVal<double>(obj, F("total_power"), &mCfg.plugin.zexport.total_power);
}
}
#endif
void jsonLed(JsonObject obj, bool set = false) { void jsonLed(JsonObject obj, bool set = false) {
if(set) { if(set) {
obj[F("0")] = mCfg.led.led[0]; obj[F("0")] = mCfg.led.led[0];

3
src/defines.h

@ -98,6 +98,8 @@ enum {
#define DEVNAME_LEN 16 #define DEVNAME_LEN 16
#define NTP_ADDR_LEN 32 // DNS Name #define NTP_ADDR_LEN 32 // DNS Name
#define ZEXPORT_ADDR_LEN 100 // Zero-Export Address
#define MQTT_ADDR_LEN 64 // DNS Name #define MQTT_ADDR_LEN 64 // DNS Name
#define MQTT_CLIENTID_LEN 22 // number of chars is limited to 23 up to v3.1 of MQTT #define MQTT_CLIENTID_LEN 22 // number of chars is limited to 23 up to v3.1 of MQTT
#define MQTT_USER_LEN 65 // there is another byte necessary for \0 #define MQTT_USER_LEN 65 // there is another byte necessary for \0
@ -106,6 +108,7 @@ enum {
#define MQTT_MAX_PACKET_SIZE 384 #define MQTT_MAX_PACKET_SIZE 384
#define PLUGIN_ZEROEXPORT
typedef struct { typedef struct {
uint32_t rxFail; uint32_t rxFail;

405
src/plugins/zeroExport/SML.cpp

@ -0,0 +1,405 @@
#include <stdio.h>
#include <string.h>
#include "sml.h"
#include "smlCrcTable.h"
#ifdef SML_DEBUG
char logBuff[200];
#ifdef SML_NATIVE
#define SML_LOG(...) \
do { \
printf(__VA_ARGS__); \
} while (0)
#define SML_TREELOG(level, ...) \
do { \
printf("%.*s", level, " "); \
printf(__VA_ARGS__); \
} while (0)
#elif ARDUINO
#include <Arduino.h>
#define SML_LOG(...) \
do { \
sprintf(logBuff, __VA_ARGS__); \
Serial.print(logBuff); \
} while (0)
#define SML_TREELOG(level, ...) \
do { \
sprintf(logBuff, __VA_ARGS__); \
Serial.print(logBuff); \
} while (0)
#endif
#else
#define SML_LOG(...) \
do { \
} while (0)
#define SML_TREELOG(level, ...) \
do { \
} while (0)
#endif
#define MAX_LIST_SIZE 80
#define MAX_TREE_SIZE 10
static sml_states_t currentState = SML_START;
static char nodes[MAX_TREE_SIZE];
static unsigned char currentLevel = 0;
static unsigned short crc = 0xFFFF;
static signed char sc;
static unsigned short crcMine = 0xFFFF;
static unsigned short crcReceived = 0x0000;
static unsigned char len = 4;
static unsigned char listBuffer[MAX_LIST_SIZE]; /* keeps a list
as length + state + data */
static unsigned char listPos = 0;
void crc16(unsigned char &byte)
{
#ifdef ARDUINO
crc =
pgm_read_word_near(&smlCrcTable[(byte ^ crc) & 0xff]) ^ (crc >> 8 & 0xff);
#else
crc = smlCrcTable[(byte ^ crc) & 0xff] ^ (crc >> 8 & 0xff);
#endif
}
void setState(sml_states_t state, int byteLen)
{
currentState = state;
len = byteLen;
}
void pushListBuffer(unsigned char byte)
{
if (listPos < MAX_LIST_SIZE) {
listBuffer[listPos++] = byte;
}
}
void reduceList()
{
if (currentLevel <= MAX_TREE_SIZE && nodes[currentLevel] > 0)
nodes[currentLevel]--;
}
void smlNewList(unsigned char size)
{
reduceList();
if (currentLevel < MAX_TREE_SIZE)
currentLevel++;
nodes[currentLevel] = size;
SML_TREELOG(currentLevel, "LISTSTART on level %i with %i nodes\n",
currentLevel, size);
setState(SML_LISTSTART, size);
// @todo workaround for lists inside obis lists
if (size > 5) {
listPos = 0;
memset(listBuffer, '\0', MAX_LIST_SIZE);
}
else {
pushListBuffer(size);
pushListBuffer(currentState);
}
}
void checkMagicByte(unsigned char &byte)
{
unsigned int size = 0;
while (currentLevel > 0 && nodes[currentLevel] == 0) {
/* go back in tree if no nodes remaining */
SML_TREELOG(currentLevel, "back to previous list\n");
currentLevel--;
}
if (byte > 0x70 && byte <= 0x7F) {
/* new list */
size = byte & 0x0F;
smlNewList(size);
}
else if (byte >= 0x01 && byte <= 0x6F && nodes[currentLevel] > 0) {
if (byte == 0x01) {
/* no data, get next */
SML_TREELOG(currentLevel, " Data %i (empty)\n", nodes[currentLevel]);
pushListBuffer(0);
pushListBuffer(currentState);
if (nodes[currentLevel] == 1) {
setState(SML_LISTEND, 1);
SML_TREELOG(currentLevel, "LISTEND\n");
}
else {
setState(SML_NEXT, 1);
}
}
else {
size = (byte & 0x0F) - 1;
setState(SML_DATA, size);
if ((byte & 0xF0) == 0x50) {
setState(SML_DATA_SIGNED_INT, size);
}
else if ((byte & 0xF0) == 0x60) {
setState(SML_DATA_UNSIGNED_INT, size);
}
else if ((byte & 0xF0) == 0x00) {
setState(SML_DATA_OCTET_STRING, size);
}
SML_TREELOG(currentLevel,
" Data %i (length = %i%s): ", nodes[currentLevel], size,
(currentState == SML_DATA_SIGNED_INT) ? ", signed int"
: (currentState == SML_DATA_UNSIGNED_INT) ? ", unsigned int"
: (currentState == SML_DATA_OCTET_STRING) ? ", octet string"
: "");
pushListBuffer(size);
pushListBuffer(currentState);
}
reduceList();
}
else if (byte == 0x00) {
/* end of block */
reduceList();
SML_TREELOG(currentLevel, "End of block at level %i\n", currentLevel);
if (currentLevel == 0) {
setState(SML_NEXT, 1);
}
else {
setState(SML_BLOCKEND, 1);
}
}
else if (byte & 0x80) {
// MSB bit is set, another TL byte will follow
if (byte >= 0x80 && byte <= 0x8F) {
// Datatype Octet String
setState(SML_HDATA, (byte & 0x0F) << 4);
}
else if (byte >= 0xF0 /*&& byte <= 0xFF*/) {
/* Datatype List of ...*/
setState(SML_LISTEXTENDED, (byte & 0x0F) << 4);
}
}
else if (byte == 0x1B && currentLevel == 0) {
/* end sequence */
setState(SML_END, 3);
}
else {
/* Unexpected Byte */
SML_TREELOG(currentLevel,
"UNEXPECTED magicbyte >%02X< at currentLevel %i\n", byte,
currentLevel);
setState(SML_UNEXPECTED, 4);
}
}
sml_states_t smlState(unsigned char &currentByte)
{
unsigned char size;
if (len > 0)
len--;
crc16(currentByte);
switch (currentState) {
case SML_UNEXPECTED:
case SML_CHECKSUM_ERROR:
case SML_FINAL:
case SML_START:
currentState = SML_START;
currentLevel = 0; // Reset current level at the begin of a new transmission
// to prevent problems
if (currentByte != 0x1b)
setState(SML_UNEXPECTED, 4);
if (len == 0) {
SML_TREELOG(0, "START\n");
/* completely clean any garbage from crc checksum */
crc = 0xFFFF;
currentByte = 0x1b;
crc16(currentByte);
crc16(currentByte);
crc16(currentByte);
crc16(currentByte);
setState(SML_VERSION, 4);
}
break;
case SML_VERSION:
if (currentByte != 0x01)
setState(SML_UNEXPECTED, 4);
if (len == 0) {
setState(SML_BLOCKSTART, 1);
}
break;
case SML_END:
if (currentByte != 0x1b) {
SML_LOG("UNEXPECTED char >%02X< at SML_END\n", currentByte);
setState(SML_UNEXPECTED, 4);
}
if (len == 0) {
setState(SML_CHECKSUM, 4);
}
break;
case SML_CHECKSUM:
// SML_LOG("CHECK: %02X\n", currentByte);
if (len == 2) {
crcMine = crc ^ 0xFFFF;
}
if (len == 1) {
crcReceived += currentByte;
}
if (len == 0) {
crcReceived = crcReceived | (currentByte << 8);
SML_LOG("Received checksum: %02X\n", crcReceived);
SML_LOG("Calculated checksum: %02X\n", crcMine);
if (crcMine == crcReceived) {
setState(SML_FINAL, 4);
}
else {
setState(SML_CHECKSUM_ERROR, 4);
}
crc = 0xFFFF;
crcReceived = 0x000; /* reset CRC */
}
break;
case SML_HDATA:
size = len + currentByte - 1;
setState(SML_DATA, size);
pushListBuffer(size);
pushListBuffer(currentState);
SML_TREELOG(currentLevel, " Data (length = %i): ", size);
break;
case SML_LISTEXTENDED:
size = len + (currentByte & 0x0F);
SML_TREELOG(currentLevel, "Extended List with Size=%i\n", size);
smlNewList(size);
break;
case SML_DATA:
case SML_DATA_SIGNED_INT:
case SML_DATA_UNSIGNED_INT:
case SML_DATA_OCTET_STRING:
SML_LOG("%02X ", currentByte);
pushListBuffer(currentByte);
if (nodes[currentLevel] == 0 && len == 0) {
SML_LOG("\n");
SML_TREELOG(currentLevel, "LISTEND on level %i\n", currentLevel);
currentState = SML_LISTEND;
}
else if (len == 0) {
currentState = SML_DATAEND;
SML_LOG("\n");
}
break;
case SML_DATAEND:
case SML_NEXT:
case SML_LISTSTART:
case SML_LISTEND:
case SML_BLOCKSTART:
case SML_BLOCKEND:
checkMagicByte(currentByte);
break;
}
return currentState;
}
bool smlOBISCheck(const unsigned char *obis)
{
return (memcmp(obis, &listBuffer[2], 6) == 0);
}
void smlOBISManufacturer(unsigned char *str, int maxSize)
{
int i = 0, pos = 0, size = 0;
while (i < listPos) {
size = (int)listBuffer[i];
i++;
pos++;
if (pos == 6) {
/* get manufacturer at position 6 in list */
size = (size > maxSize - 1) ? maxSize : size;
memcpy(str, &listBuffer[i + 1], size);
str[size + 1] = 0;
}
i += size + 1;
}
}
void smlPow(double &val, signed char &scaler)
{
if (scaler < 0) {
while (scaler++) {
val /= 10;
}
}
else {
while (scaler--) {
val *= 10;
}
}
}
void smlOBISByUnit(long long int &val, signed char &scaler, sml_units_t unit)
{
unsigned char i = 0, pos = 0, size = 0, y = 0, skip = 0;
sml_states_t type;
val = -1; /* unknown or error */
while (i < listPos) {
pos++;
size = (int)listBuffer[i++];
type = (sml_states_t)listBuffer[i++];
if (type == SML_LISTSTART && size > 0) {
// skip a list inside an obis list
skip = size;
while (skip > 0) {
size = (int)listBuffer[i++];
type = (sml_states_t)listBuffer[i++];
i += size;
skip--;
}
size = 0;
}
if (pos == 4 && listBuffer[i] != unit) {
/* return unknown (-1) if unit does not match */
return;
}
if (pos == 5) {
scaler = listBuffer[i];
}
if (pos == 6) {
y = size;
// initialize 64bit signed integer based on MSB from received value
val =
(type == SML_DATA_SIGNED_INT && (listBuffer[i] & (1 << 7))) ? ~0 : 0;
for (y = 0; y < size; y++) {
// left shift received bytes to 64 bit signed integer
val = (val << 8) | listBuffer[i + y];
}
}
i += size;
}
}
void smlOBISWh(double &wh)
{
long long int val;
smlOBISByUnit(val, sc, SML_WATT_HOUR);
wh = val;
smlPow(wh, sc);
}
void smlOBISW(double &w)
{
long long int val;
smlOBISByUnit(val, sc, SML_WATT);
w = val;
smlPow(w, sc);
}
void smlOBISVolt(double &v)
{
long long int val;
smlOBISByUnit(val, sc, SML_VOLT);
v = val;
smlPow(v, sc);
}
void smlOBISAmpere(double &a)
{
long long int val;
smlOBISByUnit(val, sc, SML_AMPERE);
a = val;
smlPow(a, sc);
}

106
src/plugins/zeroExport/SML.h

@ -0,0 +1,106 @@
#ifndef SML_H
#define SML_H
#include <stdbool.h>
typedef enum {
SML_START,
SML_END,
SML_VERSION,
SML_NEXT,
SML_LISTSTART,
SML_LISTEND,
SML_LISTEXTENDED,
SML_DATA,
SML_HDATA,
SML_DATAEND,
SML_BLOCKSTART,
SML_BLOCKEND,
SML_CHECKSUM,
SML_CHECKSUM_ERROR, /* calculated checksum does not match */
SML_UNEXPECTED, /* unexpected byte received */
SML_FINAL, /* final state, checksum OK */
SML_DATA_SIGNED_INT,
SML_DATA_UNSIGNED_INT,
SML_DATA_OCTET_STRING,
} sml_states_t;
typedef enum {
SML_YEAR = 1,
SML_MONTH = 2,
SML_WEEK = 3,
SML_DAY = 4,
SML_HOUR = 5,
SML_MIN = 6,
SML_SECOND = 7,
SML_DEGREE = 8,
SML_DEGREE_CELSIUS = 9,
SML_CURRENCY = 10,
SML_METRE = 11,
SML_METRE_PER_SECOND = 12,
SML_CUBIC_METRE = 13,
SML_CUBIC_METRE_CORRECTED = 14,
SML_CUBIC_METRE_PER_HOUR = 15,
SML_CUBIC_METRE_PER_HOUR_CORRECTED = 16,
SML_CUBIC_METRE_PER_DAY = 17,
SML_CUBIC_METRE_PER_DAY_CORRECTED = 18,
SML_LITRE = 19,
SML_KILOGRAM = 20,
SML_NEWTON = 21,
SML_NEWTONMETER = 22,
SML_PASCAL = 23,
SML_BAR = 24,
SML_JOULE = 25,
SML_JOULE_PER_HOUR = 26,
SML_WATT = 27,
SML_VOLT_AMPERE = 28,
SML_VAR = 29,
SML_WATT_HOUR = 30,
SML_VOLT_AMPERE_HOUR = 31,
SML_VAR_HOUR = 32,
SML_AMPERE = 33,
SML_COULOMB = 34,
SML_VOLT = 35,
SML_VOLT_PER_METRE = 36,
SML_FARAD = 37,
SML_OHM = 38,
SML_OHM_METRE = 39,
SML_WEBER = 40,
SML_TESLA = 41,
SML_AMPERE_PER_METRE = 42,
SML_HENRY = 43,
SML_HERTZ = 44,
SML_ACTIVE_ENERGY_METER_CONSTANT_OR_PULSE_VALUE = 45,
SML_REACTIVE_ENERGY_METER_CONSTANT_OR_PULSE_VALUE = 46,
SML_APPARENT_ENERGY_METER_CONSTANT_OR_PULSE_VALUE = 47,
SML_VOLT_SQUARED_HOURS = 48,
SML_AMPERE_SQUARED_HOURS = 49,
SML_KILOGRAM_PER_SECOND = 50,
SML_KELVIN = 52,
SML_VOLT_SQUARED_HOUR_METER_CONSTANT_OR_PULSE_VALUE = 53,
SML_AMPERE_SQUARED_HOUR_METER_CONSTANT_OR_PULSE_VALUE = 54,
SML_METER_CONSTANT_OR_PULSE_VALUE = 55,
SML_PERCENTAGE = 56,
SML_AMPERE_HOUR = 57,
SML_ENERGY_PER_VOLUME = 60,
SML_CALORIFIC_VALUE = 61,
SML_MOLE_PERCENT = 62,
SML_MASS_DENSITY = 63,
SML_PASCAL_SECOND = 64,
SML_RESERVED = 253,
SML_OTHER_UNIT = 254,
SML_COUNT = 255
} sml_units_t;
sml_states_t smlState(unsigned char &byte);
bool smlOBISCheck(const unsigned char *obis);
void smlOBISManufacturer(unsigned char *str, int maxSize);
void smlOBISByUnit(long long int &wh, signed char &scaler, sml_units_t unit);
// Be aware that double on Arduino UNO is just 32 bit
void smlOBISWh(double &wh);
void smlOBISW(double &w);
void smlOBISVolt(double &v);
void smlOBISAmpere(double &a);
#endif

50
src/plugins/zeroExport/SMLCRCTable.h

@ -0,0 +1,50 @@
#ifndef SML_CRC_TABLE_H
#define SML_CRC_TABLE_H
#include <stdint.h>
#ifdef ARDUINO
#include <Arduino.h>
/*
* This mysterious table is just the CRC of each possible byte. It can be
* computed using the standard bit-at-a-time methods. The polynomial can
* be seen in entry 128, 0x8408. This corresponds to x^0 + x^5 + x^12.
* Add the implicit x^16, and you have the standard CRC-CCITT.
*/
static const uint16_t smlCrcTable[256] PROGMEM =
#else
static const uint16_t smlCrcTable[256] =
#endif
{0x0000, 0x1189, 0x2312, 0x329B, 0x4624, 0x57AD, 0x6536, 0x74BF, 0x8C48,
0x9DC1, 0xAF5A, 0xBED3, 0xCA6C, 0xDBE5, 0xE97E, 0xF8F7, 0x1081, 0x0108,
0x3393, 0x221A, 0x56A5, 0x472C, 0x75B7, 0x643E, 0x9CC9, 0x8D40, 0xBFDB,
0xAE52, 0xDAED, 0xCB64, 0xF9FF, 0xE876, 0x2102, 0x308B, 0x0210, 0x1399,
0x6726, 0x76AF, 0x4434, 0x55BD, 0xAD4A, 0xBCC3, 0x8E58, 0x9FD1, 0xEB6E,
0xFAE7, 0xC87C, 0xD9F5, 0x3183, 0x200A, 0x1291, 0x0318, 0x77A7, 0x662E,
0x54B5, 0x453C, 0xBDCB, 0xAC42, 0x9ED9, 0x8F50, 0xFBEF, 0xEA66, 0xD8FD,
0xC974, 0x4204, 0x538D, 0x6116, 0x709F, 0x0420, 0x15A9, 0x2732, 0x36BB,
0xCE4C, 0xDFC5, 0xED5E, 0xFCD7, 0x8868, 0x99E1, 0xAB7A, 0xBAF3, 0x5285,
0x430C, 0x7197, 0x601E, 0x14A1, 0x0528, 0x37B3, 0x263A, 0xDECD, 0xCF44,
0xFDDF, 0xEC56, 0x98E9, 0x8960, 0xBBFB, 0xAA72, 0x6306, 0x728F, 0x4014,
0x519D, 0x2522, 0x34AB, 0x0630, 0x17B9, 0xEF4E, 0xFEC7, 0xCC5C, 0xDDD5,
0xA96A, 0xB8E3, 0x8A78, 0x9BF1, 0x7387, 0x620E, 0x5095, 0x411C, 0x35A3,
0x242A, 0x16B1, 0x0738, 0xFFCF, 0xEE46, 0xDCDD, 0xCD54, 0xB9EB, 0xA862,
0x9AF9, 0x8B70, 0x8408, 0x9581, 0xA71A, 0xB693, 0xC22C, 0xD3A5, 0xE13E,
0xF0B7, 0x0840, 0x19C9, 0x2B52, 0x3ADB, 0x4E64, 0x5FED, 0x6D76, 0x7CFF,
0x9489, 0x8500, 0xB79B, 0xA612, 0xD2AD, 0xC324, 0xF1BF, 0xE036, 0x18C1,
0x0948, 0x3BD3, 0x2A5A, 0x5EE5, 0x4F6C, 0x7DF7, 0x6C7E, 0xA50A, 0xB483,
0x8618, 0x9791, 0xE32E, 0xF2A7, 0xC03C, 0xD1B5, 0x2942, 0x38CB, 0x0A50,
0x1BD9, 0x6F66, 0x7EEF, 0x4C74, 0x5DFD, 0xB58B, 0xA402, 0x9699, 0x8710,
0xF3AF, 0xE226, 0xD0BD, 0xC134, 0x39C3, 0x284A, 0x1AD1, 0x0B58, 0x7FE7,
0x6E6E, 0x5CF5, 0x4D7C, 0xC60C, 0xD785, 0xE51E, 0xF497, 0x8028, 0x91A1,
0xA33A, 0xB2B3, 0x4A44, 0x5BCD, 0x6956, 0x78DF, 0x0C60, 0x1DE9, 0x2F72,
0x3EFB, 0xD68D, 0xC704, 0xF59F, 0xE416, 0x90A9, 0x8120, 0xB3BB, 0xA232,
0x5AC5, 0x4B4C, 0x79D7, 0x685E, 0x1CE1, 0x0D68, 0x3FF3, 0x2E7A, 0xE70E,
0xF687, 0xC41C, 0xD595, 0xA12A, 0xB0A3, 0x8238, 0x93B1, 0x6B46, 0x7ACF,
0x4854, 0x59DD, 0x2D62, 0x3CEB, 0x0E70, 0x1FF9, 0xF78F, 0xE606, 0xD49D,
0xC514, 0xB1AB, 0xA022, 0x92B9, 0x8330, 0x7BC7, 0x6A4E, 0x58D5, 0x495C,
0x3DE3, 0x2C6A, 0x1EF1, 0x0F78};
#endif

0
src/plugins/zeroExport/zeroExport.cpp

122
src/plugins/zeroExport/zeroExport.h

@ -0,0 +1,122 @@
#if defined(ESP32)
#ifndef __ZEROEXPORT__
#define __ZEROEXPORT__
#include <HTTPClient.h>
#include <string.h>
#include "AsyncJson.h"
#include "SML.h"
template <class HMSYSTEM>
class ZeroExport {
public:
ZeroExport() { }
void setup(cfgzeroExport_t *cfg, HMSYSTEM *sys, settings_t *config) {
mCfg = cfg;
mSys = sys;
mConfig = config;
}
void tickerSecond() {
//DPRINTLN(DBG_INFO, (F("tickerSecond()")));
if (millis() - mCfg->lastTime < mCfg->count_avg * 1000UL) {
zero(); // just refresh when it is needed. To get cpu load low.
}
}
// Sums up the power values ​​of all phases and returns them.
// If the value is negative, all power values ​​from the inverter are taken into account
double getPowertoSetnewValue()
{
float ivPower = 0;
Inverter<> *iv;
record_t<> *rec;
for (uint8_t i = 0; i < mSys->getNumInverters(); i++) {
iv = mSys->getInverterByPos(i);
rec = iv->getRecordStruct(RealTimeRunData_Debug);
if (iv == NULL)
continue;
ivPower += iv->getChannelFieldValue(CH0, FLD_PAC, rec);
}
return ((unsigned)(mCfg->total_power - mCfg->power_avg) >= mCfg->power_avg) ? ivPower + mCfg->total_power : ivPower - mCfg->total_power;
}
//C2T2-B91B
private:
HTTPClient httpClient;
// TODO: Need to improve here. 2048 for a JSON Obj is to big!?
bool zero()
{
httpClient.setFollowRedirects(HTTPC_STRICT_FOLLOW_REDIRECTS);
httpClient.setUserAgent("Ahoy-Agent");
httpClient.setConnectTimeout(1000);
httpClient.setTimeout(1000);
httpClient.addHeader("Content-Type", "application/json");
httpClient.addHeader("Accept", "application/json");
if (!httpClient.begin(mCfg->monitor_url)) {
DPRINTLN(DBG_INFO, "httpClient.begin failed");
httpClient.end();
return false;
}
if(String(mCfg->monitor_url).endsWith("data.json?node_id=1")){
httpClient.setAuthorization("admin", mCfg->tibber_pw);
}
int httpCode = httpClient.GET();
if (httpCode == HTTP_CODE_OK)
{
String responseBody = httpClient.getString();
DynamicJsonDocument json(2048);
DeserializationError err = deserializeJson(json, responseBody);
// Parse succeeded?
if (err) {
DPRINTLN(DBG_INFO, (F("ZeroExport() JSON error returned: ")));
DPRINTLN(DBG_INFO, String(err.f_str()));
}
// check if it HICHI
if(json.containsKey(F("StatusSNS")) ) {
int index = responseBody.indexOf(String(mCfg->json_path)); // find first match position
responseBody = responseBody.substring(index); // cut it and store it in value
index = responseBody.indexOf(","); // find the first seperation - Bingo!?
mCfg->total_power = responseBody.substring(responseBody.indexOf(":"), index).toDouble();
} else if(json.containsKey(F("emeters"))) {
mCfg->total_power = (double)json[F("total_power")];
} else if(String(mCfg->monitor_url).endsWith("data.json?node_id=1") ) {
tibber_parse();
} else {
DPRINTLN(DBG_INFO, (F("ZeroExport() json error: cant find value in this query: ") + responseBody));
return false;
}
}
else
{
DPRINTLN(DBG_INFO, F("ZeroExport(): Error ") + String(httpCode));
return false;
}
httpClient.end();
return true;
}
void tibber_parse()
{
}
// private member variables
cfgzeroExport_t *mCfg;
settings_t *mConfig;
HMSYSTEM *mSys;
};
#endif /*__ZEROEXPORT__*/
#endif /* #if defined(ESP32) */

20
src/web/RestApi.h

@ -615,6 +615,22 @@ class RestApi {
obj[F("interval")] = String(mConfig->mqtt.interval); obj[F("interval")] = String(mConfig->mqtt.interval);
} }
#if defined(ESP32)
void getzeroExport(JsonObject obj) {
obj[F("en_zeroexport")] = (bool) mConfig->plugin.zexport.enabled;
obj[F("two_percent")] = (bool) mConfig->plugin.zexport.two_percent;
obj[F("monitor_url")] = String(mConfig->plugin.zexport.monitor_url);
obj[F("json_path")] = String(mConfig->plugin.zexport.json_path);
obj[F("count_avg")] = (uint8_t)mConfig->plugin.zexport.count_avg;
obj[F("max_power")] = (double)mConfig->plugin.zexport.max_power;
obj[F("Iv")] = (uint8_t)mConfig->plugin.zexport.Iv;
obj[F("power_avg")] = (float)mConfig->plugin.zexport.power_avg;
obj[F("query_device")] = (float)mConfig->plugin.zexport.query_device;
obj[F("total_power")] = (double)mConfig->plugin.zexport.total_power;
//obj[F("device")] = (uint8_t)mCfg.plugin.zexport.device;
}
#endif
void getNtp(JsonObject obj) { void getNtp(JsonObject obj) {
obj[F("addr")] = String(mConfig->ntp.addr); obj[F("addr")] = String(mConfig->ntp.addr);
obj[F("port")] = String(mConfig->ntp.port); obj[F("port")] = String(mConfig->ntp.port);
@ -775,6 +791,10 @@ class RestApi {
getSerial(obj.createNestedObject(F("serial"))); getSerial(obj.createNestedObject(F("serial")));
getStaticIp(obj.createNestedObject(F("static_ip"))); getStaticIp(obj.createNestedObject(F("static_ip")));
getDisplay(obj.createNestedObject(F("display"))); getDisplay(obj.createNestedObject(F("display")));
#if defined(ESP32)
getzeroExport(obj.createNestedObject(F("zeroExport")));
#endif
} }
#if !defined(ETHERNET) #if !defined(ETHERNET)

101
src/web/html/setup.html

@ -164,6 +164,7 @@
</fieldset> </fieldset>
</div> </div>
<!-- NTP Server -->
<button type="button" class="s_collapsible">NTP Server</button> <button type="button" class="s_collapsible">NTP Server</button>
<div class="s_content"> <div class="s_content">
<fieldset class="mb-4"> <fieldset class="mb-4">
@ -218,6 +219,7 @@
</fieldset> </fieldset>
</div> </div>
<!-- MQTT -->
<button type="button" class="s_collapsible">MQTT</button> <button type="button" class="s_collapsible">MQTT</button>
<div class="s_content"> <div class="s_content">
<fieldset class="mb-4"> <fieldset class="mb-4">
@ -306,6 +308,51 @@
</fieldset> </fieldset>
</div> </div>
<!-- Zero Export -->
<button type="button" class="s_collapsible" id="zeroExport_button">Zero Export</button>
<div class="s_content" id="zeroExport">
<fieldset class="mb-4">
<legend class="des">Zero Export</legend>
<div id="zeroType"></div>
<div class="row mb-3">
<div class="col-8 col-sm-3">Enable zero export</div>
<div class="col-4 col-sm-9"><input type="checkbox" name="en_zeroexport"/></div>
<p>Please select your favorite query interface:</p>
</div>
<div class="row mb-3">
<div class="col-12 col-sm-3 my-2">Monitor IP: </div>
<input type="radio" id="html" name="dev_Tibber" value="Tibber">
<label for="html">Tibber</label>
<input type="radio" id="css" name="dev_Shelly" value="Shelly">
<label for="css">Shelly</label>
<input type="radio" id="javascript" name="dev_Other" value="Other">
<label for="javascript">Other</label>
<div class="col-12 col-sm-9">
<input type="text" name="monitor_url" maxlength="100">A JSON-Format is required to work properly.<br>
HICHI: http://IP_Address/cm?cmnd=status%208</div>
<div class="col-12 col-sm-3 my-2">Prio Inverter</div>
<div class="col-12 col-sm-9"><select name="iv" id="Inv_ID"></select>Which Inverter should be regulated.</div>
<div class="col-12 col-sm-3 my-2">JSON Path: </div>
<div class="col-12 col-sm-9"><input type="text" name="json_path" maxlength="100">Only for HICHI needed!</div>
<div class="col-8 col-sm-3">2% protection: </div>
<div class="col-4 col-sm-9"><input type="checkbox" name="two_percent"/></div>
<br>
<div class="col-8 col-sm-3">Max Power: </div>
<div class="col-4 col-sm-9"><input type="number" name="max_power" min="8" ></div>
<br>
<div class="col-12 col-sm-3 my-2">Refresh rate (sec.)<input type="number" name="count_avg" min="0" max="255"></div>
<div class="col-12 col-sm-3 my-2">Power tolerances (Watt)<input type="number" name="power_avg" min="0" max="255"></div>
</div>
<p name="total_power">Total: n/a</p>
</fieldset>
</div>
<div class="row mb-4 mt-4"> <div class="row mb-4 mt-4">
<div class="col-8 col-sm-3">{#BTN_REBOOT_SUCCESSFUL_SAVE}</div> <div class="col-8 col-sm-3">{#BTN_REBOOT_SUCCESSFUL_SAVE}</div>
<div class="col-4 col-sm-9"> <div class="col-4 col-sm-9">
@ -1157,6 +1204,56 @@
document.getElementById("date").innerHTML = toIsoDateStr((new Date((++ts) * 1000))); document.getElementById("date").innerHTML = toIsoDateStr((new Date((++ts) * 1000)));
} }
function parsezeroExport(obj, type, ) {
if ("ESP8266" == type) {
var e = document.getElementById("zeroExport");
e.remove();
var e = document.getElementById("zeroExport_button");
e.textContent += " (only for ESP32 available)";
e.disabled = true;
element.classList.add("disabled");
return;
}
document.getElementsByName("en_zeroexport")[0].checked = obj["en_zeroexport"];
document.getElementsByName("two_percent")[0].checked = obj["two_percent"];
document.getElementsByName("dev_Tibber")[0].checked = (obj["query_device"] == 1);
document.getElementsByName("dev_Shelly")[0].checked = (obj["query_device"] == 2);
document.getElementsByName("dev_Other")[0].checked = (obj["query_device"] == 3);
getAjax("/api/inverter/list", parseZeroIv);
for(var i of [["monitor_url", "monitor_url"], ["power_avg", "power_avg"], ["count_avg", "count_avg"], ["json_path", "json_path"], ["max_power", "max_power"], ["query_device", "query_device"]])
if(null != obj[i[1]])
document.getElementsByName(i[0])[0].value = obj[i[1]];
document.getElementsByName("total_power")[0].innerHTML = "Total: " + obj["total_power"].toFixed(2) + "W";
document.getElementById("Inv_ID").selectedIndex = obj["Iv"];
}
function parseZeroIv(root)
{
for(var i = 0; i < root.inverter.length; i++)
root.inverter[i];
select = document.getElementById('Inv_ID');
parseInt(select.value)
if(null == root) return;
root = root.inverter;
for(var i = 0; i < root.length; i++) {
inv = root[i];
var opt = document.createElement('option');
opt.value = inv.id;
opt.innerHTML = inv.name;
select.appendChild(opt);
}
}
function parse(root) { function parse(root) {
if(null != root) { if(null != root) {
parseGeneric(root["generic"]); parseGeneric(root["generic"]);
@ -1167,11 +1264,15 @@
parseSun(root["sun"]); parseSun(root["sun"]);
parsePinout(root["pinout"], root["system"]["esp_type"], root["system"]); parsePinout(root["pinout"], root["system"]["esp_type"], root["system"]);
parseNrfRadio(root["radioNrf"], root["pinout"], root["system"]["esp_type"], root["system"]); parseNrfRadio(root["radioNrf"], root["pinout"], root["system"]["esp_type"], root["system"]);
/*IF_ESP32*/ /*IF_ESP32*/
parseCmtRadio(root["radioCmt"], root["system"]["esp_type"], root["system"]); parseCmtRadio(root["radioCmt"], root["system"]["esp_type"], root["system"]);
/*ENDIF_ESP32*/ /*ENDIF_ESP32*/
parsezeroExport(root["zeroExport"], root["system"]["esp_type"]);
parseSerial(root["serial"]); parseSerial(root["serial"]);
parseDisplay(root["display"], root["system"]["esp_type"], root["system"]); parseDisplay(root["display"], root["system"]["esp_type"], root["system"]);
getAjax("/api/inverter/list", parseIv); getAjax("/api/inverter/list", parseIv);
} }
} }

4
src/web/html/style.css

@ -470,6 +470,10 @@ p.lic, p.lic a {
color: #fff; color: #fff;
} }
.disabled {
background-color: dimgray;
}
.s_content { .s_content {
display: none; display: none;
overflow: hidden; overflow: hidden;

2
src/web/html/visualization.html

@ -11,6 +11,8 @@
<div id="content"> <div id="content">
<div id="live"></div> <div id="live"></div>
<p>{#EVERY} <span id="refresh"></span> {#UPDATE_SECS}</p> <p>{#EVERY} <span id="refresh"></span> {#UPDATE_SECS}</p>
<div id="zero_export"></div>
<p>Every <span id="refresh"></span> seconds the values are updated</p>
</div> </div>
</div> </div>
{#HTML_FOOTER} {#HTML_FOOTER}

33
src/web/web.h

@ -547,6 +547,39 @@ class Web {
mConfig->mqtt.port = request->arg("mqttPort").toInt(); mConfig->mqtt.port = request->arg("mqttPort").toInt();
mConfig->mqtt.interval = request->arg("mqttInterval").toInt(); mConfig->mqtt.interval = request->arg("mqttInterval").toInt();
// zero-export
#if defined(ESP32)
mConfig->plugin.zexport.enabled = (request->arg("en_zeroexport") == "on");
mConfig->plugin.zexport.two_percent = (request->arg("two_percent") == "on");
mConfig->plugin.zexport.Iv = request->arg("Iv").toInt();
mConfig->plugin.zexport.count_avg = request->arg("count_avg").toInt();
mConfig->plugin.zexport.max_power = request->arg("max_power").toDouble();
mConfig->plugin.zexport.power_avg = request->arg("power_avg").toFloat();
mConfig->plugin.zexport.query_device = request->arg("query_device").toInt();
mConfig->plugin.zexport.total_power = request->arg("total_power").toDouble();
if (request->arg("monitor_url") != "") {
String addr = request->arg("monitor_url");
addr.trim();
addr.toCharArray(mConfig->plugin.zexport.monitor_url, ZEXPORT_ADDR_LEN);
} else
mConfig->plugin.zexport.monitor_url[0] = '\0';
if (request->arg("json_path") != "") {
String addr = request->arg("json_path");
addr.trim();
addr.toCharArray(mConfig->plugin.zexport.json_path, ZEXPORT_ADDR_LEN);
} else
mConfig->plugin.zexport.json_path[0] = '\0';
if (request->arg("tibber_pw") != "") {
String addr = request->arg("tibber_pw");
addr.trim();
addr.toCharArray(mConfig->plugin.zexport.tibber_pw, 10);
} else
mConfig->plugin.zexport.tibber_pw[0] = '\0';
#endif
// serial console // serial console
mConfig->serial.debug = (request->arg("serDbg") == "on"); mConfig->serial.debug = (request->arg("serDbg") == "on");
mConfig->serial.privacyLog = (request->arg("priv") == "on"); mConfig->serial.privacyLog = (request->arg("priv") == "on");

Loading…
Cancel
Save