Compare commits

...

18 Commits

Author SHA1 Message Date
Gunnar Skjold
e232b875fa Fixed Aidon timestamp 2022-08-23 21:07:06 +02:00
Gunnar Skjold
f18171fecc Fixed MQTT byte mode 2022-08-23 20:39:47 +02:00
Gunnar Skjold
a3c7a09211 Adding peaks to data.json and MQTT RAW 2022-08-23 20:21:19 +02:00
Gunnar Skjold
a6f3bc3f71 Added version to system json 2022-08-23 19:49:19 +02:00
Gunnar Skjold
4e451c51e1 HW serial fix for S2 2022-08-17 17:33:06 +02:00
Gunnar Skjold
43def1c311 Fixed Pow-P1 profile 2022-08-17 17:32:53 +02:00
Gunnar Skjold
2978116207 Fixed formatting error in HA payload 2022-08-16 11:55:07 +02:00
Gunnar Skjold
998b986604 Adjusted modem sleep to a safer value 2022-08-12 20:48:34 +02:00
Gunnar Skjold
7799431405 Remove unused files 2022-08-12 20:42:13 +02:00
Gunnar Skjold
508c14a671 Fixed modem sleep on S2 and fixed default wifi power 2022-08-12 09:02:04 +02:00
Gunnar Skjold
4a7ef87269 Fixed realtime day calculation 2022-08-10 17:43:51 +02:00
Gunnar Skjold
3a4fc707b0 Show real time production 2022-08-10 10:53:16 +02:00
Gunnar Skjold
5d2e320b07 Fixed export labels on graph 2022-08-10 10:28:31 +02:00
Gunnar Skjold
d12613b91a Trying to fix HA accumulated 2022-08-10 10:01:24 +02:00
Gunnar Skjold
e6a02f34ab USB power warning when upgrading 2022-08-10 09:31:50 +02:00
Gunnar Skjold
0b2ffbfd77 Revert to arduino 2.0.3 because of #298 2022-08-10 09:30:31 +02:00
Gunnar Skjold
cab6c54ed9 Correct Vcc GPIO for Pow-K+ 2022-08-01 12:48:31 +02:00
Gunnar Skjold
313024f273 Support OTA upgrade from ESP8266 2022-08-01 08:17:24 +02:00
22 changed files with 285 additions and 170 deletions

View File

@@ -17,8 +17,10 @@ extra_scripts =
pre:scripts/addversion.py pre:scripts/addversion.py
scripts/makeweb.py scripts/makeweb.py
# Sticking to v2.0.3 because of #298
[env:esp32] [env:esp32]
platform = https://github.com/tasmota/platform-espressif32/releases/download/v2.0.4/platform-espressif32-2.0.4.zip platform = https://github.com/tasmota/platform-espressif32/releases/download/v2.0.3/platform-espressif32-2.0.3.zip
framework = arduino framework = arduino
board = esp32dev board = esp32dev
board_build.f_cpu = 160000000L board_build.f_cpu = 160000000L
@@ -33,8 +35,8 @@ extra_scripts =
# https://github.com/Jason2866/esp32-arduino-lib-builder # https://github.com/Jason2866/esp32-arduino-lib-builder
[env:esp32s2] [env:esp32s2]
platform = https://github.com/tasmota/platform-espressif32/releases/download/v2.0.4/platform-espressif32-2.0.4.zip platform = https://github.com/tasmota/platform-espressif32/releases/download/v2.0.3/platform-espressif32-2.0.3.zip
platform_packages = framework-arduinoespressif32 @ https://github.com/espressif/arduino-esp32.git#2.0.4 platform_packages = framework-arduinoespressif32 @ https://github.com/espressif/arduino-esp32.git#2.0.3
framework = arduino framework = arduino
board = esp32dev board = esp32dev
board_build.mcu = esp32s2 board_build.mcu = esp32s2
@@ -50,7 +52,7 @@ extra_scripts =
scripts/makeweb.py scripts/makeweb.py
[env:esp32solo] [env:esp32solo]
platform = https://github.com/tasmota/platform-espressif32/releases/download/v2.0.4/platform-espressif32-solo1-2.0.4.zip platform = https://github.com/tasmota/platform-espressif32/releases/download/v.2.0.3/platform-espressif32-solo1-v.2.0.3.zip
framework = arduino framework = arduino
board = esp32dev board = esp32dev
board_build.f_cpu = 160000000L board_build.f_cpu = 160000000L

View File

