Browse Source

Merge branch 'development03' into dev03

pull/361/head
DanielR92 2 years ago
committed by GitHub
parent
commit
97d23e7ae0
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 6
      .github/workflows/compile_development.yml
  2. 17
      tools/esp8266/User_Manual.md
  3. 44
      tools/esp8266/ahoywifi.cpp
  4. 3
      tools/esp8266/ahoywifi.h
  5. 134
      tools/esp8266/app.cpp
  6. 51
      tools/esp8266/app.h
  7. 9
      tools/esp8266/config.h
  8. 4
      tools/esp8266/crc.cpp
  9. 4
      tools/esp8266/crc.h
  10. 12
      tools/esp8266/defines.h
  11. 8
      tools/esp8266/hmDefines.h
  12. 14
      tools/esp8266/hmInverter.h
  13. 62
      tools/esp8266/hmRadio.h
  14. 22
      tools/esp8266/hmSystem.h
  15. 12
      tools/esp8266/html/api.js
  16. 48
      tools/esp8266/html/index.html
  17. 12
      tools/esp8266/html/serial.html
  18. 89
      tools/esp8266/html/setup.html
  19. 10
      tools/esp8266/html/visualization.html
  20. 14
      tools/esp8266/web.cpp
  21. 27
      tools/esp8266/webApi.cpp
  22. 9
      tools/esp8266/webApi.h

6
.github/workflows/compile_development.yml

@ -13,7 +13,7 @@ jobs:
- uses: actions/checkout@v3
with:
ref: development03
- uses: benjlevesque/short-sha@v2
- uses: benjlevesque/short-sha@v2.0
id: short-sha
with:
length: 7
@ -30,7 +30,9 @@ jobs:
path: ~/.platformio
key: ${{ runner.os }}-${{ hashFiles('**/lockfiles') }}
- name: setup-python
uses: actions/setup-python@v4
uses: actions/setup-python@v4.3.0
with:
python-version: '3.10'
- name: install-platformio
run: |
python -m pip install --upgrade pip

17
tools/esp8266/User_Manual.md

