Compare commits

..

24 Commits

Author SHA1 Message Date
Gunnar Skjold
32fa2f5632 Merge branch 'master' into dev-v2.1.8 2022-10-06 17:30:49 +02:00
Gunnar Skjold
026cd25c8c Setting modem sleep to max 2022-10-06 17:30:37 +02:00
Gunnar Skjold
12be475b02 Reverted HA changes for 2.1.8 release 2022-10-06 17:29:01 +02:00
Gunnar Skjold
06ec97b42a Make sure we dont use more than 5 peaks if defined higher 2022-10-06 17:20:13 +02:00
Gunnar Skjold
b420a0e6f4 Limit peak count if over 5 2022-10-06 17:12:12 +02:00
Gunnar Skjold
fe7be81f1e Rudimentary detection for L&G data. Probably change this in the future 2022-10-01 12:11:46 +02:00
Gunnar Skjold
7674fc2ad0 Merge branch 'lng-parser' 2022-10-01 11:56:08 +02:00
Gunnar Skjold
d50181c347 Upgrade with custom URL 2022-10-01 11:43:42 +02:00
Gunnar Skjold
8ca771fa5a Changed "BUS" to "M-BUS" 2022-10-01 09:25:56 +02:00
Gunnar Skjold
7d557b2679 Only show new version when actually new in firmware page. Also be more specific on USB power in upgrade warning 2022-10-01 09:24:07 +02:00
Gunnar Skjold
2f0c912388 Exclude decimals in BUILD_EPOCH flag 2022-10-01 09:22:09 +02:00
Gunnar Skjold
57e6d0fbe3 Modifications to make price sensors work in HA 2022-09-30 19:18:23 +02:00
Gunnar Skjold
e7c25fafda Prices and realtime HA sensors 2022-09-28 20:13:03 +02:00
Gunnar Skjold
2a4772fe25 Fixed memory leak when reconnecting to MQTT/SSL 2022-09-27 19:59:37 +02:00
Gunnar Skjold
f1f7408208 Added price zones 2022-09-25 11:46:04 +02:00
Gunnar Skjold
feb8e5007b Fixed MQTT buffer size 2022-09-25 10:50:34 +02:00
Gunnar Skjold
c4af1ee74f Fixed saving tariff thresholds 2022-09-25 10:50:09 +02:00
Gunnar Skjold
992e1b6121 Net value for active power (L&G) 2022-09-12 08:03:32 +02:00
Gunnar Skjold
9cc7529934 Fixed L&G scaling 2022-08-31 08:19:09 +02:00
Gunnar Skjold
ef8715be6d Merge branch 'master' into lng-parser 2022-08-31 08:04:24 +02:00
Gunnar Skjold
44bcd386d1 aggregate obis 1.8.x and 2.8.x for L&G 2022-08-23 09:04:45 +02:00
Gunnar Skjold
01547f9a52 Adjustments to make L&G parser work 2022-08-19 13:26:29 +02:00
Gunnar Skjold
940d38af5c Merge branch 'master' into lng-parser 2022-08-17 17:33:24 +02:00
Gunnar Skjold
a055465ce0 Untestet L&G data parser 2022-08-16 08:22:43 +02:00
24 changed files with 387 additions and 105 deletions

View File

@@ -43,3 +43,37 @@ FF // Last byte of OBIS in previous block
0600000000 // Accumulated export 0600000000 // Accumulated export
8BA4 8BA4
7E 7E
7E A1 23 CE FF 03 13 21 55 E6 E7 00
0F 00 00 08 E2
0C 07 E5 07 13 01 0C 1A 0A FF 80 00 00
02 0B // 11
01 0B // 11
02 04 12 00 28 09 06 00 08 19 09 00 FF 0F 02 12 00 00
02 04 12 00 28 09 06 00 08 19 09 00 FF 0F 01 12 00 00
02 04 12 00 01 09 06 00 00 60 01 00 FF 0F 02 12 00 00
02 04 12 00 03 09 06 01 00 01 07 00 FF 0F 02 12 00 00
02 04 12 00 03 09 06 01 00 02 07 00 FF 0F 02 12 00 00
02 04 12 00 03 09 06 01 01 01 08 00 FF 0F 02 12 00 00
02 04 12 00 03 09 06 01 01 02 08 00 FF 0F 02 12 00 00
02 04 12 00 03 09 06 01 01 05 08 00 FF 0F 02 12 00 00
02 04 12 00 03 09 06 01 01 06 08 00 FF 0F 02 12 00 00
02 04 12 00 03 09 06 01 01 07 08 00 FF 0F 02 12 00 00
02 04 12 00 03 09 06 01 01 08 08 00 FF 0F 02 12 00 00
09 06 00 08 19 09 00 FF
09 08 34 33 30 39 34 33 35 31
06 00 00 00 0B
06 00 00 00 00
06 00 00 00 10
06 00 00 00 04
06 00 00 00 00
06 00 00 00 08
06 00 00 00 00
06 00 00 00 01
7C 8B
7E

View File