@@ -63,8 +63,10 @@ void AmsConfiguration::clearWifi(WiFiConfig& config) {
uint16_t chipId; uint16_t chipId;
#if defined(ESP32) #if defined(ESP32)
chipId = ESP.getEfuseMac(); chipId = ESP.getEfuseMac();
config.power = 195;
#else #else
chipId = ESP.getChipId(); chipId = ESP.getChipId();
config.power = 205;
#endif #endif
strcpy(config.hostname, (String("ams-") + String(chipId, HEX)).c_str()); strcpy(config.hostname, (String("ams-") + String(chipId, HEX)).c_str());
config.mdns = true; config.mdns = true;

View File

@@ -32,6 +32,10 @@ ADC_MODE(ADC_VCC);
#endif #endif
#define WDT_TIMEOUT 60 #define WDT_TIMEOUT 60
#if defined(CONFIG_IDF_TARGET_ESP32S2)
#include <driver/uart.h>
#endif
#include "version.h" #include "version.h"
#include "AmsToMqttBridge.h" #include "AmsToMqttBridge.h"
@@ -148,6 +152,8 @@ void setup() {
gpioConfig.hanPin = 3; gpioConfig.hanPin = 3;
gpioConfig.ledPin = 2; gpioConfig.ledPin = 2;
gpioConfig.ledInverted = true; gpioConfig.ledInverted = true;
#elif defined(CONFIG_IDF_TARGET_ESP32S2)
gpioConfig.hanPin = 18;
#elif defined(ESP32) #elif defined(ESP32)
gpioConfig.hanPin = 16; gpioConfig.hanPin = 16;
gpioConfig.ledPin = 2; gpioConfig.ledPin = 2;
@@ -155,6 +161,7 @@ void setup() {
gpioConfig.tempSensorPin = 14; gpioConfig.tempSensorPin = 14;
#endif #endif
} }
delay(1); delay(1);
config.loadTempSensors(); config.loadTempSensors();
hw.setup(&gpioConfig, &config); hw.setup(&gpioConfig, &config);
@@ -616,9 +623,7 @@ void setupHanPort(uint8_t pin, uint32_t baud, uint8_t parityOrdinal, bool invert
hwSerial = &Serial2; hwSerial = &Serial2;
} }
#elif defined(CONFIG_IDF_TARGET_ESP32S2) #elif defined(CONFIG_IDF_TARGET_ESP32S2)
if(pin == 18) { hwSerial = &Serial1;
hwSerial = &Serial1;
}
#elif defined(CONFIG_IDF_TARGET_ESP32C3) #elif defined(CONFIG_IDF_TARGET_ESP32C3)
#endif #endif
#endif #endif
@@ -651,7 +656,11 @@ void setupHanPort(uint8_t pin, uint32_t baud, uint8_t parityOrdinal, bool invert
break; break;
} }
#if defined(ESP32) #if defined(CONFIG_IDF_TARGET_ESP32S2)
hwSerial->begin(baud, serialConfig, -1, -1, invert);
hwSerial->setRxBufferSize(768);
uart_set_pin(UART_NUM_1, UART_PIN_NO_CHANGE, pin, UART_PIN_NO_CHANGE, UART_PIN_NO_CHANGE);
#elif defined(ESP32)
hwSerial->begin(baud, serialConfig, -1, -1, invert); hwSerial->begin(baud, serialConfig, -1, -1, invert);
hwSerial->setRxBufferSize(768); hwSerial->setRxBufferSize(768);
#else #else
@@ -826,6 +835,7 @@ bool readHanPort() {
len += hanSerial->readBytes(hanBuffer+len, BUF_SIZE_HAN-len); len += hanSerial->readBytes(hanBuffer+len, BUF_SIZE_HAN-len);
if(mqttEnabled && mqtt != NULL && mqttHandler == NULL) { if(mqttEnabled && mqtt != NULL && mqttHandler == NULL) {
mqtt->publish(topic.c_str(), toHex(hanBuffer+pos, len)); mqtt->publish(topic.c_str(), toHex(hanBuffer+pos, len));
mqtt->loop();
} }
while(hanSerial->available()) hanSerial->read(); // Make sure it is all empty, in case we overflowed buffer above while(hanSerial->available()) hanSerial->read(); // Make sure it is all empty, in case we overflowed buffer above
len = 0; len = 0;
@@ -842,6 +852,7 @@ bool readHanPort() {
// 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(hanBuffer+pos, ctx.length));
mqtt->loop();
} }
debugV("Using application data:"); debugV("Using application data:");
@@ -1032,6 +1043,7 @@ void WiFi_connect() {
} }
#endif #endif
WiFi.mode(WIFI_STA); WiFi.mode(WIFI_STA);
WiFi.setSleep(WIFI_PS_MIN_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);
@@ -1055,6 +1067,8 @@ void WiFi_connect() {
WiFi.setTxPower(WIFI_POWER_5dBm); WiFi.setTxPower(WIFI_POWER_5dBm);
else if(wifi.power >= 20) else if(wifi.power >= 20)
WiFi.setTxPower(WIFI_POWER_2dBm); WiFi.setTxPower(WIFI_POWER_2dBm);
else
WiFi.setTxPower(WIFI_POWER_MINUS_1dBm);
#elif defined(ESP8266) #elif defined(ESP8266)
WiFi.setOutputPower(wifi.power / 10.0); WiFi.setOutputPower(wifi.power / 10.0);
#endif #endif
@@ -1163,6 +1177,7 @@ int16_t unwrapData(uint8_t *buf, DataParserContext &context) {
// 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(buf, curLen)); mqtt->publish(topic.c_str(), toHex(buf, curLen));
mqtt->loop();
} }
break; break;
case DATA_TAG_MBUS: case DATA_TAG_MBUS:
@@ -1170,6 +1185,7 @@ int16_t unwrapData(uint8_t *buf, DataParserContext &context) {
// 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(buf, curLen)); mqtt->publish(topic.c_str(), toHex(buf, curLen));
mqtt->loop();
} }
break; break;
case DATA_TAG_GBT: case DATA_TAG_GBT:
@@ -1188,6 +1204,7 @@ int16_t unwrapData(uint8_t *buf, DataParserContext &context) {
debugV("DSMR frame:"); debugV("DSMR frame:");
if(mqttEnabled && mqtt != NULL && mqttHandler == NULL) { if(mqttEnabled && mqtt != NULL && mqttHandler == NULL) {
mqtt->publish(topic.c_str(), (char*) buf); mqtt->publish(topic.c_str(), (char*) buf);
mqtt->loop();
} }
break; break;
} }

View File

