mirror of
https://github.com/lumapu/ahoy.git
synced 2025-04-29 10:16:21 +02:00
merge development03 to PR #1155
This commit is contained in:
parent
f500999346
commit
020f8cadfa
17 changed files with 1178 additions and 7 deletions
179
.github/workflows/compile_zero-export.yml
vendored
Normal file
179
.github/workflows/compile_zero-export.yml
vendored
Normal file
|
@ -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'
|
|
@ -2,9 +2,6 @@
|
|||
"folders": [
|
||||
{
|
||||
"path": "."
|
||||
},
|
||||
{
|
||||
"path": "src"
|
||||
}
|
||||
],
|
||||
"settings": {
|
||||
|
|
64
src/app.cpp
64
src/app.cpp
|
@ -14,7 +14,7 @@
|
|||
|
||||
//-----------------------------------------------------------------------------
|
||||
app::app() : ah::Scheduler {} {
|
||||
memset(mVersion, 0, sizeof(char) * 12);
|
||||
memset(mVersion, 0, sizeof(char) * 17);
|
||||
memset(mVersionModules, 0, sizeof(char) * 12);
|
||||
}
|
||||
|
||||
|
@ -124,6 +124,11 @@ void app::setup() {
|
|||
|
||||
mPubSerial.setup(mConfig, &mSys, &mTimestamp);
|
||||
|
||||
// ZeroExport
|
||||
if (mConfig->plugin.zexport.enabled) {
|
||||
mzExport.setup(&mConfig->plugin.zexport, &mSys, mConfig);
|
||||
}
|
||||
|
||||
#if !defined(ETHERNET)
|
||||
//mImprov.setup(this, mConfig->sys.deviceName, mVersion);
|
||||
#endif
|
||||
|
@ -191,6 +196,13 @@ void app::regularTickers(void) {
|
|||
if (DISP_TYPE_T0_NONE != mConfig->plugin.display.type)
|
||||
everySec(std::bind(&DisplayType::tickerSecond, &mDisplay), "disp");
|
||||
#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");
|
||||
#if !defined(ETHERNET)
|
||||
//everySec([this]() { mImprov.tickSerial(); }, "impro");
|
||||
|
@ -445,6 +457,10 @@ void app::tickSend(void) {
|
|||
else
|
||||
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) {
|
||||
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",
|
||||
#ifdef ENABLE_PROMETHEUS_EP
|
||||
"P"
|
||||
|
@ -619,3 +635,47 @@ void app::updateLed(void) {
|
|||
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
15
src/app.h
|
@ -77,6 +77,11 @@ typedef Simulator<HmSystemType> SimulatorType;
|
|||
typedef Display<HmSystemType, Radio> DisplayType;
|
||||
#endif
|
||||
|
||||
#if defined(PLUGIN_ZEROEXPORT)
|
||||
#include "plugins/zeroExport/zeroExport.h"
|
||||
typedef ZeroExport<HmSystemType> ZeroExportType;
|
||||
#endif
|
||||
|
||||
class app : public IApp, public ah::Scheduler {
|
||||
public:
|
||||
app();
|
||||
|
@ -348,6 +353,10 @@ class app : public IApp, public ah::Scheduler {
|
|||
void setupLed();
|
||||
void updateLed();
|
||||
|
||||
#if defined(ESP32)
|
||||
void zeroexport();
|
||||
#endif
|
||||
|
||||
void tickReboot(void) {
|
||||
DPRINTLN(DBG_INFO, F("Rebooting..."));
|
||||
ah::Scheduler::resetTicker();
|
||||
|
@ -414,7 +423,7 @@ class app : public IApp, public ah::Scheduler {
|
|||
CmtRadio<> mCmtRadio;
|
||||
#endif
|
||||
|
||||
char mVersion[12];
|
||||
char mVersion[17];
|
||||
char mVersionModules[12];
|
||||
settings mSettings;
|
||||
settings_t *mConfig = nullptr;
|
||||
|
@ -450,6 +459,10 @@ class app : public IApp, public ah::Scheduler {
|
|||
#if defined(ENABLE_SIMULATOR)
|
||||
SimulatorType mSimulator;
|
||||
#endif /*ENABLE_SIMULATOR*/
|
||||
|
||||
#if defined(PLUGIN_ZEROEXPORT)
|
||||
ZeroExportType mzExport;
|
||||
#endif
|
||||
};
|
||||
|
||||
#endif /*__APP_H__*/
|
||||
|
|
|
@ -235,6 +235,9 @@
|
|||
// default MQTT broker uri
|
||||
#define DEF_MQTT_BROKER "\0"
|
||||
|
||||
// default zero-export uri
|
||||
#define DEF_ZEXPORT "\0"
|
||||
|
||||
// default MQTT port
|
||||
#define DEF_MQTT_PORT 1883
|
||||
|
||||
|
|
|
@ -139,6 +139,24 @@ typedef struct {
|
|||
uint16_t interval;
|
||||
} 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 {
|
||||
bool enabled;
|
||||
char name[MAX_NAME_LENGTH];
|
||||
|
@ -188,6 +206,9 @@ typedef struct {
|
|||
display_t display;
|
||||
char customLink[MAX_CUSTOM_LINK_LEN];
|
||||
char customLinkText[MAX_CUSTOM_LINK_TEXT_LEN];
|
||||
#if defined(ESP32)
|
||||
cfgzeroExport_t zexport;
|
||||
#endif
|
||||
} plugins_t;
|
||||
|
||||
typedef struct {
|
||||
|
@ -291,6 +312,7 @@ class settings {
|
|||
if(root.containsKey(F("nrf"))) jsonNrf(root[F("nrf")]);
|
||||
#if defined(ESP32)
|
||||
if(root.containsKey(F("cmt"))) jsonCmt(root[F("cmt")]);
|
||||
if(root.containsKey(F("zeroExport"))) jsonzeroExport(root[F("zeroExport")]);
|
||||
#endif
|
||||
if(root.containsKey(F("ntp"))) jsonNtp(root[F("ntp")]);
|
||||
if(root.containsKey(F("sun"))) jsonSun(root[F("sun")]);
|
||||
|
@ -320,6 +342,7 @@ class settings {
|
|||
jsonNrf(root[F("nrf")].to<JsonObject>(), true);
|
||||
#if defined(ESP32)
|
||||
jsonCmt(root[F("cmt")].to<JsonObject>(), true);
|
||||
jsonzeroExport(root.createNestedObject(F("zeroExport")), true);
|
||||
#endif
|
||||
jsonNtp(root[F("ntp")].to<JsonObject>(), true);
|
||||
jsonSun(root[F("sun")].to<JsonObject>(), true);
|
||||
|
@ -329,6 +352,7 @@ class settings {
|
|||
jsonPlugin(root[F("plugin")].to<JsonObject>(), true);
|
||||
jsonInst(root[F("inst")].to<JsonObject>(), true);
|
||||
|
||||
|
||||
DPRINT(DBG_INFO, F("memory usage: "));
|
||||
DBGPRINTLN(String(json.memoryUsage()));
|
||||
DPRINT(DBG_INFO, F("capacity: "));
|
||||
|
@ -446,7 +470,22 @@ class settings {
|
|||
snprintf(mCfg.mqtt.topic, MQTT_TOPIC_LEN, "%s", DEF_MQTT_TOPIC);
|
||||
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.rstValsNotAvail = 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) {
|
||||
if(set) {
|
||||
obj[F("0")] = mCfg.led.led[0];
|
||||
|
|
|
@ -98,6 +98,8 @@ enum {
|
|||
#define DEVNAME_LEN 16
|
||||
#define NTP_ADDR_LEN 32 // DNS Name
|
||||
|
||||
#define ZEXPORT_ADDR_LEN 100 // Zero-Export Address
|
||||
|
||||
#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_USER_LEN 65 // there is another byte necessary for \0
|
||||
|
@ -106,6 +108,7 @@ enum {
|
|||
|
||||
#define MQTT_MAX_PACKET_SIZE 384
|
||||
|
||||
#define PLUGIN_ZEROEXPORT
|
||||
|
||||
typedef struct {
|
||||
uint32_t rxFail;
|
||||
|
|
405
src/plugins/zeroExport/SML.cpp
Normal file
405
src/plugins/zeroExport/SML.cpp
Normal file
|
@ -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 ¤tByte)
|
||||
{
|
||||
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
Normal file
106
src/plugins/zeroExport/SML.h
Normal file
|
@ -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
Normal file
50
src/plugins/zeroExport/SMLCRCTable.h
Normal file
|
@ -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
Normal file
0
src/plugins/zeroExport/zeroExport.cpp
Normal file
122
src/plugins/zeroExport/zeroExport.h
Normal file
122
src/plugins/zeroExport/zeroExport.h
Normal file
|
@ -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) */
|
|
@ -615,6 +615,22 @@ class RestApi {
|
|||
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) {
|
||||
obj[F("addr")] = String(mConfig->ntp.addr);
|
||||
obj[F("port")] = String(mConfig->ntp.port);
|
||||
|
@ -775,6 +791,10 @@ class RestApi {
|
|||
getSerial(obj.createNestedObject(F("serial")));
|
||||
getStaticIp(obj.createNestedObject(F("static_ip")));
|
||||
getDisplay(obj.createNestedObject(F("display")));
|
||||
|
||||
#if defined(ESP32)
|
||||
getzeroExport(obj.createNestedObject(F("zeroExport")));
|
||||
#endif
|
||||
}
|
||||
|
||||
#if !defined(ETHERNET)
|
||||
|
|
|
@ -164,6 +164,7 @@
|
|||
</fieldset>
|
||||
</div>
|
||||
|
||||
<!-- NTP Server -->
|
||||
<button type="button" class="s_collapsible">NTP Server</button>
|
||||
<div class="s_content">
|
||||
<fieldset class="mb-4">
|
||||
|
@ -218,6 +219,7 @@
|
|||
</fieldset>
|
||||
</div>
|
||||
|
||||
<!-- MQTT -->
|
||||
<button type="button" class="s_collapsible">MQTT</button>
|
||||
<div class="s_content">
|
||||
<fieldset class="mb-4">
|
||||
|
@ -306,6 +308,51 @@
|
|||
</fieldset>
|
||||
</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="col-8 col-sm-3">{#BTN_REBOOT_SUCCESSFUL_SAVE}</div>
|
||||
<div class="col-4 col-sm-9">
|
||||
|
@ -1157,6 +1204,56 @@
|
|||
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) {
|
||||
if(null != root) {
|
||||
parseGeneric(root["generic"]);
|
||||
|
@ -1167,11 +1264,15 @@
|
|||
parseSun(root["sun"]);
|
||||
parsePinout(root["pinout"], root["system"]["esp_type"], root["system"]);
|
||||
parseNrfRadio(root["radioNrf"], root["pinout"], root["system"]["esp_type"], root["system"]);
|
||||
|
||||
/*IF_ESP32*/
|
||||
parseCmtRadio(root["radioCmt"], root["system"]["esp_type"], root["system"]);
|
||||
/*ENDIF_ESP32*/
|
||||
parsezeroExport(root["zeroExport"], root["system"]["esp_type"]);
|
||||
|
||||
parseSerial(root["serial"]);
|
||||
parseDisplay(root["display"], root["system"]["esp_type"], root["system"]);
|
||||
|
||||
getAjax("/api/inverter/list", parseIv);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -470,6 +470,10 @@ p.lic, p.lic a {
|
|||
color: #fff;
|
||||
}
|
||||
|
||||
.disabled {
|
||||
background-color: dimgray;
|
||||
}
|
||||
|
||||
.s_content {
|
||||
display: none;
|
||||
overflow: hidden;
|
||||
|
|
|
@ -11,6 +11,8 @@
|
|||
<div id="content">
|
||||
<div id="live"></div>
|
||||
<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>
|
||||
{#HTML_FOOTER}
|
||||
|
|
|
@ -547,6 +547,39 @@ class Web {
|
|||
mConfig->mqtt.port = request->arg("mqttPort").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
|
||||
mConfig->serial.debug = (request->arg("serDbg") == "on");
|
||||
mConfig->serial.privacyLog = (request->arg("priv") == "on");
|
||||
|
|
Loading…
Add table
Reference in a new issue