Added usage plots and ADC reading for Vcc

This commit is contained in:
Gunnar Skjold
2021-11-27 20:16:26 +01:00
parent 6f09f523e4
commit eb59245118
27 changed files with 1422 additions and 615 deletions

View File

@@ -2,7 +2,7 @@
bool AmsConfiguration::getSystemConfig(SystemConfig& config) {
if(hasConfig()) {
EEPROM.begin(EEPROM_SIZE);
EEPROM.begin(SPI_FLASH_SEC_SIZE);
EEPROM.get(CONFIG_SYSTEM_START, config);
EEPROM.end();
return true;
@@ -12,7 +12,7 @@ bool AmsConfiguration::getSystemConfig(SystemConfig& config) {
}
bool AmsConfiguration::setSystemConfig(SystemConfig& config) {
EEPROM.begin(EEPROM_SIZE);
EEPROM.begin(SPI_FLASH_SEC_SIZE);
EEPROM.put(CONFIG_SYSTEM_START, config);
bool ret = EEPROM.commit();
EEPROM.end();
@@ -21,7 +21,7 @@ bool AmsConfiguration::setSystemConfig(SystemConfig& config) {
bool AmsConfiguration::getWiFiConfig(WiFiConfig& config) {
if(hasConfig()) {
EEPROM.begin(EEPROM_SIZE);
EEPROM.begin(SPI_FLASH_SEC_SIZE);
EEPROM.get(CONFIG_WIFI_START, config);
EEPROM.end();
return true;
@@ -45,7 +45,7 @@ bool AmsConfiguration::setWiFiConfig(WiFiConfig& config) {
} else {
wifiChanged = true;
}
EEPROM.begin(EEPROM_SIZE);
EEPROM.begin(SPI_FLASH_SEC_SIZE);
EEPROM.put(CONFIG_WIFI_START, config);
bool ret = EEPROM.commit();
EEPROM.end();
@@ -85,7 +85,7 @@ void AmsConfiguration::ackWifiChange() {
bool AmsConfiguration::getMqttConfig(MqttConfig& config) {
if(hasConfig()) {
EEPROM.begin(EEPROM_SIZE);
EEPROM.begin(SPI_FLASH_SEC_SIZE);
EEPROM.get(CONFIG_MQTT_START, config);
EEPROM.end();
return true;
@@ -110,7 +110,7 @@ bool AmsConfiguration::setMqttConfig(MqttConfig& config) {
} else {
mqttChanged = true;
}
EEPROM.begin(EEPROM_SIZE);
EEPROM.begin(SPI_FLASH_SEC_SIZE);
EEPROM.put(CONFIG_MQTT_START, config);
bool ret = EEPROM.commit();
EEPROM.end();
@@ -143,7 +143,7 @@ void AmsConfiguration::ackMqttChange() {
bool AmsConfiguration::getWebConfig(WebConfig& config) {
if(hasConfig()) {
EEPROM.begin(EEPROM_SIZE);
EEPROM.begin(SPI_FLASH_SEC_SIZE);
EEPROM.get(CONFIG_WEB_START, config);
EEPROM.end();
return true;
@@ -154,7 +154,7 @@ bool AmsConfiguration::getWebConfig(WebConfig& config) {
}
bool AmsConfiguration::setWebConfig(WebConfig& config) {
EEPROM.begin(EEPROM_SIZE);
EEPROM.begin(SPI_FLASH_SEC_SIZE);
EEPROM.put(CONFIG_WEB_START, config);
bool ret = EEPROM.commit();
EEPROM.end();
@@ -169,7 +169,7 @@ void AmsConfiguration::clearAuth(WebConfig& config) {
bool AmsConfiguration::getMeterConfig(MeterConfig& config) {
if(hasConfig()) {
EEPROM.begin(EEPROM_SIZE);
EEPROM.begin(SPI_FLASH_SEC_SIZE);
EEPROM.get(CONFIG_METER_START, config);
EEPROM.end();
return true;
@@ -193,7 +193,7 @@ bool AmsConfiguration::setMeterConfig(MeterConfig& config) {
} else {
meterChanged = true;
}
EEPROM.begin(EEPROM_SIZE);
EEPROM.begin(SPI_FLASH_SEC_SIZE);
EEPROM.put(CONFIG_METER_START, config);
bool ret = EEPROM.commit();
EEPROM.end();
@@ -221,7 +221,7 @@ void AmsConfiguration::ackMeterChanged() {
bool AmsConfiguration::getDebugConfig(DebugConfig& config) {
if(hasConfig()) {
EEPROM.begin(EEPROM_SIZE);
EEPROM.begin(SPI_FLASH_SEC_SIZE);
EEPROM.get(CONFIG_DEBUG_START, config);
EEPROM.end();
return true;
@@ -232,7 +232,7 @@ bool AmsConfiguration::getDebugConfig(DebugConfig& config) {
}
bool AmsConfiguration::setDebugConfig(DebugConfig& config) {
EEPROM.begin(EEPROM_SIZE);
EEPROM.begin(SPI_FLASH_SEC_SIZE);
EEPROM.put(CONFIG_DEBUG_START, config);
bool ret = EEPROM.commit();
EEPROM.end();
@@ -247,7 +247,7 @@ void AmsConfiguration::clearDebug(DebugConfig& config) {
bool AmsConfiguration::getDomoticzConfig(DomoticzConfig& config) {
if(hasConfig()) {
EEPROM.begin(EEPROM_SIZE);
EEPROM.begin(SPI_FLASH_SEC_SIZE);
EEPROM.get(CONFIG_DOMOTICZ_START, config);
EEPROM.end();
return true;
@@ -269,7 +269,7 @@ bool AmsConfiguration::setDomoticzConfig(DomoticzConfig& config) {
domoChanged = true;
}
mqttChanged = domoChanged;
EEPROM.begin(EEPROM_SIZE);
EEPROM.begin(SPI_FLASH_SEC_SIZE);
EEPROM.put(CONFIG_DOMOTICZ_START, config);
bool ret = EEPROM.commit();
EEPROM.end();
@@ -310,7 +310,7 @@ bool AmsConfiguration::pinUsed(uint8_t pin, GpioConfig& config) {
bool AmsConfiguration::getGpioConfig(GpioConfig& config) {
if(hasConfig()) {
EEPROM.begin(EEPROM_SIZE);
EEPROM.begin(SPI_FLASH_SEC_SIZE);
EEPROM.get(CONFIG_GPIO_START, config);
EEPROM.end();
return true;
@@ -366,7 +366,7 @@ bool AmsConfiguration::setGpioConfig(GpioConfig& config) {
if(config.apPin >= 0)
pinMode(config.apPin, INPUT_PULLUP);
EEPROM.begin(EEPROM_SIZE);
EEPROM.begin(SPI_FLASH_SEC_SIZE);
EEPROM.put(CONFIG_GPIO_START, config);
bool ret = EEPROM.commit();
EEPROM.end();
@@ -388,11 +388,13 @@ void AmsConfiguration::clearGpio(GpioConfig& config) {
config.vccOffset = 0;
config.vccMultiplier = 1000;
config.vccBootLimit = 0;
config.vccResistorGnd = 0;
config.vccResistorVcc = 0;
}
bool AmsConfiguration::getNtpConfig(NtpConfig& config) {
if(hasConfig()) {
EEPROM.begin(EEPROM_SIZE);
EEPROM.begin(SPI_FLASH_SEC_SIZE);
EEPROM.get(CONFIG_NTP_START, config);
EEPROM.end();
return true;
@@ -419,7 +421,7 @@ bool AmsConfiguration::setNtpConfig(NtpConfig& config) {
} else {
ntpChanged = true;
}
EEPROM.begin(EEPROM_SIZE);
EEPROM.begin(SPI_FLASH_SEC_SIZE);
EEPROM.put(CONFIG_NTP_START, config);
bool ret = EEPROM.commit();
EEPROM.end();
@@ -444,7 +446,7 @@ void AmsConfiguration::clearNtp(NtpConfig& config) {
bool AmsConfiguration::getEntsoeConfig(EntsoeConfig& config) {
if(hasConfig()) {
EEPROM.begin(EEPROM_SIZE);
EEPROM.begin(SPI_FLASH_SEC_SIZE);
EEPROM.get(CONFIG_ENTSOE_START, config);
EEPROM.end();
return true;
@@ -463,7 +465,7 @@ bool AmsConfiguration::setEntsoeConfig(EntsoeConfig& config) {
} else {
entsoeChanged = true;
}
EEPROM.begin(EEPROM_SIZE);
EEPROM.begin(SPI_FLASH_SEC_SIZE);
EEPROM.put(CONFIG_ENTSOE_START, config);
bool ret = EEPROM.commit();
EEPROM.end();
@@ -486,7 +488,7 @@ void AmsConfiguration::ackEntsoeChange() {
}
void AmsConfiguration::clear() {
EEPROM.begin(EEPROM_SIZE);
EEPROM.begin(SPI_FLASH_SEC_SIZE);
MeterConfig meter;
clearMeter(meter);
EEPROM.put(CONFIG_METER_START, meter);
@@ -522,7 +524,7 @@ void AmsConfiguration::clear() {
bool AmsConfiguration::hasConfig() {
if(configVersion == 0) {
EEPROM.begin(EEPROM_SIZE);
EEPROM.begin(SPI_FLASH_SEC_SIZE);
configVersion = EEPROM.read(EEPROM_CONFIG_ADDRESS);
EEPROM.end();
}
@@ -553,6 +555,14 @@ bool AmsConfiguration::hasConfig() {
configVersion = 0;
return false;
}
case 88:
configVersion = -1; // Prevent loop
if(relocateConfig88()) {
configVersion = 89;
} else {
configVersion = 0;
return false;
}
case EEPROM_CHECK_SUM:
return true;
default:
@@ -567,7 +577,7 @@ int AmsConfiguration::getConfigVersion() {
}
void AmsConfiguration::loadTempSensors() {
EEPROM.begin(EEPROM_SIZE);
EEPROM.begin(SPI_FLASH_SEC_SIZE);
TempSensorConfig* tempSensors[32];
int address = EEPROM_TEMP_CONFIG_ADDRESS;
int c = 0;
@@ -606,7 +616,7 @@ void AmsConfiguration::saveTempSensors() {
bool AmsConfiguration::loadConfig83(int address) {
ConfigObject83 c;
EEPROM.begin(EEPROM_SIZE);
EEPROM.begin(SPI_FLASH_SEC_SIZE);
EEPROM.get(address, c);
EntsoeConfig entsoe {"", "", "", 1000};
@@ -644,7 +654,9 @@ bool AmsConfiguration::loadConfig83(int address) {
c.vccPin,
c.vccOffset,
c.vccMultiplier,
c.vccBootLimit
c.vccBootLimit,
0,
0
};
EEPROM.put(CONFIG_GPIO_START, gpio);
@@ -715,7 +727,7 @@ bool AmsConfiguration::loadConfig83(int address) {
bool AmsConfiguration::relocateConfig86() {
MqttConfig86 mqtt86;
MqttConfig mqtt;
EEPROM.begin(EEPROM_SIZE);
EEPROM.begin(SPI_FLASH_SEC_SIZE);
EEPROM.get(CONFIG_MQTT_START_86, mqtt86);
strcpy(mqtt.host, mqtt86.host);
mqtt.port = mqtt86.port;
@@ -736,7 +748,7 @@ bool AmsConfiguration::relocateConfig86() {
bool AmsConfiguration::relocateConfig87() {
MeterConfig87 meter87;
MeterConfig meter;
EEPROM.begin(EEPROM_SIZE);
EEPROM.begin(SPI_FLASH_SEC_SIZE);
EEPROM.get(CONFIG_METER_START_87, meter87);
if(meter87.type < 5) {
meter.baud = 2400;
@@ -757,8 +769,38 @@ bool AmsConfiguration::relocateConfig87() {
return ret;
}
bool AmsConfiguration::relocateConfig88() {
GpioConfig88 gpio88;
EEPROM.begin(SPI_FLASH_SEC_SIZE);
EEPROM.get(CONFIG_GPIO_START_88, gpio88);
GpioConfig gpio {
gpio88.hanPin,
gpio88.apPin,
gpio88.ledPin,
gpio88.ledInverted,
gpio88.ledPinRed,
gpio88.ledPinGreen,
gpio88.ledPinBlue,
gpio88.ledRgbInverted,
gpio88.tempSensorPin,
gpio88.tempAnalogSensorPin,
gpio88.vccPin,
gpio88.vccOffset,
gpio88.vccMultiplier,
gpio88.vccBootLimit,
0,
0
};
EEPROM.put(CONFIG_GPIO_START, gpio);
EEPROM.put(EEPROM_CONFIG_ADDRESS, 89);
bool ret = EEPROM.commit();
EEPROM.end();
return ret;
}
bool AmsConfiguration::save() {
EEPROM.begin(EEPROM_SIZE);
EEPROM.begin(SPI_FLASH_SEC_SIZE);
EEPROM.put(EEPROM_CONFIG_ADDRESS, EEPROM_CHECK_SUM);
saveTempSensors();
bool success = EEPROM.commit();
@@ -938,9 +980,17 @@ void AmsConfiguration::print(Print* debugger)
debugger->printf("Temperature pin: %i\r\n", gpio.tempSensorPin);
debugger->printf("Temp analog pin: %i\r\n", gpio.tempAnalogSensorPin);
debugger->printf("Vcc pin: %i\r\n", gpio.vccPin);
if(gpio.vccMultiplier > 0) {
debugger->printf("Vcc multiplier: %f\r\n", gpio.vccMultiplier / 1000.0);
}
if(gpio.vccOffset > 0) {
debugger->printf("Vcc offset: %f\r\n", gpio.vccOffset / 100.0);
}
if(gpio.vccBootLimit > 0) {
debugger->printf("Vcc boot limit: %f\r\n", gpio.vccBootLimit / 10.0);
}
debugger->printf("GND resistor: %i\r\n", gpio.vccResistorGnd);
debugger->printf("Vcc resistor: %i\r\n", gpio.vccResistorVcc);
debugger->println("");
delay(10);
Serial.flush();

View File

@@ -3,17 +3,16 @@
#include <EEPROM.h>
#include "Arduino.h"
#define EEPROM_SIZE 1024 * 3
#define EEPROM_CHECK_SUM 88 // Used to check if config is stored. Change if structure changes
#define EEPROM_CHECK_SUM 89 // Used to check if config is stored. Change if structure changes
#define EEPROM_CONFIG_ADDRESS 0
#define EEPROM_TEMP_CONFIG_ADDRESS 2048
#define CONFIG_SYSTEM_START 8
#define CONFIG_WIFI_START 16
#define CONFIG_METER_START 224
#define CONFIG_GPIO_START 266
#define CONFIG_WEB_START 648
#define CONFIG_DEBUG_START 824
#define CONFIG_GPIO_START 832
#define CONFIG_DOMOTICZ_START 856
#define CONFIG_NTP_START 872
#define CONFIG_ENTSOE_START 944
@@ -21,6 +20,7 @@
#define CONFIG_MQTT_START_86 224
#define CONFIG_METER_START_87 784
#define CONFIG_GPIO_START_88 832
struct SystemConfig {
@@ -111,6 +111,25 @@ struct GpioConfig {
int16_t vccOffset;
uint16_t vccMultiplier;
uint8_t vccBootLimit;
uint16_t vccResistorGnd;
uint16_t vccResistorVcc;
}; // 20
struct GpioConfig88 {
uint8_t hanPin;
uint8_t apPin;
uint8_t ledPin;
bool ledInverted;
uint8_t ledPinRed;
uint8_t ledPinGreen;
uint8_t ledPinBlue;
bool ledRgbInverted;
uint8_t tempSensorPin;
uint8_t tempAnalogSensorPin;
uint8_t vccPin;
int16_t vccOffset;
uint16_t vccMultiplier;
uint8_t vccBootLimit;
}; // 16
struct DomoticzConfig {
@@ -295,6 +314,7 @@ private:
bool loadConfig83(int address);
bool relocateConfig86();
bool relocateConfig87();
bool relocateConfig88(); // dev 1.6
int readString(int pAddress, char* pString[]);
int readInt(int pAddress, int *pValue);

308
src/AmsDataStorage.cpp Normal file
View File

@@ -0,0 +1,308 @@
#include "AmsDataStorage.h"
#include <lwip/apps/sntp.h>
#include "EEPROM.h"
#include "LittleFS.h"
#include "AmsStorage.h"
AmsDataStorage::AmsDataStorage(RemoteDebug* debugger) {
day.version = 3;
month.version = 4;
this->debugger = debugger;
}
void AmsDataStorage::setTimezone(Timezone* tz) {
this->tz = tz;
}
bool AmsDataStorage::update(AmsData* data) {
if(data->getListType() != 3) return false;
time_t now = time(nullptr);
if(debugger->isActive(RemoteDebug::DEBUG)) {
debugger->printf("(AmsDataStorage) Time is: %d\n", now);
}
if(now < EPOCH_2021_01_01) {
now = data->getMeterTimestamp();
if(debugger->isActive(RemoteDebug::DEBUG)) {
debugger->printf("(AmsDataStorage) New time is: %d\n", now);
}
}
if(now-day.lastMeterReadTime < 3590) {
if(debugger->isActive(RemoteDebug::INFO)) {
debugger->printf("(AmsDataStorage) It is only %d seconds since last update, ignoring\n", (now-day.lastMeterReadTime));
}
return false;
}
tmElements_t tm;
breakTime(now, tm);
if(tm.Minute > 5) {
if(debugger->isActive(RemoteDebug::DEBUG)) {
debugger->printf("(AmsDataStorage) Already %d minutes into the hour, ignoring\n", tm.Minute);
}
return false;
}
int16_t val = (day.activeImport == 0 ? 0 : ((data->getActiveImportCounter()*1000) - day.activeImport) - ((data->getActiveExportCounter()*1000) - day.activeExport)) / 10;
if(debugger->isActive(RemoteDebug::DEBUG)) {
debugger->printf("(AmsDataStorage) Usage for hour %d: %d", tm.Hour, val);
}
if(tm.Hour == 1) {
day.h00 = val;
} else if(tm.Hour == 2) {
day.h01 = val;
} else if(tm.Hour == 3) {
day.h02 = val;
} else if(tm.Hour == 4) {
day.h03 = val;
} else if(tm.Hour == 5) {
day.h04 = val;
} else if(tm.Hour == 6) {
day.h05 = val;
} else if(tm.Hour == 7) {
day.h06 = val;
} else if(tm.Hour == 8) {
day.h07 = val;
} else if(tm.Hour == 9) {
day.h08 = val;
} else if(tm.Hour == 10) {
day.h09 = val;
} else if(tm.Hour == 11) {
day.h10 = val;
} else if(tm.Hour == 12) {
day.h11 = val;
} else if(tm.Hour == 13) {
day.h12 = val;
} else if(tm.Hour == 14) {
day.h13 = val;
} else if(tm.Hour == 15) {
day.h14 = val;
} else if(tm.Hour == 16) {
day.h15 = val;
} else if(tm.Hour == 17) {
day.h16 = val;
} else if(tm.Hour == 18) {
day.h17 = val;
} else if(tm.Hour == 19) {
day.h18 = val;
} else if(tm.Hour == 20) {
day.h19 = val;
} else if(tm.Hour == 21) {
day.h20 = val;
} else if(tm.Hour == 22) {
day.h21 = val;
} else if(tm.Hour == 23) {
day.h22 = val;
} else if(tm.Hour == 0) {
day.h23 = val;
}
day.activeImport = data->getActiveImportCounter()*1000;
day.activeExport = data->getActiveExportCounter()*1000;
day.lastMeterReadTime = now;
// Update month plot
if(tz != NULL) {
time_t local = tz->toLocal(now);
breakTime(local, tm);
}
if(tm.Hour == 0) {
val = (month.activeImport == 0 ? 0 : ((data->getActiveImportCounter()*1000) - month.activeImport) - ((data->getActiveExportCounter()*1000) - month.activeExport)) / 10;
if(debugger->isActive(RemoteDebug::DEBUG)) {
debugger->printf("(AmsDataStorage) Usage for day %d: %d", tm.Day, val);
}
if(tm.Day == 1) {
time_t yesterday = now-3600;
breakTime(yesterday, tm);
if(tm.Day == 29) {
month.d28 = val;
} else if(tm.Day == 30) {
month.d29 = val;
} else if(tm.Day == 31) {
month.d30 = val;
}
} else if(tm.Day == 2) {
month.d01 = val;
} else if(tm.Day == 3) {
month.d02 = val;
} else if(tm.Day == 4) {
month.d03 = val;
} else if(tm.Day == 5) {
month.d04 = val;
} else if(tm.Day == 6) {
month.d05 = val;
} else if(tm.Day == 7) {
month.d06 = val;
} else if(tm.Day == 8) {
month.d07 = val;
} else if(tm.Day == 9) {
month.d08 = val;
} else if(tm.Day == 10) {
month.d09 = val;
} else if(tm.Day == 11) {
month.d10 = val;
} else if(tm.Day == 12) {
month.d11 = val;
} else if(tm.Day == 13) {
month.d12 = val;
} else if(tm.Day == 14) {
month.d13 = val;
} else if(tm.Day == 15) {
month.d14 = val;
} else if(tm.Day == 16) {
month.d15 = val;
} else if(tm.Day == 17) {
month.d16 = val;
} else if(tm.Day == 18) {
month.d17 = val;
} else if(tm.Day == 19) {
month.d18 = val;
} else if(tm.Day == 20) {
month.d19 = val;
} else if(tm.Day == 21) {
month.d20 = val;
} else if(tm.Day == 22) {
month.d21 = val;
} else if(tm.Day == 23) {
month.d22 = val;
} else if(tm.Day == 24) {
month.d23 = val;
} else if(tm.Day == 25) {
month.d24 = val;
} else if(tm.Day == 26) {
month.d25 = val;
} else if(tm.Day == 27) {
month.d26 = val;
} else if(tm.Day == 28) {
month.d27 = val;
} else if(tm.Day == 29) {
month.d28 = val;
} else if(tm.Day == 30) {
month.d29 = val;
} else if(tm.Day == 31) {
month.d30 = val;
}
month.activeImport = data->getActiveImportCounter()*1000;
month.activeExport = data->getActiveExportCounter()*1000;
}
return true;
}
DayDataPoints AmsDataStorage::getDayDataPoints() {
return day;
}
MonthDataPoints AmsDataStorage::getMonthDataPoints() {
return month;
}
bool AmsDataStorage::load(AmsData* meterState) {
if(!LittleFS.begin()) {
printE("Unable to load LittleFS");
return false;
}
bool ret = false;
if(LittleFS.exists(FILE_DAYPLOT)) {
File file = LittleFS.open(FILE_DAYPLOT, "r");
char buf[file.size()];
file.readBytes(buf, file.size());
DayDataPoints* day = (DayDataPoints*) buf;
file.close();
if(day->version == 3) {
memcpy(&this->day, day, sizeof(this->day));
ret = true;
} else {
ret = false;
}
}
if(LittleFS.exists(FILE_MONTHPLOT)) {
File file = LittleFS.open(FILE_MONTHPLOT, "r");
char buf[file.size()];
file.readBytes(buf, file.size());
MonthDataPoints* month = (MonthDataPoints*) buf;
file.close();
if(month->version == 3) { // dev-1.6
month->d25 = month->d26;
month->d26 = month->d27;
month->d27 = month->d28;
month->d28 = month->d29;
month->d29 = month->d30;
month->d30 = month->d31;
month->version = 4;
}
if(month->version == 4) {
memcpy(&this->month, month, sizeof(this->month));
ret = true;
} else {
ret = false;
}
}
LittleFS.end();
return ret;
}
bool AmsDataStorage::save() {
if(!LittleFS.begin()) {
printE("Unable to load LittleFS");
return false;
}
{
File file = LittleFS.open(FILE_DAYPLOT, "w");
char buf[sizeof(day)];
memcpy(buf, &day, sizeof(day));
for(int i = 0; i < sizeof(day); i++) {
file.write(buf[i]);
}
file.close();
}
{
File file = LittleFS.open(FILE_MONTHPLOT, "w");
char buf[sizeof(month)];
memcpy(buf, &month, sizeof(month));
for(int i = 0; i < sizeof(month); i++) {
file.write(buf[i]);
}
file.close();
}
LittleFS.end();
return true;
}
void AmsDataStorage::printD(String fmt, ...) {
va_list args;
va_start(args, fmt);
if(debugger->isActive(RemoteDebug::DEBUG)) debugger->printf(String("(AmsDataStorage)" + fmt + "\n").c_str(), args);
va_end(args);
}
void AmsDataStorage::printI(String fmt, ...) {
va_list args;
va_start(args, fmt);
if(debugger->isActive(RemoteDebug::INFO)) debugger->printf(String("(AmsDataStorage)" + fmt + "\n").c_str(), args);
va_end(args);
}
void AmsDataStorage::printW(String fmt, ...) {
va_list args;
va_start(args, fmt);
if(debugger->isActive(RemoteDebug::WARNING)) debugger->printf(String("(AmsDataStorage)" + fmt + "\n").c_str(), args);
va_end(args);
}
void AmsDataStorage::printE(String fmt, ...) {
va_list args;
va_start(args, fmt);
if(debugger->isActive(RemoteDebug::ERROR)) debugger->printf(String("(AmsDataStorage)" + fmt + "\n").c_str(), args);
va_end(args);
}

101
src/AmsDataStorage.h Normal file
View File

@@ -0,0 +1,101 @@
#ifndef _AMSDATASTORAGE_H
#define _AMSDATASTORAGE_H
#include "Arduino.h"
#include "AmsData.h"
#include "RemoteDebug.h"
#include "Timezone.h"
#define EPOCH_2021_01_01 1609459200
struct DayDataPoints {
uint8_t version;
int16_t h00;
int16_t h01;
int16_t h02;
int16_t h03;
int16_t h04;
int16_t h05;
int16_t h06;
int16_t h07;
int16_t h08;
int16_t h09;
int16_t h10;
int16_t h11;
int16_t h12;
int16_t h13;
int16_t h14;
int16_t h15;
int16_t h16;
int16_t h17;
int16_t h18;
int16_t h19;
int16_t h20;
int16_t h21;
int16_t h22;
int16_t h23;
time_t lastMeterReadTime;
uint32_t activeImport;
uint32_t activeExport;
}; // 37 bytes
struct MonthDataPoints {
uint8_t version;
int16_t d01;
int16_t d02;
int16_t d03;
int16_t d04;
int16_t d05;
int16_t d06;
int16_t d07;
int16_t d08;
int16_t d09;
int16_t d10;
int16_t d11;
int16_t d12;
int16_t d13;
int16_t d14;
int16_t d15;
int16_t d16;
int16_t d17;
int16_t d18;
int16_t d19;
int16_t d20;
int16_t d21;
int16_t d22;
int16_t d23;
int16_t d24;
int16_t d25;
int16_t d26;
int16_t d27;
int16_t d28;
int16_t d29;
int16_t d30;
int16_t d31;
time_t lastMeterReadTime;
uint32_t activeImport;
uint32_t activeExport;
}; // 75 bytes
class AmsDataStorage {
public:
AmsDataStorage(RemoteDebug*);
void setTimezone(Timezone*);
bool update(AmsData*);
DayDataPoints getDayDataPoints();
MonthDataPoints getMonthDataPoints();
bool load(AmsData*);
bool save();
private:
Timezone* tz;
DayDataPoints day;
MonthDataPoints month;
RemoteDebug* debugger;
void printD(String fmt, ...);
void printI(String fmt, ...);
void printW(String fmt, ...);
void printE(String fmt, ...);
};
#endif

View File

@@ -7,4 +7,7 @@
#define FILE_MQTT_CERT "/mqtt-cert.pem"
#define FILE_MQTT_KEY "/mqtt-key.pem"
#define FILE_DAYPLOT "/dayplot.bin"
#define FILE_MONTHPLOT "/monthplot.bin"
#endif

View File

@@ -5,8 +5,6 @@
#define INVALID_BUTTON_PIN 0xFFFFFFFF
#define EPOCH_2021_01_01 1609459200
#define MAX_PEM_SIZE 4096
#include <SoftwareSerial.h>

View File

@@ -18,14 +18,11 @@
#include "AmsToMqttBridge.h"
#include "AmsStorage.h"
#include "AmsDataStorage.h"
#include <MQTT.h>
#include <DNSServer.h>
#include <lwip/apps/sntp.h>
#if defined(ESP8266)
ADC_MODE(ADC_VCC);
#endif
#include "HwTools.h"
#include "entsoe/EntsoeApi.h"
@@ -74,6 +71,8 @@ String topic = "ams";
AmsData meterState;
bool ntpEnabled = false;
AmsDataStorage ds(&Debug);
void setup() {
WiFiConfig wifi;
Serial.begin(115200);
@@ -143,7 +142,7 @@ void setup() {
SerialConfig serialConfig;
#elif defined(ESP32)
uint32_t serialConfig;
#endif;
#endif
switch(meterConfig.parity) {
case 2:
serialConfig = SERIAL_7N1;
@@ -186,10 +185,10 @@ void setup() {
}
float vccBootLimit = gpioConfig.vccBootLimit == 0 ? 0 : gpioConfig.vccBootLimit / 10.0;
if(vccBootLimit > 0 && (gpioConfig.apPin == 0xFF || digitalRead(gpioConfig.apPin) == HIGH)) { // Skip if user is holding AP button while booting (HIGH = button is released)
if(vccBootLimit > 2.5 && vccBootLimit < 3.3 && (gpioConfig.apPin == 0xFF || digitalRead(gpioConfig.apPin) == HIGH)) { // Skip if user is holding AP button while booting (HIGH = button is released)
if (vcc < vccBootLimit) {
if(Debug.isActive(RemoteDebug::INFO)) {
debugI("Voltage is too low, sleeping");
Debug.printf("(setup) Voltage is too low (%.2f < %.2f), sleeping\n", vcc, vccBootLimit);
Serial.flush();
}
ESP.deepSleep(10000000); //Deep sleep to allow output cap to charge up
@@ -214,7 +213,7 @@ void setup() {
if(hasFs) {
bool flashed = false;
if(LittleFS.exists(FILE_FIRMWARE)) {
if (digitalRead(gpioConfig.apPin) == HIGH) {
if (gpioConfig.apPin == 0xFF || digitalRead(gpioConfig.apPin) == HIGH) {
if(Debug.isActive(RemoteDebug::INFO)) debugI("Found firmware");
#if defined(ESP8266)
WiFi.setSleepMode(WIFI_LIGHT_SLEEP);
@@ -286,7 +285,10 @@ void setup() {
TimeChangeRule dst = {"DST", Last, Sun, Mar, 2, (ntp.offset + ntp.summerOffset) / 6};
tz = new Timezone(dst, std);
ws.setTimezone(tz);
ds.setTimezone(tz);
}
ds.load(&meterState);
} else {
if(Debug.isActive(RemoteDebug::INFO)) {
debugI("No configuration, booting AP");
@@ -294,7 +296,7 @@ void setup() {
swapWifiMode();
}
ws.setup(&config, &gpioConfig, &meterConfig, &meterState, &mqtt);
ws.setup(&config, &gpioConfig, &meterConfig, &meterState, &ds, &mqtt);
}
int buttonTimer = 0;
@@ -472,7 +474,7 @@ void loop() {
}
void setupHanPort(uint8_t pin, uint32_t baud, uint8_t parityOrdinal, bool invert) {
debugI("Setting up HAN on pin %d with baud %d and parity %d", pin, baud, parityOrdinal);
if(Debug.isActive(RemoteDebug::INFO)) Debug.printf("(setupHanPort) Setting up HAN on pin %d with baud %d and parity %d\n", pin, baud, parityOrdinal);
HardwareSerial *hwSerial = NULL;
if(pin == 3) {
@@ -630,6 +632,7 @@ void mqttMessageReceived(String &topic, String &payload)
// Ideas could be to query for values or to initiate OTA firmware update
}
int len = 0;
uint8_t buf[BUF_SIZE];
HDLCConfig* hc = NULL;
int currentMeterType = -1;
@@ -648,12 +651,22 @@ void readHanPort() {
hanSerial->readBytes(buf, BUF_SIZE);
return;
}
CosemDateTime timestamp;
CosemDateTime timestamp = {0};
AmsData data;
if(currentMeterType == 1) {
size_t len = hanSerial->readBytes(buf, BUF_SIZE); // TODO: read one byte at the time. This blocks up the GUI
while(hanSerial->available()) {
buf[len++] = hanSerial->read();
}
if(len > 0) {
int pos = HDLC_validate((uint8_t *) buf, len, hc, &timestamp);
if(pos == HDLC_FRAME_INCOMPLETE) {
if(len >= BUF_SIZE) {
hanSerial->readBytes(buf, BUF_SIZE);
len = 0;
debugI("Buffer overflow, resetting");
}
return;
}
if(pos == HDLC_ENCRYPTION_CONFIG_MISSING) {
hc = new HDLCConfig();
memcpy(hc->encryption_key, meterConfig.encryptionKey, 16);
@@ -673,6 +686,7 @@ void readHanPort() {
debugPrint(hc->authentication_tag, 0, 8);
}
}
len = 0;
if(pos >= 0) {
debugI("Valid HDLC, start at %d", pos);
data = IEC6205675(((char *) (buf)) + pos, meterState.getMeterType(), timestamp);
@@ -726,6 +740,14 @@ void readHanPort() {
mqtt.loop();
delay(10);
}
if(ds.update(&data)) {
debugI("Saving day plot");
ds.save();
} else if(data.getListType() == 3) {
debugE("Unable to update day plot");
}
meterState.apply(data);
}
}

View File

@@ -1,4 +1,7 @@
#include "HwTools.h"
#if defined(ESP8266)
ADC_MODE(ADC_VCC);
#endif
void HwTools::setup(GpioConfig* config, AmsConfiguration* amsConf) {
this->config = config;
@@ -14,18 +17,35 @@ void HwTools::setup(GpioConfig* config, AmsConfiguration* amsConf) {
config->tempSensorPin = 0xFF;
}
if(config->vccPin > 0 && config->vccPin < 40) {
getAdcChannel(config->vccPin, voltAdc);
if(voltAdc.unit != 0xFF) {
#if defined(ESP32)
if(voltAdc.unit == ADC_UNIT_1) {
voltAdcChar = (esp_adc_cal_characteristics_t*) calloc(1, sizeof(esp_adc_cal_characteristics_t));
esp_adc_cal_value_t adcVal = esp_adc_cal_characterize((adc_unit_t) voltAdc.unit, ADC_ATTEN_DB_6, ADC_WIDTH_BIT_12, 1100, voltAdcChar);
adc1_config_channel_atten((adc1_channel_t) voltAdc.channel, ADC_ATTEN_DB_6);
} else if(voltAdc.unit == ADC_UNIT_2) {
voltAdcChar = (esp_adc_cal_characteristics_t*) calloc(1, sizeof(esp_adc_cal_characteristics_t));
esp_adc_cal_value_t adcVal = esp_adc_cal_characterize((adc_unit_t) voltAdc.unit, ADC_ATTEN_DB_6, ADC_WIDTH_BIT_12, 1100, voltAdcChar);
adc2_config_channel_atten((adc2_channel_t) voltAdc.channel, ADC_ATTEN_DB_6);
}
#endif
} else {
pinMode(config->vccPin, INPUT);
}
} else {
voltAdc.unit = 0xFF;
voltAdc.channel = 0xFF;
config->vccPin = 0xFF;
}
if(config->tempAnalogSensorPin > 0 && config->tempAnalogSensorPin < 40) {
pinMode(config->tempAnalogSensorPin, INPUT);
} else {
config->tempAnalogSensorPin = 0xFF;
}
if(config->vccPin > 0 && config->vccPin < 40) {
pinMode(config->vccPin, INPUT);
} else {
config->vccPin = 0xFF;
}
if(config->ledPin > 0 && config->ledPin < 40) {
pinMode(config->ledPin, OUTPUT);
ledOff(LED_INTERNAL);
@@ -55,19 +75,128 @@ void HwTools::setup(GpioConfig* config, AmsConfiguration* amsConf) {
}
}
void HwTools::getAdcChannel(uint8_t pin, AdcConfig& config) {
config.unit = 0xFF;
config.channel = 0xFF;
#if defined(ESP32)
switch(pin) {
case ADC1_CHANNEL_0_GPIO_NUM:
config.unit = ADC_UNIT_1;
config.channel = ADC1_CHANNEL_0;
break;
case ADC1_CHANNEL_1_GPIO_NUM:
config.unit = ADC_UNIT_1;
config.channel = ADC1_CHANNEL_1;
break;
case ADC1_CHANNEL_2_GPIO_NUM:
config.unit = ADC_UNIT_1;
config.channel = ADC1_CHANNEL_2;
break;
case ADC1_CHANNEL_3_GPIO_NUM:
config.unit = ADC_UNIT_1;
config.channel = ADC1_CHANNEL_3;
break;
case ADC1_CHANNEL_4_GPIO_NUM:
config.unit = ADC_UNIT_1;
config.channel = ADC1_CHANNEL_4;
break;
case ADC1_CHANNEL_5_GPIO_NUM:
config.unit = ADC_UNIT_1;
config.channel = ADC1_CHANNEL_5;
break;
case ADC1_CHANNEL_6_GPIO_NUM:
config.unit = ADC_UNIT_1;
config.channel = ADC1_CHANNEL_6;
break;
case ADC1_CHANNEL_7_GPIO_NUM:
config.unit = ADC_UNIT_1;
config.channel = ADC1_CHANNEL_7;
break;
case ADC2_CHANNEL_0_GPIO_NUM:
config.unit = ADC_UNIT_2;
config.channel = ADC2_CHANNEL_0;
break;
case ADC2_CHANNEL_1_GPIO_NUM:
config.unit = ADC_UNIT_2;
config.channel = ADC2_CHANNEL_1;
break;
case ADC2_CHANNEL_2_GPIO_NUM:
config.unit = ADC_UNIT_2;
config.channel = ADC2_CHANNEL_2;
break;
case ADC2_CHANNEL_3_GPIO_NUM:
config.unit = ADC_UNIT_2;
config.channel = ADC2_CHANNEL_3;
break;
case ADC2_CHANNEL_4_GPIO_NUM:
config.unit = ADC_UNIT_2;
config.channel = ADC2_CHANNEL_4;
break;
case ADC2_CHANNEL_5_GPIO_NUM:
config.unit = ADC_UNIT_2;
config.channel = ADC2_CHANNEL_5;
break;
case ADC2_CHANNEL_6_GPIO_NUM:
config.unit = ADC_UNIT_2;
config.channel = ADC2_CHANNEL_6;
break;
case ADC2_CHANNEL_7_GPIO_NUM:
config.unit = ADC_UNIT_2;
config.channel = ADC2_CHANNEL_7;
break;
case ADC2_CHANNEL_8_GPIO_NUM:
config.unit = ADC_UNIT_2;
config.channel = ADC2_CHANNEL_8;
break;
case ADC2_CHANNEL_9_GPIO_NUM:
config.unit = ADC_UNIT_2;
config.channel = ADC2_CHANNEL_9;
break;
}
#endif
}
double HwTools::getVcc() {
double volts = 0.0;
if(config->vccPin != 0xFF) {
#if defined(ESP8266)
volts = (analogRead(config->vccPin) / 1024.0) * 3.3;
uint32_t x = 0;
for (int i = 0; i < 10; i++) {
x += analogRead(config->vccPin);
}
volts = x / 10240;
#elif defined(ESP32)
volts = (analogRead(config->vccPin) / 4095.0) * 3.3;
if(voltAdc.unit != 0xFF) {
uint32_t x = 0;
for (int i = 0; i < 10; i++) {
if(voltAdc.unit == ADC_UNIT_1) {
x += adc1_get_raw((adc1_channel_t) voltAdc.channel);
} else if(voltAdc.unit == ADC_UNIT_2) {
int v = 0;
adc2_get_raw((adc2_channel_t) voltAdc.channel, ADC_WIDTH_BIT_12, &v);
x += v;
}
}
x = x / 10;
uint32_t voltage = esp_adc_cal_raw_to_voltage(x, voltAdcChar);
volts = voltage / 1000.0;
} else {
uint32_t x = 0;
for (int i = 0; i < 10; i++) {
x += analogRead(config->vccPin);
}
volts = x / 40950;
}
#endif
} else {
#if defined(ESP8266)
volts = ((double) ESP.getVcc()) / 1024.0;
volts = ESP.getVcc() / 1024.0;
#endif
}
if(config->vccResistorGnd > 0 && config->vccResistorVcc > 0) {
volts *= ((double) (config->vccResistorGnd + config->vccResistorVcc) / config->vccResistorGnd);
}
float vccOffset = config->vccOffset / 100.0;
float vccMultiplier = config->vccMultiplier / 1000.0;

View File

@@ -7,6 +7,9 @@
#include <ESP8266WiFi.h>
#elif defined(ESP32)
#include <WiFi.h>
#include <driver/adc.h>
#include <esp_adc_cal.h>
#include <soc/adc_channel.h>
#endif
#include <DallasTemperature.h>
@@ -26,6 +29,11 @@ struct TempSensorData {
bool changed;
};
struct AdcConfig {
uint8_t unit;
uint8_t channel;
};
class HwTools {
public:
void setup(GpioConfig*, AmsConfiguration*);
@@ -43,6 +51,10 @@ public:
HwTools() {};
private:
AdcConfig voltAdc, tempAdc;
#if defined(ESP32)
esp_adc_cal_characteristics_t* voltAdcChar, tempAdcChar;
#endif
GpioConfig* config;
AmsConfiguration* amsConf;
bool tempSensorInit;
@@ -53,6 +65,7 @@ private:
bool writeLedPin(uint8_t color, uint8_t state);
bool isSensorAddressEqual(uint8_t a[8], uint8_t b[8]);
void getAdcChannel(uint8_t pin, AdcConfig&);
};
#endif

View File

@@ -1,5 +1,6 @@
#include "IEC6205675.h"
#include "lwip/def.h"
#include "Timezone.h"
IEC6205675::IEC6205675(const char* d, uint8_t useMeterType, CosemDateTime packageTimestamp) {
uint32_t u32;
@@ -261,9 +262,21 @@ IEC6205675::IEC6205675(const char* d, uint8_t useMeterType, CosemDateTime packag
}
}
time_t ts = getTimestamp(AMS_OBIS_METER_TIMESTAMP, sizeof(AMS_OBIS_METER_TIMESTAMP), ((char *) (d)));
if(ts > 0) {
meterTimestamp = ts;
CosemData* meterTs = findObis(AMS_OBIS_METER_TIMESTAMP, sizeof(AMS_OBIS_METER_TIMESTAMP), ((char *) (d)));
if(meterTs != NULL) {
TimeChangeRule CEST = {"CEST", Last, Sun, Mar, 2, 120};
TimeChangeRule CET = {"CET ", Last, Sun, Oct, 3, 60};
Timezone tz(CEST, CET);
AmsOctetTimestamp* amst = (AmsOctetTimestamp*) meterTs;
time_t ts = getTimestamp(amst->dt);
if(meterType == AmsTypeKamstrup || meterType == AmsTypeAidon) {
this->packageTimestamp = tz.toUTC(ts);
this->meterTimestamp = tz.toUTC(ts);
Serial.printf("\nKamstrup/Aidon time: %d\n", meterTimestamp);
} else {
meterTimestamp = ts;
}
}
u32 = getUnsignedNumber(AMS_OBIS_POWER_FACTOR, sizeof(AMS_OBIS_POWER_FACTOR), ((char *) (d)));
@@ -425,7 +438,6 @@ time_t IEC6205675::getTimestamp(uint8_t* obis, int matchlength, const char* ptr)
case CosemTypeOctetString: {
if(item->oct.length == 0x0C) {
AmsOctetTimestamp* ts = (AmsOctetTimestamp*) item;
//Serial.printf("\nYear: %d, Month: %d, Day: %d, Hour: %d, Minutes %d, Second: %d, Deviation: %d\n", ntohs(ts->dt.year), ts->dt.month, ts->dt.dayOfMonth, ts->dt.hour, ts->dt.minute, ts->dt.second, ntohs(ts->dt.deviation));
return getTimestamp(ts->dt);
}
}
@@ -436,13 +448,17 @@ time_t IEC6205675::getTimestamp(uint8_t* obis, int matchlength, const char* ptr)
time_t IEC6205675::getTimestamp(CosemDateTime timestamp) {
tmElements_t tm;
tm.Year = ntohs(timestamp.year) - 1970;
uint16_t year = ntohs(timestamp.year);
if(year < 1970) return 0;
tm.Year = year - 1970;
tm.Month = timestamp.month;
tm.Day = timestamp.dayOfMonth;
tm.Hour = timestamp.hour;
tm.Minute = timestamp.minute;
tm.Second = timestamp.second;
Serial.printf("\nY: %d, M: %d, D: %d, h: %d, m: %d, s: %d, deviation: 0x%2X, status: 0x%1X\n", tm.Year, tm.Month, tm.Day, tm.Hour, tm.Minute, tm.Second, timestamp.deviation, timestamp.status);
time_t time = makeTime(tm);
int16_t deviation = ntohs(timestamp.deviation);
if(deviation >= -720 && deviation <= 720) {

View File

@@ -15,13 +15,15 @@ void mbus_hexdump(const uint8_t* buf, int len) {
printf("]\n");
}
int HDLC_validate(const uint8_t* d, int len, HDLCConfig* config, CosemDateTime* timestamp) {
int HDLC_validate(const uint8_t* d, int length, HDLCConfig* config, CosemDateTime* timestamp) {
//mbus_hexdump(d, len);
HDLCHeader* h = (HDLCHeader*) d;
// Length field (11 lsb of format)
len = (ntohs(h->format) & 0x7FF) + 2;
int len = (ntohs(h->format) & 0x7FF) + 2;
if(len > length)
return -4;
HDLCFooter* f = (HDLCFooter*) (d + len - sizeof *f);
@@ -81,11 +83,12 @@ int HDLC_validate(const uint8_t* d, int len, HDLCConfig* config, CosemDateTime*
}
ptr += 2 + dateTime->base.length;
} else if(dateTime->base.type == CosemTypeNull) {
timestamp = 0;
ptr++;
} else if(dateTime->base.type == CosemTypeDateTime) {
memcpy(timestamp, ptr, dateTime->base.length);
} else if(dateTime->base.type == 0x0C) { // Kamstrup bug...
memcpy(timestamp, ptr, dateTime->base.length);
memcpy(timestamp, ptr, 0x0C);
ptr += 13;
} else {
return -99;

View File

@@ -5,6 +5,8 @@
#include <stdint.h>
#define HDLC_FLAG 0x7E
#define HDLC_BOUNDRY_FLAG_MISSING -1
#define HDLC_FRAME_INCOMPLETE -4
#define HDLC_ENCRYPTION_CONFIG_MISSING -90
struct HDLCConfig {

View File

@@ -179,9 +179,9 @@ bool EntsoeApi::retrieve(const char* url, Stream* doc) {
client.setBufferSizes(bufSize, bufSize);
}
*/
client.setInsecure();
#endif
client.setInsecure();
HTTPClient https;
https.setFollowRedirects(HTTPC_STRICT_FOLLOW_REDIRECTS);

View File

@@ -30,9 +30,9 @@
#include "root/restart_html.h"
#include "root/restartwait_html.h"
#include "root/boot_css.h"
#include "root/gaugemeter_js.h"
#include "root/github_svg.h"
#include "root/upload_html.h"
#include "root/firmware_html.h"
#include "root/delete_html.h"
#include "root/reset_html.h"
#include "root/temperature_html.h"
@@ -41,6 +41,8 @@
#include "root/data_json.h"
#include "root/tempsensor_json.h"
#include "root/lowmem_html.h"
#include "root/dayplot_json.h"
#include "root/monthplot_json.h"
#include "base64.h"
@@ -49,11 +51,12 @@ AmsWebServer::AmsWebServer(RemoteDebug* Debug, HwTools* hw) {
this->hw = hw;
}
void AmsWebServer::setup(AmsConfiguration* config, GpioConfig* gpioConfig, MeterConfig* meterConfig, AmsData* meterState, MQTTClient* mqtt) {
void AmsWebServer::setup(AmsConfiguration* config, GpioConfig* gpioConfig, MeterConfig* meterConfig, AmsData* meterState, AmsDataStorage* ds, MQTTClient* mqtt) {
this->config = config;
this->gpioConfig = gpioConfig;
this->meterConfig = meterConfig;
this->meterState = meterState;
this->ds = ds;
this->mqtt = mqtt;
char jsuri[32];
@@ -73,9 +76,10 @@ void AmsWebServer::setup(AmsConfiguration* config, GpioConfig* gpioConfig, Meter
server.on("/domoticz",HTTP_GET, std::bind(&AmsWebServer::configDomoticzHtml, this));
server.on("/entsoe",HTTP_GET, std::bind(&AmsWebServer::configEntsoeHtml, this));
server.on("/boot.css", HTTP_GET, std::bind(&AmsWebServer::bootCss, this));
server.on("/gaugemeter.js", HTTP_GET, std::bind(&AmsWebServer::gaugemeterJs, this));
server.on("/github.svg", HTTP_GET, std::bind(&AmsWebServer::githubSvg, this));
server.on("/data.json", HTTP_GET, std::bind(&AmsWebServer::dataJson, this));
server.on("/dayplot.json", HTTP_GET, std::bind(&AmsWebServer::dayplotJson, this));
server.on("/monthplot.json", HTTP_GET, std::bind(&AmsWebServer::monthplotJson, this));
server.on("/save", HTTP_POST, std::bind(&AmsWebServer::handleSave, this));
@@ -660,13 +664,6 @@ void AmsWebServer::bootCss() {
server.send_P(200, "text/css", BOOT_CSS);
}
void AmsWebServer::gaugemeterJs() {
printD("Serving /gaugemeter.js over http...");
server.sendHeader("Cache-Control", "public, max-age=3600");
server.send_P(200, "application/javascript", GAUGEMETER_JS);
}
void AmsWebServer::githubSvg() {
printD("Serving /github.svg over http...");
@@ -782,6 +779,95 @@ void AmsWebServer::dataJson() {
server.send(200, "application/json", json);
}
void AmsWebServer::dayplotJson() {
printD("Serving /dayplot.json over http...");
DayDataPoints d = ds->getDayDataPoints();
char json[384];
snprintf_P(json, sizeof(json), DAYPLOT_JSON,
d.h00 / 100.0,
d.h01 / 100.0,
d.h02 / 100.0,
d.h03 / 100.0,
d.h04 / 100.0,
d.h05 / 100.0,
d.h06 / 100.0,
d.h07 / 100.0,
d.h08 / 100.0,
d.h09 / 100.0,
d.h10 / 100.0,
d.h11 / 100.0,
d.h12 / 100.0,
d.h13 / 100.0,
d.h14 / 100.0,
d.h15 / 100.0,
d.h16 / 100.0,
d.h17 / 100.0,
d.h18 / 100.0,
d.h19 / 100.0,
d.h20 / 100.0,
d.h21 / 100.0,
d.h22 / 100.0,
d.h23 / 100.0
);
server.sendHeader("Cache-Control", "no-cache, no-store, must-revalidate");
server.sendHeader("Pragma", "no-cache");
server.sendHeader("Expires", "-1");
server.setContentLength(strlen(json));
server.send(200, "application/json", json);
}
void AmsWebServer::monthplotJson() {
printD("Serving /monthplot.json over http...");
MonthDataPoints m = ds->getMonthDataPoints();
char json[512];
snprintf_P(json, sizeof(json), MONTHPLOT_JSON,
m.d01 / 100.0,
m.d02 / 100.0,
m.d03 / 100.0,
m.d04 / 100.0,
m.d05 / 100.0,
m.d06 / 100.0,
m.d07 / 100.0,
m.d08 / 100.0,
m.d09 / 100.0,
m.d10 / 100.0,
m.d11 / 100.0,
m.d12 / 100.0,
m.d13 / 100.0,
m.d14 / 100.0,
m.d15 / 100.0,
m.d16 / 100.0,
m.d17 / 100.0,
m.d18 / 100.0,
m.d19 / 100.0,
m.d20 / 100.0,
m.d21 / 100.0,
m.d22 / 100.0,
m.d23 / 100.0,
m.d24 / 100.0,
m.d25 / 100.0,
m.d26 / 100.0,
m.d27 / 100.0,
m.d28 / 100.0,
m.d29 / 100.0,
m.d30 / 100.0,
m.d31 / 100.0
);
server.sendHeader("Cache-Control", "no-cache, no-store, must-revalidate");
server.sendHeader("Pragma", "no-cache");
server.sendHeader("Expires", "-1");
server.setContentLength(strlen(json));
server.send(200, "application/json", json);
}
void AmsWebServer::handleSetup() {
printD("Handling setup method from http");
@@ -796,9 +882,6 @@ void AmsWebServer::handleSetup() {
config->clear();
config->clearGpio(*gpioConfig);
config->clearMeter(*meterConfig);
switch(sys.boardType) {
case 0: // roarfred
gpioConfig->hanPin = 3;
@@ -922,10 +1005,6 @@ void AmsWebServer::handleSetup() {
printD("Unable to set web config");
success = false;
}
if(!config->setMeterConfig(*meterConfig)) {
printD("Unable to set meter config");
success = false;
}
if(!config->setGpioConfig(*gpioConfig)) {
printD("Unable to set GPIO config");
success = false;
@@ -1066,6 +1145,8 @@ void AmsWebServer::handleSave() {
gpioConfig->vccOffset = server.hasArg("vccOffset") && !server.arg("vccOffset").isEmpty() ? server.arg("vccOffset").toFloat() * 100 : 0;
gpioConfig->vccMultiplier = server.hasArg("vccMultiplier") && !server.arg("vccMultiplier").isEmpty() ? server.arg("vccMultiplier").toFloat() * 1000 : 1000;
gpioConfig->vccBootLimit = server.hasArg("vccBootLimit") && !server.arg("vccBootLimit").isEmpty() ? server.arg("vccBootLimit").toFloat() * 10 : 0;
gpioConfig->vccResistorGnd = server.hasArg("vccResistorGnd") && !server.arg("vccResistorGnd").isEmpty() ? server.arg("vccResistorGnd").toInt() : 0;
gpioConfig->vccResistorVcc = server.hasArg("vccResistorVcc") && !server.arg("vccResistorVcc").isEmpty() ? server.arg("vccResistorVcc").toInt() : 0;
config->setGpioConfig(*gpioConfig);
}
@@ -1201,6 +1282,9 @@ void AmsWebServer::configGpioHtml() {
html.replace("${config.vccMultiplier}", gpioConfig->vccMultiplier > 0 ? String(gpioConfig->vccMultiplier / 1000.0, 2) : "");
html.replace("${config.vccBootLimit}", gpioConfig->vccBootLimit > 0 ? String(gpioConfig->vccBootLimit / 10.0, 1) : "");
html.replace("${config.vccResistorGnd}", gpioConfig->vccResistorGnd > 0 ? String(gpioConfig->vccResistorGnd) : "");
html.replace("${config.vccResistorVcc}", gpioConfig->vccResistorVcc > 0 ? String(gpioConfig->vccResistorVcc) : "");
server.sendHeader("Cache-Control", "no-cache, no-store, must-revalidate");
server.sendHeader("Pragma", "no-cache");
@@ -1372,7 +1456,21 @@ void AmsWebServer::firmwareHtml() {
if(!checkSecurity(1))
return;
uploadHtml("Firmware", "/firmware", "system");
server.sendHeader("Cache-Control", "no-cache, no-store, must-revalidate");
server.sendHeader("Pragma", "no-cache");
String html = String((const __FlashStringHelper*) FIRMWARE_HTML);
#if defined(ESP8266)
html.replace("{chipset}", "ESP8266");
#elif defined(ESP32)
html.replace("{chipset}", "ESP32");
#endif
server.setContentLength(html.length() + HEAD_HTML_LEN + FOOT_HTML_LEN);
server.send_P(200, "text/html", HEAD_HTML);
server.sendContent(html);
server.sendContent_P(FOOT_HTML);
}
void AmsWebServer::firmwareUpload() {
@@ -1519,7 +1617,7 @@ void AmsWebServer::restartWaitHtml() {
yield();
if(performRestart) {
LittleFS.end();
ds->save();
printI("Rebooting");
delay(1000);
#if defined(ESP8266)

View File

@@ -8,6 +8,7 @@
#include "AmsConfiguration.h"
#include "HwTools.h"
#include "AmsData.h"
#include "AmsDataStorage.h"
#include "Uptime.h"
#include "RemoteDebug.h"
#include "entsoe/EntsoeApi.h"
@@ -29,7 +30,7 @@
class AmsWebServer {
public:
AmsWebServer(RemoteDebug* Debug, HwTools* hw);
void setup(AmsConfiguration*, GpioConfig*, MeterConfig*, AmsData*, MQTTClient*);
void setup(AmsConfiguration*, GpioConfig*, MeterConfig*, AmsData*, AmsDataStorage* ds, MQTTClient*);
void loop();
void setTimezone(Timezone* tz);
void setMqttEnabled(bool);
@@ -47,6 +48,7 @@ private:
MeterConfig* meterConfig;
WebConfig webConfig;
AmsData* meterState;
AmsDataStorage* ds;
MQTTClient* mqtt;
bool uploading = false;
File file;
@@ -76,9 +78,10 @@ private:
void configGpioHtml();
void configDebugHtml();
void bootCss();
void gaugemeterJs();
void githubSvg();
void dataJson();
void dayplotJson();
void monthplotJson();
void handleSetup();
void handleSave();

View File

@@ -1,46 +1,132 @@
var nextVersion;
var im, em, vm, am;
$(function() {
im = $("#im");
if(im && im.gaugeMeter) {
im.gaugeMeter({
percent: 0,
text: "-",
append: "W"
});
}
em = $("#em");
if(em && em.gaugeMeter) {
em.gaugeMeter({
percent: 0,
text: "-",
append: "W"
});
}
vm = $("#vm");
if(vm && vm.gaugeMeter) {
vm.gaugeMeter({
percent: 0,
text: "-",
append: "V"
});
}
am = $("#am");
if(am && am.gaugeMeter) {
am.gaugeMeter({
percent: 0,
text: "-",
append: "A"
});
}
var im, em;
var meters = $('.SimpleMeter');
// Day plot
var ep;
var ea;
var eo = {
title: 'Last 24 hours',
curveType: 'function',
legend: { position: 'none' },
vAxis: {
viewWindowMode: 'maximized'
}
};
// Month plot
var mp;
var ma;
var mo = {
title: 'Last month',
curveType: 'function',
legend: { position: 'none' },
vAxis: {
viewWindowMode: 'maximized'
}
};
// Voltage plot
var vp;
var va;
var vo = {
title: 'Phase voltage',
titleTextStyle: {
fontSize: 14
},
bar: { groupWidth: '90%' },
vAxis: {
minValue: 200,
maxValue: 260,
ticks: [
{ v: 207, f: '-10%'},
{ v: 230, f: '230V'},
{ v: 253, f: '+10%'}
]
},
legend: { position: 'none' },
tooltip: { trigger: 'none'},
enableInteractivity: false,
};
// Amperage plot
var ap;
var aa;
var ao = {
title: 'Phase current',
titleTextStyle: {
fontSize: 14
},
bar: { groupWidth: '90%' },
vAxis: {
minValue: 0,
maxValue: 100,
ticks: [
{ v: 25, f: '25%'},
{ v: 50, f: '50%'},
{ v: 75, f: '75%'},
{ v: 100, f: '100%'}
]
},
legend: { position: 'none' },
tooltip: { trigger: 'none'},
enableInteractivity: false,
};
// Import plot
var ip;
var ia;
var io = {
legend: 'none',
pieHole: 0.6,
pieSliceText: 'none',
pieStartAngle: 216,
slices: {
0: { color: 'green' },
1: { color: '#eee' },
2: { color: 'transparent' }
},
legend: { position: 'none' },
tooltip: { trigger: 'none'},
enableInteractivity: false,
chartArea: {
left: 0,
top: 0,
width: '100%',
height: '100%'
}
};
// Export plot
var xp;
var xa;
var xo = {
legend: 'none',
pieHole: 0.6,
pieSliceText: 'none',
pieStartAngle: 216,
slices: {
0: { color: 'green' },
1: { color: '#eee' },
2: { color: 'transparent' }
},
legend: { position: 'none' },
tooltip: { trigger: 'none'},
enableInteractivity: false,
chartArea: {
left: 0,
top: 0,
width: '100%',
height: '100%'
}
};
$(function() {
var meters = $('.plot1');
if(meters.length > 0) {
fetch();
// Chart
google.charts.load('current', {'packages':['corechart']});
google.charts.setOnLoadCallback(setupChart);
}
// For mqtt
@@ -97,6 +183,9 @@ $(function() {
var fileName = $(this).val();
$(this).next('.custom-file-label').html(fileName);
})
$('.upload-form').on('submit', function(i, form) {
$('#loading-indicator').show();
});
// For NTP
$('#n').on('change', function() {
@@ -135,8 +224,9 @@ $(function() {
}
// Check for software upgrade
var swv = $('#swVersion')
if(meters.length > 0 && swv.length == 1 && swv.text() != "SNAPSHOT") {
var swv = $('#swVersion');
var fwl = $('#fwLink');
if((meters.length > 0 || fwl.length > 0) && swv.length == 1) {
var v = swv.text().substring(1).split('.');
var v_major = parseInt(v[0]);
var v_minor = parseInt(v[1]);
@@ -145,48 +235,66 @@ $(function() {
url: swv.data('url'),
dataType: 'json'
}).done(function(releases) {
releases.reverse();
var me;
var next_patch;
var next_minor;
var next_major;
$.each(releases, function(i, release) {
var ver2 = release.tag_name;
var v2 = ver2.substring(1).split('.');
var v2_major = parseInt(v2[0]);
var v2_minor = parseInt(v2[1]);
var v2_patch = parseInt(v2[2]);
if(!swv.text().match("^v\d{1,2}\.\d{1,2}\.\d{1,2}$")) {
nextVersion = releases[0];
} else {
releases.reverse();
var next_patch;
var next_minor;
var next_major;
$.each(releases, function(i, release) {
var ver2 = release.tag_name;
var v2 = ver2.substring(1).split('.');
var v2_major = parseInt(v2[0]);
var v2_minor = parseInt(v2[1]);
var v2_patch = parseInt(v2[2]);
if(v2_major == v_major) {
if(v2_minor == v_minor) {
if(v2_patch > v_patch) {
next_patch = release;
if(v2_major == v_major) {
if(v2_minor == v_minor) {
if(v2_patch > v_patch) {
next_patch = release;
}
} else if(v2_minor == v_minor+1) {
next_minor = release;
}
} else if(v2_minor == v_minor+1) {
next_minor = release;
}
} else if(v2_major == v_major+1) {
if(next_major) {
var mv = next_major.tag_name.substring(1).split('.');
var mv_major = parseInt(mv[0]);
var mv_minor = parseInt(mv[1]);
var mv_patch = parseInt(mv[2]);
if(v2_minor == mv_minor) {
} else if(v2_major == v_major+1) {
if(next_major) {
var mv = next_major.tag_name.substring(1).split('.');
var mv_major = parseInt(mv[0]);
var mv_minor = parseInt(mv[1]);
var mv_patch = parseInt(mv[2]);
if(v2_minor == mv_minor) {
next_major = release;
}
} else {
next_major = release;
}
} else {
next_major = release;
}
});
if(next_minor) {
nextVersion = next_minor;
} else if(next_major) {
nextVersion = next_major;
} else if(next_patch) {
nextVersion = next_patch;
}
});
if(next_minor) {
nextVersion = next_minor;
} else if(next_major) {
nextVersion = next_major;
} else if(next_patch) {
nextVersion = next_patch;
}
if(nextVersion) {
if(fwl.length > 0) {
var chipset = fwl.data('chipset').toLowerCase();
$.each(releases, function(i, release) {
if(release.tag_name == nextVersion.tag_name) {
$.each(release.assets, function(i, asset) {
if(asset.name.includes(chipset) && !asset.name.includes("partitions")) {
fwl.prop('href', asset.browser_download_url);
$('#fwDownload').show();
return false;
}
});
}
});
};
$('#newVersionTag').text(nextVersion.tag_name);
$('#newVersionUrl').prop('href', nextVersion.html_url);
$('#newVersion').removeClass('d-none');
@@ -201,6 +309,97 @@ $(function() {
}
});
var resizeTO;
$( window ).resize(function() {
if(resizeTO) clearTimeout(resizeTO);
resizeTO = setTimeout(function() {
$(this).trigger('resizeEnd');
}, 250);
});
$(window).on('resizeEnd', function() {
redraw();
});
var zeropad = function(num) {
num = num.toString();
while (num.length < 2) num = "0" + num;
return num;
}
var setupChart = function() {
ep = new google.visualization.LineChart(document.getElementById('ep'));
mp = new google.visualization.LineChart(document.getElementById('mp'));
vp = new google.visualization.ColumnChart(document.getElementById('vp'));
ap = new google.visualization.ColumnChart(document.getElementById('ap'));
ip = new google.visualization.PieChart(document.getElementById('ip'));
xp = new google.visualization.PieChart(document.getElementById('xp'));
fetch();
drawEnergy();
};
var redraw = function() {
ep.draw(ea, eo);
mp.draw(ma, mo);
vp.draw(va, vo);
ap.draw(aa, ao);
ip.draw(ia, io);
xp.draw(xa, xo);
};
var drawEnergy = function() {
$.ajax({
url: '/dayplot.json',
timeout: 30000,
dataType: 'json',
}).done(function(json) {
data = [['Hour','Value']];
var r = 1;
var hour = moment.utc().hours();
var offset = moment().utcOffset()/60;
var min = 0;
for(var i = hour; i<24; i++) {
var val = json["h"+zeropad(i)];
data[r++] = [zeropad((i+offset)%24), val];
Math.min(0, val);
};
for(var i = 0; i < hour; i++) {
var val = json["h"+zeropad(i)];
data[r++] = [zeropad((i+offset)%24), val];
Math.min(0, val);
};
ea = google.visualization.arrayToDataTable(data);
if(min == 0)
eo.vAxis.minValue = 0;
ep.draw(ea, eo);
});
$.ajax({
url: '/monthplot.json',
timeout: 30000,
dataType: 'json',
}).done(function(json) {
data = [['Day','Value']];
var r = 1;
var day = moment().date();
var start = moment().subtract(1, 'months').endOf('month').date();
var min = 0;
for(var i = day; i<=start; i++) {
var val = json["d"+zeropad(i)];
data[r++] = [zeropad((i)), val];
Math.min(0, val);
}
for(var i = 1; i < day; i++) {
var val = json["d"+zeropad(i)];
data[r++] = [zeropad((i)), val];
Math.min(0, val);
}
ma = google.visualization.arrayToDataTable(data);
if(min == 0)
mo.vAxis.minValue = 0;
mp.draw(ma, mo);
});
};
var setStatus = function(id, sid) {
var item = $('#'+id);
item.removeClass('d-none');
@@ -220,6 +419,26 @@ var setStatus = function(id, sid) {
item.addClass('badge badge-' + status);
};
var voltcol = function(pct) {
if(pct > 85) return '#d90000';
else if(pct > 75) return'#e32100';
else if(pct > 70) return '#ffb800';
else if(pct > 65) return '#dcd800';
else if(pct > 35) return '#32d900';
else if(pct > 25) return '#dcd800';
else if(pct > 20) return '#ffb800';
else if(pct > 15) return'#e32100';
else return '#d90000';
};
var ampcol = function(pct) {
if(pct > 85) return '#d90000';
else if(pct > 75) return'#e32100';
else if(pct > 70) return '#ffb800';
else if(pct > 65) return '#dcd800';
else return '#32d900';
};
var interval = 5000;
var fetch = function() {
$.ajax({
@@ -231,8 +450,7 @@ var fetch = function() {
$(".SimpleMeter").hide();
im.show();
em.show();
vm.show();
am.show();
}
for(var id in json) {
@@ -265,7 +483,7 @@ var fetch = function() {
setStatus("mqtt", json.mm);
if(im && im.gaugeMeter) {
if(ip) {
var v = parseInt(json.i);
var pct = (v*100)/parseInt(json.im);
var append = "W";
@@ -273,14 +491,20 @@ var fetch = function() {
v = (v/1000).toFixed(1);
append = "kW";
}
im.gaugeMeter({
percent: pct,
text: v,
append: append
});
$('.ipo').html(v);
$('.ipoa').html(append);
var arr = [
['Slice', 'Value'],
['', (pct*2.88)],
['', ((100-pct)*2.88)],
['', 72],
];
io.slices[0].color = ampcol(pct);
ia = google.visualization.arrayToDataTable(arr);
ip.draw(ia, io);
}
if(em && em.gaugeMeter && json.om) {
if(xp) {
var v = parseInt(json.e);
var pct = (v*100)/(parseInt(json.om)*1000);
var append = "W";
@@ -288,52 +512,78 @@ var fetch = function() {
v = (v/1000).toFixed(1);
append = "kW";
}
em.gaugeMeter({
percent: pct,
text: v,
append: append
});
$('.epo').html(v);
$('.epoa').html(append);
var arr = [
['Slice', 'Value'],
['', (pct*2.88)],
['', ((100-pct)*2.88)],
['', 72],
];
xo.slices[0].color = ampcol(pct);
xa = google.visualization.arrayToDataTable(arr);
xp.draw(xa, xo);
}
if(vm && vm.gaugeMeter) {
if(vp) {
var c = 0;
var t = 0;
var r = 1;
var arr = [['Phase', 'Voltage', { role: 'style' }, { role: 'annotation' }]];
if(json.u1) {
t += parseFloat(json.u1);
var u1 = parseFloat(json.u1);
t += u1;
c++;
var pct = (Math.max(parseFloat(json.u1)-195.5, 1)*100/69);
arr[r++] = ['L1', u1, voltcol(pct), u1 + "V"];
}
if(json.u2) {
t += parseFloat(json.u2);
var u2 = parseFloat(json.u2);
t += u2;
c++;
var pct = (Math.max(parseFloat(json.u2)-195.5, 1)*100/69);
arr[r++] = ['L2', u2, voltcol(pct), u2 + "V"];
}
if(json.u3) {
t += parseFloat(json.u3);
var u3 = parseFloat(json.u3);
t += u3;
c++;
var pct = (Math.max(parseFloat(json.u3)-195.5, 1)*100/69);
arr[r++] = ['L3', u3, voltcol(pct), u3 + "V"];
}
v = t/c;
var pct = (Math.max(v-207, 1)*100/46);
vm.gaugeMeter({
percent: pct,
text: v.toFixed(1)
});
if(v > 0) {
va = google.visualization.arrayToDataTable(arr);
vp.draw(va, vo);
}
}
if(am && am.gaugeMeter && json.mf) {
if(ap && json.mf) {
var a = 0;
var r = 1;
var arr = [['Phase', 'Amperage', { role: 'style' }, { role: 'annotation' }]];
if(json.i1) {
a = Math.max(a, parseFloat(json.i1));
var i1 = parseFloat(json.i1);
a = Math.max(a, i1);
var pct = (parseFloat(json.i1)/parseInt(json.mf))*100;
arr[r++] = ['L1', pct, ampcol(pct), i1 + "A"];
}
if(json.i2) {
a = Math.max(a, parseFloat(json.i2));
var i2 = parseFloat(json.i2);
a = Math.max(a, i2);
var pct = (parseFloat(json.i2)/parseInt(json.mf))*100;
arr[r++] = ['L2', pct, ampcol(pct), i2 + "A"];
}
if(json.i3) {
a = Math.max(a, parseFloat(json.i3));
var i3 = parseFloat(json.i3);
a = Math.max(a, i3);
var pct = (parseFloat(json.i3)/parseInt(json.mf))*100;
arr[r++] = ['L3', pct, ampcol(pct), i3 + "A"];
}
if(a > 0) {
aa = google.visualization.arrayToDataTable(arr);
ap.draw(aa, ao);
}
var pct = (a*100)/parseInt(json.mf);
am.gaugeMeter({
percent: pct,
text: a.toFixed(1)
});
}
if(json.me) {
@@ -363,6 +613,7 @@ var fetch = function() {
var upgrade = function() {
if(nextVersion) {
if(confirm("Are you sure you want to perform upgrade to " + nextVersion.tag_name + "?")) {
$('#loading-indicator').show();
window.location.href="/upgrade?version=" + nextVersion.tag_name;
}
}

26
web/dayplot.json Normal file
View File

@@ -0,0 +1,26 @@
{
"h00" : %.2f,
"h01" : %.2f,
"h02" : %.2f,
"h03" : %.2f,
"h04" : %.2f,
"h05" : %.2f,
"h06" : %.2f,
"h07" : %.2f,
"h08" : %.2f,
"h09" : %.2f,
"h10" : %.2f,
"h11" : %.2f,
"h12" : %.2f,
"h13" : %.2f,
"h14" : %.2f,
"h15" : %.2f,
"h16" : %.2f,
"h17" : %.2f,
"h18" : %.2f,
"h19" : %.2f,
"h20" : %.2f,
"h21" : %.2f,
"h22" : %.2f,
"h23" : %.2f
}

34
web/firmware.html Normal file
View File

@@ -0,0 +1,34 @@
<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.
</div>
<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.
<span id="fwDownload" style="display: none;"><br/>Download latest firmware file <a id="fwLink" href="#" data-chipset="{chipset}">here</a></span>
</div>
<form method="post" enctype="multipart/form-data" class="upload-form">
<div class="my-3 p-3 bg-white rounded shadow">
<div class="row">
<div class="col-lg-6">
<div class="input-group mb-3">
<div class="input-group-prepend">
<span class="input-group-text">Upload</span>
</div>
<div class="custom-file">
<input name="file" type="file" class="custom-file-input" id="fileUploadField">
<label class="custom-file-label" for="fileUploadField">Choose file</label>
</div>
</div>
</div>
</div>
</div>
<hr/>
<div class="row form-group">
<div class="col-6">
<a href="javascript:history.back();" class="btn btn-outline-secondary">Back</a>
</div>
<div class="col-6 text-right">
<button class="btn btn-primary">Upload</button>
</div>
</div>
</form>

View File

@@ -1,12 +1,20 @@
<div id="newVersion" class="alert alert-info d-none">New version <span id="newVersionTag"></span>! <a id="newVersionUrl" href="#" target="_blank">view</a><span class="d-none ssl-capable"> or <a href="javascript:upgrade();">upgrade</a></span></div>
<div id="newVersion" class="alert alert-info d-none">New version <span id="newVersionTag"></span>!
<a id="newVersionUrl" href="#" target="_blank">view</a>
<span class="d-none ssl-capable"> or <a href="javascript:upgrade();">upgrade</a></span>
</div>
</main>
<div id="loading-indicator" style="position: absolute; top: 0; left: 0; width: 100%; height: 100%; background-color: #dddddd99; z-index: 999999; padding-top: 20%; display: none;" class="text-center">
<div class="spinner-border text-primary" role="status" style="width: 5rem; height: 5rem;">
<span class="sr-only">Loading...</span>
</div>
</div>
<script src="https://code.jquery.com/jquery-3.4.1.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.24.0/moment.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/popper.js@1.16.0/dist/umd/popper.min.js"></script>
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/js/bootstrap.min.js"></script>
<script src="gaugemeter.js"></script>
<script src="https://www.gstatic.com/charts/loader.js"></script>
<script src="application-${version}.js"></script>
</body>

View File

@@ -1,276 +0,0 @@
/*
* AshAlom Gauge Meter. Version 2.0.0
* Copyright AshAlom.com All rights reserved.
* https://github.com/AshAlom/GaugeMeter <- Deleted!
* https://github.com/githubsrinath/GaugeMeter <- Backup original.
*
* Original created by Dr Ash Alom
*
* This is a bug fixed and modified version of the AshAlom Gauge Meter.
* Copyright 2018 Michael Wolf (Mictronics)
* https://github.com/mictronics/GaugeMeter
*
*/
!function ($) {
$.fn.gaugeMeter = function (t) {
var defaults = $.extend({
id: "",
percent: 0,
used: null,
min: null,
total: null,
size: 100,
prepend: "",
append: "",
theme: "Red-Gold-Green",
color: "",
back: "RGBa(0,0,0,.06)",
width: 3,
style: "Full",
stripe: "0",
animationstep: 1,
animate_gauge_colors: false,
animate_text_colors: false,
label: "",
label_color: "Black",
text: "",
text_size: 0.22,
fill: "",
showvalue: false
}, t);
return this.each(function () {
function getThemeColor(e) {
var t = "#2C94E0";
return e || (e = 1e-14),
"Red-Gold-Green" === option.theme && (e > 0 && (t = "#d90000"), e > 10 && (t = "#e32100"), e > 20 && (t = "#f35100"), e > 30 && (t = "#ff8700"), e > 40 && (t = "#ffb800"), e > 50 && (t = "#ffd900"), e > 60 && (t = "#dcd800"), e > 70 && (t = "#a6d900"), e > 80 && (t = "#69d900"), e > 90 && (t = "#32d900")),
"Green-Gold-Red" === option.theme && (e > 0 && (t = "#32d900"), e > 10 && (t = "#69d900"), e > 20 && (t = "#a6d900"), e > 30 && (t = "#dcd800"), e > 40 && (t = "#ffd900"), e > 50 && (t = "#ffb800"), e > 60 && (t = "#ff8700"), e > 70 && (t = "#f35100"), e > 80 && (t = "#e32100"), e > 90 && (t = "#d90000")),
"Green-Red" === option.theme && (e > 0 && (t = "#32d900"), e > 10 && (t = "#41c900"), e > 20 && (t = "#56b300"), e > 30 && (t = "#6f9900"), e > 40 && (t = "#8a7b00"), e > 50 && (t = "#a75e00"), e > 60 && (t = "#c24000"), e > 70 && (t = "#db2600"), e > 80 && (t = "#f01000"), e > 90 && (t = "#ff0000")),
"Red-Green" === option.theme && (e > 0 && (t = "#ff0000"), e > 10 && (t = "#f01000"), e > 20 && (t = "#db2600"), e > 30 && (t = "#c24000"), e > 40 && (t = "#a75e00"), e > 50 && (t = "#8a7b00"), e > 60 && (t = "#6f9900"), e > 70 && (t = "#56b300"), e > 80 && (t = "#41c900"), e > 90 && (t = "#32d900")),
"DarkBlue-LightBlue" === option.theme && (e > 0 && (t = "#2c94e0"), e > 10 && (t = "#2b96e1"), e > 20 && (t = "#2b99e4"), e > 30 && (t = "#2a9ce7"), e > 40 && (t = "#28a0e9"), e > 50 && (t = "#26a4ed"), e > 60 && (t = "#25a8f0"), e > 70 && (t = "#24acf3"), e > 80 && (t = "#23aff5"), e > 90 && (t = "#21b2f7")),
"LightBlue-DarkBlue" === option.theme && (e > 0 && (t = "#21b2f7"), e > 10 && (t = "#23aff5"), e > 20 && (t = "#24acf3"), e > 30 && (t = "#25a8f0"), e > 40 && (t = "#26a4ed"), e > 50 && (t = "#28a0e9"), e > 60 && (t = "#2a9ce7"), e > 70 && (t = "#2b99e4"), e > 80 && (t = "#2b96e1"), e > 90 && (t = "#2c94e0")),
"DarkRed-LightRed" === option.theme && (e > 0 && (t = "#d90000"), e > 10 && (t = "#dc0000"), e > 20 && (t = "#e00000"), e > 30 && (t = "#e40000"), e > 40 && (t = "#ea0000"), e > 50 && (t = "#ee0000"), e > 60 && (t = "#f30000"), e > 70 && (t = "#f90000"), e > 80 && (t = "#fc0000"), e > 90 && (t = "#ff0000")),
"LightRed-DarkRed" === option.theme && (e > 0 && (t = "#ff0000"), e > 10 && (t = "#fc0000"), e > 20 && (t = "#f90000"), e > 30 && (t = "#f30000"), e > 40 && (t = "#ee0000"), e > 50 && (t = "#ea0000"), e > 60 && (t = "#e40000"), e > 70 && (t = "#e00000"), e > 80 && (t = "#dc0000"), e > 90 && (t = "#d90000")),
"DarkGreen-LightGreen" === option.theme && (e > 0 && (t = "#32d900"), e > 10 && (t = "#33db00"), e > 20 && (t = "#34df00"), e > 30 && (t = "#34e200"), e > 40 && (t = "#36e700"), e > 50 && (t = "#37ec00"), e > 60 && (t = "#38f100"), e > 70 && (t = "#38f600"), e > 80 && (t = "#39f900"), e > 90 && (t = "#3afc00")),
"LightGreen-DarkGreen" === option.theme && (e > 0 && (t = "#3afc00"), e > 10 && (t = "#39f900"), e > 20 && (t = "#38f600"), e > 30 && (t = "#38f100"), e > 40 && (t = "#37ec00"), e > 50 && (t = "#36e700"), e > 60 && (t = "#34e200"), e > 70 && (t = "#34df00"), e > 80 && (t = "#33db00"), e > 90 && (t = "#32d900")),
"DarkGold-LightGold" === option.theme && (e > 0 && (t = "#ffb800"), e > 10 && (t = "#ffba00"), e > 20 && (t = "#ffbd00"), e > 30 && (t = "#ffc200"), e > 40 && (t = "#ffc600"), e > 50 && (t = "#ffcb00"), e > 60 && (t = "#ffcf00"), e > 70 && (t = "#ffd400"), e > 80 && (t = "#ffd600"), e > 90 && (t = "#ffd900")),
"LightGold-DarkGold" === option.theme && (e > 0 && (t = "#ffd900"), e > 10 && (t = "#ffd600"), e > 20 && (t = "#ffd400"), e > 30 && (t = "#ffcf00"), e > 40 && (t = "#ffcb00"), e > 50 && (t = "#ffc600"), e > 60 && (t = "#ffc200"), e > 70 && (t = "#ffbd00"), e > 80 && (t = "#ffba00"), e > 90 && (t = "#ffb800")),
"Voltage" === option.theme && (e <= 0 && (t = "#d90000"), e > 0 && (t = "#e32100"), e > 10 && (t = "#ffb800"), e > 20 && (t = "#dcd800"), e > 30 && (t = "#32d900"), e > 40 && (t = "#32d900"), e > 50 && (t = "#32d900"), e > 60 && (t = "#32d900"), e > 70 && (t = "#dcd800"), e > 80 && (t = "#ffb800"), e > 90 && (t = "#e32100"), e >= 100 && (t = "#d90000")),
"White" === option.theme && (t = "#fff"),
"Black" === option.theme && (t = "#000"),
t;
};
/* The label below gauge. */
function createLabel(t, a) {
if(t.children("b").length === 0){
$("<b></b>").appendTo(t).html(option.label).css({
"line-height": option.size + 5 * a + "px",
color: option.label_color
});
}
};
/* Prepend and append text, the gauge text or percentage value. */
function createSpanTag(t) {
var fgcolor = "";
if (option.animate_text_colors === true){
fgcolor = option.fgcolor;
}
var child = t.children("span");
if(child.length !== 0){
child.html(r).css({color: fgcolor});
return;
}
if(option.text_size <= 0.0 || Number.isNaN(option.text_size)){
option.text_size = 0.22;
}
if(option.text_size > 0.5){
option.text_size = 0.5;
}
$("<span></span>").appendTo(t).html(r).css({
"line-height": option.size + "px",
"font-size": option.text_size * option.size + "px",
color: fgcolor
});
};
/* Get data attributes as options from div tag. Fall back to defaults when not exists. */
function getDataAttr(t) {
$.each(dataAttr, function (index, element) {
if(t.data(element) !== undefined && t.data(element) !== null){
option[element] = t.data(element);
} else {
option[element] = $(defaults).attr(element);
}
if(element === "fill"){
s = option[element];
}
if((element === "size" ||
element === "width" ||
element === "animationstep" ||
element === "stripe"
) && !Number.isInteger(option[element])){
option[element] = parseInt(option[element]);
}
if(element === "text_size"){
option[element] = parseFloat(option[element]);
}
});
};
/* Draws the gauge. */
function drawGauge(a) {
if(M < 0) M = 0;
if(M > 100) M = 100;
var lw = option.width < 1 || isNaN(option.width) ? option.size / 20 : option.width;
g.clearRect(0, 0, b.width, b.height);
g.beginPath();
g.arc(m, v, x, G, k, !1);
if(s){
g.fillStyle = option.fill;
g.fill();
}
g.lineWidth = lw;
g.strokeStyle = option.back;
option.stripe > parseInt(0) ? g.setLineDash([option.stripe], 1) : g.lineCap = "round";
g.stroke();
g.beginPath();
g.arc(m, v, x, -I, P * a - I, !1);
g.lineWidth = lw;
g.strokeStyle = option.fgcolor;
g.stroke();
c > M && (M += z, requestAnimationFrame(function(){
drawGauge(Math.min(M, c) / 100);
}, p));
};
$(this).attr("data-id", $(this).attr("id"));
var r,
dataAttr = ["percent",
"used",
"min",
"total",
"size",
"prepend",
"append",
"theme",
"color",
"back",
"width",
"style",
"stripe",
"animationstep",
"animate_gauge_colors",
"animate_text_colors",
"label",
"label_color",
"text",
"text_size",
"fill",
"showvalue"],
option = {},
c = 0,
p = $(this),
s = false;
p.addClass("gaugeMeter");
getDataAttr(p);
if(Number.isInteger(option.used) && Number.isInteger(option.total)){
var u = option.used;
var t = option.total;
if(Number.isInteger(option.min)) {
if(option.min < 0) {
t -= option.min;
u -= option.min;
}
}
c = u / (t / 100);
} else {
if(Number.isInteger(option.percent)){
c = option.percent;
} else {
c = parseInt(defaults.percent);
}
}
if(c < 0) c = 0;
if(c > 100) c = 100;
if( option.text !== "" && option.text !== null && option.text !== undefined){
if(option.append !== "" && option.append !== null && option.append !== undefined){
r = option.text + "<u>" + option.append + "</u>";
} else {
r = option.text;
}
if(option.prepend !== "" && option.prepend !== null && option.prepend !== undefined){
r = "<s>" + option.prepend + "</s>" + r;
}
} else {
if(defaults.showvalue === true || option.showvalue === true){
r = option.used;
} else {
r = c.toString();
}
if(option.prepend !== "" && option.prepend !== null && option.prepend !== undefined){
r = "<s>" + option.prepend + "</s>" + r;
}
if(option.append !== "" && option.append !== null && option.append !== undefined){
r = r + "<u>" + option.append + "</u>";
}
}
option.fgcolor = getThemeColor(c);
if(option.color !== "" && option.color !== null && option.color !== undefined){
option.fgcolor = option.color;
}
if(option.animate_gauge_colors === true){
option.fgcolor = getThemeColor(c);
}
createSpanTag(p);
if(option.style !== "" && option.style !== null && option.style !== undefined){
createLabel(p, option.size / 13);
}
$(this).width(option.size + "px");
var b = $("<canvas></canvas>").attr({width: option.size, height: option.size}).get(0),
g = b.getContext("2d"),
m = b.width / 2,
v = b.height / 2,
_ = 360 * option.percent,
x = (_ * (Math.PI / 180), b.width / 2.5),
k = 2.3 * Math.PI,
G = 0,
M = 0 === option.animationstep ? c : 0,
z = Math.max(option.animationstep, 0),
P = 2 * Math.PI,
I = Math.PI / 2,
R = option.style;
var child = $(this).children("canvas");
if(child.length !== 0){
/* Replace existing canvas when new percentage was written. */
child.replaceWith(b);
} else {
/* Initially create canvas. */
$(b).appendTo($(this));
}
if ("Semi" === R){
k = 2 * Math.PI;
G = 3.13;
P = 1 * Math.PI;
I = Math.PI / .996;
}
if ("Arch" === R){
k = 2.195 * Math.PI;
G = 1, G = 655.99999;
P = 1.4 * Math.PI;
I = Math.PI / .8335;
}
drawGauge(M / 100);
});
};
}
(jQuery);

View File

@@ -60,6 +60,24 @@
</div>
<input name="vccPin" type="number" min="0" max="${gpio.max}" class="form-control" value="${config.vccPin}"/>
</div>
<div class="m-2 input-group input-group-sm" style="width: 200px;">
<div class="input-group-prepend">
<span class="input-group-text">GND resistor</span>
</div>
<input type="number" min="1" max="1000" step="1" class="form-control" name="vccResistorGnd" value="${config.vccResistorGnd}" />
<div class="input-group-append" title="Inverted">
<label class="input-group-text">k&ohm;</label>
</div>
</div>
<div class="m-2 input-group input-group-sm" style="width: 190px;">
<div class="input-group-prepend">
<span class="input-group-text">Vcc resistor</span>
</div>
<input type="number" min="1" max="1000" step="1" class="form-control" name="vccResistorVcc" value="${config.vccResistorVcc}" />
<div class="input-group-append" title="Inverted">
<label class="input-group-text">k&ohm;</label>
</div>
</div>
<div class="m-2 input-group input-group-sm" style="width: 140px;">
<div class="input-group-prepend">
<span class="input-group-text">Multiplier</span>
@@ -70,7 +88,7 @@
<div class="input-group-prepend">
<span class="input-group-text">Offset</span>
</div>
<input type="number" min="0.1" max="3.5" step="0.01" class="form-control" name="vccOffset" value="${config.vccOffset}" />
<input type="number" min="0.0" max="3.5" step="0.01" class="form-control" name="vccOffset" value="${config.vccOffset}" />
</div>
<div class="m-2 input-group input-group-sm" style="width: 130px;">
<div class="input-group-prepend">

View File

@@ -6,51 +6,40 @@
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/css/bootstrap.min.css">
<style>
.GaugeMeter {
position: Relative;
text-align: Center;
overflow: Hidden;
cursor: Default;
display: inline-block;
}
.GaugeMeter SPAN, .GaugeMeter B {
width: 54%;
position: Absolute;
text-align: Center;
display: Inline-Block;
color: RGBa(0,0,0,.8);
font-weight: 100;
font-family: "Open Sans", Arial;
overflow: Hidden;
white-space: NoWrap;
text-overflow: Ellipsis;
margin: 0 23%;
}
.GaugeMeter[data-style="Semi"] B {
width: 80%;
margin: 0 10%;
}
.GaugeMeter S, .GaugeMeter U {
text-decoration: None;
font-size: .60em;
font-weight: 200;
opacity: .6;
}
.GaugeMeter B {
color: #000;
font-weight: 200;
opacity: .8;
}
.navbar-expand .navbar-nav .nav-link,.navbar-brand {
padding-top: 0px;
padding-bottom: 0px;
margin-top: 0px;
margin-bottom: 0px;
}
.plot1 {
width: 100%;
height: 200px;
}
.plot2 {
width: 100%;
height: 224px;
}
.overlay-plot {
position: relative;
}
.plot-overlay {
position: absolute;
top: 70px;
left: 25%;
width: 50%;
text-align: center;
}
.ipo,.xpo {
font-size: 1.7rem;
}
.ipoa,.xpoa {
font-size: 1.0rem;
color: grey;
}
.pol {
font-size: 1.0rem;
}
</style>
</head>
<body class="bg-light">

View File

@@ -6,51 +6,40 @@
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/css/bootstrap.min.css">
<style>
.GaugeMeter {
position: Relative;
text-align: Center;
overflow: Hidden;
cursor: Default;
display: inline-block;
}
.GaugeMeter SPAN, .GaugeMeter B {
width: 54%;
position: Absolute;
text-align: Center;
display: Inline-Block;
color: RGBa(0,0,0,.8);
font-weight: 100;
font-family: "Open Sans", Arial;
overflow: Hidden;
white-space: NoWrap;
text-overflow: Ellipsis;
margin: 0 23%;
}
.GaugeMeter[data-style="Semi"] B {
width: 80%;
margin: 0 10%;
}
.GaugeMeter S, .GaugeMeter U {
text-decoration: None;
font-size: .60em;
font-weight: 200;
opacity: .6;
}
.GaugeMeter B {
color: #000;
font-weight: 200;
opacity: .8;
}
.navbar-expand .navbar-nav .nav-link,.navbar-brand {
padding-top: 0px;
padding-bottom: 0px;
margin-top: 0px;
margin-bottom: 0px;
}
.plot1 {
width: 100%;
height: 200px;
}
.plot2 {
width: 100%;
height: 224px;
}
.overlay-plot {
position: relative;
}
.plot-overlay {
position: absolute;
top: 70px;
left: 25%;
width: 50%;
text-align: center;
}
.ipo,.xpo {
font-size: 1.7rem;
}
.ipoa,.xpoa {
font-size: 1.0rem;
color: grey;
}
.pol {
font-size: 1.0rem;
}
</style>
</head>
<body class="bg-light">

View File

@@ -1,4 +1,4 @@
<div class="bg-white rounded shadow p-1">
<div class="bg-white rounded shadow p-1 mb-3">
<div class="row">
<div class="col-md-2 col-6">
<div class="text-center">Up <span class="ju">{cs}</span></div>
@@ -21,51 +21,39 @@
<div class="row">
<div class="col-lg-6">
<div class="row">
<div class="col-sm-6 mt-3">
<div class="col-sm-6 mb-3">
<div class="bg-white rounded shadow p-3">
<div class="text-center">
<div class="SimpleMeter ji" style="display: inline;">
{P} W
</div>
<div id="im" class="GaugeMeter rounded"
style="display: none;"
data-size="180px"
data-text_size="0.15"
data-width="25"
data-style="Arch"
data-theme="Green-Gold-Red"
data-animationstep="0"
data-label="{ti}"
></div>
<div class="text-center overlay-plot">
<div id="ip" class="plot1"></div>
<span class="plot-overlay">
<span class="ipo">{P}</span>
<span class="ipoa">W</span>
<br/>
<span class="pol">{ti}</span>
</span>
</div>
<div class="row ric" style="display: {da};">
<div class="col-12 text-right"><span class="jic">{tPI}</span> kWh</div>
</div>
</div>
</div>
<div class="col-sm-6 mt-3" style="display: {de};">
<div class="col-sm-6 mb-3" style="display: {de};">
<div class="bg-white rounded shadow p-3">
<div class="text-center">
<div class="SimpleMeter je" style="display: inline;">
{PO} W
</div>
<div id="em" class="GaugeMeter rounded"
style="display: none;"
data-size="180px"
data-text_size="0.15"
data-width="25"
data-style="Arch"
data-theme="DarkGreen-LightGreen"
data-animationstep="0"
data-label="Export"
></div>
<div class="text-center overlay-plot">
<div id="xp" class="plot1"></div>
<span class="plot-overlay">
<span class="xpo">{PO}</span>
<span class="xpoa">W</span>
<br/>
<span class="pol">Export</span>
</span>
</div>
<div class="row rec" style="display: {da};">
<div class="col-12 text-right"><span class="jec">{tPO}</span> kWh</div>
</div>
</div>
</div>
<div class="col-sm-6 mt-3" style="display: {dn};">
<div class="col-sm-6 mb-3" style="display: {dn};">
<div class="bg-white rounded shadow p-3" style="display: {da};">
<h5 class="text-center">Reactive</h5>
<div class="row rric">
@@ -76,7 +64,7 @@
</div>
</div>
</div>
<div class="col-sm-12 mt-3" style="display: {de};">
<div class="col-sm-12 mb-3" style="display: {de};">
<div class="bg-white rounded shadow p-3" style="display: {da};">
<div class="row rrec">
<div class="col-4 col-sm-2">In</div>
@@ -88,48 +76,26 @@
</div>
</div>
</div>
<div class="col-lg-3 col-sm-6 mt-3">
<div class="col-lg-3 col-sm-6 mb-3">
<div class="bg-white rounded shadow p-3">
<div class="text-center">
<div id="vm" class="GaugeMeter rounded"
style="display: none;"
data-size="180px"
data-text_size="0.15"
data-width="25"
data-style="Arch"
data-theme="Voltage"
data-animationstep="0"
data-label="Volt"
></div>
</div>
<div class="row ru2" style="display: {3p};">
<div class="col-4"><span class="ju1">{U1}</span>V</div>
<div class="col-4 text-center"><span class="ju2">{U2}</span>V</div>
<div class="col-4 text-right"><span class="ju3">{U3}</span>V</div>
<div id="vp" class="plot2"></div>
</div>
</div>
</div>
<div class="col-lg-3 col-sm-6 mb-3 mt-3">
<div class="col-lg-3 col-sm-6 mb-3">
<div class="bg-white rounded shadow p-3">
<div class="text-center">
<div id="am" class="GaugeMeter rounded"
style="display: none;"
data-size="180px"
data-text_size="0.15"
data-width="25"
data-style="Arch"
data-theme="Green-Gold-Red"
data-animationstep="0"
data-label="Ampere"
></div>
</div>
<div class="row ru2" style="display: {3p};">
<div class="col-4"><span class="ji1">{I1}</span>A</div>
<div class="col-4 text-center"><span class="ji2">{I2}</span>A</div>
<div class="col-4 text-right"><span class="ji3">{I3}</span>A</div>
<div id="ap" class="plot2"></div>
</div>
</div>
</div>
<div class="col-xl-12 mb-3">
<div class="bg-white rounded shadow" id="ep" style="width: 100%; height: 224px;"></div>
</div>
<div class="col-xl-12 mb-3">
<div class="bg-white rounded shadow" id="mp" style="width: 100%; height: 224px;"></div>
</div>
<div class="col-lg-3 col-sm-6 mb-3 d-none me me-1 me-2 me-3 me-4 me-5 me-6 me-7 me-8 me-9 me-10 me-11 me-12 me-13">
<div class="d-none badge badge-danger me me-1 me-2 me-5 me-6 me-7 me-8 me-9 me-12">MQTT communication error (<span id="ml">-</span>)</div>
<div class="d-none badge badge-danger me me-3">MQTT failed to connect</div>
@@ -139,3 +105,4 @@
<div class="d-none badge badge-danger me me-13">MQTT lost connection</div>
</div>
</div>

View File

@@ -80,6 +80,8 @@
</div>
<select class="form-control" name="f">
<option value="0" {f0}></option>
<option value="16" {f16}>16A</option>
<option value="20" {f20}>20A</option>
<option value="25" {f25}>25A</option>
<option value="32" {f32}>32A</option>
<option value="35" {f35}>35A</option>

33
web/monthplot.json Normal file
View File

@@ -0,0 +1,33 @@
{
"d01" : %.2f,
"d02" : %.2f,
"d03" : %.2f,
"d04" : %.2f,
"d05" : %.2f,
"d06" : %.2f,
"d07" : %.2f,
"d08" : %.2f,
"d09" : %.2f,
"d10" : %.2f,
"d11" : %.2f,
"d12" : %.2f,
"d13" : %.2f,
"d14" : %.2f,
"d15" : %.2f,
"d16" : %.2f,
"d17" : %.2f,
"d18" : %.2f,
"d19" : %.2f,
"d20" : %.2f,
"d21" : %.2f,
"d22" : %.2f,
"d23" : %.2f,
"d24" : %.2f,
"d25" : %.2f,
"d26" : %.2f,
"d27" : %.2f,
"d28" : %.2f,
"d29" : %.2f,
"d30" : %.2f,
"d31" : %.2f
}

View File

@@ -1,4 +1,4 @@
<form method="post" enctype="multipart/form-data">
<form method="post" enctype="multipart/form-data" class="upload-form">
<div class="my-3 p-3 bg-white rounded shadow">
<div class="row">
<div class="col-lg-6">