@@ -80,6 +80,7 @@ bool EnergyAccounting::update(AmsData* amsData) {
} }
use = 0; use = 0;
produce = 0;
costHour = 0; costHour = 0;
currentHour = local.Hour; currentHour = local.Hour;
@@ -106,19 +107,24 @@ bool EnergyAccounting::update(AmsData* amsData) {
} }
unsigned long ms = this->lastUpdateMillis > amsData->getLastUpdateMillis() ? 0 : amsData->getLastUpdateMillis() - this->lastUpdateMillis; unsigned long ms = this->lastUpdateMillis > amsData->getLastUpdateMillis() ? 0 : amsData->getLastUpdateMillis() - this->lastUpdateMillis;
float kwh = (amsData->getActiveImportPower() * (((float) ms) / 3600000.0)) / 1000.0; float kwhi = (amsData->getActiveImportPower() * (((float) ms) / 3600000.0)) / 1000.0;
float kwhe = (amsData->getActiveExportPower() * (((float) ms) / 3600000.0)) / 1000.0;
lastUpdateMillis = amsData->getLastUpdateMillis(); lastUpdateMillis = amsData->getLastUpdateMillis();
if(kwh > 0) { if(kwhi > 0) {
if(debugger->isActive(RemoteDebug::VERBOSE)) debugger->printf("(EnergyAccounting) Adding %.4f kWh\n", kwh); if(debugger->isActive(RemoteDebug::VERBOSE)) debugger->printf("(EnergyAccounting) Adding %.4f kWh import\n", kwhi);
use += kwh; use += kwhi;
if(eapi != NULL && eapi->getValueForHour(0) != ENTSOE_NO_VALUE) { if(eapi != NULL && eapi->getValueForHour(0) != ENTSOE_NO_VALUE) {
float price = eapi->getValueForHour(0); float price = eapi->getValueForHour(0);
float cost = price * kwh; float cost = price * kwhi;
if(debugger->isActive(RemoteDebug::VERBOSE)) debugger->printf("(EnergyAccounting) and %.4f %s\n", cost / 100.0, eapi->getCurrency()); if(debugger->isActive(RemoteDebug::VERBOSE)) debugger->printf("(EnergyAccounting) and %.4f %s\n", cost / 100.0, eapi->getCurrency());
costHour += cost; costHour += cost;
costDay += cost; costDay += cost;
} }
} }
if(kwhe > 0) {
if(debugger->isActive(RemoteDebug::VERBOSE)) debugger->printf("(EnergyAccounting) Adding %.4f kWh export\n", kwhe);
produce += kwhe;
}
if(config != NULL) { if(config != NULL) {
if(debugger->isActive(RemoteDebug::VERBOSE)) debugger->printf("(EnergyAccounting) calculating threshold, currently at %d\n", currentThresholdIdx); if(debugger->isActive(RemoteDebug::VERBOSE)) debugger->printf("(EnergyAccounting) calculating threshold, currently at %d\n", currentThresholdIdx);
@@ -136,10 +142,10 @@ void EnergyAccounting::calcDayCost() {
if(eapi != NULL && eapi->getValueForHour(0) != ENTSOE_NO_VALUE) { if(eapi != NULL && eapi->getValueForHour(0) != ENTSOE_NO_VALUE) {
if(initPrice) costDay = 0; if(initPrice) costDay = 0;
for(int i = 0; i < local.Hour; i++) { for(int i = 0; i < currentHour; i++) {
float price = eapi->getValueForHour(i - local.Hour); float price = eapi->getValueForHour(i - currentHour);
if(price == ENTSOE_NO_VALUE) break; if(price == ENTSOE_NO_VALUE) break;
breakTime(now - ((local.Hour - i) * 3600), utc); breakTime(now - ((currentHour - i) * 3600), utc);
int16_t wh = ds->getHourImport(utc.Hour); int16_t wh = ds->getHourImport(utc.Hour);
costDay += price * (wh / 1000.0); costDay += price * (wh / 1000.0);
} }
@@ -151,23 +157,59 @@ double EnergyAccounting::getUseThisHour() {
return use; return use;
} }
double EnergyAccounting::getCostThisHour() {
return costHour;
}
double EnergyAccounting::getUseToday() { double EnergyAccounting::getUseToday() {
float ret = 0.0; float ret = 0.0;
time_t now = time(nullptr); time_t now = time(nullptr);
if(now < BUILD_EPOCH) return 0; if(now < BUILD_EPOCH) return 0;
tmElements_t local, utc; tmElements_t utc;
breakTime(tz->toLocal(now), local); for(int i = 0; i < currentHour; i++) {
for(int i = 0; i < local.Hour; i++) { breakTime(now - ((currentHour - i) * 3600), utc);
breakTime(now - ((local.Hour - i) * 3600), utc);
ret += ds->getHourImport(utc.Hour) / 1000.0; ret += ds->getHourImport(utc.Hour) / 1000.0;
} }
return ret + getUseThisHour(); return ret + getUseThisHour();
} }
double EnergyAccounting::getUseThisMonth() {
time_t now = time(nullptr);
if(now < BUILD_EPOCH) return 0;
float ret = 0;
for(int i = 0; i < currentDay; i++) {
ret += ds->getDayImport(i) / 1000.0;
}
return ret + getUseToday();
}
double EnergyAccounting::getProducedThisHour() {
return produce;
}
double EnergyAccounting::getProducedToday() {
float ret = 0.0;
time_t now = time(nullptr);
if(now < BUILD_EPOCH) return 0;
tmElements_t utc;
for(int i = 0; i < currentHour; i++) {
breakTime(now - ((currentHour - i) * 3600), utc);
ret += ds->getHourExport(utc.Hour) / 1000.0;
}
return ret + getProducedThisHour();
}
double EnergyAccounting::getProducedThisMonth() {
time_t now = time(nullptr);
if(now < BUILD_EPOCH) return 0;
float ret = 0;
for(int i = 0; i < currentDay; i++) {
ret += ds->getDayExport(i) / 1000.0;
}
return ret + getProducedToday();
}
double EnergyAccounting::getCostThisHour() {
return costHour;
}
double EnergyAccounting::getCostToday() { double EnergyAccounting::getCostToday() {
return costDay; return costDay;
} }
@@ -176,21 +218,6 @@ double EnergyAccounting::getCostYesterday() {
return data.costYesterday / 10.0; return data.costYesterday / 10.0;
} }
double EnergyAccounting::getUseThisMonth() {
time_t now = time(nullptr);
if(now < BUILD_EPOCH) return 0;
tmElements_t tm;
if(tz != NULL)
breakTime(tz->toLocal(now), tm);
else
breakTime(now, tm);
float ret = 0;
for(int i = 0; i < tm.Day; i++) {
ret += ds->getDayImport(i) / 1000.0;
}
return ret + getUseToday();
}
double EnergyAccounting::getCostThisMonth() { double EnergyAccounting::getCostThisMonth() {
return data.costThisMonth + getCostToday(); return data.costThisMonth + getCostToday();
} }
@@ -233,6 +260,37 @@ float EnergyAccounting::getMonthMax() {
return maxHour > 0 ? maxHour / count / 100.0 : 0.0; return maxHour > 0 ? maxHour / count / 100.0 : 0.0;
} }
float EnergyAccounting::getPeak(uint8_t num) {
if(num < 1 || num > 5) return 0.0;
uint8_t count = 0;
bool included[5] = { false, false, false, false, false };
while(count < config->hours) {
uint8_t maxIdx = 0;
uint16_t maxVal = 0;
for(uint8_t i = 0; i < 5; i++) {
if(included[i]) continue;
if(data.peaks[i].value > maxVal) {
maxVal = data.peaks[i].value;
maxIdx = i;
}
}
included[maxIdx] = true;
count++;
}
uint8_t pos = 0;
for(uint8_t i = 0; i < 5; i++) {
if(!included[i]) continue;
pos++;
if(pos == num) {
return data.peaks[i].value / 100.0;
}
}
return 0.0;
}
bool EnergyAccounting::load() { bool EnergyAccounting::load() {
if(!LittleFS.begin()) { if(!LittleFS.begin()) {
if(debugger->isActive(RemoteDebug::ERROR)) { if(debugger->isActive(RemoteDebug::ERROR)) {

View File

@@ -42,16 +42,22 @@ public:
bool save(); bool save();
double getUseThisHour(); double getUseThisHour();
double getCostThisHour();
double getUseToday(); double getUseToday();
double getUseThisMonth();
double getProducedThisHour();
double getProducedToday();
double getProducedThisMonth();
double getCostThisHour();
double getCostToday(); double getCostToday();
double getCostYesterday(); double getCostYesterday();
double getUseThisMonth();
double getCostThisMonth(); double getCostThisMonth();
uint16_t getCostLastMonth(); uint16_t getCostLastMonth();
float getMonthMax(); float getMonthMax();
uint8_t getCurrentThreshold(); uint8_t getCurrentThreshold();
float getPeak(uint8_t);
EnergyAccountingData getData(); EnergyAccountingData getData();
void setData(EnergyAccountingData&); void setData(EnergyAccountingData&);
@@ -66,6 +72,7 @@ private:
Timezone *tz = NULL; Timezone *tz = NULL;
uint8_t currentHour = 0, currentDay = 0, currentThresholdIdx = 0; uint8_t currentHour = 0, currentDay = 0, currentThresholdIdx = 0;
double use, costHour, costDay; double use, costHour, costDay;
double produce;
EnergyAccountingData data = { 0, 0, 0, 0, 0, 0 }; EnergyAccountingData data = { 0, 0, 0, 0, 0, 0 };
void calcDayCost(); void calcDayCost();

View File

@@ -266,7 +266,9 @@ IEC6205675::IEC6205675(const char* d, uint8_t useMeterType, MeterConfig* meterCo
if(meterTs != NULL) { if(meterTs != NULL) {
AmsOctetTimestamp* amst = (AmsOctetTimestamp*) meterTs; AmsOctetTimestamp* amst = (AmsOctetTimestamp*) meterTs;
time_t ts = decodeCosemDateTime(amst->dt); time_t ts = decodeCosemDateTime(amst->dt);
if(meterType == AmsTypeKamstrup || meterType == AmsTypeAidon) { if(meterType == AmsTypeAidon) {
meterTimestamp = ts - 3600;
} else if(meterType == AmsTypeKamstrup) {
meterTimestamp = tz.toUTC(ts); meterTimestamp = tz.toUTC(ts);
} else { } else {
meterTimestamp = ts; meterTimestamp = ts;

View File

@@ -24,6 +24,7 @@ bool HomeAssistantMqttHandler::publish(AmsData* data, AmsData* previousState, En
data->getMeterTimestamp() data->getMeterTimestamp()
); );
mqtt->publish(topic + "/energy", json); mqtt->publish(topic + "/energy", json);
mqtt->loop();
} }
String meterModel = data->getMeterModel(); String meterModel = data->getMeterModel();
meterModel.replace("\\", "\\\\"); meterModel.replace("\\", "\\\\");
@@ -202,7 +203,8 @@ bool HomeAssistantMqttHandler::publishSystem(HwTools* hw) {
(uint32_t) (millis64()/1000), (uint32_t) (millis64()/1000),
hw->getVcc(), hw->getVcc(),
hw->getWifiRssi(), hw->getWifiRssi(),
hw->getTemperature() hw->getTemperature(),
VERSION
); );
mqtt->publish(topic + "/state", json); mqtt->publish(topic + "/state", json);
} }

View File

@@ -1,4 +1,5 @@
#include "JsonMqttHandler.h" #include "JsonMqttHandler.h"
#include "version.h"
#include "hexutils.h" #include "hexutils.h"
#include "Uptime.h" #include "Uptime.h"
#include "web/root/json1_json.h" #include "web/root/json1_json.h"
@@ -27,7 +28,9 @@ bool JsonMqttHandler::publish(AmsData* data, AmsData* previousState, EnergyAccou
ea->getUseThisHour(), ea->getUseThisHour(),
ea->getUseToday(), ea->getUseToday(),
ea->getCurrentThreshold(), ea->getCurrentThreshold(),
ea->getMonthMax() ea->getMonthMax(),
ea->getProducedThisHour(),
ea->getProducedToday()
); );
return mqtt->publish(topic, json); return mqtt->publish(topic, json);
} else if(data->getListType() == 2) { } else if(data->getListType() == 2) {
@@ -55,7 +58,9 @@ bool JsonMqttHandler::publish(AmsData* data, AmsData* previousState, EnergyAccou
ea->getUseThisHour(), ea->getUseThisHour(),
ea->getUseToday(), ea->getUseToday(),
ea->getCurrentThreshold(), ea->getCurrentThreshold(),
ea->getMonthMax() ea->getMonthMax(),
ea->getProducedThisHour(),
ea->getProducedToday()
); );
return mqtt->publish(topic, json); return mqtt->publish(topic, json);
} else if(data->getListType() == 3) { } else if(data->getListType() == 3) {
@@ -88,7 +93,9 @@ bool JsonMqttHandler::publish(AmsData* data, AmsData* previousState, EnergyAccou
ea->getUseThisHour(), ea->getUseThisHour(),
ea->getUseToday(), ea->getUseToday(),
ea->getCurrentThreshold(), ea->getCurrentThreshold(),
ea->getMonthMax() ea->getMonthMax(),
ea->getProducedThisHour(),
ea->getProducedToday()
); );
return mqtt->publish(topic, json); return mqtt->publish(topic, json);
} else if(data->getListType() == 4) { } else if(data->getListType() == 4) {
@@ -125,7 +132,9 @@ bool JsonMqttHandler::publish(AmsData* data, AmsData* previousState, EnergyAccou
ea->getUseThisHour(), ea->getUseThisHour(),
ea->getUseToday(), ea->getUseToday(),
ea->getCurrentThreshold(), ea->getCurrentThreshold(),
ea->getMonthMax() ea->getMonthMax(),
ea->getProducedThisHour(),
ea->getProducedToday()
); );
return mqtt->publish(topic, json); return mqtt->publish(topic, json);
} }
@@ -272,7 +281,8 @@ bool JsonMqttHandler::publishSystem(HwTools* hw) {
(uint32_t) (millis64()/1000), (uint32_t) (millis64()/1000),
hw->getVcc(), hw->getVcc(),
hw->getWifiRssi(), hw->getWifiRssi(),
hw->getTemperature() hw->getTemperature(),
VERSION
); );
init = mqtt->publish(topic, json); init = mqtt->publish(topic, json);
return init; return init;