@ -259,3 +259,20 @@ In case all commands are processed (`_commandQueue.empty() == true`) then as a d
In case a Device Control command (Power Limit, Off, On) is requested via MQTT or REST API this request will be send before any other enqueued command.
In case of a accepted change in power limit the command get active power limit in percent (`SystemConfigPara = 5 // 0x05`) will be enqueued. The acceptance is checked by the reponse packets on the devive control commands (tx id 0x51 --> rx id 0xD1) if in byte 12 the requested sub-command (eg. power limit) is present.
## How To
### Sunrise & Sunset
In order to display the sunrise and sunset on the start page, the location coordinates (latitude and longitude) must be set in decimal in the setup under Sunrise & Sunset. If the coordinates are set, the current sunrise and sunset are calculated and displayed daily.
If this is set, you can also tick "disable night communication", then sending to the inverter is switched off outside of the day (i.e. at night), this avoids unnecessary communication attempts and thus also the incrementing of "RX no anwser".
Here you can get easy your GeoLocation: [https://www.mapsdirections.info/en/gps-coordinates.html](https://www.mapsdirections.info/en/gps-coordinates.html)
### Commands and informations
Turn On - turns on the inverter/feeder (LED flashes green if there is no error)
Turn Off - switches off the inverter/feeder (LED flashes fast red), can be switched on again with Turn On or by disconnecting and reconnecting the DC voltage
Restart - restarts the microcontroller in the inverter, which deletes the error memory and the YieldDay values, feed-in stops briefly and starts with the last persistent limit
Send Power Limit:
- A limitation of the AC power can be sent in relative (in %) or in absolute (Watt).
- It can be set to a different value non-persistently (temporarily) at any time (regardless of what you have set for persistent), this should be normal in order to limit the power (zero feed/battery control) and does not damage the EEPROM in the WR either.
- With persistent you send a saving limit (only use it seldom, otherwise the EEPROM in the HM can break!), This is then used as the next switch-on limit when DC comes on, i.e. when the sun comes up early or the WR on batteries is switched on the limit is applied immediately when sending, like any other, but it is stored in the EEPROM of the WR.
- A persistent limit is only needed if you want to throttle your inverter permanently or you can use it to set a start value on the battery, which is then always the switch-on limit when switching on, otherwise it would ramp up to 100% without regulation, which is continuous load is not healthy.
- You can set a new limit in the turn-off state, which is then used for on (switching on again), otherwise the last limit from before the turn-off is used, but of course this only applies if DC voltage is applied the whole time.
- If the DC voltage is missing for a few seconds, the microcontroller in the inverter goes off and forgets everything that was temporary/non-persistent in the RAM: YieldDay, error memory, non-persistent limit.

44
tools/esp8266/ahoywifi.cpp

@ -12,7 +12,6 @@
// NTP CONFIG
#define NTP_PACKET_SIZE 48
#define TIMEZONE 1 // Central European time +1
//-----------------------------------------------------------------------------
@ -185,7 +184,6 @@ time_t ahoywifi::getNtpTime(void) {
WiFi.hostByName(mConfig->ntpAddr, timeServer);
mUdp->begin(mConfig->ntpPort);
sendNTPpacket(timeServer);
while(retry++ < 5) {
@ -200,7 +198,6 @@ time_t ahoywifi::getNtpTime(void) {
secsSince1900 |= (buf[43] );
date = secsSince1900 - 2208988800UL; // UTC time
date += (TIMEZONE + offsetDayLightSaving(date)) * 3600;
break;
}
else
@ -212,6 +209,28 @@ time_t ahoywifi::getNtpTime(void) {
}
//-----------------------------------------------------------------------------
void ahoywifi::getAvailNetworks(JsonObject obj) {
JsonArray nets = obj.createNestedArray("networks");
int n = WiFi.scanComplete();
if(n == -2) {
WiFi.scanNetworks(true);
} else if(n) {
for (int i = 0; i < n; ++i) {
nets[i]["ssid"] = WiFi.SSID(i);
nets[i]["rssi"] = WiFi.RSSI(i);
// TODO: does github workflow use another version of this library?
// ahoywifi.cpp:223:38: error: 'class WiFiClass' has no member named 'isHidden'
//nets[i]["hidden"] = WiFi.isHidden(i) ? true : false;
}
WiFi.scanDelete();
if(WiFi.scanComplete() == -2)
WiFi.scanNetworks(true);
}
}
//-----------------------------------------------------------------------------
void ahoywifi::sendNTPpacket(IPAddress& address) {
//DPRINTLN(DBG_VERBOSE, F("wifi::sendNTPpacket"));
@ -231,22 +250,3 @@ void ahoywifi::sendNTPpacket(IPAddress& address) {
mUdp->write(buf, NTP_PACKET_SIZE);
mUdp->endPacket();
}
//-----------------------------------------------------------------------------
// calculates the daylight saving time for middle Europe. Input: Unixtime in UTC
// from: https://forum.arduino.cc/index.php?topic=172044.msg1278536#msg1278536
time_t ahoywifi::offsetDayLightSaving (uint32_t local_t) {
//DPRINTLN(DBG_VERBOSE, F("wifi::offsetDayLightSaving"));
int m = month (local_t);
if(m < 3 || m > 10) return 0; // no DSL in Jan, Feb, Nov, Dez
if(m > 3 && m < 10) return 1; // DSL in Apr, May, Jun, Jul, Aug, Sep
int y = year (local_t);
int h = hour (local_t);
int hToday = (h + 24 * day(local_t));
if((m == 3 && hToday >= (1 + TIMEZONE + 24 * (31 - (5 * y /4 + 4) % 7)))
|| (m == 10 && hToday < (1 + TIMEZONE + 24 * (31 - (5 * y /4 + 1) % 7))) )
return 1;
else
return 0;
}

3
tools/esp8266/ahoywifi.h

@ -30,11 +30,10 @@ class ahoywifi {
bool setupStation(uint32_t timeout);
bool getApActive(void);
time_t getNtpTime(void);
void getAvailNetworks(JsonObject obj);
private:
void sendNTPpacket(IPAddress& address);
time_t offsetDayLightSaving (uint32_t local_t);
config_t *mConfig;
sysConfig_t *mSysCfg;

134
tools/esp8266/app.cpp

@ -40,7 +40,7 @@ void app::setup(uint32_t timeout) {
#ifndef AP_ONLY
setupMqtt();
#endif
mSys->setup(&mConfig);
mSys->setup(mConfig.amplifierPower, mConfig.pinIrq, mConfig.pinCe, mConfig.pinCs);
mWebInst = new web(this, &mSysConfig, &mConfig, &mStat, mVersion);
mWebInst->setup();
@ -56,8 +56,8 @@ void app::loop(void) {
if(millis() - mPrevMillis >= 1000) {
mPrevMillis += 1000;
mUptimeSecs++;
if(0 != mTimestamp)
mTimestamp++;
if(0 != mUtcTimestamp)
mUtcTimestamp++;
}
if(checkTicker(&mNtpRefreshTicker, mNtpRefreshInterval)) {
@ -67,8 +67,8 @@ void app::loop(void) {
if(mUpdateNtp) {
mUpdateNtp = false;
mTimestamp = mWifi->getNtpTime();
DPRINTLN(DBG_INFO, "[NTP]: " + getDateTimeStr(mTimestamp));
mUtcTimestamp = mWifi->getNtpTime();
DPRINTLN(DBG_INFO, F("[NTP]: ") + getDateTimeStr(mUtcTimestamp) + F(" UTC"));
}
if(mFlagSendDiscoveryConfig) {
@ -166,6 +166,14 @@ void app::loop(void) {
mMqtt.loop();
if(checkTicker(&mTicker, 1000)) {
if(mUtcTimestamp > 946684800 && mConfig.sunLat && mConfig.sunLon && (mUtcTimestamp + mCalculatedTimezoneOffset) / 86400 != (mLatestSunTimestamp + mCalculatedTimezoneOffset) / 86400) { // update on reboot or midnight
if (!mLatestSunTimestamp) { // first call: calculate time zone from longitude to refresh at local midnight
mCalculatedTimezoneOffset = (int8_t)((mConfig.sunLon >= 0 ? mConfig.sunLon + 7.5 : mConfig.sunLon - 7.5) / 15) * 3600;
}
calculateSunriseSunset();
mLatestSunTimestamp = mUtcTimestamp;
}
if((++mMqttTicker >= mMqttInterval) && (mMqttInterval != 0xffff) && mMqttActive) {
mMqttTicker = 0;
mMqtt.isConnected(true); // really needed? See comment from HorstG-57 #176
@ -188,7 +196,7 @@ void app::loop(void) {
Inverter<> *iv = mSys->getInverterByPos(id);
if(NULL != iv) {
record_t<> *rec = iv->getRecordStruct(RealTimeRunData_Debug);
if(iv->isAvailable(mTimestamp, rec)) {
if(iv->isAvailable(mUtcTimestamp, rec)) {
DPRINTLN(DBG_INFO, "Inverter: " + String(id));
for(uint8_t i = 0; i < rec->length; i++) {
if(0.0f != iv->getValue(i, rec)) {
@ -208,7 +216,7 @@ void app::loop(void) {
if(++mSendTicker >= mConfig.sendInterval) {
mSendTicker = 0;
if(0 != mTimestamp) {
if(mUtcTimestamp > 946684800 && (!mConfig.sunDisNightCom || !mLatestSunTimestamp || (mUtcTimestamp >= mSunrise && mUtcTimestamp <= mSunset))) { // Timestamp is set and (inverter communication only during the day if the option is activated and sunrise/sunset is set)
if(mConfig.serialDebug)
DPRINTLN(DBG_DEBUG, F("Free heap: 0x") + String(ESP.getFreeHeap(), HEX));
@ -272,9 +280,8 @@ void app::loop(void) {
}
}
}
else if(mConfig.serialDebug) {
DPRINTLN(DBG_WARN, F("time not set, can't request inverter!"));
}
else if(mConfig.serialDebug)
DPRINTLN(DBG_WARN, F("Time not set or it is night time, therefore no communication to the inverter!"));
yield();
}
}
@ -295,19 +302,15 @@ bool app::buildPayload(uint8_t id) {
if(mPayload[id].maxPackId > MAX_PAYLOAD_ENTRIES)
mPayload[id].maxPackId = MAX_PAYLOAD_ENTRIES;
for(uint8_t i = 0; i < mPayload[id].maxPackId; i ++)
{
if(mPayload[id].len[i] > 0)
{
if(i == (mPayload[id].maxPackId-1))
{
crc = Ahoy::crc16(mPayload[id].data[i], mPayload[id].len[i] - 2, crc);
crcRcv = (mPayload[id].data[i][mPayload[id].len[i] - 2] << 8) | (mPayload[id].data[i][mPayload[id].len[i] - 1]);
for(uint8_t i = 0; i < mPayload[id].maxPackId; i ++) {
if(mPayload[id].len[i] > 0) {
if(i == (mPayload[id].maxPackId-1)) {
crc = ah::crc16(mPayload[id].data[i], mPayload[id].len[i] - 2, crc);
crcRcv = (mPayload[id].data[i][mPayload[id].len[i] - 2] << 8)
| (mPayload[id].data[i][mPayload[id].len[i] - 1]);
}
else
{
crc = Ahoy::crc16(mPayload[id].data[i], mPayload[id].len[i], crc);
}
crc = ah::crc16(mPayload[id].data[i], mPayload[id].len[i], crc);
}
yield();
}
@ -387,30 +390,31 @@ void app::processPayload(bool retransmit) {
DPRINTLN(DBG_DEBUG, F("procPyld: max: ") + String(mPayload[iv->id].maxPackId));
record_t<> *rec = iv->getRecordStruct(mPayload[iv->id].txCmd); // choose the parser
mPayload[iv->id].complete = true;
if(mPayload[iv->id].txId == (TX_REQ_INFO + ALL_FRAMES))
mStat.rxSuccess++;
uint8_t payload[128];
uint8_t offs = 0;
uint8_t payloadLen = 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]);
memcpy(&payload[payloadLen], mPayload[iv->id].data[i], (mPayload[iv->id].len[i]));
payloadLen += (mPayload[iv->id].len[i]);
yield();
}
offs-=2;
payloadLen-=2;
if(mConfig.serialDebug) {
DPRINT(DBG_INFO, F("Payload (") + String(offs) + "): ");
mSys->Radio.dumpBuf(NULL, payload, offs);
DPRINT(DBG_INFO, F("Payload (") + String(payloadLen) + "): ");
mSys->Radio.dumpBuf(NULL, payload, payloadLen);
}
if(NULL == rec) {
DPRINTLN(DBG_ERROR, F("record is NULL!"));
}
else
{
else if((rec->pyldLen == payloadLen) || (0 == rec->pyldLen)) {
if(mPayload[iv->id].txId == (TX_REQ_INFO + 0x80))
mStat.rxSuccess++;
rec->ts = mPayload[iv->id].ts;
for(uint8_t i = 0; i < rec->length; i++) {
iv->addValue(i, payload, rec);
@ -427,7 +431,7 @@ void app::processPayload(bool retransmit) {
for (uint8_t id = 0; id < mSys->getNumInverters(); id++) {
Inverter<> *iv = mSys->getInverterByPos(id);
if (NULL != iv) {
if (iv->isAvailable(mTimestamp, rec)) {
if (iv->isAvailable(mUtcTimestamp, rec)) {
for (uint8_t i = 0; i < rec->length; i++) {
snprintf(topic, 32 + MAX_NAME_LENGTH, "%s/ch%d/%s", iv->name, rec->assign[i].ch, fields[rec->assign[i].fieldId]);
snprintf(val, 10, "%.3f", iv->getValue(i, rec));
@ -443,11 +447,8 @@ void app::processPayload(bool retransmit) {
}
}
// Todo: make this section nice to read.
if(iv->isProducing(mUtcTimestamp, rec)){
snprintf(topic, 32 + MAX_NAME_LENGTH, "%s/available_text", iv->name);
if(iv->isProducing(mTimestamp, rec))
{
snprintf(val, 32, DEF_MQTT_IV_MESSAGE_INVERTER_AVAIL_AND_PRODUCED);
mMqtt.sendMsg(topic, val);
@ -493,6 +494,10 @@ void app::processPayload(bool retransmit) {
}
}
}
else {
DPRINTLN(DBG_ERROR, F("plausibility check failed, expected ") + String(rec->pyldLen) + F(" bytes"));
mStat.rxFail++;
}
iv->setQueuedCmdFinished();
@ -505,7 +510,7 @@ void app::processPayload(bool retransmit) {
if(mMqttActive) {
record_t<> *rec = iv->getRecordStruct(RealTimeRunData_Debug);
char topic[32 + MAX_NAME_LENGTH], val[32];
if (!iv->isAvailable(mTimestamp, rec) && !iv->isProducing(mTimestamp, rec)){
if (!iv->isAvailable(mUtcTimestamp, rec) && !iv->isProducing(mUtcTimestamp, rec)){
snprintf(topic, 32 + MAX_NAME_LENGTH, "%s/available_text", iv->name);
snprintf(val, 32, DEF_MQTT_IV_MESSAGE_NOT_AVAIL_AND_NOT_PRODUCED);
mMqtt.sendMsg(topic, val);
@ -647,6 +652,12 @@ bool app::getWifiApActive(void) {
}
//-----------------------------------------------------------------------------
void app::getAvailNetworks(JsonObject obj) {
mWifi->getAvailNetworks(obj);
}
//-----------------------------------------------------------------------------
void app::sendMqttDiscoveryConfig(void) {
DPRINTLN(DBG_VERBOSE, F("app::sendMqttDiscoveryConfig"));
@ -657,7 +668,7 @@ void app::sendMqttDiscoveryConfig(void) {
if(NULL != iv) {
record_t<> *rec = iv->getRecordStruct(RealTimeRunData_Debug);
// TODO: next line makes no sense if discovery config is send manually by button
//if(iv->isAvailable(mTimestamp, rec) && mMqttConfigSendState[id] != true) {
//if(iv->isAvailable(mUtcTimestamp, rec) && mMqttConfigSendState[id] != true) {
DynamicJsonDocument deviceDoc(128);
deviceDoc["name"] = iv->name;
deviceDoc["ids"] = String(iv->serial.u64, HEX);
@ -739,9 +750,9 @@ void app::resetSystem(void) {
mNtpRefreshInterval = NTP_REFRESH_INTERVAL; // [ms]
#ifdef AP_ONLY
mTimestamp = 1;
mUtcTimestamp = 1;
#else
mTimestamp = 0;
mUtcTimestamp = 0;
#endif
mHeapStatCnt = 0;
@ -782,15 +793,20 @@ void app::loadDefaultConfig(void) {
// nrf24
mConfig.sendInterval = SEND_INTERVAL;
mConfig.maxRetransPerPyld = DEF_MAX_RETRANS_PER_PYLD;
mConfig.pinCs = DEF_RF24_CS_PIN;
mConfig.pinCe = DEF_RF24_CE_PIN;
mConfig.pinIrq = DEF_RF24_IRQ_PIN;
mConfig.pinCs = DEF_CS_PIN;
mConfig.pinCe = DEF_CE_PIN;
mConfig.pinIrq = DEF_IRQ_PIN;
mConfig.amplifierPower = DEF_AMPLIFIERPOWER & 0x03;
// ntp
snprintf(mConfig.ntpAddr, NTP_ADDR_LEN, "%s", DEF_NTP_SERVER_NAME);
mConfig.ntpPort = DEF_NTP_PORT;
// Latitude + Longitude
mConfig.sunLat = 0.0;
mConfig.sunLon = 0.0;
mConfig.sunDisNightCom = false;
// mqtt
snprintf(mConfig.mqtt.broker, MQTT_ADDR_LEN, "%s", DEF_MQTT_BROKER);
mConfig.mqtt.port = DEF_MQTT_PORT;
@ -870,6 +886,9 @@ void app::saveValues(void) {
}
updateCrc();
// update sun
mLatestSunTimestamp = 0;
}
@ -907,5 +926,32 @@ void app::resetPayload(Inverter<>* iv) {
mPayload[iv->id].maxPackId = 0;
mPayload[iv->id].complete = false;
mPayload[iv->id].requested = false;
mPayload[iv->id].ts = mTimestamp;
mPayload[iv->id].ts = mUtcTimestamp;
}
void app::calculateSunriseSunset() {
// Source: https://en.wikipedia.org/wiki/Sunrise_equation#Complete_calculation_on_Earth
// Julian day since 1.1.2000 12:00 + correction 69.12s
double n_JulianDay = (mUtcTimestamp + mCalculatedTimezoneOffset) / 86400 - 10957.0 + 0.0008;
// Mean solar time
double J = n_JulianDay - mConfig.sunLon / 360;
// Solar mean anomaly
double M = fmod((357.5291 + 0.98560028 * J), 360);
// Equation of the center
double C = 1.9148 * SIN(M) + 0.02 * SIN(2 * M) + 0.0003 * SIN(3 * M);
// Ecliptic longitude
double lambda = fmod((M + C + 180 + 102.9372), 360);
// Solar transit
double Jtransit = 2451545.0 + J + 0.0053 * SIN(M) - 0.0069 * SIN(2 * lambda);
// Declination of the sun
double delta = ASIN(SIN(lambda) * SIN(23.44));
// Hour angle
double omega = ACOS(SIN(-0.83) - SIN(mConfig.sunLat) * SIN(delta) / COS(mConfig.sunLat) * COS(delta));
// Calculate sunrise and sunset
double Jrise = Jtransit - omega / 360;
double Jset = Jtransit + omega / 360;
// Julian sunrise/sunset to UTC unix timestamp (days incl. fraction to seconds + unix offset 1.1.2000 12:00)
mSunrise = (Jrise - 2451545.0) * 86400 + 946728000; // OPTIONAL: Add an offset of +-seconds to the end of the line
mSunset = (Jset - 2451545.0) * 86400 + 946728000; // OPTIONAL: Add an offset of +-seconds to the end of the line
}

51
tools/esp8266/app.h

@ -24,6 +24,12 @@
#include "ahoywifi.h"
#include "web.h"
// convert degrees and radians for sun calculation
#define SIN(x) (sin(radians(x)))
#define COS(x) (cos(radians(x)))
#define ASIN(x) (degrees(asin(x)))
#define ACOS(x) (degrees(acos(x)))
// hier läst sich das Verhalten der app in Bezug auf MQTT
// durch PER-Conpiler defines anpassen
//
@ -31,10 +37,7 @@
#define __MQTT_AFTER_RX__ // versendet die MQTT Daten sobald die WR daten Aufbereitet wurden ( gehört eigentlich ins Setup )
// #define __MQTT_NO_DISCOVERCONFIG__ // das versenden der MQTTDiscoveryConfig abschalten ( gehört eigentlich ins Setup )
typedef CircularBuffer<packet_t, PACKET_BUFFER_SIZE> BufferType;
typedef HmRadio<DEF_RF24_CE_PIN, DEF_RF24_CS_PIN, BufferType> RadioType;
typedef Inverter<float> InverterType;
typedef HmSystem<RadioType, BufferType, MAX_NUM_INVERTERS, InverterType> HmSystemType;
typedef HmSystem<MAX_NUM_INVERTERS> HmSystemType;
typedef struct {
@ -65,6 +68,7 @@ class app {
void saveValues(void);
void resetPayload(Inverter<>* iv);
bool getWifiApActive(void);
void getAvailNetworks(JsonObject obj);
uint8_t getIrqPin(void) {
return mConfig.pinIrq;
@ -95,12 +99,12 @@ class app {
return String(str);
}
String getTimeStr(void) {
char str[20];
if(0 == mTimestamp)
String getTimeStr(uint32_t offset = 0) {
char str[10];
if(0 == mUtcTimestamp)
sprintf(str, "n/a");
else
sprintf(str, "%02d:%02d:%02d ", hour(mTimestamp), minute(mTimestamp), second(mTimestamp));
sprintf(str, "%02d:%02d:%02d ", hour(mUtcTimestamp + offset), minute(mUtcTimestamp + offset), second(mUtcTimestamp + offset));
return String(str);
}
@ -109,15 +113,27 @@ class app {
}
inline uint32_t getTimestamp(void) {
return mTimestamp;
return mUtcTimestamp;
}
inline void setTimestamp(uint32_t newTime) {
void setTimestamp(uint32_t newTime) {
DPRINTLN(DBG_DEBUG, F("setTimestamp: ") + String(newTime));
if(0 == newTime)
mUpdateNtp = true;
else
mTimestamp = newTime;
{
mUtcTimestamp = newTime;
}
}
inline uint32_t getSunrise(void) {
return mSunrise;
}
inline uint32_t getSunset(void) {
return mSunset;
}
inline uint32_t getLatestSunTimestamp(void) {
return mLatestSunTimestamp;
}
void eraseSettings(bool all = false) {
@ -156,7 +172,6 @@ class app {
inline bool getSettingsValid(void) { return mSettingsValid; }
inline bool getRebootRequestState(void) { return mShowRebootRequest; }
HmSystemType *mSys;
bool mShouldReboot;
bool mFlagSendDiscoveryConfig;
@ -171,7 +186,6 @@ class app {
bool buildPayload(uint8_t id);
void processPayload(bool retransmit);
void processPayload(bool retransmit, uint8_t cmd);
const char* getFieldDeviceClass(uint8_t fieldId);
const char* getFieldStateClass(uint8_t fieldId);
@ -185,7 +199,7 @@ class app {
while(length > 0) {
len = (length < 32) ? length : 32;
mEep->read(start, buf, len);
crc = Ahoy::crc16(buf, len, crc);
crc = ah::crc16(buf, len, crc);
start += len;
length -= len;
}
@ -237,6 +251,7 @@ class app {
DPRINTLN(DBG_VERBOSE, F(" - frag: ") + String(frag));
}
void calculateSunriseSunset(void);
uint32_t mUptimeSecs;
uint32_t mPrevMillis;
@ -249,7 +264,7 @@ class app {
bool mSettingsValid;
eep *mEep;
uint32_t mTimestamp;
uint32_t mUtcTimestamp;
bool mUpdateNtp;
bool mShowRebootRequest;
@ -280,6 +295,12 @@ class app {
// serial
uint16_t mSerialTicker;
// sun
int32_t mCalculatedTimezoneOffset;
uint32_t mSunrise;
uint32_t mSunset;
uint32_t mLatestSunTimestamp;
};
#endif /*__APP_H__*/

9
tools/esp8266/config.h

@ -41,9 +41,9 @@
#define DEF_DEVICE_NAME "AHOY-DTU"
// default pinout (GPIO Number)
#define DEF_RF24_CS_PIN 15
#define DEF_RF24_CE_PIN 2
#define DEF_RF24_IRQ_PIN 0
#define DEF_CS_PIN 15
#define DEF_CE_PIN 2
#define DEF_IRQ_PIN 0
// default NRF24 power, possible values (0 - 3)
#define DEF_AMPLIFIERPOWER 1
@ -78,6 +78,9 @@
// threshold of minimum power on which the inverter is marked as inactive
#define INACT_PWR_THRESH 3
// Timezone
#define TIMEZONE 1
// default NTP server uri
#define DEF_NTP_SERVER_NAME "pool.ntp.org"

4
tools/esp8266/crc.cpp

@ -1,11 +1,11 @@
//-----------------------------------------------------------------------------
// 2022 Ahoy, https://www.mikrocontroller.net/topic/525778
// 2022 Ahoy, https://github.com/lumpapu/ahoy
// Creative Commons - http://creativecommons.org/licenses/by-nc-sa/3.0/de/
//-----------------------------------------------------------------------------
#include "crc.h"
namespace Ahoy {
namespace ah {
uint8_t crc8(uint8_t buf[], uint8_t len) {
uint8_t crc = CRC8_INIT;
for(uint8_t i = 0; i < len; i++) {

4
tools/esp8266/crc.h

@ -1,5 +1,5 @@
//-----------------------------------------------------------------------------
// 2022 Ahoy, https://www.mikrocontroller.net/topic/525778
// 2022 Ahoy, https://github.com/lumpapu/ahoy
// Creative Commons - http://creativecommons.org/licenses/by-nc-sa/3.0/de/
//-----------------------------------------------------------------------------
@ -14,7 +14,7 @@
#define CRC16_MODBUS_POLYNOM 0xA001
namespace Ahoy {
namespace ah {
uint8_t crc8(uint8_t buf[], uint8_t len);
uint16_t crc16(uint8_t buf[], uint8_t len, uint16_t start = 0xffff);
}

12
tools/esp8266/defines.h

@ -13,7 +13,7 @@
//-------------------------------------
#define VERSION_MAJOR 0
#define VERSION_MINOR 5
#define VERSION_PATCH 21
#define VERSION_PATCH 22
//-------------------------------------
@ -83,7 +83,8 @@ typedef enum {
#define INV_CH_CH_NAME_LEN MAX_NUM_INVERTERS * MAX_NAME_LENGTH * 4 // (4 channels)
#define INV_INTERVAL_LEN 2 // uint16_t
#define INV_MAX_RTRY_LEN 1 // uint8_t
#define INV_PWR_LIM_LEN MAX_NUM_INVERTERS * 2 // uint16_t
#define CFG_SUN_LEN 9 // 2x float(4+4) + bool(1)
#define NTP_ADDR_LEN 32 // DNS Name
@ -136,6 +137,11 @@ typedef struct {
// mqtt
mqttConfig_t mqtt;
// sun
float sunLat;
float sunLon;
bool sunDisNightCom; // disable night communication
// serial
uint16_t serialInterval;
bool serialShowIv;
@ -153,7 +159,7 @@ typedef struct {
#define CFG_MQTT_LEN MQTT_ADDR_LEN + 2 + MQTT_USER_LEN + MQTT_PWD_LEN +MQTT_TOPIC_LEN
#define CFG_SYS_LEN DEVNAME_LEN + SSID_LEN + PWD_LEN + 1
#define CFG_LEN 7 + NTP_ADDR_LEN + 2 + CFG_MQTT_LEN + 4 + DISCLAIMER
#define CFG_LEN 7 + NTP_ADDR_LEN + 2 + CFG_MQTT_LEN + CFG_SUN_LEN + 4 + DISCLAIMER
#define ADDR_START 0
#define ADDR_CFG_SYS ADDR_START

8
tools/esp8266/hmDefines.h

@ -1,5 +1,5 @@
//-----------------------------------------------------------------------------
// 2022 Ahoy, https://www.mikrocontroller.net/topic/525778
// 2022 Ahoy, https://github.com/lumpapu/ahoy
// Creative Commons - http://creativecommons.org/licenses/by-nc-sa/3.0/de/
//-----------------------------------------------------------------------------
@ -98,6 +98,7 @@ const byteAssign_t InfoAssignment[] = {
{ FLD_HW_ID, UNIT_NONE, CH0, 8, 2, 1 }
};
#define HMINFO_LIST_LEN (sizeof(InfoAssignment) / sizeof(byteAssign_t))
#define HMINFO_PAYLOAD_LEN 14
const byteAssign_t SystemConfigParaAssignment[] = {
{ FLD_ACT_ACTIVE_PWR_LIMIT, UNIT_PCT, CH0, 2, 2, 10 }/*,
@ -105,11 +106,13 @@ const byteAssign_t SystemConfigParaAssignment[] = {
{ FLD_ACT_PF, UNIT_NONE, CH0, 6, 2, 1000 }*/
};
#define HMSYSTEM_LIST_LEN (sizeof(SystemConfigParaAssignment) / sizeof(byteAssign_t))
#define HMSYSTEM_PAYLOAD_LEN 14
const byteAssign_t AlarmDataAssignment[] = {
{ FLD_LAST_ALARM_CODE, UNIT_NONE, CH0, 0, 2, 1 }
};
#define HMALARMDATA_LIST_LEN (sizeof(AlarmDataAssignment) / sizeof(byteAssign_t))
#define HMALARMDATA_PAYLOAD_LEN 0 // 0: means check is off
@ -138,6 +141,7 @@ const byteAssign_t hm1chAssignment[] = {
{ FLD_EFF, UNIT_PCT, CH0, CALC_EFF_CH0, 0, CMD_CALC }
};
#define HM1CH_LIST_LEN (sizeof(hm1chAssignment) / sizeof(byteAssign_t))
#define HM1CH_PAYLOAD_LEN 30
//-------------------------------------
@ -173,6 +177,7 @@ const byteAssign_t hm2chAssignment[] = {
};
#define HM2CH_LIST_LEN (sizeof(hm2chAssignment) / sizeof(byteAssign_t))
#define HM2CH_PAYLOAD_LEN 42
//-------------------------------------
@ -221,6 +226,7 @@ const byteAssign_t hm4chAssignment[] = {
{ FLD_EFF, UNIT_PCT, CH0, CALC_EFF_CH0, 0, CMD_CALC }
};
#define HM4CH_LIST_LEN (sizeof(hm4chAssignment) / sizeof(byteAssign_t))
#define HM4CH_PAYLOAD_LEN 62
#endif /*__HM_DEFINES_H__*/

14
tools/esp8266/hmInverter.h

@ -61,6 +61,7 @@ struct record_t {
uint8_t length; // length of the assignment list
T *record; // data pointer
uint32_t ts; // timestamp of last received payload
uint8_t pyldLen; // expected payload length for plausibility check
};
class CommandAbstract {
@ -242,12 +243,18 @@ class Inverter {
val <<= 8;
val |= buf[ptr];
} while(++ptr != end);
if(FLD_T == rec->assign[pos].fieldId) {
// temperature is a signed value!
rec->record[pos] = (REC_TYP)((int16_t)val) / (REC_TYP)(div);
}
else {
if ((REC_TYP)(div) > 1)
rec->record[pos] = (REC_TYP)(val) / (REC_TYP)(div);
else
rec->record[pos] = (REC_TYP)(val);
}
}
}
if(rec == &recordMeas) {
DPRINTLN(DBG_VERBOSE, "add real time");
@ -350,35 +357,42 @@ class Inverter {
if (INV_TYPE_1CH == type) {
rec->length = (uint8_t)(HM1CH_LIST_LEN);
rec->assign = (byteAssign_t *)hm1chAssignment;
rec->pyldLen = HM1CH_PAYLOAD_LEN;
channels = 1;
}
else if (INV_TYPE_2CH == type) {
rec->length = (uint8_t)(HM2CH_LIST_LEN);
rec->assign = (byteAssign_t *)hm2chAssignment;
rec->pyldLen = HM2CH_PAYLOAD_LEN;
channels = 2;
}
else if (INV_TYPE_4CH == type) {
rec->length = (uint8_t)(HM4CH_LIST_LEN);
rec->assign = (byteAssign_t *)hm4chAssignment;
rec->pyldLen = HM4CH_PAYLOAD_LEN;
channels = 4;
}
else {
rec->length = 0;
rec->assign = NULL;
rec->pyldLen = 0;
channels = 0;
}
break;
case InverterDevInform_All:
rec->length = (uint8_t)(HMINFO_LIST_LEN);
rec->assign = (byteAssign_t *)InfoAssignment;
rec->pyldLen = HMINFO_PAYLOAD_LEN;
break;
case SystemConfigPara:
rec->length = (uint8_t)(HMSYSTEM_LIST_LEN);
rec->assign = (byteAssign_t *)SystemConfigParaAssignment;
rec->pyldLen = HMSYSTEM_PAYLOAD_LEN;
break;
case AlarmData:
rec->length = (uint8_t)(HMALARMDATA_LIST_LEN);
rec->assign = (byteAssign_t *)AlarmDataAssignment;
rec->pyldLen = HMALARMDATA_PAYLOAD_LEN;
break;
default:
DPRINTLN(DBG_INFO, F("initAssignment: Parser not implemented"));

62
tools/esp8266/hmRadio.h

@ -1,5 +1,5 @@
//-----------------------------------------------------------------------------
// 2022 Ahoy, https://www.mikrocontroller.net/topic/525778
// 2022 Ahoy, https://github.com/lumpapu/ahoy
// Creative Commons - http://creativecommons.org/licenses/by-nc-sa/3.0/de/
//-----------------------------------------------------------------------------
@ -8,9 +8,20 @@
#include "dbg.h"
#include <RF24.h>
#include <RF24_config.h>
#include "crc.h"
#ifndef DISABLE_IRQ
#if defined(ESP8266) || defined(ESP32)
#define DISABLE_IRQ noInterrupts()
#define RESTORE_IRQ interrupts()
#else
#define DISABLE_IRQ \
uint8_t sreg = SREG; \
cli();
#define RESTORE_IRQ \
SREG = sreg;
#endif
#endif
//#define CHANNEL_HOP // switch between channels or use static channel to send
#define DEFAULT_RECV_CHANNEL 3
@ -54,7 +65,7 @@ const char* const rf24AmpPowerNames[] = {"MIN", "LOW", "HIGH", "MAX"};
//-----------------------------------------------------------------------------
// HM Radio class
//-----------------------------------------------------------------------------
template <uint8_t CE_PIN, uint8_t CS_PIN, class BUFFER>
template <class BUFFER, uint8_t IRQ_PIN = DEF_IRQ_PIN, uint8_t CE_PIN = DEF_CE_PIN, uint8_t CS_PIN = DEF_CS_PIN, uint8_t AMP_PWR = RF24_PA_LOW>
class HmRadio {
public:
HmRadio() : mNrf24(CE_PIN, CS_PIN, SPI_SPEED) {
@ -84,32 +95,31 @@ class HmRadio {
}
~HmRadio() {}
void setup(config_t *config, BUFFER *ctrl) {
void setup(BUFFER *ctrl, uint8_t ampPwr = RF24_PA_LOW, uint8_t irq = IRQ_PIN, uint8_t ce = CE_PIN, uint8_t cs = CS_PIN) {
DPRINTLN(DBG_VERBOSE, F("hmRadio.h:setup"));
pinMode(config->pinIrq, INPUT_PULLUP);
pinMode(irq, INPUT_PULLUP);
mBufCtrl = ctrl;
mSerialDebug = config->serialDebug;
uint32_t DTU_SN = 0x87654321;
uint32_t dtuSn = 0x87654321;
uint32_t chipID = 0; // will be filled with last 3 bytes of MAC
#ifdef ESP32
#ifdef ESP32
uint64_t MAC = ESP.getEfuseMac();
chipID = ((MAC >> 8) & 0xFF0000) | ((MAC >> 24) & 0xFF00) | ((MAC >> 40) & 0xFF);
#else
#else
chipID = ESP.getChipId();
#endif
#endif
if(chipID) {
DTU_SN = 0x80000000; // the first digit is an 8 for DTU production year 2022, the rest is filled with the ESP chipID in decimal
dtuSn = 0x80000000; // the first digit is an 8 for DTU production year 2022, the rest is filled with the ESP chipID in decimal
for(int i = 0; i < 7; i++) {
DTU_SN |= (chipID % 10) << (i * 4);
dtuSn |= (chipID % 10) << (i * 4);
chipID /= 10;
}
}
// change the byte order of the DTU serial number and append the required 0x01 at the end
DTU_RADIO_ID = ((uint64_t)(((DTU_SN >> 24) & 0xFF) | ((DTU_SN >> 8) & 0xFF00) | ((DTU_SN << 8) & 0xFF0000) | ((DTU_SN << 24) & 0xFF000000)) << 8) | 0x01;
DTU_RADIO_ID = ((uint64_t)(((dtuSn >> 24) & 0xFF) | ((dtuSn >> 8) & 0xFF00) | ((dtuSn << 8) & 0xFF0000) | ((dtuSn << 24) & 0xFF000000)) << 8) | 0x01;
mNrf24.begin(config->pinCe, config->pinCs);
mNrf24.begin(ce, cs);
mNrf24.setRetries(0, 0);
mNrf24.setChannel(DEFAULT_RECV_CHANNEL);
@ -125,8 +135,8 @@ class HmRadio {
mNrf24.maskIRQ(true, true, false);
DPRINT(DBG_INFO, F("RF24 Amp Pwr: RF24_PA_"));
DPRINTLN(DBG_INFO, String(rf24AmpPowerNames[config->amplifierPower]));
mNrf24.setPALevel(config->amplifierPower & 0x03);
DPRINTLN(DBG_INFO, String(rf24AmpPowerNames[ampPwr]));
mNrf24.setPALevel(ampPwr & 0x03);
mNrf24.startListening();
DPRINTLN(DBG_INFO, F("Radio Config:"));
@ -169,6 +179,10 @@ class HmRadio {
RESTORE_IRQ;
}
void enableDebug() {
mSerialDebug = true;
}
void handleIntr(void) {
//DPRINTLN(DBG_VERBOSE, F("hmRadio.h:handleIntr"));
mIrqRcvd = true;
@ -195,12 +209,12 @@ class HmRadio {
}
// crc control data
uint16_t crc = Ahoy::crc16(&mTxBuf[10], cnt);
uint16_t crc = ah::crc16(&mTxBuf[10], cnt);
mTxBuf[10 + cnt++] = (crc >> 8) & 0xff;
mTxBuf[10 + cnt++] = (crc ) & 0xff;
// crc over all
mTxBuf[10 + cnt] = Ahoy::crc8(mTxBuf, 10 + cnt);
mTxBuf[10 + cnt] = ah::crc8(mTxBuf, 10 + cnt);
sendPacket(invId, mTxBuf, 10 + cnt + 1, true);
}
@ -215,10 +229,10 @@ class HmRadio {
mTxBuf[18] = (alarmMesId >> 8) & 0xff;
mTxBuf[19] = (alarmMesId ) & 0xff;
}
uint16_t crc = Ahoy::crc16(&mTxBuf[10], 14);
uint16_t crc = ah::crc16(&mTxBuf[10], 14);
mTxBuf[24] = (crc >> 8) & 0xff;
mTxBuf[25] = (crc ) & 0xff;
mTxBuf[26] = Ahoy::crc8(mTxBuf, 26);
mTxBuf[26] = ah::crc8(mTxBuf, 26);
sendPacket(invId, mTxBuf, 27, true);
}
@ -231,7 +245,7 @@ class HmRadio {
CP_U32_BigEndian(&mTxBuf[5], (DTU_RADIO_ID >> 8));
mTxBuf[9] = pid;
if(calcCrc) {
mTxBuf[10] = Ahoy::crc8(mTxBuf, 10);
mTxBuf[10] = ah::crc8(mTxBuf, 10);
sendPacket(invId, mTxBuf, 11, false);
}
}
@ -245,7 +259,7 @@ class HmRadio {
buf[i-1] = (buf[i] << 1) | (buf[i+1] >> 7);
}
uint8_t crc = Ahoy::crc8(buf, *len-1);
uint8_t crc = ah::crc8(buf, *len-1);
bool valid = (crc == buf[*len-1]);
return valid;

22
tools/esp8266/hmSystem.h

@ -1,5 +1,5 @@
//-----------------------------------------------------------------------------
// 2022 Ahoy, https://www.mikrocontroller.net/topic/525778
// 2022 Ahoy, https://github.com/lumpapu/ahoy
// Creative Commons - http://creativecommons.org/licenses/by-nc-sa/3.0/de/
//-----------------------------------------------------------------------------
@ -7,12 +7,13 @@
#define __HM_SYSTEM_H__
#include "hmInverter.h"
#ifndef NO_RADIO
#include "hmRadio.h"
#endif
#include "CircularBuffer.h"
typedef CircularBuffer<packet_t, PACKET_BUFFER_SIZE> BufferType;
typedef HmRadio<BufferType> RadioType;
template <class RADIO, class BUFFER, uint8_t MAX_INVERTER=3, class INVERTERTYPE=Inverter<float>>
template <uint8_t MAX_INVERTER=3, class RADIO = RadioType, class BUFFER = BufferType, class INVERTERTYPE=Inverter<float>>
class HmSystem {
public:
typedef RADIO RadioType;
@ -28,9 +29,12 @@ class HmSystem {
// TODO: cleanup
}
void setup(config_t *config) {
DPRINTLN(DBG_VERBOSE, F("hmSystem.h:setup"));
Radio.setup(config, &BufCtrl);
void setup() {
Radio.setup(&BufCtrl);
}
void setup(uint8_t ampPwr, uint8_t irqPin, uint8_t cePin, uint8_t csPin) {
Radio.setup(&BufCtrl, ampPwr, irqPin, cePin, csPin);
}
INVERTERTYPE *addInverter(const char *name, uint64_t serial, uint16_t chMaxPwr[]) {
@ -96,6 +100,10 @@ class HmSystem {
return mNumInv;
}
void enableDebug() {
Radio.enableDebug();
}
private:
INVERTERTYPE mInverter[MAX_INVERTER];
uint8_t mNumInv;

12
tools/esp8266/html/api.js

@ -19,10 +19,12 @@ function getAjax(url, ptr, method="GET", json=null) {
}
function p() {
if(xhr.readyState == 4) {
if(null != xhr.responseText)
if(null != xhr.responseText) {
if(null != ptr)
ptr(JSON.parse(xhr.responseText));
}
}
}
}
function des(val) {
@ -66,6 +68,14 @@ function sel(name, opt, selId) {
return e;
}
function opt(val, html) {
o = document.createElement('option');
o.value = val;
o.innerHTML = html;
e.appendChild(o);
return o;
}
function div(cl) {
e = document.createElement('div');
e.classList.add(...cl);

48
tools/esp8266/html/index.html

@ -12,10 +12,9 @@
<SCRIPT>
function promptFunction() {
var Text = prompt("This project was started from https://www.mikrocontroller.net/topic/525778 this discussion.\n\n" +
"The Hoymiles protocol was decrypted through the voluntary efforts of many participants. ahoy, among others, was developed based on this work. The project is licensed under an Open Source " +
"License (https://github.com/lumapu/ahoy/blob/main/LICENSE) GNU General Public License v3.0.\n\n" +
"The Hoymiles protocol was decrypted through the voluntary efforts of many participants. ahoy, among others, was developed based on this work.\n" +
"The software was developed to the best of our knowledge and belief. Nevertheless, no liability can be accepted for a malfunction or guarantee loss of the inverter.\n\n" +
"Ahoy is freely available. If you paid money for the software, you probably got ripped off.\n\nPlease type in 'YeS', you are accept our Disclaim.", "");
"Ahoy is freely available. If you paid money for the software, you probably got ripped off.\n\nPlease type in 'YeS', you are accept our Disclaim. You should then save your config.", "");
if (Text != "YeS")
promptFunction();
else
@ -28,10 +27,15 @@
<a href="/live">Visualization</a><br/>
<br/>
<a href="/setup">Setup</a><br/>
<br/>
<a href="/serial">Webserial & Commands</a><br/>
</p>
<p><span class="des">Uptime: </span><span id="uptime"></span></p>
<p><span class="des">ESP-Time: </span><span id="date"></span></p>
<div id="sun">
<span class="des">Sunrise: </span><span id="sunrise"></span><br>
<span class="des">Sunset: </span><span id="sunset"></span>
</div>
<p><span class="des">WiFi RSSI: </span><span id="wifi_rssi"></span> dBm</p>
<p>
<span class="des">Statistics: </span>
@ -42,15 +46,15 @@
<p>Every <span id="refresh"></span> seconds the values are updated</p>
<div id="note">
This project was started from <a href="https://www.mikrocontroller.net/topic/525778" target="_blank">this discussion. (Mikrocontroller.net)</a><br/>
New updates can be found on Github: <a href="https://github.com/lumapu/ahoy" target="_blank">https://github.com/lumapu/ahoy</a><br/>
<br/>
Please report issues using the feature provided by <a href="https://github.com/lumapu/ahoy/issues">Github</a><br/>
<br/>
Discuss with us on <a href="https://discord.gg/WzhxEY62mB">Discord</a>
<br/>
Please report issues in <a href="https://github.com/lumapu/ahoy/issues">Github</a><br/>
Discuss with us on <a href="https://discord.gg/WzhxEY62mB">Discord</a><br/>
Support this project: <a href="https://paypal.me/lupusch">Donate</a><br/>
<p class="lic"><a href="https://creativecommons.org/licenses/by-nc-sa/3.0/de">Creative Commons - https://creativecommons.org/licenses/by-nc-sa/3.0/de/</a><br/>
Check the licenses which are published on <a href="https://github.com/lumapu/ahoy">https://github.com/lumapu/ahoy</a> as well</p>
Check the licenses which are published on <a href="https://github.com/lumapu/ahoy">https://github.com/lumapu/ahoy</a> as well</p><br/>
This project was started from <a href="https://www.mikrocontroller.net/topic/525778" target="_blank">this discussion. (Mikrocontroller.net)</a>
</div>
</div>
<div id="footer">
@ -75,16 +79,15 @@
function setTime() {
var date = new Date();
var offset = date.getTimezoneOffset() * -60;
var obj = new Object();
obj.cmd = "set_time";
obj.ts = parseInt(offset + (date.getTime() / 1000));
obj.ts = parseInt(date.getTime() / 1000);
getAjax("/api/setup", apiCb, "POST", JSON.stringify(obj));
}
function parseSys(obj) {
// Disclaimer
if(obj["disclaimer"] == false) sessionStorage.setItem("gDisclaimer", promptFunction());
//if(obj["disclaimer"] == false) sessionStorage.setItem("gDisclaimer", promptFunction());
document.getElementById("version").innerHTML = "Git SHA: " + obj["build"] + " :: " + obj["version"];
document.getElementById("wifi_rssi").innerHTML = obj["wifi_rssi"];
@ -95,20 +98,33 @@
var hrs = parseInt(up / 3600) % 24;
var min = parseInt(up / 60) % 60;
var sec = up % 60;
var sunrise = new Date(obj["ts_sunrise"] * 1000);
var sunset = new Date(obj["ts_sunset"] * 1000);
document.getElementById("uptime").innerHTML = days + " Days, "
+ ("0"+hrs).substr(-2) + ":"
+ ("0"+min).substr(-2) + ":"
+ ("0"+sec).substr(-2);
var dSpan = document.getElementById("date")
var dSpan = document.getElementById("date");
if(0 != obj["ts_now"])
dSpan.innerHTML = date.toLocaleString('de-DE', {timeZone: 'UTC'});
dSpan.innerHTML = date.toLocaleString('de-DE');
else {
dSpan.innerHTML = "";
var e = inp("set", "sync from browser", 0, ["btn"], "set", "button");
dSpan.appendChild(span("NTP timeserver unreachable. "));
dSpan.appendChild(e);
dSpan.appendChild(span("", ["span"], "apiResult"));
e.addEventListener("click", setTime);
}
if(!obj["ts_sun_upd"]) {
var e = document.getElementById("sun");
if(null != e)
e.parentNode.removeChild(e);
}
else {
document.getElementById("sunrise").innerHTML = sunrise.toLocaleString('de-DE');
document.getElementById("sunset").innerHTML = sunset.toLocaleString('de-DE');
}
}
function parseStat(obj) {
@ -116,7 +132,7 @@
+ "\nRX fail: " + obj["rx_fail"]
+ "\nRX no answer: " + obj["rx_fail_answer"]
+ "\nFrames received: " + obj["frame_cnt"]
+ "\nTX Cnt: " + obj["tx_cnt"];
+ "\nTX cnt: " + obj["tx_cnt"];
}
function parseIv(obj) {
@ -133,7 +149,7 @@
if(false == i["is_avail"]) {
if(i["ts_last_success"] > 0) {
var date = new Date(i["ts_last_success"] * 1000);
html += "-> last successful transmission: " + date.toLocaleString('de-DE', {timeZone: 'UTC'});
html += "-> last successful transmission: " + date.toLocaleString('de-DE');
}
}

12
tools/esp8266/html/serial.html

@ -85,14 +85,19 @@
if(null == root) return;
root = root.inverter;
for(var i = 0; i < root.inverter.length; i++)
{
for(var i = 0; i < root.inverter.length; i++) {
inv = root.inverter[i];
var opt = document.createElement('option');
opt.value = inv.id;
opt.innerHTML = inv.name;
select.appendChild(opt);
}
// set time offset for serial console
var obj = new Object();
obj.cmd = "serial_utc_offset";
obj.ts = new Date().getTimezoneOffset() * -60;
getAjax("/api/setup", null, "POST", JSON.stringify(obj));
}
document.getElementById("clear").addEventListener("click", function() {
@ -133,8 +138,7 @@
e.innerHTML = "Error: " + obj["error"];
}
function get_selected_iv()
{
function get_selected_iv() {
var e = document.getElementById("InvID");
return parseInt(e.value);
}

89
tools/esp8266/html/setup.html

@ -36,6 +36,10 @@
<fieldset>
<legend class="des">WiFi</legend>
<p>Enter the credentials to your prefered WiFi station. After rebooting the device tries to connect with this information.</p>
<label for="networks">Avail Networks</label>
<select name="networks" id="networks" onChange="selNet()">
<option value="-1">scanning ...</option>
</select>
<label for="ssid">SSID</label>
<input type="text" name="ssid" class="text"/>
<label for="pwd">Password</label>
@ -72,6 +76,20 @@
</fieldset>
</div>
<button type="button" class="s_collapsible">Sunrise & Sunset</button>
<div class="s_content">
<fieldset>
<legend class="des">Sunrise & Sunset</legend>
<label for="sunLat">Latitude (decimal)</label>
<input type="text" class="text" name="sunLat"/>
<label for="sunLon">Longitude (decimal)</label>
<input type="text" class="text" name="sunLon"/>
<br>
<label for="sunDisNightCom">disable night communication</label>
<input type="checkbox" class="cb" name="sunDisNightCom"/><br/>
</fieldset>
</div>
<button type="button" class="s_collapsible">MQTT</button>
<div class="s_content">
<fieldset>
@ -156,10 +174,9 @@
function setTime() {
var date = new Date();
var offset = date.getTimezoneOffset() * -60;
var obj = new Object();
obj.cmd = "set_time";
obj.ts = parseInt(offset + (date.getTime() / 1000));
obj.ts = parseInt(date.getTime() / 1000);
getAjax("/api/setup", apiCbNtp, "POST", JSON.stringify(obj));
}
@ -219,7 +236,7 @@
}
});
for(var i of [["Name", "name", "Name*", 32]]) { // so richtig?
for(var i of [["Name", "name", "Name*", 32]]) {
iv.appendChild(lbl(id + i[0], i[2]));
iv.appendChild(inp(id + i[0], obj[i[1]], i[3]));
}
@ -271,11 +288,18 @@
document.getElementsByName(i[0])[0].value = obj[i[1]];
}
function parsePinout(obj) {
function parseSun(obj) {
document.getElementsByName("sunLat")[0].value = obj["lat"];
document.getElementsByName("sunLon")[0].value = obj["lon"];
document.getElementsByName("sunDisNightCom")[0].checked = obj["disnightcom"];
}
function parsePinout(obj, type) {
var e = document.getElementById("pinout");
pins = [['cs', 'pinCs'], ['ce', 'pinCe'], ['irq', 'pinIrq']];
for(p of pins) {
e.appendChild(lbl(p[1], p[0].toUpperCase()));
if("ESP8266" == type) {
e.appendChild(sel(p[1], [
[0, "D3 (GPIO0)"],
[1, "TX (GPIO1)"],
@ -296,6 +320,37 @@
[16, "D0 (GPIO16 - no IRQ!)"]
], obj[p[0]]));
}
else {
e.appendChild(sel(p[1], [
[0, "GPIO0"],
[1, "TX (GPIO1)"],
[2, "GPIO2 (LED)"],
[3, "RX (GPIO3)"],
[4, "GPIO4"],
[5, "GPIO5"],
[12, "GPIO12"],
[13, "GPIO13"],
[14, "GPIO14"],
[15, "GPIO15"],
[16, "GPIO16"],
[17, "GPIO17"],
[18, "GPIO18"],
[19, "GPIO19"],
[21, "GPIO21"],
[22, "GPIO22"],
[23, "GPIO23"],
[25, "GPIO25"],
[26, "GPIO26"],
[27, "GPIO27"],
[32, "GPIO32"],
[33, "GPIO33"],
[34, "GPIO34"],
[35, "GPIO35"],
[36, "VP (GPIO36)"],
[39, "VN (GPIO39)"]
], obj[p[0]]));
}
}
}
function parseRadio(obj) {
@ -321,16 +376,40 @@
parseIv(root["inverter"]);
parseMqtt(root["mqtt"]);
parseNtp(root["ntp"]);
parsePinout(root["pinout"]);
parseSun(root["sun"]);
parsePinout(root["pinout"], root["system"]["esp_type"]);
parseRadio(root["radio"]);
parseSerial(root["serial"]);
}
getAjax('/api/setup/networks', listNetworks);
window.setInterval("getAjax('/api/setup/networks', listNetworks)", 7000);
}
function listNetworks(root) {
if(root["networks"].length > 0) {
var s = document.getElementById("networks");
var i, l = s.options.length - 1;
for(i = l; i >= 0; i--) {
s.remove(i);
}
for(i = 0; i < root["networks"].length; i++) {
s.appendChild(opt(root["networks"][i]["ssid"], root["networks"][i]["ssid"] + " (" + root["networks"][i]["rssi"] + " dBm)"));
}
}
}
function selNet() {
var s = document.getElementById("networks");
var e = document.getElementsByName("ssid")[0];
e.value = s.value;
}
hiddenInput = document.getElementById("disclaimer")
hiddenInput.value = sessionStorage.getItem("gDisclaimer");
getAjax("/api/setup", parse);
</script>
</body>
</html>

10
tools/esp8266/html/visualization.html

@ -76,8 +76,16 @@
}
var ts = div(["ts"]);
var ageInfo = "Last received data requested at: ";
if(iv["ts_last_success"] > 0) {
var date = new Date(iv["ts_last_success"] * 1000);
ts.innerHTML = "Last received data requested at: " + date.toLocaleString('de-DE', {timeZone: 'UTC'});
ageInfo += date.toLocaleString('de-DE');
}
else
ageInfo += "nothing received";
ts.innerHTML = ageInfo;
main.appendChild(ts);
ivHtml.push(main);
}

14
tools/esp8266/web.cpp

@ -283,6 +283,18 @@ void web::showSave(AsyncWebServerRequest *request) {
mConfig->ntpPort = request->arg("ntpPort").toInt() & 0xffff;
}
// sun
if(request->arg("sunLat") == "" || (request->arg("sunLon") == "")) {
mConfig->sunLat = 0.0;
mConfig->sunLon = 0.0;
mConfig->sunDisNightCom = false;
} else {
mConfig->sunLat = request->arg("sunLat").toFloat();
mConfig->sunLon = request->arg("sunLon").toFloat();
mConfig->sunDisNightCom = (request->arg("sunDisNightCom") == "on");
}
// mqtt
if(request->arg("mqttAddr") != "") {
String addr = request->arg("mqttAddr");
@ -462,7 +474,7 @@ void web::serialCb(String msg) {
msg.replace("\r\n", "<rn>");
if(mSerialAddTime) {
if((9 + mSerialBufFill) <= WEB_SERIAL_BUF_SIZE) {
strncpy(&mSerialBuf[mSerialBufFill], mMain->getTimeStr().c_str(), 9);
strncpy(&mSerialBuf[mSerialBufFill], mMain->getTimeStr(mApi->getTimezoneOffset()).c_str(), 9);
mSerialBufFill += 9;
}
else {

27
tools/esp8266/webApi.cpp

@ -18,6 +18,8 @@ webApi::webApi(AsyncWebServer *srv, app *app, sysConfig_t *sysCfg, config_t *con
mConfig = config;
mStat = stat;
mVersion = version;
mTimezoneOffset = 0;
}
@ -48,6 +50,7 @@ void webApi::onApi(AsyncWebServerRequest *request) {
else if(path == "inverter/list") getInverterList(root);
else if(path == "index") getIndex(root);
else if(path == "setup") getSetup(root);
else if(path == "setup/networks") getNetworks(root);
else if(path == "live") getLive(root);
else if(path == "record/info") getRecord(root, iv->getRecordStruct(InverterDevInform_All));
else if(path == "record/alarm") getRecord(root, iv->getRecordStruct(AlarmData));
@ -142,8 +145,16 @@ void webApi::getSystem(JsonObject obj) {
obj[F("build")] = String(AUTO_GIT_HASH);
obj[F("ts_uptime")] = mApp->getUptime();
obj[F("ts_now")] = mApp->getTimestamp();
obj[F("ts_sunrise")] = mApp->getSunrise();
obj[F("ts_sunset")] = mApp->getSunset();
obj[F("ts_sun_upd")] = mApp->getLatestSunTimestamp();
obj[F("wifi_rssi")] = WiFi.RSSI();
obj[F("disclaimer")] = mConfig->disclaimer;
#if defined(ESP32)
obj[F("esp_type")] = F("ESP32");
#else
obj[F("esp_type")] = F("ESP8266");
#endif
}
@ -200,6 +211,13 @@ void webApi::getNtp(JsonObject obj) {
obj[F("port")] = String(mConfig->ntpPort);
}
//-----------------------------------------------------------------------------
void webApi::getSun(JsonObject obj) {
obj[F("lat")] = mConfig->sunLat ? String(mConfig->sunLat, 5) : "";
obj[F("lon")] = mConfig->sunLat ? String(mConfig->sunLon, 5) : "";
obj[F("disnightcom")] = mConfig->sunDisNightCom;
}
//-----------------------------------------------------------------------------
void webApi::getPinout(JsonObject obj) {
@ -267,12 +285,19 @@ void webApi::getSetup(JsonObject obj) {
getInverterList(obj.createNestedObject(F("inverter")));
getMqtt(obj.createNestedObject(F("mqtt")));
getNtp(obj.createNestedObject(F("ntp")));
getSun(obj.createNestedObject(F("sun")));
getPinout(obj.createNestedObject(F("pinout")));
getRadio(obj.createNestedObject(F("radio")));
getSerial(obj.createNestedObject(F("serial")));
}
//-----------------------------------------------------------------------------
void webApi::getNetworks(JsonObject obj) {
mApp->getAvailNetworks(obj);
}
//-----------------------------------------------------------------------------
void webApi::getLive(JsonObject obj) {
getSystem(obj.createNestedObject(F("system")));
@ -414,6 +439,8 @@ bool webApi::setSetup(DynamicJsonDocument jsonIn, JsonObject jsonOut) {
mApp->setTimestamp(jsonIn[F("ts")]);
else if(F("sync_ntp") == jsonIn[F("cmd")])
mApp->setTimestamp(0); // 0: update ntp flag
else if(F("serial_utc_offset") == jsonIn[F("cmd")])
mTimezoneOffset = jsonIn[F("ts")];
else if(F("discovery_cfg") == jsonIn[F("cmd")])
mApp->mFlagSendDiscoveryConfig = true; // for homeassistant
else {

9
tools/esp8266/webApi.h

@ -21,6 +21,10 @@ class webApi {
void setup(void);
void loop(void);
uint32_t getTimezoneOffset() {
return mTimezoneOffset;
}
private:
void onApi(AsyncWebServerRequest *request);
void onApiPost(AsyncWebServerRequest *request);
@ -33,13 +37,14 @@ class webApi {
void getInverterList(JsonObject obj);
void getMqtt(JsonObject obj);
void getNtp(JsonObject obj);
void getSun(JsonObject obj);
void getPinout(JsonObject obj);
void getRadio(JsonObject obj);
void getSerial(JsonObject obj);
void getIndex(JsonObject obj);
void getSetup(JsonObject obj);
void getNetworks(JsonObject obj);
void getLive(JsonObject obj);
void getRecord(JsonObject obj, record_t<> *rec);
@ -59,6 +64,8 @@ class webApi {
sysConfig_t *mSysCfg;
statistics_t *mStat;
char *mVersion;
uint32_t mTimezoneOffset;
};
#endif /*__WEB_API_H__*/

Loading…
Cancel
Save