@@ -19,6 +19,6 @@ hf = """
#define VERSION "{}" #define VERSION "{}"
#endif #endif
#define BUILD_EPOCH {} #define BUILD_EPOCH {}
""".format(version, time()) """.format(version, round(time()))
with open(FILENAME_VERSION_H, 'w+') as f: with open(FILENAME_VERSION_H, 'w+') as f:
f.write(hf) f.write(hf)

View File

@@ -517,6 +517,7 @@ bool AmsConfiguration::getEnergyAccountingConfig(EnergyAccountingConfig& config)
} }
bool AmsConfiguration::setEnergyAccountingConfig(EnergyAccountingConfig& config) { bool AmsConfiguration::setEnergyAccountingConfig(EnergyAccountingConfig& config) {
if(config.hours > 5) config.hours = 5;
EnergyAccountingConfig existing; EnergyAccountingConfig existing;
if(getEnergyAccountingConfig(existing)) { if(getEnergyAccountingConfig(existing)) {
for(int i = 0; i < 9; i++) { for(int i = 0; i < 9; i++) {
@@ -525,6 +526,7 @@ bool AmsConfiguration::setEnergyAccountingConfig(EnergyAccountingConfig& config)
} }
} }
config.thresholds[9] = 255; config.thresholds[9] = 255;
energyAccountingChanged |= config.hours != existing.hours;
} else { } else {
energyAccountingChanged = true; energyAccountingChanged = true;
} }

View File

@@ -12,6 +12,7 @@ enum AmsType {
AmsTypeIskra = 0x08, AmsTypeIskra = 0x08,
AmsTypeLandis = 0x09, AmsTypeLandis = 0x09,
AmsTypeSagemcom = 0x0A, AmsTypeSagemcom = 0x0A,
AmsTypeLng = 0x0B,
AmsTypeCustom = 0x88, AmsTypeCustom = 0x88,
AmsTypeUnknown = 0xFF AmsTypeUnknown = 0xFF
}; };

View File

@@ -65,10 +65,11 @@ ADC_MODE(ADC_VCC);
#include "RemoteDebug.h" #include "RemoteDebug.h"
#define BUF_SIZE_COMMON (2048) #define BUF_SIZE_COMMON (2048)
#define BUF_SIZE_HAN (1024) #define BUF_SIZE_HAN (1280)
#include "IEC6205621.h" #include "IEC6205621.h"
#include "IEC6205675.h" #include "IEC6205675.h"
#include "LNG.h"
#include "ams/DataParsers.h" #include "ams/DataParsers.h"
@@ -512,6 +513,7 @@ void loop() {
if (mqttEnabled || config.isMqttChanged()) { if (mqttEnabled || config.isMqttChanged()) {
if(mqtt == NULL || !mqtt->connected() || config.isMqttChanged()) { if(mqtt == NULL || !mqtt->connected() || config.isMqttChanged()) {
MQTT_connect(); MQTT_connect();
config.ackMqttChange();
} }
} else if(mqtt != NULL && mqtt->connected()) { } else if(mqtt != NULL && mqtt->connected()) {
mqttClient->stop(); mqttClient->stop();
@@ -593,7 +595,7 @@ void loop() {
} }
if(now - lastSysupdate > 10000) { if(now - lastSysupdate > 10000) {
if(mqtt != NULL && mqttHandler != NULL && WiFi.getMode() != WIFI_AP && WiFi.status() == WL_CONNECTED && mqtt->connected() && !topic.isEmpty()) { if(mqtt != NULL && mqttHandler != NULL && WiFi.getMode() != WIFI_AP && WiFi.status() == WL_CONNECTED && mqtt->connected() && !topic.isEmpty()) {
mqttHandler->publishSystem(&hw); mqttHandler->publishSystem(&hw, eapi, &ea);
} }
lastSysupdate = now; lastSysupdate = now;
} }
@@ -848,20 +850,26 @@ bool readHanPort() {
} }
AmsData data; AmsData data;
char* payload = ((char *) (hanBuffer)) + pos;
if(ctx.type == DATA_TAG_DLMS) { if(ctx.type == DATA_TAG_DLMS) {
// If MQTT bytestream payload is selected (mqttHandler == NULL), send the payload to MQTT // If MQTT bytestream payload is selected (mqttHandler == NULL), send the payload to MQTT
if(mqttEnabled && mqtt != NULL && mqttHandler == NULL) { if(mqttEnabled && mqtt != NULL && mqttHandler == NULL) {
mqtt->publish(topic.c_str(), toHex(hanBuffer+pos, ctx.length)); mqtt->publish(topic.c_str(), toHex((byte*) payload, ctx.length));
mqtt->loop(); mqtt->loop();
} }
debugV("Using application data:"); debugV("Using application data:");
if(Debug.isActive(RemoteDebug::VERBOSE)) debugPrint(hanBuffer+pos, 0, ctx.length); if(Debug.isActive(RemoteDebug::VERBOSE)) debugPrint((byte*) payload, 0, ctx.length);
// TODO: Split IEC6205675 into DataParserKaifa and DataParserObis. This way we can add other means of parsing, for those other proprietary formats // Rudimentary detector for L&G proprietary format
data = IEC6205675(((char *) (hanBuffer)) + pos, meterState.getMeterType(), &meterConfig, ctx); if(payload[0] == CosemTypeStructure && payload[2] == CosemTypeArray && payload[1] == payload[3]) {
data = LNG(payload, meterState.getMeterType(), &meterConfig, ctx, &Debug);
} else {
// TODO: Split IEC6205675 into DataParserKaifa and DataParserObis. This way we can add other means of parsing, for those other proprietary formats
data = IEC6205675(payload, meterState.getMeterType(), &meterConfig, ctx);
}
} else if(ctx.type == DATA_TAG_DSMR) { } else if(ctx.type == DATA_TAG_DSMR) {
data = IEC6205621(((char *) (hanBuffer)) + pos); data = IEC6205621(payload);
} }
len = 0; len = 0;
@@ -1043,7 +1051,7 @@ void WiFi_connect() {
} }
#endif #endif
WiFi.mode(WIFI_STA); WiFi.mode(WIFI_STA);
WiFi.setSleep(WIFI_PS_MIN_MODEM); WiFi.setSleep(WIFI_PS_MAX_MODEM);
#if defined(ESP32) #if defined(ESP32)
if(wifi.power >= 195) if(wifi.power >= 195)
WiFi.setTxPower(WIFI_POWER_19_5dBm); WiFi.setTxPower(WIFI_POWER_19_5dBm);
@@ -1243,7 +1251,6 @@ void MQTT_connect() {
if(Debug.isActive(RemoteDebug::WARNING)) debugW("No MQTT config"); if(Debug.isActive(RemoteDebug::WARNING)) debugW("No MQTT config");
mqttEnabled = false; mqttEnabled = false;
ws.setMqttEnabled(false); ws.setMqttEnabled(false);
config.ackMqttChange();
return; return;
} }
if(mqtt != NULL) { if(mqtt != NULL) {
@@ -1258,20 +1265,19 @@ void MQTT_connect() {
} }
mqtt->disconnect(); mqtt->disconnect();
if(config.isMqttChanged()) {
if(mqttSecureClient != NULL) {
mqttSecureClient->stop();
delete mqttSecureClient;
mqttSecureClient = NULL;
} else {
mqttClient->stop();
}
mqttClient = NULL;
}
yield(); yield();
} else { } else {
uint16_t size = 256; mqtt = new MQTTClient(1024);
switch(mqttConfig.payloadFormat) {
case 0: // JSON
case 4: // Home Assistant
size = 768;
break;
case 255: // Raw frame
size = 1024;
break;
}
mqtt = new MQTTClient(size);
ws.setMqtt(mqtt); ws.setMqtt(mqtt);
} }
@@ -1306,54 +1312,54 @@ void MQTT_connect() {
debugI("MQTT SSL is configured (%dkb free heap)", ESP.getFreeHeap()); debugI("MQTT SSL is configured (%dkb free heap)", ESP.getFreeHeap());
if(mqttSecureClient == NULL) { if(mqttSecureClient == NULL) {
mqttSecureClient = new WiFiClientSecure(); mqttSecureClient = new WiFiClientSecure();
} #if defined(ESP8266)
#if defined(ESP8266) mqttSecureClient->setBufferSizes(512, 512);
mqttSecureClient->setBufferSizes(512, 512); #endif
#endif
if(LittleFS.begin()) {
if(LittleFS.begin()) { File file;
File file;
if(LittleFS.exists(FILE_MQTT_CA)) { if(LittleFS.exists(FILE_MQTT_CA)) {
debugI("Found MQTT CA file (%dkb free heap)", ESP.getFreeHeap()); debugI("Found MQTT CA file (%dkb free heap)", ESP.getFreeHeap());
file = LittleFS.open(FILE_MQTT_CA, "r"); file = LittleFS.open(FILE_MQTT_CA, "r");
#if defined(ESP8266) #if defined(ESP8266)
BearSSL::X509List *serverTrustedCA = new BearSSL::X509List(file); BearSSL::X509List *serverTrustedCA = new BearSSL::X509List(file);
mqttSecureClient->setTrustAnchors(serverTrustedCA); mqttSecureClient->setTrustAnchors(serverTrustedCA);
#elif defined(ESP32) #elif defined(ESP32)
mqttSecureClient->loadCACert(file, file.size()); mqttSecureClient->loadCACert(file, file.size());
#endif #endif
file.close(); file.close();
}
if(LittleFS.exists(FILE_MQTT_CERT) && LittleFS.exists(FILE_MQTT_KEY)) {
#if defined(ESP8266)
debugI("Found MQTT certificate file (%dkb free heap)", ESP.getFreeHeap());
file = LittleFS.open(FILE_MQTT_CERT, "r");
BearSSL::X509List *serverCertList = new BearSSL::X509List(file);
file.close();
debugI("Found MQTT key file (%dkb free heap)", ESP.getFreeHeap());
file = LittleFS.open(FILE_MQTT_KEY, "r");
BearSSL::PrivateKey *serverPrivKey = new BearSSL::PrivateKey(file);
file.close();
debugD("Setting client certificates (%dkb free heap)", ESP.getFreeHeap());
mqttSecureClient->setClientRSACert(serverCertList, serverPrivKey);
#elif defined(ESP32)
debugI("Found MQTT certificate file (%dkb free heap)", ESP.getFreeHeap());
file = LittleFS.open(FILE_MQTT_CERT, "r");
mqttSecureClient->loadCertificate(file, file.size());
file.close();
debugI("Found MQTT key file (%dkb free heap)", ESP.getFreeHeap());
file = LittleFS.open(FILE_MQTT_KEY, "r");
mqttSecureClient->loadPrivateKey(file, file.size());
file.close();
#endif
}
LittleFS.end();
debugD("MQTT SSL setup complete (%dkb free heap)", ESP.getFreeHeap());
} }
if(LittleFS.exists(FILE_MQTT_CERT) && LittleFS.exists(FILE_MQTT_KEY)) {
#if defined(ESP8266)
debugI("Found MQTT certificate file (%dkb free heap)", ESP.getFreeHeap());
file = LittleFS.open(FILE_MQTT_CERT, "r");
BearSSL::X509List *serverCertList = new BearSSL::X509List(file);
file.close();
debugI("Found MQTT key file (%dkb free heap)", ESP.getFreeHeap());
file = LittleFS.open(FILE_MQTT_KEY, "r");
BearSSL::PrivateKey *serverPrivKey = new BearSSL::PrivateKey(file);
file.close();
debugD("Setting client certificates (%dkb free heap)", ESP.getFreeHeap());
mqttSecureClient->setClientRSACert(serverCertList, serverPrivKey);
#elif defined(ESP32)
debugI("Found MQTT certificate file (%dkb free heap)", ESP.getFreeHeap());
file = LittleFS.open(FILE_MQTT_CERT, "r");
mqttSecureClient->loadCertificate(file, file.size());
file.close();
debugI("Found MQTT key file (%dkb free heap)", ESP.getFreeHeap());
file = LittleFS.open(FILE_MQTT_KEY, "r");
mqttSecureClient->loadPrivateKey(file, file.size());
file.close();
#endif
}
LittleFS.end();
debugD("MQTT SSL setup complete (%dkb free heap)", ESP.getFreeHeap());
} }
mqttClient = mqttSecureClient; mqttClient = mqttSecureClient;
} else if(mqttClient == NULL) { } else if(mqttClient == NULL) {
@@ -1378,10 +1384,9 @@ void MQTT_connect() {
if ((strlen(mqttConfig.username) == 0 && mqtt->connect(mqttConfig.clientId)) || if ((strlen(mqttConfig.username) == 0 && mqtt->connect(mqttConfig.clientId)) ||
(strlen(mqttConfig.username) > 0 && mqtt->connect(mqttConfig.clientId, mqttConfig.username, mqttConfig.password))) { (strlen(mqttConfig.username) > 0 && mqtt->connect(mqttConfig.clientId, mqttConfig.username, mqttConfig.password))) {
if (Debug.isActive(RemoteDebug::INFO)) debugI("Successfully connected to MQTT!"); if (Debug.isActive(RemoteDebug::INFO)) debugI("Successfully connected to MQTT!");
config.ackMqttChange();
if(mqttHandler != NULL) { if(mqttHandler != NULL) {
mqttHandler->publishSystem(&hw); mqttHandler->publishSystem(&hw, eapi, &ea);
} }
// Subscribe to the chosen MQTT topic, if set in configuration // Subscribe to the chosen MQTT topic, if set in configuration

View File

@@ -237,11 +237,12 @@ float EnergyAccounting::getMonthMax() {
uint32_t maxHour = 0.0; uint32_t maxHour = 0.0;
bool included[5] = { false, false, false, false, false }; bool included[5] = { false, false, false, false, false };
while(count < config->hours) { while(count < config->hours && count <= 5) {
uint8_t maxIdx = 0; uint8_t maxIdx = 0;
uint16_t maxVal = 0; uint16_t maxVal = 0;
for(uint8_t i = 0; i < 5; i++) { for(uint8_t i = 0; i < 5; i++) {
if(included[i]) continue; if(included[i]) continue;
if(data.peaks[i].day == 0) continue;
if(data.peaks[i].value > maxVal) { if(data.peaks[i].value > maxVal) {
maxVal = data.peaks[i].value; maxVal = data.peaks[i].value;
maxIdx = i; maxIdx = i;
@@ -253,9 +254,7 @@ float EnergyAccounting::getMonthMax() {
for(uint8_t i = 0; i < 5; i++) { for(uint8_t i = 0; i < 5; i++) {
if(!included[i]) continue; if(!included[i]) continue;
if(data.peaks[i].day > 0) { maxHour += data.peaks[i].value;
maxHour += data.peaks[i].value;
}
} }
return maxHour > 0 ? maxHour / count / 100.0 : 0.0; return maxHour > 0 ? maxHour / count / 100.0 : 0.0;
} }
@@ -266,7 +265,7 @@ float EnergyAccounting::getPeak(uint8_t num) {
uint8_t count = 0; uint8_t count = 0;
bool included[5] = { false, false, false, false, false }; bool included[5] = { false, false, false, false, false };
while(count < config->hours) { while(count < config->hours && count <= 5) {
uint8_t maxIdx = 0; uint8_t maxIdx = 0;
uint16_t maxVal = 0; uint16_t maxVal = 0;
for(uint8_t i = 0; i < 5; i++) { for(uint8_t i = 0; i < 5; i++) {
@@ -341,7 +340,7 @@ bool EnergyAccounting::load() {
this->data.peaks[b].day = b; this->data.peaks[b].day = b;
memcpy(&this->data.peaks[b].value, buf+i, 2); memcpy(&this->data.peaks[b].value, buf+i, 2);
b++; b++;
if(b >= config->hours) break; if(b >= config->hours || b >= 5) break;
} }
ret = true; ret = true;
} else if(buf[0] == 1) { } else if(buf[0] == 1) {

111
src/LNG.cpp Normal file
View File

@@ -0,0 +1,111 @@
#include "LNG.h"
#include "lwip/def.h"
#include "ams/Cosem.h"
LNG::LNG(const char* payload, uint8_t useMeterType, MeterConfig* meterConfig, DataParserContext &ctx, RemoteDebug* debugger) {
LngHeader* h = (LngHeader*) payload;
if(h->tag == CosemTypeStructure && h->arrayTag == CosemTypeArray) {
meterType = AmsTypeLng;
this->packageTimestamp = ctx.timestamp;
uint8_t* ptr = (uint8_t*) &h[1];
uint8_t* data = ptr + (18*h->arrayLength); // Skip descriptors
uint16_t o170 = 0, o270 = 0;
uint16_t o181 = 0, o182 = 0;
uint16_t o281 = 0, o282 = 0;
LngObisDescriptor* descriptor = (LngObisDescriptor*) ptr;
for(uint8_t x = 0; x < h->arrayLength-1; x++) {
ptr = (uint8_t*) &descriptor[1];
descriptor = (LngObisDescriptor*) ptr;
if(debugger->isActive(RemoteDebug::VERBOSE)) debugger->printf("(L&G) OBIS %d.%d.%d with type 0x%02X", descriptor->obis[2], descriptor->obis[3], descriptor->obis[4], *data);
CosemData* item = (CosemData*) data;
if(descriptor->obis[2] == 1) {
if(descriptor->obis[3] == 7) {
if(descriptor->obis[4] == 0) {
o170 = ntohl(item->dlu.data);
if(debugger->isActive(RemoteDebug::VERBOSE)) debugger->printf(" and value %d (dlu)", ntohl(item->dlu.data));
}
} else if(descriptor->obis[3] == 8) {
if(descriptor->obis[4] == 0) {
activeImportCounter = ntohl(item->dlu.data) / 1000.0;
listType = listType >= 3 ? listType : 3;
if(debugger->isActive(RemoteDebug::VERBOSE)) debugger->printf(" and value %d (dlu)", ntohl(item->dlu.data));
} else if(descriptor->obis[4] == 1) {
o181 = ntohl(item->dlu.data);
if(debugger->isActive(RemoteDebug::VERBOSE)) debugger->printf(" and value %d (dlu)", ntohl(item->dlu.data));
} else if(descriptor->obis[4] == 2) {
o182 = ntohl(item->dlu.data);
if(debugger->isActive(RemoteDebug::VERBOSE)) debugger->printf(" and value %d (dlu)", ntohl(item->dlu.data));
}
}
} else if(descriptor->obis[2] == 2) {
if(descriptor->obis[3] == 7) {
if(descriptor->obis[4] == 0) {
o270 = ntohl(item->dlu.data);
if(debugger->isActive(RemoteDebug::VERBOSE)) debugger->printf(" and value %d (dlu)", ntohl(item->dlu.data));
}
} else if(descriptor->obis[3] == 8) {
if(descriptor->obis[4] == 0) {
activeExportCounter = ntohl(item->dlu.data) / 1000.0;
listType = listType >= 3 ? listType : 3;
if(debugger->isActive(RemoteDebug::VERBOSE)) debugger->printf(" and value %d (dlu)", ntohl(item->dlu.data));
} else if(descriptor->obis[4] == 1) {
o281 = ntohl(item->dlu.data);
if(debugger->isActive(RemoteDebug::VERBOSE)) debugger->printf(" and value %d (dlu)", ntohl(item->dlu.data));
} else if(descriptor->obis[4] == 2) {
o282 = ntohl(item->dlu.data);
if(debugger->isActive(RemoteDebug::VERBOSE)) debugger->printf(" and value %d (dlu)", ntohl(item->dlu.data));
}
}
} else if(descriptor->obis[2] == 96) {
if(descriptor->obis[3] == 1) {
if(descriptor->obis[4] == 0) {
char str[item->oct.length+1];
memcpy(str, item->oct.data, item->oct.length);
str[item->oct.length] = '\0';
meterId = String(str);
if(debugger->isActive(RemoteDebug::VERBOSE)) debugger->printf(" and value %s (oct)", str);
} else if(descriptor->obis[4] == 1) {
char str[item->oct.length+1];
memcpy(str, item->oct.data, item->oct.length);
str[item->oct.length] = '\0';
meterModel = String(str);
if(debugger->isActive(RemoteDebug::VERBOSE)) debugger->printf(" and value %s (oct)", str);
}
}
}
if(debugger->isActive(RemoteDebug::VERBOSE)) debugger->printf("\n");
if(o170 > 0 || o270 > 0) {
int32_t sum = o170-o270;
if(sum > 0) {
listType = listType >= 1 ? listType : 1;
activeImportPower = sum;
} else {
listType = listType >= 2 ? listType : 2;
activeExportPower = sum * -1;
}
}
if(o181 > 0 || o182 > 0) {
activeImportCounter = (o181 + o182) / 1000.0;
listType = listType >= 3 ? listType : 3;
}
if(o281 > 0 || o282 > 0) {
activeExportCounter = (o281 + o282) / 1000.0;
listType = listType >= 3 ? listType : 3;
}
if((*data) == 0x09) {
data += (*(data+1))+2;
} else {
data += 5;
}
lastUpdateMillis = millis();
}
}
}

30
src/LNG.h Normal file
View File

@@ -0,0 +1,30 @@
#ifndef _LNG_H
#define _LNG_H
#include "AmsData.h"
#include "AmsConfiguration.h"
#include "ams/DataParser.h"
#include "RemoteDebug.h"
struct LngHeader {
uint8_t tag;
uint8_t values;
uint8_t arrayTag;
uint8_t arrayLength;
} __attribute__((packed));
struct LngObisDescriptor {
uint8_t ignore1[5];
uint8_t octetTag;
uint8_t octetLength;
uint8_t obis[6];
uint8_t ignore2[5];
} __attribute__((packed));
class LNG : public AmsData {
public:
LNG(const char* payload, uint8_t useMeterType, MeterConfig* meterConfig, DataParserContext &ctx, RemoteDebug* debugger);
};
#endif

View File

@@ -19,7 +19,7 @@ public:
virtual bool publish(AmsData* data, AmsData* previousState, EnergyAccounting* ea); virtual bool publish(AmsData* data, AmsData* previousState, EnergyAccounting* ea);
virtual bool publishTemperatures(AmsConfiguration*, HwTools*); virtual bool publishTemperatures(AmsConfiguration*, HwTools*);
virtual bool publishPrices(EntsoeApi* eapi); virtual bool publishPrices(EntsoeApi* eapi);
virtual bool publishSystem(HwTools*); virtual bool publishSystem(HwTools*, EntsoeApi*, EnergyAccounting*);
protected: protected:
MQTTClient* mqtt; MQTTClient* mqtt;

View File

@@ -71,6 +71,6 @@ bool DomoticzMqttHandler::publishPrices(EntsoeApi* eapi) {
return false; return false;
} }
bool DomoticzMqttHandler::publishSystem(HwTools* hw) { bool DomoticzMqttHandler::publishSystem(HwTools* hw, EntsoeApi* eapi, EnergyAccounting* ea) {
return false; return false;
} }

View File

@@ -12,7 +12,7 @@ public:
bool publish(AmsData* data, AmsData* previousState, EnergyAccounting* ea); bool publish(AmsData* data, AmsData* previousState, EnergyAccounting* ea);
bool publishTemperatures(AmsConfiguration*, HwTools*); bool publishTemperatures(AmsConfiguration*, HwTools*);
bool publishPrices(EntsoeApi*); bool publishPrices(EntsoeApi*);
bool publishSystem(HwTools*); bool publishSystem(HwTools* hw, EntsoeApi* eapi, EnergyAccounting* ea);
private: private:
DomoticzConfig config; DomoticzConfig config;

View File

@@ -9,6 +9,7 @@
#include "web/root/jsonsys_json.h" #include "web/root/jsonsys_json.h"
#include "web/root/jsonprices_json.h" #include "web/root/jsonprices_json.h"
#include "web/root/hadiscover_json.h" #include "web/root/hadiscover_json.h"
#include "web/root/realtime_json.h"
bool HomeAssistantMqttHandler::publish(AmsData* data, AmsData* previousState, EnergyAccounting* ea) { bool HomeAssistantMqttHandler::publish(AmsData* data, AmsData* previousState, EnergyAccounting* ea) {
if(topic.isEmpty() || !mqtt->connected()) if(topic.isEmpty() || !mqtt->connected())
@@ -55,8 +56,7 @@ bool HomeAssistantMqttHandler::publish(AmsData* data, AmsData* previousState, En
); );
return mqtt->publish(topic + "/power", json); return mqtt->publish(topic + "/power", json);
} }
return false; return false;}
}
bool HomeAssistantMqttHandler::publishTemperatures(AmsConfiguration* config, HwTools* hw) { bool HomeAssistantMqttHandler::publishTemperatures(AmsConfiguration* config, HwTools* hw) {
int count = hw->getTempSensorCount(); int count = hw->getTempSensorCount();
@@ -190,7 +190,7 @@ bool HomeAssistantMqttHandler::publishPrices(EntsoeApi* eapi) {
return mqtt->publish(topic + "/prices", json); return mqtt->publish(topic + "/prices", json);
} }
bool HomeAssistantMqttHandler::publishSystem(HwTools* hw) { bool HomeAssistantMqttHandler::publishSystem(HwTools* hw, EntsoeApi* eapi, EnergyAccounting* ea) {
if(topic.isEmpty() || !mqtt->connected()){ if(topic.isEmpty() || !mqtt->connected()){
sequence = 0; sequence = 0;
return false; return false;
@@ -218,7 +218,7 @@ bool HomeAssistantMqttHandler::publishSystem(HwTools* hw) {
String haUrl = "http://" + haUID + ".local/"; String haUrl = "http://" + haUID + ".local/";
// Could this be necessary? haUID.replace("-", "_"); // Could this be necessary? haUID.replace("-", "_");
for(int i=0;i<sensors;i++){ for(int i=0;i<17;i++){
snprintf_P(json, BufferSize, HADISCOVER_JSON, snprintf_P(json, BufferSize, HADISCOVER_JSON,
FPSTR(HA_NAMES[i]), FPSTR(HA_NAMES[i]),
topic.c_str(), FPSTR(HA_TOPICS[i]), topic.c_str(), FPSTR(HA_TOPICS[i]),
@@ -241,5 +241,4 @@ bool HomeAssistantMqttHandler::publishSystem(HwTools* hw) {
autodiscoverInit = true; autodiscoverInit = true;
} }
if(listType>0) sequence++; if(listType>0) sequence++;
return true; return true;}
}

View File

@@ -13,11 +13,9 @@ public:
bool publish(AmsData* data, AmsData* previousState, EnergyAccounting* ea); bool publish(AmsData* data, AmsData* previousState, EnergyAccounting* ea);
bool publishTemperatures(AmsConfiguration*, HwTools*); bool publishTemperatures(AmsConfiguration*, HwTools*);
bool publishPrices(EntsoeApi*); bool publishPrices(EntsoeApi*);
bool publishSystem(HwTools*); bool publishSystem(HwTools* hw, EntsoeApi* eapi, EnergyAccounting* ea);
private: private:
static const uint8_t sensors = 17;
String haTopic = "homeassistant/sensor/"; String haTopic = "homeassistant/sensor/";
String haName = "AMS reader"; String haName = "AMS reader";

View File

@@ -11,4 +11,4 @@ const char* HA_UOM[17] PROGMEM = {"dBm", "V", "C", "W", "W", "W", "W", "A", "A",
const char* HA_DEVCL[17] PROGMEM = {"signal_strength", "voltage", "temperature", "power", "power", "power", "power", "current", "current", "current", "voltage", "voltage", "voltage", "energy", "energy", "energy", "energy"}; const char* HA_DEVCL[17] PROGMEM = {"signal_strength", "voltage", "temperature", "power", "power", "power", "power", "current", "current", "current", "voltage", "voltage", "voltage", "energy", "energy", "energy", "energy"};
const char* HA_STACL[17] PROGMEM = {"", "", "", "\"measurement\"", "\"measurement\"", "\"measurement\"", "\"measurement\"", "", "", "", "", "", "", "\"total_increasing\"", "\"total_increasing\"", "\"total_increasing\"", "\"total_increasing\""}; const char* HA_STACL[17] PROGMEM = {"", "", "", "\"measurement\"", "\"measurement\"", "\"measurement\"", "\"measurement\"", "", "", "", "", "", "", "\"total_increasing\"", "\"total_increasing\"", "\"total_increasing\"", "\"total_increasing\""};
#endif #endif

View File

@@ -271,7 +271,7 @@ bool JsonMqttHandler::publishPrices(EntsoeApi* eapi) {
return mqtt->publish(topic, json); return mqtt->publish(topic, json);
} }
bool JsonMqttHandler::publishSystem(HwTools* hw) { bool JsonMqttHandler::publishSystem(HwTools* hw, EntsoeApi* eapi, EnergyAccounting* ea) {
if(init || topic.isEmpty() || !mqtt->connected()) if(init || topic.isEmpty() || !mqtt->connected())
return false; return false;

View File

@@ -13,7 +13,7 @@ public:
bool publish(AmsData* data, AmsData* previousState, EnergyAccounting* ea); bool publish(AmsData* data, AmsData* previousState, EnergyAccounting* ea);
bool publishTemperatures(AmsConfiguration*, HwTools*); bool publishTemperatures(AmsConfiguration*, HwTools*);
bool publishPrices(EntsoeApi*); bool publishPrices(EntsoeApi*);
bool publishSystem(HwTools*); bool publishSystem(HwTools* hw, EntsoeApi* eapi, EnergyAccounting* ea);
private: private:
String clientId; String clientId;

View File

@@ -211,7 +211,7 @@ bool RawMqttHandler::publishPrices(EntsoeApi* eapi) {
return true; return true;
} }
bool RawMqttHandler::publishSystem(HwTools* hw) { bool RawMqttHandler::publishSystem(HwTools* hw, EntsoeApi* eapi, EnergyAccounting* ea) {
if(topic.isEmpty() || !mqtt->connected()) if(topic.isEmpty() || !mqtt->connected())
return false; return false;

View File

@@ -12,7 +12,7 @@ public:
bool publish(AmsData* data, AmsData* previousState, EnergyAccounting* ea); bool publish(AmsData* data, AmsData* previousState, EnergyAccounting* ea);
bool publishTemperatures(AmsConfiguration*, HwTools*); bool publishTemperatures(AmsConfiguration*, HwTools*);
bool publishPrices(EntsoeApi*); bool publishPrices(EntsoeApi*);
bool publishSystem(HwTools*); bool publishSystem(HwTools* hw, EntsoeApi* eapi, EnergyAccounting* ea);
private: private:
String topic; String topic;

View File

@@ -90,7 +90,7 @@ void AmsWebServer::setup(AmsConfiguration* config, GpioConfig* gpioConfig, Meter
server.on("/debugging", HTTP_GET, std::bind(&AmsWebServer::configDebugHtml, this)); server.on("/debugging", HTTP_GET, std::bind(&AmsWebServer::configDebugHtml, this));
server.on("/firmware", HTTP_GET, std::bind(&AmsWebServer::firmwareHtml, this)); server.on("/firmware", HTTP_GET, std::bind(&AmsWebServer::firmwareHtml, this));
server.on("/firmware", HTTP_POST, std::bind(&AmsWebServer::uploadPost, this), std::bind(&AmsWebServer::firmwareUpload, this)); server.on("/firmware", HTTP_POST, std::bind(&AmsWebServer::firmwarePost, this), std::bind(&AmsWebServer::firmwareUpload, this));
server.on("/upgrade", HTTP_GET, std::bind(&AmsWebServer::firmwareDownload, this)); server.on("/upgrade", HTTP_GET, std::bind(&AmsWebServer::firmwareDownload, this));
server.on("/restart", HTTP_GET, std::bind(&AmsWebServer::restartHtml, this)); server.on("/restart", HTTP_GET, std::bind(&AmsWebServer::restartHtml, this));
server.on("/restart", HTTP_POST, std::bind(&AmsWebServer::restartPost, this)); server.on("/restart", HTTP_POST, std::bind(&AmsWebServer::restartPost, this));
@@ -336,6 +336,9 @@ void AmsWebServer::configMeterHtml() {
case AmsTypeSagemcom: case AmsTypeSagemcom:
manufacturer = "Sagemcom"; manufacturer = "Sagemcom";
break; break;
case AmsTypeLng:
manufacturer = "L&G";
break;
default: default:
manufacturer = "Unknown"; manufacturer = "Unknown";
break; break;
@@ -567,6 +570,20 @@ void AmsWebServer::configEntsoeHtml() {
html.replace("{eaDk1}", strcmp(entsoe.area, "10YDK-1--------W") == 0 ? "selected" : ""); html.replace("{eaDk1}", strcmp(entsoe.area, "10YDK-1--------W") == 0 ? "selected" : "");
html.replace("{eaDk2}", strcmp(entsoe.area, "10YDK-2--------M") == 0 ? "selected" : ""); html.replace("{eaDk2}", strcmp(entsoe.area, "10YDK-2--------M") == 0 ? "selected" : "");
html.replace("{at}", strcmp(entsoe.area, "10YAT-APG------L") == 0 ? "selected" : "");
html.replace("{be}", strcmp(entsoe.area, "10YBE----------2") == 0 ? "selected" : "");
html.replace("{cz}", strcmp(entsoe.area, "10YCZ-CEPS-----N") == 0 ? "selected" : "");
html.replace("{ee}", strcmp(entsoe.area, "10Y1001A1001A39I") == 0 ? "selected" : "");
html.replace("{fi}", strcmp(entsoe.area, "10YFI-1--------U") == 0 ? "selected" : "");
html.replace("{fr}", strcmp(entsoe.area, "10YFR-RTE------C") == 0 ? "selected" : "");
html.replace("{de}", strcmp(entsoe.area, "10Y1001A1001A83F") == 0 ? "selected" : "");
html.replace("{gb}", strcmp(entsoe.area, "10YGB----------A") == 0 ? "selected" : "");
html.replace("{lv}", strcmp(entsoe.area, "10YLV-1001A00074") == 0 ? "selected" : "");
html.replace("{lt}", strcmp(entsoe.area, "10YLT-1001A0008Q") == 0 ? "selected" : "");
html.replace("{nl}", strcmp(entsoe.area, "10YNL----------L") == 0 ? "selected" : "");
html.replace("{pl}", strcmp(entsoe.area, "10YPL-AREA-----S") == 0 ? "selected" : "");
html.replace("{ch}", strcmp(entsoe.area, "10YCH-SWISSGRIDZ") == 0 ? "selected" : "");
html.replace("{ecNOK}", strcmp(entsoe.currency, "NOK") == 0 ? "selected" : ""); html.replace("{ecNOK}", strcmp(entsoe.currency, "NOK") == 0 ? "selected" : "");
html.replace("{ecSEK}", strcmp(entsoe.currency, "SEK") == 0 ? "selected" : ""); html.replace("{ecSEK}", strcmp(entsoe.currency, "SEK") == 0 ? "selected" : "");
html.replace("{ecDKK}", strcmp(entsoe.currency, "DKK") == 0 ? "selected" : ""); html.replace("{ecDKK}", strcmp(entsoe.currency, "DKK") == 0 ? "selected" : "");
@@ -1649,13 +1666,40 @@ void AmsWebServer::firmwareHtml() {
server.sendContent_P(FOOT_HTML); server.sendContent_P(FOOT_HTML);
} }
void AmsWebServer::firmwarePost() {
printD("Handlling firmware post...");
if(!checkSecurity(1))
return;
if(rebootForUpgrade) {
server.send(200);
} else {
if(server.hasArg("url")) {
String url = server.arg("url");
if(!url.isEmpty() && (url.startsWith("http://") || url.startsWith("https://"))) {
printD("Custom firmware URL was provided");
customFirmwareUrl = url;
performUpgrade = true;
server.sendHeader("Location","/restart-wait");
server.send(303);
return;
}
}
server.sendHeader("Location","/firmware");
server.send(303);
}
}
void AmsWebServer::firmwareUpload() { void AmsWebServer::firmwareUpload() {
printD("Handlling firmware upload...");
if(!checkSecurity(1)) if(!checkSecurity(1))
return; return;
HTTPUpload& upload = server.upload(); HTTPUpload& upload = server.upload();
String filename = upload.filename;
if(filename.isEmpty()) return;
if(upload.status == UPLOAD_FILE_START) { if(upload.status == UPLOAD_FILE_START) {
String filename = upload.filename;
if(!filename.endsWith(".bin")) { if(!filename.endsWith(".bin")) {
server.send(500, MIME_PLAIN, "500: couldn't create file"); server.send(500, MIME_PLAIN, "500: couldn't create file");
} else { } else {
@@ -1764,7 +1808,7 @@ void AmsWebServer::restartWaitHtml() {
performRestart = false; performRestart = false;
} else if(performUpgrade) { } else if(performUpgrade) {
WiFiClient client; WiFiClient client;
String url = "http://ams2mqtt.rewiredinvent.no/hub/firmware/update"; String url = customFirmwareUrl.isEmpty() || !customFirmwareUrl.startsWith("http") ? "http://ams2mqtt.rewiredinvent.no/hub/firmware/update" : customFirmwareUrl;
#if defined(ESP8266) #if defined(ESP8266)
String chipType = "esp8266"; String chipType = "esp8266";
#elif defined(CONFIG_IDF_TARGET_ESP32S2) #elif defined(CONFIG_IDF_TARGET_ESP32S2)
@@ -1778,9 +1822,11 @@ void AmsWebServer::restartWaitHtml() {
#endif #endif
#if defined(ESP8266) #if defined(ESP8266)
ESPhttpUpdate.setFollowRedirects(HTTPC_STRICT_FOLLOW_REDIRECTS);
t_httpUpdate_return ret = ESPhttpUpdate.update(client, url, VERSION); t_httpUpdate_return ret = ESPhttpUpdate.update(client, url, VERSION);
#elif defined(ESP32) #elif defined(ESP32)
HTTPUpdate httpUpdate; HTTPUpdate httpUpdate;
httpUpdate.setFollowRedirects(HTTPC_STRICT_FOLLOW_REDIRECTS);
HTTPUpdateResult ret = httpUpdate.update(client, url, String(VERSION) + "-" + chipType); HTTPUpdateResult ret = httpUpdate.update(client, url, String(VERSION) + "-" + chipType);
#endif #endif

View File

@@ -61,6 +61,7 @@ private:
bool performRestart = false; bool performRestart = false;
bool performUpgrade = false; bool performUpgrade = false;
bool rebootForUpgrade = false; bool rebootForUpgrade = false;
String customFirmwareUrl;
static const uint16_t BufferSize = 2048; static const uint16_t BufferSize = 2048;
char* buf; char* buf;
@@ -104,6 +105,7 @@ private:
String getSerialSelectOptions(int selected); String getSerialSelectOptions(int selected);
void firmwareHtml(); void firmwareHtml();
void firmwarePost();
void firmwareUpload(); void firmwareUpload();
void firmwareDownload(); void firmwareDownload();
void restartHtml(); void restartHtml();

View File

@@ -316,6 +316,7 @@ $(function() {
url: swv.data('url'), url: swv.data('url'),
dataType: 'json' dataType: 'json'
}).done(function(releases) { }).done(function(releases) {
var isnew = false;
if(/^v\d{1,2}\.\d{1,2}\.\d{1,2}$/.test(swv.text()) && fwl.length == 0) { if(/^v\d{1,2}\.\d{1,2}\.\d{1,2}$/.test(swv.text()) && fwl.length == 0) {
releases.reverse(); releases.reverse();
var next_patch; var next_patch;
@@ -352,10 +353,13 @@ $(function() {
}); });
if(next_minor) { if(next_minor) {
nextVersion = next_minor; nextVersion = next_minor;
isnew = true;
} else if(next_major) { } else if(next_major) {
nextVersion = next_major; nextVersion = next_major;
isnew = true;
} else if(next_patch) { } else if(next_patch) {
nextVersion = next_patch; nextVersion = next_patch;
isnew = true;
} }
} else { } else {
nextVersion = releases[0]; nextVersion = releases[0];
@@ -375,9 +379,11 @@ $(function() {
} }
}); });
}; };
$('#newVersionTag').text(nextVersion.tag_name); if(isnew) {
$('#newVersionUrl').prop('href', nextVersion.html_url); $('#newVersionTag').text(nextVersion.tag_name);
$('#newVersion').removeClass('d-none'); $('#newVersionUrl').prop('href', nextVersion.html_url);
$('#newVersion').removeClass('d-none');
}
} }
}); });
} }
@@ -884,7 +890,7 @@ var fetch = function() {
var upgrade = function() { var upgrade = function() {
if(nextVersion) { if(nextVersion) {
if(confirm("WARNING: Please keep USB power connected while upgrading. Are you sure you want to perform upgrade to " + nextVersion.tag_name + "?")) { if(confirm("WARNING: If you have a M-BUS powered device (Pow-U), please keep USB power connected while upgrading.\n\nAre you sure you want to perform upgrade to " + nextVersion.tag_name + "?")) {
$('#loading-indicator').show(); $('#loading-indicator').show();
window.location.href="/upgrade?version=" + nextVersion.tag_name; window.location.href="/upgrade?version=" + nextVersion.tag_name;
} }

View File

@@ -34,6 +34,19 @@
<option value="10YDK-1--------W" {eaDk1}>DK1</option> <option value="10YDK-1--------W" {eaDk1}>DK1</option>
<option value="10YDK-2--------M" {eaDk2}>DK2</option> <option value="10YDK-2--------M" {eaDk2}>DK2</option>
</optgroup> </optgroup>
<option value="10YAT-APG------L" {at}>Austria</option>
<option value="10YBE----------2" {be}>Belgium</option>
<option value="10YCZ-CEPS-----N" {cz}>Czech Republic</option>
<option value="10Y1001A1001A39I" {ee}>Estonia</option>
<option value="10YFI-1--------U" {fi}>Finland</option>
<option value="10YFR-RTE------C" {fr}>France</option>
<option value="10Y1001A1001A83F" {de}>Germany</option>
<option value="10YGB----------A" {gb}>Great Britain</option>
<option value="10YLV-1001A00074" {lv}>Latvia</option>
<option value="10YLT-1001A0008Q" {lt}>Lithuania</option>
<option value="10YNL----------L" {nl}>Netherland</option>
<option value="10YPL-AREA-----S" {pl}>Poland</option>
<option value="10YCH-SWISSGRIDZ" {ch}>Switzerland</option>
</select> </select>
</div> </div>
</div> </div>

View File

@@ -1,18 +1,21 @@
<div class="alert alert-danger"> <div class="alert alert-danger">
WARNING: Units powered over M-bus must be connected to an external power supply during firmware upload. Failure to do so may cause power-down during upload resulting in non-functioning unit. WARNING: Units powered by M-BUS (Pow-U) must be connected to an external power supply during firmware upload. Failure to do so may cause power-down during upload resulting in non-functioning unit.
</div> </div>
<div class="alert alert-warning"> <div class="alert alert-warning">
Your board is using {chipset} chipset. Only upload firmware designed for this chipset. Failure to do so may result in non-functioning unit. Your board is using {chipset} chipset. Only upload firmware designed for this chipset. Failure to do so may result in non-functioning unit.
<span id="fwDownload" style="display: none;"><br/>Download latest firmware file <a id="fwLink" href="#" data-chipset="{chipset}">here</a></span> <span id="fwDownload" style="display: none;"><br/>Download latest firmware file <a id="fwLink" href="#" data-chipset="{chipset}">here</a></span>
</div> </div>
<div class="alert alert-warning">
When using URL, only a valid ESP OTA server response will be accepted.
</div>
<form method="post" enctype="multipart/form-data" class="upload-form"> <form method="post" enctype="multipart/form-data" class="upload-form">
<div class="my-3 p-3 bg-white rounded shadow"> <div class="my-3 p-3 bg-white rounded shadow">
<div class="row"> <div class="row">
<div class="col-lg-6"> <div class="col-lg-6">
<div class="input-group mb-3"> <div class="input-group">
<div class="input-group-prepend"> <div class="input-group-prepend">
<span class="input-group-text">Upload</span> <span class="input-group-text">Upload file</span>
</div> </div>
<div class="custom-file"> <div class="custom-file">
<input name="file" type="file" class="custom-file-input" id="fileUploadField"> <input name="file" type="file" class="custom-file-input" id="fileUploadField">
@@ -21,6 +24,19 @@
</div> </div>
</div> </div>
</div> </div>
<div class="row">
<div class="col-lg-6">or</div>
</div>
<div class="row">
<div class="col-lg-6">
<div class="input-group">
<div class="input-group-prepend">
<span class="input-group-text">Use URL</span>
</div>
<input type="text" name="url" class="form-control"/>
</div>
</div>
</div>
</div> </div>
<hr/> <hr/>
<div class="row form-group"> <div class="row form-group">
@@ -28,7 +44,7 @@
<a href="javascript:history.back();" class="btn btn-outline-secondary">Back</a> <a href="javascript:history.back();" class="btn btn-outline-secondary">Back</a>
</div> </div>
<div class="col-6 text-right"> <div class="col-6 text-right">
<button class="btn btn-primary">Upload</button> <button class="btn btn-primary">Upgrade firmware</button>
</div> </div>
</div> </div>
</form> </form>

20
web/realtime.json Normal file
View File

@@ -0,0 +1,20 @@
{
"max" : %.1f,
"peaks" : [ %s ],
"threshold" : %d,
"hour" : {
"use" : %.2f,
"cost" : %.2f,
"produced" : %.2f
},
"day" : {
"use" : %.2f,
"cost" : %.2f,
"produced" : %.2f
},
"month" : {
"use" : %.2f,
"cost" : %.2f,
"produced" : %.2f
}
}