View File

@@ -74,8 +74,13 @@ bool RawMqttHandler::publish(AmsData* data, AmsData* meterState, EnergyAccountin
} }
mqtt->publish(topic + "/realtime/import/hour", String(ea->getUseThisHour(), 3)); mqtt->publish(topic + "/realtime/import/hour", String(ea->getUseThisHour(), 3));
mqtt->publish(topic + "/realtime/import/day", String(ea->getUseToday(), 2)); mqtt->publish(topic + "/realtime/import/day", String(ea->getUseToday(), 2));
for(uint8_t i = 1; i <= ea->getConfig()->hours; i++) {
mqtt->publish(topic + "/realtime/import/peak/" + String(i, 10), String(ea->getPeak(i), 10), true, 0);
}
mqtt->publish(topic + "/realtime/import/threshold", String(ea->getCurrentThreshold(), 10), true, 0); mqtt->publish(topic + "/realtime/import/threshold", String(ea->getCurrentThreshold(), 10), true, 0);
mqtt->publish(topic + "/realtime/import/monthmax", String(ea->getMonthMax(), 3), true, 0); mqtt->publish(topic + "/realtime/import/monthmax", String(ea->getMonthMax(), 3), true, 0);
mqtt->publish(topic + "/realtime/export/hour", String(ea->getProducedThisHour(), 3));
mqtt->publish(topic + "/realtime/export/day", String(ea->getProducedToday(), 2));
return true; return true;
} }

View File

@@ -591,9 +591,9 @@ void AmsWebServer::configThresholdsHtml() {
String html = String((const __FlashStringHelper*) THRESHOLDS_HTML); String html = String((const __FlashStringHelper*) THRESHOLDS_HTML);
for(int i = 0; i < 9; i++) { for(int i = 0; i < 9; i++) {
html.replace("{t" + String(i) + "}", String(config->thresholds[i])); html.replace("{t" + String(i) + "}", String(config->thresholds[i], 10));
} }
html.replace("{h}", String(config->hours)); html.replace("{h}", String(config->hours, 10));
server.setContentLength(html.length() + HEAD_HTML_LEN + FOOT_HTML_LEN); server.setContentLength(html.length() + HEAD_HTML_LEN + FOOT_HTML_LEN);
server.send_P(200, MIME_HTML, HEAD_HTML); server.send_P(200, MIME_HTML, HEAD_HTML);
@@ -708,6 +708,12 @@ void AmsWebServer::dataJson() {
if(eapi != NULL && strlen(eapi->getToken()) > 0) if(eapi != NULL && strlen(eapi->getToken()) > 0)
price = eapi->getValueForHour(0); price = eapi->getValueForHour(0);
String peaks = "";
for(uint8_t i = 1; i <= ea->getConfig()->hours; i++) {
if(!peaks.isEmpty()) peaks += ",";
peaks += String(ea->getPeak(i));
}
snprintf_P(buf, BufferSize, DATA_JSON, snprintf_P(buf, BufferSize, DATA_JSON,
maxPwr == 0 ? meterState->isThreePhase() ? 20000 : 10000 : maxPwr, maxPwr == 0 ? meterState->isThreePhase() ? 20000 : 10000 : maxPwr,
meterConfig->productionCapacity, meterConfig->productionCapacity,
@@ -744,13 +750,17 @@ void AmsWebServer::dataJson() {
meterState->getMeterType(), meterState->getMeterType(),
meterConfig->distributionSystem, meterConfig->distributionSystem,
ea->getMonthMax(), ea->getMonthMax(),
peaks.c_str(),
ea->getCurrentThreshold(), ea->getCurrentThreshold(),
ea->getUseThisHour(), ea->getUseThisHour(),
ea->getCostThisHour(), ea->getCostThisHour(),
ea->getProducedThisHour(),
ea->getUseToday(), ea->getUseToday(),
ea->getCostToday(), ea->getCostToday(),
ea->getProducedToday(),
ea->getUseThisMonth(), ea->getUseThisMonth(),
ea->getCostThisMonth(), ea->getCostThisMonth(),
ea->getProducedThisMonth(),
(uint32_t) time(nullptr) (uint32_t) time(nullptr)
); );
@@ -1036,12 +1046,12 @@ void AmsWebServer::handleSetup() {
gpioConfig->ledPinRed = 13; gpioConfig->ledPinRed = 13;
gpioConfig->ledPinGreen = 14; gpioConfig->ledPinGreen = 14;
gpioConfig->ledRgbInverted = true; gpioConfig->ledRgbInverted = true;
gpioConfig->vccPin = 35; gpioConfig->vccPin = 10;
gpioConfig->vccResistorGnd = 22; gpioConfig->vccResistorGnd = 22;
gpioConfig->vccResistorVcc = 33; gpioConfig->vccResistorVcc = 33;
break; break;
case 6: // Pow-P1 case 6: // Pow-P1
gpioConfig->hanPin = 18; gpioConfig->hanPin = 16;
gpioConfig->apPin = 0; gpioConfig->apPin = 0;
gpioConfig->ledPinRed = 13; gpioConfig->ledPinRed = 13;
gpioConfig->ledPinGreen = 14; gpioConfig->ledPinGreen = 14;
@@ -1659,7 +1669,7 @@ void AmsWebServer::firmwareUpload() {
} }
uploadFile(FILE_FIRMWARE); uploadFile(FILE_FIRMWARE);
if(upload.status == UPLOAD_FILE_END) { if(upload.status == UPLOAD_FILE_END) {
performRestart = true; rebootForUpgrade = true;
server.sendHeader("Location","/restart-wait"); server.sendHeader("Location","/restart-wait");
server.send(303); server.send(303);
} }
@@ -1672,102 +1682,9 @@ void AmsWebServer::firmwareDownload() {
return; return;
printD("Firmware download URL triggered"); printD("Firmware download URL triggered");
if(server.hasArg("version")) { performUpgrade = true;
String version = server.arg("version"); server.sendHeader("Location","/restart-wait");
String versionStripped = version.substring(1); server.send(303);
printI("Downloading firmware...");
HTTPClient httpClient;
httpClient.setFollowRedirects(HTTPC_STRICT_FOLLOW_REDIRECTS);
httpClient.setReuse(false);
httpClient.setTimeout(60000);
httpClient.setUserAgent("ams2mqtt/" + String(VERSION));
#if defined(ESP8266)
WiFiClient client;
String url = "http://ams2mqtt.no23.cc/hub/firmware/update";
server.sendHeader("Location","/");
server.send(303);
t_httpUpdate_return ret = ESPhttpUpdate.update(client, url, VERSION);
switch(ret) {
case HTTP_UPDATE_FAILED:
printE("[update] Update failed.");
break;
case HTTP_UPDATE_NO_UPDATES:
printI("[update] Update no Update.");
break;
case HTTP_UPDATE_OK:
printI("[update] Update ok.");
break;
}
return;
#elif defined(CONFIG_IDF_TARGET_ESP32S2)
httpClient.setConnectTimeout(60000);
String url = "https://github.com/gskjold/AmsToMqttBridge/releases/download/" + version + "/ams2mqtt-esp32s2-" + versionStripped + ".bin";
httpClient.addHeader("Referer", "https://github.com/gskjold/AmsToMqttBridge/releases");
#elif defined(ESP32)
httpClient.setConnectTimeout(60000);
#if defined(CONFIG_FREERTOS_UNICORE)
String url = "https://github.com/gskjold/AmsToMqttBridge/releases/download/" + version + "/ams2mqtt-esp32solo-" + versionStripped + ".bin";
#else
String url = "https://github.com/gskjold/AmsToMqttBridge/releases/download/" + version + "/ams2mqtt-esp32-" + versionStripped + ".bin";
#endif
httpClient.addHeader("Referer", "https://github.com/gskjold/AmsToMqttBridge/releases");
#endif
printD("Downloading from URL:");
printD(url);
#if defined(ESP8266)
if(httpClient.begin(client, url)) {
#elif defined(ESP32)
if(httpClient.begin(url)) {
#endif
printD("HTTP client setup successful");
int status = httpClient.GET();
if(status == HTTP_CODE_OK) {
printD("Received OK from server");
if(LittleFS.begin()) {
#if defined(ESP32)
esp_task_wdt_delete(NULL);
esp_task_wdt_deinit();
#elif defined(ESP8266)
ESP.wdtDisable();
#endif
printI("Downloading firmware to LittleFS");
file = LittleFS.open(FILE_FIRMWARE, "w");
int len = httpClient.writeToStream(&file);
file.close();
LittleFS.end();
performRestart = true;
server.sendHeader("Location","/restart-wait");
server.send(303);
} else {
printE("Unable to open LittleFS for writing");
server.sendHeader("Location","/");
server.send(303);
}
} else {
printE("Communication error: ");
debugger->printf("%d\n", status);
printE(httpClient.errorToString(status));
printD(httpClient.getString());
server.sendHeader("Location","/");
server.send(303);
}
} else {
printE("Unable to configure HTTP client");
server.sendHeader("Location","/");
server.send(303);
}
httpClient.end();
} else {
printI("No firmware version specified...");
server.sendHeader("Location","/");
server.send(303);
}
} }
void AmsWebServer::restartHtml() { void AmsWebServer::restartHtml() {
@@ -1817,6 +1734,14 @@ void AmsWebServer::restartWaitHtml() {
} }
html.replace("${hostname}", wifi.hostname); html.replace("${hostname}", wifi.hostname);
if(performUpgrade || rebootForUpgrade) {
html.replace("{rs}", "d-none");
html.replace("{us}", "");
} else {
html.replace("{rs}", "");
html.replace("{us}", "d-none");
}
server.sendHeader(HEADER_CACHE_CONTROL, CACHE_CONTROL_NO_CACHE); server.sendHeader(HEADER_CACHE_CONTROL, CACHE_CONTROL_NO_CACHE);
server.sendHeader(HEADER_PRAGMA, PRAGMA_NO_CACHE); server.sendHeader(HEADER_PRAGMA, PRAGMA_NO_CACHE);
server.sendHeader(HEADER_EXPIRES, EXPIRES_OFF); server.sendHeader(HEADER_EXPIRES, EXPIRES_OFF);
@@ -1825,7 +1750,7 @@ void AmsWebServer::restartWaitHtml() {
server.send(200, MIME_HTML, html); server.send(200, MIME_HTML, html);
yield(); yield();
if(performRestart) { if(performRestart || rebootForUpgrade) {
if(ds != NULL) { if(ds != NULL) {
ds->save(); ds->save();
} }
@@ -1837,6 +1762,40 @@ void AmsWebServer::restartWaitHtml() {
ESP.restart(); ESP.restart();
#endif #endif
performRestart = false; performRestart = false;
} else if(performUpgrade) {
WiFiClient client;
String url = "http://ams2mqtt.rewiredinvent.no/hub/firmware/update";
#if defined(ESP8266)
String chipType = "esp8266";
#elif defined(CONFIG_IDF_TARGET_ESP32S2)
String chipType = "esp32s2";
#elif defined(ESP32)
#if defined(CONFIG_FREERTOS_UNICORE)
String chipType = "esp32solo";
#else
String chipType = "esp32";
#endif
#endif
#if defined(ESP8266)
t_httpUpdate_return ret = ESPhttpUpdate.update(client, url, VERSION);
#elif defined(ESP32)
HTTPUpdate httpUpdate;
HTTPUpdateResult ret = httpUpdate.update(client, url, String(VERSION) + "-" + chipType);
#endif
switch(ret) {
case HTTP_UPDATE_FAILED:
printE("Update failed");
break;
case HTTP_UPDATE_NO_UPDATES:
printI("No Update");
break;
case HTTP_UPDATE_OK:
printI("Update OK");
break;
}
performUpgrade = false;
} }
} }

View File

@@ -24,6 +24,7 @@
#include <WiFi.h> #include <WiFi.h>
#include <WebServer.h> #include <WebServer.h>
#include <HTTPClient.h> #include <HTTPClient.h>
#include <HTTPUpdate.h>
#else #else
#warning "Unsupported board type" #warning "Unsupported board type"
#endif #endif
@@ -58,6 +59,8 @@ private:
bool uploading = false; bool uploading = false;
File file; File file;
bool performRestart = false; bool performRestart = false;
bool performUpgrade = false;
bool rebootForUpgrade = false;
static const uint16_t BufferSize = 2048; static const uint16_t BufferSize = 2048;
char* buf; char* buf;

View File

@@ -478,7 +478,7 @@ var drawDay = function() {
timeout: 30000, timeout: 30000,
dataType: 'json', dataType: 'json',
}).done(function(json) { }).done(function(json) {
data = [['Hour', 'Import', { role: 'style' }, { role: 'annotation' }, 'Export', { role: 'style' }]]; data = [['Hour', 'Import', { role: 'style' }, { role: 'annotation' }, 'Export', { role: 'style' }, { role: 'annotation' }]];
var r = 1; var r = 1;
var hour = moment.utc().hours(); var hour = moment.utc().hours();
var offset = moment().utcOffset()/60; var offset = moment().utcOffset()/60;
@@ -486,13 +486,13 @@ var drawDay = function() {
for(var i = hour; i<24; i++) { for(var i = hour; i<24; i++) {
var imp = json["i"+zeropad(i)]; var imp = json["i"+zeropad(i)];
var exp = json["e"+zeropad(i)]; var exp = json["e"+zeropad(i)];
data[r++] = [zeropad((i+offset)%24), imp, "opacity: 0.9;", exp == 0 ? imp.toFixed(1) : imp.toFixed(1) + '\n' + -exp.toFixed(1), exp == 0 ? 0 : -exp, "opacity: 0.9;"]; data[r++] = [zeropad((i+offset)%24), imp, "opacity: 0.9;", imp == 0 ? "" : imp.toFixed(1), exp == 0 ? 0 : -exp, "opacity: 0.9;", exp == 0 ? "" : -exp.toFixed(1)];
min = Math.min(0, -exp); min = Math.min(0, -exp);
}; };
for(var i = 0; i < hour; i++) { for(var i = 0; i < hour; i++) {
var imp = json["i"+zeropad(i)]; var imp = json["i"+zeropad(i)];
var exp = json["e"+zeropad(i)]; var exp = json["e"+zeropad(i)];
data[r++] = [zeropad((i+offset)%24), imp, "opacity: 0.9;", exp == 0 ? imp.toFixed(1) : imp.toFixed(1) + '\n' + -exp.toFixed(1), exp == 0 ? 0 : -exp, "opacity: 0.9;"]; data[r++] = [zeropad((i+offset)%24), imp, "opacity: 0.9;", imp == 0 ? "" : imp.toFixed(1), exp == 0 ? 0 : -exp, "opacity: 0.9;", exp == 0 ? "" : -exp.toFixed(1)];
min = Math.min(0, -exp); min = Math.min(0, -exp);
}; };
ea = google.visualization.arrayToDataTable(data); ea = google.visualization.arrayToDataTable(data);
@@ -511,7 +511,7 @@ var drawMonth = function() {
timeout: 30000, timeout: 30000,
dataType: 'json', dataType: 'json',
}).done(function(json) { }).done(function(json) {
data = [['Hour', 'Import', { role: 'style' }, { role: 'annotation' }, 'Export', { role: 'style' }]]; data = [['Hour', 'Import', { role: 'style' }, { role: 'annotation' }, 'Export', { role: 'style' }, { role: 'annotation' }]];
var r = 1; var r = 1;
var day = moment().date(); var day = moment().date();
var eom = moment().subtract(1, 'months').endOf('month').date(); var eom = moment().subtract(1, 'months').endOf('month').date();
@@ -519,13 +519,13 @@ var drawMonth = function() {
for(var i = day; i<=eom; i++) { for(var i = day; i<=eom; i++) {
var imp = json["i"+zeropad(i)]; var imp = json["i"+zeropad(i)];
var exp = json["e"+zeropad(i)]; var exp = json["e"+zeropad(i)];
data[r++] = [zeropad(i), imp, "opacity: 0.9;", exp == 0 ? imp.toFixed(0) : imp.toFixed(0) + '\n' + -exp.toFixed(0), exp == 0 ? 0 : -exp, "opacity: 0.9;"]; data[r++] = [zeropad(i), imp, "opacity: 0.9;", imp == 0 ? "" : imp.toFixed(0), exp == 0 ? 0 : -exp, "opacity: 0.9;", exp == 0 ? "" : -exp.toFixed(0)];
min = Math.min(0, -exp); min = Math.min(0, -exp);
} }
for(var i = 1; i < day; i++) { for(var i = 1; i < day; i++) {
var imp = json["i"+zeropad(i)]; var imp = json["i"+zeropad(i)];
var exp = json["e"+zeropad(i)]; var exp = json["e"+zeropad(i)];
data[r++] = [zeropad(i), imp, "opacity: 0.9;", exp == 0 ? imp.toFixed(0) : imp.toFixed(0) + '\n' + -exp.toFixed(0), exp == 0 ? 0 : -exp, "opacity: 0.9;"]; data[r++] = [zeropad(i), imp, "opacity: 0.9;", imp == 0 ? "" : imp.toFixed(0), exp == 0 ? 0 : -exp, "opacity: 0.9;", exp == 0 ? "" : -exp.toFixed(0)];
min = Math.min(0, -exp); min = Math.min(0, -exp);
} }
ma = google.visualization.arrayToDataTable(data); ma = google.visualization.arrayToDataTable(data);
@@ -790,6 +790,12 @@ var fetch = function() {
if(currency) { if(currency) {
$('.sp').show(); $('.sp').show();
} }
if(om > 0) {
$('.se').removeClass('d-none');
$('#eache').html(json.ea.h.p.toFixed(2));
$('#eacde').html(json.ea.d.p.toFixed(1));
$('#eacme').html(json.ea.m.p.toFixed(0));
}
} }
if(json.me) { if(json.me) {
@@ -878,7 +884,7 @@ var fetch = function() {
var upgrade = function() { var upgrade = function() {
if(nextVersion) { if(nextVersion) {
if(confirm("Are you sure you want to perform upgrade to " + nextVersion.tag_name + "?")) { if(confirm("WARNING: Please keep USB power connected while upgrading. Are 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

@@ -35,18 +35,22 @@
"ds" : %d, "ds" : %d,
"ea" : { "ea" : {
"x" : %.1f, "x" : %.1f,
"p" : [ %s ],
"t" : %d, "t" : %d,
"h" : { "h" : {
"u" : %.2f, "u" : %.2f,
"c" : %.2f "c" : %.2f,
"p" : %.2f
}, },
"d" : { "d" : {
"u" : %.2f, "u" : %.2f,
"c" : %.2f "c" : %.2f,
"p" : %.2f
}, },
"m" : { "m" : {
"u" : %.2f, "u" : %.2f,
"c" : %.2f "c" : %.2f,
"p" : %.2f
} }
}, },
"c" : %lu "c" : %lu

View File

@@ -1,6 +1,5 @@
<div id="newVersion" class="alert alert-info d-none">New version <span id="newVersionTag"></span>! <div id="newVersion" class="alert alert-info d-none">New version <span id="newVersionTag"></span>!
<a id="newVersionUrl" href="#" target="_blank">view</a> <a id="newVersionUrl" href="#" target="_blank">view</a> or <a href="javascript:upgrade();">click here to upgrade</a>
<span class="d-none ssl-capable"> or <a href="javascript:upgrade();">upgrade</a></span>
</div> </div>
</main> </main>

View File

@@ -3,5 +3,5 @@
"tPO" : %.2f, "tPO" : %.2f,
"tQI" : %.2f, "tQI" : %.2f,
"tQO" : %.2f, "tQO" : %.2f,
"rtc" : %llu "rtc" : %lu
} }

View File

@@ -113,7 +113,7 @@
<div class="col-xl-12 mb-3"> <div class="col-xl-12 mb-3">
<div class="bg-white rounded shadow pt-3 pb-3" style="font-size: 14px;"> <div class="bg-white rounded shadow pt-3 pb-3" style="font-size: 14px;">
<strong class="mr-3 ml-3">Real time calculation</strong><br/> <strong class="mr-3 ml-3">Real time consumption</strong><br/>
<div class="row"> <div class="row">
<div class="col-lg-3 col-sm-6"> <div class="col-lg-3 col-sm-6">
<div class="mr-3 ml-3 d-flex"> <div class="mr-3 ml-3 d-flex">
@@ -151,6 +151,35 @@
</div> </div>
</div> </div>
</div> </div>
<strong class="mr-3 ml-3 se d-none">Real time production</strong><br/>
<div class="row se d-none">
<div class="col-lg-3 col-sm-6">
<div class="mr-3 ml-3 d-flex">
<div>Hour</div>
<div class="flex-fill text-right">
<span id="eache"></span> kWh
</div>
</div>
</div>
<div class="col-lg-3 col-sm-6">
<div class="mr-3 ml-3 d-flex">
<div>Day</div>
<div class="flex-fill text-right">
<span id="eacde"></span> kWh
</div>
</div>
</div>
<div class="col-lg-3 col-sm-6">
<div class="mr-3 ml-3 d-flex">
<div>Month</div>
<div class="flex-fill text-right">
<span id="eacme"></span> kWh
</div>
</div>
</div>
<div class="col-lg-3 col-sm-6"></div>
</div>
</div> </div>
</div> </div>

View File

@@ -13,6 +13,8 @@
"h" : %.2f, "h" : %.2f,
"d" : %.1f, "d" : %.1f,
"t" : %d, "t" : %d,
"x" : %.2f "x" : %.2f,
"he" : %.2f,
"de" : %.1f
} }
} }

View File

@@ -25,6 +25,8 @@
"h" : %.2f, "h" : %.2f,
"d" : %.1f, "d" : %.1f,
"t" : %d, "t" : %d,
"x" : %.2f "x" : %.2f,
"he" : %.2f,
"de" : %.1f
} }
} }

View File

@@ -30,6 +30,8 @@
"h" : %.2f, "h" : %.2f,
"d" : %.1f, "d" : %.1f,
"t" : %d, "t" : %d,
"x" : %.2f "x" : %.2f,
"he" : %.2f,
"de" : %.1f
} }
} }

View File

@@ -34,6 +34,8 @@
"h" : %.2f, "h" : %.2f,
"d" : %.1f, "d" : %.1f,
"t" : %d, "t" : %d,
"x" : %.2f "x" : %.2f,
"he" : %.2f,
"de" : %.1f
} }
} }

View File

@@ -4,5 +4,6 @@
"up" : %d, "up" : %d,
"vcc" : %.3f, "vcc" : %.3f,
"rssi": %d, "rssi": %d,
"temp": %.2f "temp": %.2f,
"version": "%s"
} }

View File

@@ -18,9 +18,10 @@
</li> </li>
</ul> </ul>
</header> </header>
<div class="my-3 p-3 bg-white rounded shadow"> <div class="my-3 p-3 bg-white rounded shadow {rs}">
Device is rebooting. You will be redirected to the main page when it is available again. Device is rebooting. You will be redirected to the main page when it is available again.
</div> </div>
<div class="alert alert-warning shadow {us}">Firmware upgrade in progress, DO NOT disconnect power from the device. You will be redirected to the main page when firmware upgrade is complete.</div>
</main> </main>
<script> <script>
var tries = 0; var tries = 0;