Compare commits

..

4 Commits

Author SHA1 Message Date
Gunnar Skjold
bdee066c33 Fixed reboot loop 2023-01-18 21:14:40 +01:00
Gunnar Skjold
dd23a0fa60 Fixed reboot look 2023-01-18 20:48:27 +01:00
Gunnar Skjold
e8fc6d48bf Fixed issue where GPIO setup becomes invalid 2023-01-18 17:11:01 +01:00
Gunnar Skjold
4b15ac74fc Update FUNDING.yml 2023-01-17 18:05:44 +01:00
202 changed files with 824 additions and 12579 deletions

2
.github/FUNDING.yml vendored
View File

@@ -1 +1 @@
custom: ["https://paypal.me/gskjold"]
custom: ["https://amsleser.no"]

View File

@@ -8,7 +8,6 @@ on:
- scripts/**
- web/**
- platformio.ini
- .github/workflows/**
branches:
- '*'
tags:
@@ -23,12 +22,6 @@ jobs:
steps:
- name: Check out code from repo
uses: actions/checkout@v1
- name: Inject secrets into ini file
run: |
sed -i 's/NO_AMS2MQTT_PRICE_KEY/AMS2MQTT_PRICE_KEY="${{secrets.AMS2MQTT_PRICE_KEY}}"/g' platformio.ini
sed -i 's/NO_AMS2MQTT_PRICE_AUTHENTICATION/AMS2MQTT_PRICE_AUTHENTICATION="${{secrets.AMS2MQTT_PRICE_AUTHENTICATION}}"/g' platformio.ini
- name: Cache Python dependencies
uses: actions/cache@v1
with:
@@ -47,18 +40,6 @@ jobs:
run: |
python -m pip install --upgrade pip
pip install -U platformio css_html_js_minify
- name: Set up node
uses: actions/setup-node@v1
with:
node-version: '16.x'
- name: Build with node
run: |
cd lib/SvelteUi/app
npm ci
npm run build
cd -
env:
CI: true
- name: PlatformIO lib install
run: pio lib install
- name: PlatformIO run

View File

@@ -23,12 +23,6 @@ jobs:
env:
GITHUB_REF: ${{ github.ref }}
run: echo "GITHUB_TAG=$(echo ${GITHUB_REF##*/})" >> $GITHUB_ENV
- name: Inject secrets into ini file
run: |
sed -i 's/NO_AMS2MQTT_PRICE_KEY/AMS2MQTT_PRICE_KEY="${{secrets.AMS2MQTT_PRICE_KEY}}"/g' platformio.ini
sed -i 's/NO_AMS2MQTT_PRICE_AUTHENTICATION/AMS2MQTT_PRICE_AUTHENTICATION="${{secrets.AMS2MQTT_PRICE_AUTHENTICATION}}"/g' platformio.ini
- name: Cache Python dependencies
uses: actions/cache@v1
with:
@@ -47,23 +41,12 @@ jobs:
run: |
python -m pip install --upgrade pip
pip install -U platformio css_html_js_minify
- name: Set up node
uses: actions/setup-node@v1
with:
node-version: '16.x'
- name: Build with node
run: |
cd lib/SvelteUi/app
npm ci
npm run build
cd -
env:
CI: false
- name: PlatformIO lib install
run: pio lib install
- name: PlatformIO run
run: pio run
- name: Create zip files
run: /bin/sh scripts/mkzip.sh
- name: Create Release
id: create_release
uses: actions/create-release@v1.0.0
@@ -75,19 +58,6 @@ jobs:
draft: false
prerelease: false
- name: Build esp8266 firmware
run: pio run -e esp8266
- name: Create esp8266 zip file
run: /bin/sh scripts/esp8266/mkzip.sh
- name: Upload esp8266 binary to release
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ steps.create_release.outputs.upload_url }}
asset_path: .pio/build/esp8266/firmware.bin
asset_name: ams2mqtt-esp8266-${{ steps.release_tag.outputs.tag }}.bin
asset_content_type: application/octet-stream
- name: Upload esp8266 zip to release
uses: actions/upload-release-asset@v1
env:
@@ -97,20 +67,6 @@ jobs:
asset_path: esp8266.zip
asset_name: ams2mqtt-esp8266-${{ steps.release_tag.outputs.tag }}.zip
asset_content_type: application/zip
- name: Build esp32 firmware
run: pio run -e esp32
- name: Create esp32 zip file
run: /bin/sh scripts/esp32/mkzip.sh
- name: Upload esp32 binary to release
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ steps.create_release.outputs.upload_url }}
asset_path: .pio/build/esp32/firmware.bin
asset_name: ams2mqtt-esp32-${{ steps.release_tag.outputs.tag }}.bin
asset_content_type: application/octet-stream
- name: Upload esp32 zip to release
uses: actions/upload-release-asset@v1
env:
@@ -120,20 +76,6 @@ jobs:
asset_path: esp32.zip
asset_name: ams2mqtt-esp32-${{ steps.release_tag.outputs.tag }}.zip
asset_content_type: application/zip
- name: Build esp32s2 firmware
run: pio run -e esp32s2
- name: Create esp32s2 zip file
run: /bin/sh scripts/esp32s2/mkzip.sh
- name: Upload esp32s2 binary to release
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ steps.create_release.outputs.upload_url }}
asset_path: .pio/build/esp32s2/firmware.bin
asset_name: ams2mqtt-esp32s2-${{ steps.release_tag.outputs.tag }}.bin
asset_content_type: application/octet-stream
- name: Upload esp32s2 zip to release
uses: actions/upload-release-asset@v1
env:
@@ -144,10 +86,24 @@ jobs:
asset_name: ams2mqtt-esp32s2-${{ steps.release_tag.outputs.tag }}.zip
asset_content_type: application/zip
- name: Build esp32solo firmware
run: pio run -e esp32solo
- name: Create esp32solo zip file
run: /bin/sh scripts/esp32solo/mkzip.sh
- name: Upload esp8266 binary to release
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ steps.create_release.outputs.upload_url }}
asset_path: .pio/build/esp8266/firmware.bin
asset_name: ams2mqtt-esp8266-${{ steps.release_tag.outputs.tag }}.bin
asset_content_type: application/octet-stream
- name: Upload esp32 binary to release
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ steps.create_release.outputs.upload_url }}
asset_path: .pio/build/esp32/firmware.bin
asset_name: ams2mqtt-esp32-${{ steps.release_tag.outputs.tag }}.bin
asset_content_type: application/octet-stream
- name: Upload esp32solo binary to release
uses: actions/upload-release-asset@v1
env:
@@ -157,12 +113,12 @@ jobs:
asset_path: .pio/build/esp32solo/firmware.bin
asset_name: ams2mqtt-esp32solo-${{ steps.release_tag.outputs.tag }}.bin
asset_content_type: application/octet-stream
- name: Upload esp32solo zip to release
- name: Upload esp32s2 binary to release
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ steps.create_release.outputs.upload_url }}
asset_path: esp32solo.zip
asset_name: ams2mqtt-esp32solo-${{ steps.release_tag.outputs.tag }}.zip
asset_content_type: application/zip
asset_path: .pio/build/esp32s2/firmware.bin
asset_name: ams2mqtt-esp32s2-${{ steps.release_tag.outputs.tag }}.bin
asset_content_type: application/octet-stream

5
.gitignore vendored
View File

@@ -7,7 +7,7 @@
.vscode
.pio
platformio-user.ini
/lib/AmsConfiguration/include/version.h
/src/version.h
/src/web/root
/src/AmsToMqttBridge.ino.cpp
/test
@@ -15,6 +15,3 @@ platformio-user.ini
/sdkconfig
/.tmp
/*.zip
node_modules
/gui/dist
/scripts/*dev

View File

@@ -1,86 +0,0 @@
#include <Timezone.h>
#define JULY1970 15634800
TimeChangeRule TC_GMT = {"GMT", Last, Sun, Jan, 0, 0};
TimeChangeRule TC_WET = {"WET", Last, Sun, Oct, 2, 0};
TimeChangeRule TC_WEST = {"WEST", Last, Sun, Mar, 1, 60};
TimeChangeRule TC_CET = {"CET", Last, Sun, Oct, 3, 60};
TimeChangeRule TC_CEST = {"CEST", Last, Sun, Mar, 2, 120};
TimeChangeRule TC_EET = {"EET", Last, Sun, Oct, 4, 120};
TimeChangeRule TC_EEST = {"EEST", Last, Sun, Mar, 3, 180};
Timezone GMT = Timezone(TC_GMT);
Timezone WesterEuropean = Timezone(TC_WET, TC_WEST);
Timezone CentralEuropean = Timezone(TC_CET, TC_CEST);
Timezone EasternEuropean = Timezone(TC_EET, TC_EEST);
Timezone* resolveTimezone(char* name) {
if(strncmp_P(name, PSTR("Europe/"), 7) == 0) {
if(strncmp_P(name+7, PSTR("Amsterdam"), 9) == 0)
return &CentralEuropean;
if(strncmp_P(name+7, PSTR("Athens"), 6) == 0)
return &EasternEuropean;
if(strncmp_P(name+7, PSTR("Belfast"), 7) == 0)
return &WesterEuropean;
if(strncmp_P(name+7, PSTR("Berlin"), 6) == 0)
return &CentralEuropean;
if(strncmp_P(name+7, PSTR("Bratislava"), 10) == 0)
return &CentralEuropean;
if(strncmp_P(name+7, PSTR("Brussels"), 8) == 0)
return &CentralEuropean;
if(strncmp_P(name+7, PSTR("Bucharest"), 9) == 0)
return &EasternEuropean;
if(strncmp_P(name+7, PSTR("Budapest"), 8) == 0)
return &CentralEuropean;
if(strncmp_P(name+7, PSTR("Copenhagen"), 10) == 0)
return &CentralEuropean;
if(strncmp_P(name+7, PSTR("Dublin"), 6) == 0)
return &WesterEuropean;
if(strncmp_P(name+7, PSTR("Helsinki"), 8) == 0)
return &EasternEuropean;
if(strncmp_P(name+7, PSTR("Lisbon"), 6) == 0)
return &WesterEuropean;
if(strncmp_P(name+7, PSTR("Ljubljana"), 9) == 0)
return &CentralEuropean;
if(strncmp_P(name+7, PSTR("London"), 6) == 0)
return &WesterEuropean;
if(strncmp_P(name+7, PSTR("Luxembourg"), 10) == 0)
return &CentralEuropean;
if(strncmp_P(name+7, PSTR("Madrid"), 6) == 0)
return &CentralEuropean;
if(strncmp_P(name+7, PSTR("Malta"), 5) == 0)
return &CentralEuropean;
if(strncmp_P(name+7, PSTR("Nicosia"), 7) == 0)
return &EasternEuropean;
if(strncmp_P(name+7, PSTR("Oslo"), 4) == 0)
return &CentralEuropean;
if(strncmp_P(name+7, PSTR("Paris"), 5) == 0)
return &CentralEuropean;
if(strncmp_P(name+7, PSTR("Podgorica"), 9) == 0)
return &CentralEuropean;
if(strncmp_P(name+7, PSTR("Prague"), 6) == 0)
return &CentralEuropean;
if(strncmp_P(name+7, PSTR("Riga"), 4) == 0)
return &EasternEuropean;
if(strncmp_P(name+7, PSTR("Rome"), 4) == 0)
return &CentralEuropean;
if(strncmp_P(name+7, PSTR("Sofia"), 5) == 0)
return &EasternEuropean;
if(strncmp_P(name+7, PSTR("Stockholm"), 9) == 0)
return &CentralEuropean;
if(strncmp_P(name+7, PSTR("Tallinn"), 7) == 0)
return &EasternEuropean;
if(strncmp_P(name+7, PSTR("Vienna"), 6) == 0)
return &CentralEuropean;
if(strncmp_P(name+7, PSTR("Vilnius"), 7) == 0)
return &EasternEuropean;
if(strncmp_P(name+7, PSTR("Warsaw"), 6) == 0)
return &CentralEuropean;
if(strncmp_P(name+7, PSTR("Zagreb"), 6) == 0)
return &CentralEuropean;
if(strncmp_P(name+7, PSTR("Zurich"), 6) == 0)
return &CentralEuropean;
}
return &GMT;
}

View File

@@ -1 +0,0 @@
root/*.h

View File

@@ -1 +0,0 @@
json/*.h

View File

@@ -1,76 +0,0 @@
import os
import re
import shutil
import subprocess
try:
from css_html_js_minify import js_minify
except:
from SCons.Script import (
ARGUMENTS,
COMMAND_LINE_TARGETS,
DefaultEnvironment,
)
env = DefaultEnvironment()
env.Execute(
env.VerboseAction(
'$PYTHONEXE -m pip install "css_html_js_minify" ',
"Installing Python dependencies",
)
)
try:
from css_html_js_minify import js_minify
except:
print("WARN: Unable to load minifier")
webroot = "lib/DomoticzMqttHandler/json"
srcroot = "lib/DomoticzMqttHandler/include/json"
version = os.environ.get('GITHUB_TAG')
if version == None:
try:
result = subprocess.run(['git','rev-parse','--short','HEAD'], capture_output=True, check=False)
if result.returncode == 0:
version = result.stdout.decode('utf-8').strip()
else:
version = "SNAPSHOT"
except:
version = "SNAPSHOT"
if os.path.exists(srcroot):
shutil.rmtree(srcroot)
os.mkdir(srcroot)
else:
os.mkdir(srcroot)
for filename in os.listdir(webroot):
basename = re.sub("[^0-9a-zA-Z]+", "_", filename)
srcfile = webroot + "/" + filename
dstfile = srcroot + "/" + basename + ".h"
varname = basename.upper()
with open(srcfile, encoding="utf-8") as f:
content = f.read().replace("${version}", version)
try:
if (filename.endswith(".js") and filename != 'gaugemeter.js') or filename.endswith(".json"):
content = js_minify(content)
except:
print("WARN: Unable to minify")
with open(dstfile, "w") as dst:
dst.write("static const char ")
dst.write(varname)
dst.write("[] PROGMEM = R\"==\"==(")
dst.write(content)
dst.write(")==\"==\";\n")
dst.write("const int ");
dst.write(varname)
dst.write("_LEN PROGMEM = ");
dst.write(str(len(content)))
dst.write(";");

View File

@@ -1,8 +0,0 @@
#ifndef _PRICESCONTAINER_H
#define _PRICESCONTAINER_H
struct PricesContainer {
char currency[4];
char measurementUnit[4];
int32_t points[24];
};
#endif

View File

@@ -1,410 +0,0 @@
#include "EntsoeApi.h"
#include <EEPROM.h>
#include "Uptime.h"
#include "TimeLib.h"
#include "DnbCurrParser.h"
#include "version.h"
#include "GcmParser.h"
#if defined(ESP32)
#include <esp_task_wdt.h>
#endif
EntsoeApi::EntsoeApi(RemoteDebug* Debug) {
this->buf = (char*) malloc(BufferSize);
debugger = Debug;
// Entso-E uses CET/CEST
TimeChangeRule CEST = {"CEST", Last, Sun, Mar, 2, 120};
TimeChangeRule CET = {"CET ", Last, Sun, Oct, 3, 60};
tz = new Timezone(CEST, CET);
tomorrowFetchMinute = 15 + random(45); // Random between 13:15 and 14:00
}
void EntsoeApi::setup(EntsoeConfig& config) {
if(this->config == NULL) {
this->config = new EntsoeConfig();
}
memcpy(this->config, &config, sizeof(config));
lastTodayFetch = lastTomorrowFetch = lastCurrencyFetch = 0;
if(today != NULL) delete today;
if(tomorrow != NULL) delete tomorrow;
today = tomorrow = NULL;
http.setFollowRedirects(HTTPC_STRICT_FOLLOW_REDIRECTS);
http.setReuse(false);
http.setTimeout(60000);
http.setUserAgent("ams2mqtt/" + String(VERSION));
http.useHTTP10(true);
#if defined(AMS2MQTT_PRICE_KEY)
key = new uint8_t[16] AMS2MQTT_PRICE_KEY;
hub = true;
#else
hub = false;
#endif
#if defined(AMS2MQTT_PRICE_AUTHENTICATION)
auth = new uint8_t[16] AMS2MQTT_PRICE_AUTHENTICATION;
hub = hub && true;
#else
hub = false;
#endif
}
char* EntsoeApi::getToken() {
return this->config->token;
}
char* EntsoeApi::getCurrency() {
return this->config->currency;
}
char* EntsoeApi::getArea() {
return this->config->area;
}
float EntsoeApi::getValueForHour(int8_t hour) {
time_t cur = time(nullptr);
return getValueForHour(cur, hour);
}
float EntsoeApi::getValueForHour(time_t cur, int8_t hour) {
tmElements_t tm;
if(tz != NULL)
cur = tz->toLocal(cur);
breakTime(cur, tm);
int pos = tm.Hour + hour;
if(pos >= 48)
return ENTSOE_NO_VALUE;
double value = ENTSOE_NO_VALUE;
double multiplier = config->multiplier / 1000.0;
if(pos > 23) {
if(tomorrow == NULL)
return ENTSOE_NO_VALUE;
if(tomorrow->points[pos-24] == ENTSOE_NO_VALUE)
return ENTSOE_NO_VALUE;
value = tomorrow->points[pos-24] / 10000.0;
if(strcmp(tomorrow->measurementUnit, "KWH") == 0) {
// Multiplier is 1
} else if(strcmp(tomorrow->measurementUnit, "MWH") == 0) {
multiplier *= 0.001;
} else {
return ENTSOE_NO_VALUE;
}
float mult = getCurrencyMultiplier(tomorrow->currency, config->currency, cur);
if(mult == 0) return ENTSOE_NO_VALUE;
multiplier *= mult;
} else if(pos >= 0) {
if(today == NULL)
return ENTSOE_NO_VALUE;
if(today->points[pos] == ENTSOE_NO_VALUE)
return ENTSOE_NO_VALUE;
value = today->points[pos] / 10000.0;
if(strcmp(today->measurementUnit, "KWH") == 0) {
// Multiplier is 1
} else if(strcmp(today->measurementUnit, "MWH") == 0) {
multiplier *= 0.001;
} else {
return ENTSOE_NO_VALUE;
}
float mult = getCurrencyMultiplier(today->currency, config->currency, cur);
if(mult == 0) return ENTSOE_NO_VALUE;
multiplier *= mult;
}
return value * multiplier;
}
bool EntsoeApi::loop() {
uint64_t now = millis64();
if(now < 10000) return false; // Grace period
time_t t = time(nullptr);
if(t < BUILD_EPOCH) return false;
#ifndef AMS2MQTT_PRICE_KEY
if(strlen(getToken()) == 0) {
return false;
}
#endif
if(!config->enabled)
return false;
if(strlen(config->area) == 0)
return false;
if(strlen(config->currency) == 0)
return false;
tmElements_t tm;
breakTime(tz->toLocal(t), tm);
if(currentDay == 0) {
currentDay = tm.Day;
currentHour = tm.Hour;
}
if(currentDay != tm.Day) {
if(debugger->isActive(RemoteDebug::INFO)) debugger->printf("(EntsoeApi) Rotating price objects at %lu\n", t);
if(today != NULL) delete today;
if(tomorrow != NULL) {
today = tomorrow;
tomorrow = NULL;
}
currentDay = tm.Day;
currentHour = tm.Hour;
return today != NULL; // Only trigger MQTT publish if we have todays prices.
} else if(currentHour != tm.Hour) {
currentHour = tm.Hour;
return today != NULL; // Only trigger MQTT publish if we have todays prices.
}
if(today == NULL && (lastTodayFetch == 0 || now - lastTodayFetch > 60000)) {
try {
lastTodayFetch = now;
today = fetchPrices(t);
} catch(const std::exception& e) {
if(lastError == 0) lastError = 900;
today = NULL;
}
return today != NULL; // Only trigger MQTT publish if we have todays prices.
}
// Prices for next day are published at 13:00 CE(S)T, but to avoid heavy server traffic at that time, we will
// fetch with one hour (with some random delay) and retry every 15 minutes
if(tomorrow == NULL && (tm.Hour > 13 || (tm.Hour == 13 && tm.Minute >= tomorrowFetchMinute)) && (lastTomorrowFetch == 0 || now - lastTomorrowFetch > 900000)) {
try {
lastTomorrowFetch = now;
tomorrow = fetchPrices(t+SECS_PER_DAY);
} catch(const std::exception& e) {
if(lastError == 0) lastError = 900;
tomorrow = NULL;
}
return tomorrow != NULL;
}
return false;
}
bool EntsoeApi::retrieve(const char* url, Stream* doc) {
#if defined(ESP32)
if(http.begin(url)) {
printD("Connection established");
#if defined(ESP32)
esp_task_wdt_reset();
#elif defined(ESP8266)
ESP.wdtFeed();
#endif
int status = http.GET();
#if defined(ESP32)
esp_task_wdt_reset();
#elif defined(ESP8266)
ESP.wdtFeed();
#endif
if(status == HTTP_CODE_OK) {
printD("Receiving data");
http.writeToStream(doc);
http.end();
lastError = 0;
return true;
} else {
lastError = status;
if(debugger->isActive(RemoteDebug::ERROR)) debugger->printf("(EntsoeApi) Communication error, returned status: %d\n", status);
printE(http.errorToString(status));
printD(http.getString());
http.end();
return false;
}
} else {
return false;
}
#endif
return false;
}
float EntsoeApi::getCurrencyMultiplier(const char* from, const char* to, time_t t) {
if(strcmp(from, to) == 0)
return 1.00;
uint64_t now = millis64();
if(now > lastCurrencyFetch && (lastCurrencyFetch == 0 || (now - lastCurrencyFetch) > 60000)) {
lastCurrencyFetch = now;
DnbCurrParser p;
#if defined(ESP32)
esp_task_wdt_reset();
#elif defined(ESP8266)
ESP.wdtFeed();
#endif
snprintf(buf, BufferSize, "https://data.norges-bank.no/api/data/EXR/M.%s.NOK.SP?lastNObservations=1", from);
if(debugger->isActive(RemoteDebug::INFO)) debugger->printf("(EntsoeApi) Retrieving %s to NOK conversion\n", from);
if(debugger->isActive(RemoteDebug::DEBUG)) debugger->printf("(EntsoeApi) url: %s\n", buf);
if(retrieve(buf, &p)) {
if(debugger->isActive(RemoteDebug::DEBUG)) debugger->printf("(EntsoeApi) got exchange rate %.4f\n", p.getValue());
currencyMultiplier = p.getValue();
if(strncmp(to, "NOK", 3) != 0) {
snprintf(buf, BufferSize, "https://data.norges-bank.no/api/data/EXR/M.%s.NOK.SP?lastNObservations=1", to);
if(debugger->isActive(RemoteDebug::INFO)) debugger->printf("(EntsoeApi) Retrieving %s to NOK conversion\n", to);
if(debugger->isActive(RemoteDebug::DEBUG)) debugger->printf("(EntsoeApi) url: %s\n", buf);
if(retrieve(buf, &p)) {
if(debugger->isActive(RemoteDebug::DEBUG)) debugger->printf("(EntsoeApi) got exchange rate %.4f\n", p.getValue());
currencyMultiplier /= p.getValue();
} else {
return 0;
}
}
} else {
return 0;
}
if(debugger->isActive(RemoteDebug::DEBUG)) debugger->printf("(EntsoeApi) Resulting currency multiplier: %.4f\n", currencyMultiplier);
tmElements_t tm;
breakTime(t, tm);
lastCurrencyFetch = now + (SECS_PER_DAY * 1000) - (((((tm.Hour * 60) + tm.Minute) * 60) + tm.Second) * 1000);
}
return currencyMultiplier;
}
PricesContainer* EntsoeApi::fetchPrices(time_t t) {
tmElements_t tm;
breakTime(t, tm);
if(strlen(getToken()) > 0) {
time_t e1 = t - (tm.Hour * 3600) - (tm.Minute * 60) - tm.Second; // UTC midnight
time_t e2 = e1 + SECS_PER_DAY;
tmElements_t d1, d2;
breakTime(tz->toUTC(e1), d1); // To get day and hour for CET/CEST at UTC midnight
breakTime(tz->toUTC(e2), d2);
snprintf(buf, BufferSize, "%s?securityToken=%s&documentType=A44&periodStart=%04d%02d%02d%02d%02d&periodEnd=%04d%02d%02d%02d%02d&in_Domain=%s&out_Domain=%s",
"https://transparency.entsoe.eu/api", getToken(),
d1.Year+1970, d1.Month, d1.Day, d1.Hour, 00,
d2.Year+1970, d2.Month, d2.Day, d2.Hour, 00,
config->area, config->area);
#if defined(ESP32)
esp_task_wdt_reset();
#elif defined(ESP8266)
ESP.wdtFeed();
#endif
if(debugger->isActive(RemoteDebug::INFO)) debugger->printf("(EntsoeApi) Fetching prices for %d.%d.%d\n", tm.Day, tm.Month, tm.Year+1970);
if(debugger->isActive(RemoteDebug::DEBUG)) debugger->printf("(EntsoeApi) url: %s\n", buf);
EntsoeA44Parser a44;
if(retrieve(buf, &a44) && a44.getPoint(0) != ENTSOE_NO_VALUE) {
PricesContainer* ret = new PricesContainer();
a44.get(ret);
return ret;
} else {
return NULL;
}
} else if(hub) {
String data;
snprintf(buf, BufferSize, "%s/%s/%d/%d/%d?currency=%s",
"http://ams2mqtt.rewiredinvent.no/hub/price",
config->area,
tm.Year+1970,
tm.Month,
tm.Day,
config->currency
);
#if defined(ESP8266)
WiFiClient client;
client.setTimeout(5000);
if(http.begin(client, buf)) {
#elif defined(ESP32)
if(http.begin(buf)) {
#endif
int status = http.GET();
#if defined(ESP32)
esp_task_wdt_reset();
#elif defined(ESP8266)
ESP.wdtFeed();
#endif
if(status == HTTP_CODE_OK) {
printD("Receiving data");
data = http.getString();
http.end();
lastError = 0;
} else {
lastError = status;
if(debugger->isActive(RemoteDebug::ERROR)) debugger->printf("(EntsoeApi) Communication error, returned status: %d\n", status);
printE(http.errorToString(status));
printD(http.getString());
http.end();
}
}
uint8_t* content = (uint8_t*) (data.c_str());
if(debugger->isActive(RemoteDebug::DEBUG)) {
printD("Received content for prices:");
debugPrint(content, 0, data.length());
}
DataParserContext ctx;
ctx.length = data.length();
GCMParser gcm(key, auth);
int8_t gcmRet = gcm.parse(content, ctx);
if(debugger->isActive(RemoteDebug::DEBUG)) {
printD("Decrypted content for prices:");
debugPrint(content, 0, data.length());
}
if(gcmRet > 0) {
if(debugger->isActive(RemoteDebug::DEBUG)) debugger->printf("(EntsoeApi) Price data starting at: %d\n", gcmRet);
PricesContainer* ret = new PricesContainer();
memcpy(ret, content+gcmRet, sizeof(*ret));
for(uint8_t i = 0; i < 24; i++) {
ret->points[i] = ntohl(ret->points[i]);
}
lastError = 0;
return ret;
} else {
lastError = gcmRet;
if(debugger->isActive(RemoteDebug::ERROR)) debugger->printf("(EntsoeApi) Error code while decrypting prices: %d\n", gcmRet);
}
}
return NULL;
}
void EntsoeApi::printD(String fmt, ...) {
va_list args;
va_start(args, fmt);
if(debugger->isActive(RemoteDebug::DEBUG)) debugger->printf(String("(EntsoeApi)" + fmt + "\n").c_str(), args);
va_end(args);
}
void EntsoeApi::printE(String fmt, ...) {
va_list args;
va_start(args, fmt);
if(debugger->isActive(RemoteDebug::ERROR)) debugger->printf(String("(EntsoeApi)" + fmt + "\n").c_str(), args);
va_end(args);
}
void EntsoeApi::debugPrint(byte *buffer, int start, int length) {
for (int i = start; i < start + length; i++) {
if (buffer[i] < 0x10)
debugger->print("0");
debugger->print(buffer[i], HEX);
debugger->print(" ");
if ((i - start + 1) % 16 == 0)
debugger->println("");
else if ((i - start + 1) % 4 == 0)
debugger->print(" ");
yield(); // Let other get some resources too
}
debugger->println("");
}
int16_t EntsoeApi::getLastError() {
return lastError;
}

View File

@@ -1 +0,0 @@
json/*.h

View File

@@ -1,76 +0,0 @@
import os
import re
import shutil
import subprocess
try:
from css_html_js_minify import js_minify
except:
from SCons.Script import (
ARGUMENTS,
COMMAND_LINE_TARGETS,
DefaultEnvironment,
)
env = DefaultEnvironment()
env.Execute(
env.VerboseAction(
'$PYTHONEXE -m pip install "css_html_js_minify" ',
"Installing Python dependencies",
)
)
try:
from css_html_js_minify import js_minify
except:
print("WARN: Unable to load minifier")
webroot = "lib/HomeAssistantMqttHandler/json"
srcroot = "lib/HomeAssistantMqttHandler/include/json"
version = os.environ.get('GITHUB_TAG')
if version == None:
try:
result = subprocess.run(['git','rev-parse','--short','HEAD'], capture_output=True, check=False)
if result.returncode == 0:
version = result.stdout.decode('utf-8').strip()
else:
version = "SNAPSHOT"
except:
version = "SNAPSHOT"
if os.path.exists(srcroot):
shutil.rmtree(srcroot)
os.mkdir(srcroot)
else:
os.mkdir(srcroot)
for filename in os.listdir(webroot):
basename = re.sub("[^0-9a-zA-Z]+", "_", filename)
srcfile = webroot + "/" + filename
dstfile = srcroot + "/" + basename + ".h"
varname = basename.upper()
with open(srcfile, encoding="utf-8") as f:
content = f.read().replace("${version}", version)
try:
if (filename.endswith(".js") and filename != 'gaugemeter.js') or filename.endswith(".json"):
content = js_minify(content)
except:
print("WARN: Unable to minify")
with open(dstfile, "w") as dst:
dst.write("static const char ")
dst.write(varname)
dst.write("[] PROGMEM = R\"==\"==(")
dst.write(content)
dst.write(")==\"==\";\n")
dst.write("const int ");
dst.write(varname)
dst.write("_LEN PROGMEM = ");
dst.write(str(len(content)))
dst.write(";");

View File

@@ -1 +0,0 @@
json/*.h

View File

@@ -1,76 +0,0 @@
import os
import re
import shutil
import subprocess
try:
from css_html_js_minify import js_minify
except:
from SCons.Script import (
ARGUMENTS,
COMMAND_LINE_TARGETS,
DefaultEnvironment,
)
env = DefaultEnvironment()
env.Execute(
env.VerboseAction(
'$PYTHONEXE -m pip install "css_html_js_minify" ',
"Installing Python dependencies",
)
)
try:
from css_html_js_minify import js_minify
except:
print("WARN: Unable to load minifier")
webroot = "lib/JsonMqttHandler/json"
srcroot = "lib/JsonMqttHandler/include/json"
version = os.environ.get('GITHUB_TAG')
if version == None:
try:
result = subprocess.run(['git','rev-parse','--short','HEAD'], capture_output=True, check=False)
if result.returncode == 0:
version = result.stdout.decode('utf-8').strip()
else:
version = "SNAPSHOT"
except:
version = "SNAPSHOT"
if os.path.exists(srcroot):
shutil.rmtree(srcroot)
os.mkdir(srcroot)
else:
os.mkdir(srcroot)
for filename in os.listdir(webroot):
basename = re.sub("[^0-9a-zA-Z]+", "_", filename)
srcfile = webroot + "/" + filename
dstfile = srcroot + "/" + basename + ".h"
varname = basename.upper()
with open(srcfile, encoding="utf-8") as f:
content = f.read().replace("${version}", version)
try:
if (filename.endswith(".js") and filename != 'gaugemeter.js') or filename.endswith(".json"):
content = js_minify(content)
except:
print("WARN: Unable to minify")
with open(dstfile, "w") as dst:
dst.write("static const char ")
dst.write(varname)
dst.write("[] PROGMEM = R\"==\"==(")
dst.write(content)
dst.write(")==\"==\";\n")
dst.write("const int ");
dst.write(varname)
dst.write("_LEN PROGMEM = ");
dst.write(str(len(content)))
dst.write(";");

View File

@@ -1,24 +0,0 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

View File

@@ -1,14 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="icon" href="/favicon.svg">
<link rel="mask-icon" href="/favicon.svg" color="#000000">
<title>AMS reader</title>
</head>
<body class="bg-gray-100">
<div id="app"></div>
<script type="module" src="/src/main.js"></script>
</body>
</html>

View File

@@ -1,34 +0,0 @@
{
"compilerOptions": {
"moduleResolution": "Node",
"target": "ESNext",
"module": "ESNext",
/**
* svelte-preprocess cannot figure out whether you have
* a value or a type, so tell TypeScript to enforce using
* `import type` instead of `import` for Types.
*/
"importsNotUsedAsValues": "error",
"isolatedModules": true,
"resolveJsonModule": true,
/**
* To have warnings / errors of the Svelte compiler at the
* correct position, enable source maps by default.
*/
"sourceMap": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"baseUrl": ".",
/**
* Typecheck JS in `.svelte` and `.js` files by default.
* Disable this if you'd like to use dynamic types.
*/
"checkJs": true
},
/**
* Use global.d.ts instead of compilerOptions.types
* to avoid limiting type declarations.
*/
"include": ["src/**/*.d.ts", "src/**/*.js", "src/**/*.svelte"]
}

View File

@@ -1,19 +0,0 @@
// HTTPS required for this to work
// Remember: <link rel="manifest" href="manifest.json" />
{
"short_name": "amsreader",
"name": "AMS reader",
"icons": [
{
"src": "/favicon.svg",
"sizes": "any",
"type": "image/svg+xml",
"purpose": "any"
}
],
"start_url": "/",
"background_color": "#f3f4f6",
"display": "standalone",
"scope": "/",
"theme_color": "#7c3aed"
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,27 +0,0 @@
{
"name": "svelte-gui",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview"
},
"devDependencies": {
"@sveltejs/vite-plugin-svelte": "^1.0.1",
"@tailwindcss/forms": "^0.5.2",
"autoprefixer": "^10.4.7",
"http-proxy-middleware": "^2.0.1",
"postcss": "^8.4.14",
"postcss-load-config": "^4.0.1",
"svelte": "^3.49.0",
"svelte-navigator": "^3.2.2",
"svelte-preprocess": "^4.10.7",
"tailwindcss": "^3.1.5",
"vite": "^3.0.7"
},
"dependencies": {
"cssnano": "^5.1.14"
}
}

View File

@@ -1,15 +0,0 @@
const tailwindcss = require("tailwindcss");
const autoprefixer = require("autoprefixer");
const cssnano = require("cssnano");
const config = {
plugins: [
//Some plugins, like tailwindcss/nesting, need to run before Tailwind,
tailwindcss(),
//But others, like autoprefixer, need to run after,
autoprefixer,
cssnano()
],
};
module.exports = config;

View File

@@ -1,11 +0,0 @@
self.addEventListener('install', (event) => {
self.skipWaiting();
});
self.addEventListener('activate', (event) => {
return self.clients.claim();
});
self.addEventListener('fetch', function(event) {
event.respondWith(fetch(event.request));
});

View File

@@ -1,69 +0,0 @@
<script>
import { Router, Route, navigate } from "svelte-navigator";
import { getSysinfo, sysinfoStore, dataStore } from './lib/DataStores.js';
import Header from './lib/Header.svelte';
import Dashboard from './lib/Dashboard.svelte';
import ConfigurationPanel from './lib/ConfigurationPanel.svelte';
import StatusPage from './lib/StatusPage.svelte';
import VendorPanel from './lib/VendorPanel.svelte';
import SetupPanel from './lib/SetupPanel.svelte';
import Mask from './lib/Mask.svelte';
import FileUploadComponent from "./lib/FileUploadComponent.svelte";
import ConsentComponent from "./lib/ConsentComponent.svelte";
let sysinfo = {};
sysinfoStore.subscribe(update => {
sysinfo = update;
if(sysinfo.vndcfg === false) {
navigate("/vendor");
} else if(sysinfo.usrcfg === false) {
navigate("/setup");
} else if(sysinfo.fwconsent === 0) {
navigate("/consent");
}
});
getSysinfo();
let data = {};
dataStore.subscribe(update => {
data = update;
});
</script>
<div class="container mx-auto m-3">
<Router>
<Header data={data}/>
<Route path="/">
<Dashboard data={data} sysinfo={sysinfo}/>
</Route>
<Route path="/configuration">
<ConfigurationPanel sysinfo={sysinfo}/>
</Route>
<Route path="/status">
<StatusPage sysinfo={sysinfo} data={data}/>
</Route>
<Route path="/mqtt-ca">
<FileUploadComponent title="CA" action="/mqtt-ca"/>
</Route>
<Route path="/mqtt-cert">
<FileUploadComponent title="certificate" action="/mqtt-cert"/>
</Route>
<Route path="/mqtt-key">
<FileUploadComponent title="private key" action="/mqtt-key"/>
</Route>
<Route path="/consent">
<ConsentComponent sysinfo={sysinfo}/>
</Route>
<Route path="/setup">
<SetupPanel sysinfo={sysinfo}/>
</Route>
<Route path="/vendor">
<VendorPanel sysinfo={sysinfo}/>
</Route>
</Router>
{#if sysinfo.upgrading}
<Mask active=true message="Device is upgrading, please wait"/>
{:else if sysinfo.booting}
<Mask active=true message="Device is booting, please wait"/>
{/if}
</div>

View File

@@ -1,152 +0,0 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
.gh-logo {
width: 2rem;
height: 2rem;
}
.cnt {
@apply bg-white m-2 p-2 rounded shadow-lg
}
.in-pre {
@apply flex items-center bg-gray-100 rounded-l-md border border-r-0 border-gray-300 px-3 whitespace-nowrap text-sm
}
.in-post {
@apply flex items-center bg-gray-100 rounded-r-md border border-l-0 border-gray-300 px-3 whitespace-nowrap text-sm
}
.in-txt {
@apply h-10 shadow-sm border-gray-300 disabled:bg-gray-200
}
.in-f {
@apply in-txt rounded-l-md
}
.in-m {
@apply in-txt border-l-0
}
.in-l {
@apply in-txt border-l-0 rounded-r-md
}
.in-s {
@apply in-txt rounded-md w-full
}
.tr {
@apply text-right
}
.bd-grn {
@apply my-auto bg-green-500 text-green-100 text-xs font-semibold mr-2 px-2.5 py-0.5 rounded
}
.bd-ylo {
@apply my-auto bg-yellow-500 text-yellow-100 text-xs font-semibold mr-2 px-2.5 py-0.5 rounded
}
.bd-red {
@apply my-auto bg-red-500 text-red-100 text-xs font-semibold mr-2 px-2.5 py-0.5 rounded
}
.bd-blu {
@apply my-auto bg-blue-500 text-blue-100 text-xs font-semibold mr-2 px-2.5 py-0.5 rounded
}
.bd-gry {
@apply my-auto bg-gray-500 text-gray-100 text-xs font-semibold mr-2 px-2.5 py-0.5 rounded
}
.btn-pri {
@apply py-2 px-4 rounded bg-blue-500 text-white mr-3
}
.pl-root {
position: relative;
}
.pl-ov {
position: absolute;
top: 27%;
left: 25%;
width: 50%;
text-align: center;
}
.pl-val {
font-size: 1.7rem;
}
.pl-unt {
font-size: 1.0rem;
color: grey;
}
.pl-sub {
padding-top: 10px;
font-size: 1.0rem;
}
.pl-snt {
font-size: 0.7rem;
color: grey;
}
.pl-lab {
font-size: 1.0rem;
}
.chart {
width: 100%;
height: 100%;
margin: 0 auto;
}
svg {
position: relative;
width: 100%;
}
.tick {
font-family: Helvetica, Arial;
font-size: 0.85em;
font-weight: 200;
}
.tick line {
stroke: #e2e2e2;
stroke-dasharray: 2;
}
.tick text {
fill: #999;
text-anchor: start;
}
.tick.tick-0 line {
stroke-dasharray: 0;
}
.tick.tick-green line {
stroke: #32d900 !important;
}
.tick.tick-green text {
fill: #32d900 !important;
}
.tick.tick-orange line {
stroke: #d95600 !important;
}
.tick.tick-orange text {
fill: #d95600 !important;
}
.x-axis .tick text {
text-anchor: middle;
}
.bars rect {
stroke: rgb(0,0,0);
stroke-opacity: 0.25;
opacity: 0.9;
}
.bars text {
font-family: Helvetica, Arial;
font-size: 0.85em;
display: block;
text-align: center;
}

View File

@@ -1,19 +0,0 @@
<?xml version="1.0" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.0//EN" "http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 52 52">
<title>Amsleser</title>
<g transform="translate(-29.5,-83)">
<circle r="4.8016944" cy="123.56455" cx="55.064552"
style="fill:none;stroke:#045c7c;stroke-width:3;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
<path d="m 41.298717,103.9049 a 24,24 0 0 1 27.531669,0"
style="fill:none;stroke:#045c7c;stroke-width:3.3;stroke-linecap:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
<path d="m 35.562952,95.713384 a 34,34 0 0 1 39.003199,-2e-6"
style="fill:none;stroke:#045c7c;stroke-width:3.3;stroke-linecap:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
<path d="m 47.034482,112.09642 a 14,14 0 0 1 16.06014,0"
style="fill:none;stroke:#045c7c;stroke-width:3.3;stroke-linecap:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
<circle r="3" cy="105.99158" cx="38.181862"
style="fill:none;stroke:#045c7c;stroke-width:2.4;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
<circle r="3" cy="97.959579" cx="77.491386"
style="fill:none;stroke:#045c7c;stroke-width:2.4;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
</g>
</svg>

Before

Width:  |  Height:  |  Size: 1.4 KiB

View File

@@ -1,6 +0,0 @@
<?xml version="1.0" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.0//EN" "http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 499.36" focusable="false">
<title>GitHub</title>
<path d="M256 0C114.64 0 0 114.61 0 256c0 113.09 73.34 209 175.08 242.9 12.8 2.35 17.47-5.56 17.47-12.34 0-6.08-.22-22.18-.35-43.54-71.2 15.49-86.2-34.34-86.2-34.34-11.64-29.57-28.42-37.45-28.42-37.45-23.27-15.84 1.73-15.55 1.73-15.55 25.69 1.81 39.21 26.38 39.21 26.38 22.84 39.12 59.92 27.82 74.5 21.27 2.33-16.54 8.94-27.82 16.25-34.22-56.84-6.43-116.6-28.43-116.6-126.49 0-27.95 10-50.8 26.35-68.69-2.63-6.48-11.42-32.5 2.51-67.75 0 0 21.49-6.88 70.4 26.24a242.65 242.65 0 0 1 128.18 0c48.87-33.13 70.33-26.24 70.33-26.24 14 35.25 5.18 61.27 2.55 67.75 16.41 17.9 26.31 40.75 26.31 68.69 0 98.35-59.85 120-116.88 126.32 9.19 7.9 17.38 23.53 17.38 47.41 0 34.22-.31 61.83-.31 70.23 0 6.85 4.61 14.81 17.6 12.31C438.72 464.97 512 369.08 512 256.02 512 114.62 397.37 0 256 0z" fill="#f8f9fa" fill-rule="evenodd"></path>
</svg>

Before

Width:  |  Height:  |  Size: 1.1 KiB

View File

@@ -1,54 +0,0 @@
<script>
export let data;
export let currency;
let hasExport = data && (data.om || data.e > 0);
</script>
<div class="mx-2 text-sm">
<strong>Real time calculation</strong>
{#if data && data.h !== undefined}
<div class="flex">
<div>Hour</div>
<div class="flex-auto text-right">{data.h.u ? data.h.u.toFixed(2) : '-'} kWh {#if currency && (hasExport)}/ {data.h.c ? data.h.c.toFixed(2) : '-'} {currency}{/if}</div>
</div>
<div class="flex">
<div>Day</div>
<div class="flex-auto text-right">{data.d.u ? data.d.u.toFixed(1) : '-'} kWh {#if currency && (hasExport)}/ {data.d.c ? data.d.c.toFixed(2) : '-'} {currency}{/if}</div>
</div>
<div class="flex">
<div>Month</div>
<div class="flex-auto text-right">{data.m.u ? data.m.u.toFixed(0) : '-'} kWh {#if currency && (hasExport)}/ {data.m.c ? data.m.c.toFixed(2) : '-'} {currency}{/if}</div>
</div>
<div class="mt-4">
{#if hasExport}
<div class="flex">
<div>Hour</div>
<div class="flex-auto text-right">{data.h.p ? data.h.p.toFixed(2) : '-'} kWh {#if currency}/ {data.h.i ? data.h.i.toFixed(2) : '-'} {currency}{/if}</div>
</div>
<div class="flex">
<div>Day</div>
<div class="flex-auto text-right">{data.d.p ? data.d.p.toFixed(1) : '-'} kWh {#if currency}/ {data.d.i ? data.d.i.toFixed(2) : '-'} {currency}{/if}</div>
</div>
<div class="flex">
<div>Month</div>
<div class="flex-auto text-right">{data.m.p ? data.m.p.toFixed(0) : '-'} kWh {#if currency}/ {data.m.i ? data.m.i.toFixed(2) : '-'} {currency}{/if}</div>
</div>
{:else}
<div class="flex">
<div>Hour</div>
<div class="flex-auto text-right">{data.h.c ? data.h.c.toFixed(2) : '-'} {currency}</div>
</div>
<div class="flex">
<div>Day</div>
<div class="flex-auto text-right">{data.d.c ? data.d.c.toFixed(2) : '-'} {currency}</div>
</div>
<div class="flex">
<div>Month</div>
<div class="flex-auto text-right">{data.m.c ? data.m.c.toFixed(2) : '-'} {currency}</div>
</div>
{/if}
</div>
{/if}
</div>

View File

@@ -1,62 +0,0 @@
<script>
import BarChart from './BarChart.svelte';
import { ampcol } from './Helpers.js';
export let u1;
export let u2;
export let u3;
export let i1;
export let i2;
export let i3;
export let max;
let config = {};
$: {
let xTicks = [];
let points = [];
if(u1 > 0) {
xTicks.push({ label: 'L1' });
points.push({
label: i1 ? (i1 > 10 ? i1.toFixed(0) : i1.toFixed(1)) + 'A' : '-',
value: i1 ? i1 : 0,
color: ampcol(i1 ? (i1)/(max)*100 : 0)
});
}
if(u2 > 0) {
xTicks.push({ label: 'L2' });
points.push({
label: i2 ? (i2 > 10 ? i2.toFixed(0) : i2.toFixed(1)) + 'A' : '-',
value: i2 ? i2 : 0,
color: ampcol(i2 ? (i2)/(max)*100 : 0)
});
}
if(u3 > 0) {
xTicks.push({ label: 'L3' });
points.push({
label: i3 ? (i3 > 10 ? i3.toFixed(0) : i3.toFixed(1)) + 'A' : '-',
value: i3 ? i3 : 0,
color: ampcol(i3 ? (i3)/(max)*100 : 0)
});
}
config = {
padding: { top: 20, right: 15, bottom: 20, left: 35 },
y: {
min: 0,
max: max,
ticks: [
{ value: 0, label: '0%' },
{ value: max/4, label: '25%' },
{ value: max/2, label: '50%' },
{ value: (max/4)*3, label: '75%' },
{ value: max, label: '100%' }
]
},
x: {
ticks: xTicks
},
points: points
};
};
</script>
<BarChart config={config} />

View File

@@ -1,16 +0,0 @@
<script>
export let color;
export let title;
export let text;
</script>
{#if color == 'green'}
<span title={title} class="bd-grn">{text}</span>
{:else if color === `yellow`}
<span title={title} class="bd-ylo">{text}</span>
{:else if color === `red`}
<span title={title} class="bd-red">{text}</span>
{:else if color === `blue`}
<span title={title} class="bd-blu">{text}</span>
{:else if color === `gray`}
<span title={title} class="bd-gry">{text}</span>
{/if}

View File

@@ -1,106 +0,0 @@
<script>
export let config;
let width;
let height;
let barWidth;
let xScale;
let yScale;
let heightAvailable;
let labelOffset;
$: {
heightAvailable = height-(config.title ? 20 : 0);
let innerWidth = width - (config.padding.left + config.padding.right);
barWidth = innerWidth / config.points.length;
labelOffset = barWidth < 25 ? 28 : 17;
let yPerUnit = (heightAvailable-config.padding.top-config.padding.bottom)/(config.y.max-config.y.min);
xScale = function(i) {
return (i*barWidth)+config.padding.left;
};
yScale = function(i) {
let ret = 0;
if(i > config.y.max)
ret = config.padding.bottom;
else if(i < config.y.min)
ret = heightAvailable-config.padding.bottom;
else
ret = heightAvailable-config.padding.bottom-((i-config.y.min)*yPerUnit);
return ret > heightAvailable || ret < 0.0 ? 0.0 : ret;
};
};
</script>
<div class="chart" bind:clientWidth={width} bind:clientHeight={height}>
{#if config.title}
<strong class="text-sm">{config.title}</strong>
{/if}
<svg height="{heightAvailable}">
<!-- y axis -->
<g class="axis y-axis">
{#each config.y.ticks as tick}
<g class="tick tick-{tick.value} tick-{tick.color}" transform="translate(0, {yScale(tick.value)})">
<line x2="100%"></line>
<text y="-4" x={tick.align == 'right' ? '85%' : ''}>{tick.label}</text>
</g>
{/each}
</g>
<!-- x axis -->
<g class="axis x-axis">
{#each config.x.ticks as point, i}
<g class="tick" transform="translate({xScale(i)},{heightAvailable})">
<text x="{barWidth/2}" y="-4">{point.label}</text>
</g>
{/each}
</g>
<g class='bars'>
{#each config.points as point, i}
{#if point.value !== undefined}
<rect
x="{xScale(i) + 2}"
y="{yScale(point.value)}"
width="{barWidth - 4}"
height="{yScale(config.y.min) - yScale(Math.min(config.y.min, 0) + point.value)}"
fill="{point.color}"
/>
{#if barWidth > 15}
<text
y="{yScale(point.value) > yScale(0)-labelOffset ? yScale(point.value) - labelOffset : yScale(point.value)+10}"
x="{xScale(i) + barWidth/2}"
width="{barWidth - 4}"
dominant-baseline="middle"
text-anchor="{barWidth < 25 ? 'left' : 'middle'}"
fill="{yScale(point.value) > yScale(0)-labelOffset ? point.color : 'white'}"
transform="rotate({barWidth < 25 ? 90 : 0}, {xScale(i) + (barWidth/2)}, {yScale(point.value) > yScale(0)-labelOffset ? yScale(point.value) - labelOffset : yScale(point.value)+9})"
>{point.label}</text>
{/if}
{/if}
{#if point.value2 > 0.0001}
<rect
x="{xScale(i) + 2}"
y="{yScale(0)}"
width="{barWidth - 4}"
height="{yScale(config.y.min) - yScale(config.y.min + point.value2)}"
fill="{point.color}"
/>
{#if barWidth > 15}
<text
y="{yScale(-point.value2) < yScale(0)+12 ? yScale(-point.value2) + 12 : yScale(-point.value2)-10}"
x="{xScale(i) + barWidth/2}"
width="{barWidth - 4}"
dominant-baseline="middle"
text-anchor="{barWidth < 25 ? 'left' : 'middle'}"
fill="{yScale(-point.value2) < yScale(0)+12 ? point.color : 'white'}"
transform="rotate({barWidth < 25 ? 90 : 0}, {xScale(i) + (barWidth/2)}, {yScale(point.value2 - config.y.min) > yScale(0)-12 ? yScale(point.value2 - config.y.min) - 12 : yScale(point.value2 - config.y.min)+9})"
>{point.label2}</text>
{/if}
{/if}
{/each}
</g>
</svg>
</div>

View File

@@ -1,48 +0,0 @@
<script>
import {boardtype} from './Helpers.js'
export let chip;
</script>
<option value={-1}></option>
{#if chip == 'esp8266'}
<optgroup label="amsleser.no">
<option value={7}>{boardtype(chip, 7)}</option>
<option value={5}>{boardtype(chip, 5)}</option>
<option value={4}>{boardtype(chip, 4)}</option>
<option value={3}>{boardtype(chip, 3)}</option>
</optgroup>
<optgroup label="Custom hardware">
<option value={2}>{boardtype(chip, 2)}</option>
<option value={1}>{boardtype(chip, 1)}</option>
<option value={0}>{boardtype(chip, 0)}</option>
</optgroup>
<optgroup label="Generic hardware">
<option value={101}>{boardtype(chip, 101)}</option>
<option value={100}>{boardtype(chip, 100)}</option>
</optgroup>
{/if}
{#if chip == 'esp32'}
<optgroup label="Generic hardware">
<option value={201}>{boardtype(chip, 201)}</option>
<option value={202}>{boardtype(chip, 202)}</option>
<option value={203}>{boardtype(chip, 203)}</option>
<option value={200}>{boardtype(chip, 200)}</option>
</optgroup>
{/if}
{#if chip == 'esp32s2'}
<optgroup label="amsleser.no">
<option value={7}>{boardtype(chip, 7)}</option>
<option value={6}>{boardtype(chip, 6)}</option>
<option value={5}>{boardtype(chip, 5)}</option>
</optgroup>
<optgroup label="Generic hardware">
<option value={51}>{boardtype(chip, 51)}</option>
<option value={50}>{boardtype(chip, 50)}</option>
</optgroup>
{/if}
{#if chip == 'esp32solo'}
<optgroup label="Generic hardware">
<option value={200}>{boardtype(chip, 200)}</option>
</optgroup>
{/if}

View File

@@ -1,11 +0,0 @@
<script>
import { zeropad, monthnames } from './Helpers.js';
export let timestamp;
</script>
{#if Math.abs(new Date().getTime()-timestamp.getTime()) < 300000 }
{`${zeropad(timestamp.getDate())}. ${monthnames[timestamp.getMonth()]} ${zeropad(timestamp.getHours())}:${zeropad(timestamp.getMinutes())}`}
{:else}
<span class="text-red-500">{`${zeropad(timestamp.getDate())}.${zeropad(timestamp.getMonth())}.${timestamp.getFullYear()} ${zeropad(timestamp.getHours())}:${zeropad(timestamp.getMinutes())}`}</span>
{/if}

View File

@@ -1,767 +0,0 @@
<script>
import { getConfiguration, configurationStore } from './ConfigurationStore'
import { sysinfoStore } from './DataStores.js';
import UartSelectOptions from './UartSelectOptions.svelte';
import Mask from './Mask.svelte'
import Badge from './Badge.svelte';
import HelpIcon from './HelpIcon.svelte';
import CountrySelectOptions from './CountrySelectOptions.svelte';
import { Link, navigate } from 'svelte-navigator';
export let sysinfo = {}
let loading = true;
let saving = false;
let configuration = {
g: {
t: '', h: '', s: 0, u: '', p: ''
},
m: {
b: 2400, p: 11, i: false, d: 0, f: 0, r: 0,
e: { e: false, k: '', a: '' },
m: { e: false, w: false, v: false, a: false, c: false }
},
w: { s: '', p: '', w: 0.0, z: 255, a: true },
n: {
m: '', i: '', s: '', g: '', d1: '', d2: '', d: false, n1: '', n2: '', h: false
},
q: {
h: '', p: 1883, u: '', a: '', b: '',
s: { e: false, c: false, r: true, k: false }
},
o: {
e: '',
c: '',
u1: '',
u2: '',
u3: ''
},
t: {
t: [0,0,0,0,0,0,0,0,0,0], h: 1
},
p: {
e: false, t: '', r: '', c: '', m: 1.0
},
d: {
s: false, t: false, l: 5
},
u: {
i: 0, e: 0, v: 0, a: 0, r: 0, c: 0, t: 0, p: 0, d: 0, m: 0, s: 0
},
i: {
h: null, a: null,
l: { p: null, i: false },
r: { r: null, g: null, b: null, i: false },
t: { d: null, a: null },
v: { p: null, d: { v: null, g: null }, o: null, m: null, b: null }
}
};
configurationStore.subscribe(update => {
if(update.version) {
configuration = update;
loading = false;
}
});
getConfiguration();
let isFactoryReset = false;
async function factoryReset() {
if(confirm("Are you sure you want to factory reset the device?")) {
const data = new URLSearchParams();
data.append("perform", "true");
const response = await fetch('/reset', {
method: 'POST',
body: data
});
let res = (await response.json());
isFactoryReset = res.success;
}
}
async function handleSubmit(e) {
saving = true;
const formData = new FormData(e.target);
const data = new URLSearchParams();
for (let field of formData) {
const [key, value] = field
data.append(key, value)
}
const response = await fetch('/save', {
method: 'POST',
body: data
});
let res = (await response.json())
sysinfoStore.update(s => {
s.booting = res.reboot;
s.ui = configuration.u;
return s;
});
saving = false;
navigate("/");
}
async function reboot() {
const response = await fetch('/reboot', {
method: 'POST'
});
let res = (await response.json())
}
const askReboot = function() {
if(confirm('Are you sure you want to reboot the device?')) {
sysinfoStore.update(s => {
s.booting = true;
return s;
});
reboot();
}
}
const updateMqttPort = function() {
if(configuration.q.s.e) {
if(configuration.q.p == 1883) configuration.q.p = 8883;
} else {
if(configuration.q.p == 8883) configuration.q.p = 1883;
}
}
</script>
<form on:submit|preventDefault={handleSubmit} autocomplete="off">
<div class="grid xl:grid-cols-4 lg:grid-cols-2 md:grid-cols-2">
<div class="cnt">
<strong class="text-sm">General</strong>
<input type="hidden" name="g" value="true"/>
<div class="my-1">
<div class="flex">
<div>
Hostname<br/>
<input name="gh" bind:value={configuration.g.h} type="text" class="in-f w-full"/>
</div>
<div>
Time zone<br/>
<select name="gt" bind:value={configuration.g.t} class="in-l">
<CountrySelectOptions/>
</select>
</div>
</div>
</div>
<input type="hidden" name="p" value="true"/>
<div class="my-1">
Price region<br/>
<select name="pr" bind:value={configuration.p.r} class="in-s">
<optgroup label="Norway">
<option value="10YNO-1--------2">NO1</option>
<option value="10YNO-2--------T">NO2</option>
<option value="10YNO-3--------J">NO3</option>
<option value="10YNO-4--------9">NO4</option>
<option value="10Y1001A1001A48H">NO5</option>
</optgroup>
<optgroup label="Sweden">
<option value="10Y1001A1001A44P">SE1</option>
<option value="10Y1001A1001A45N">SE2</option>
<option value="10Y1001A1001A46L">SE3</option>
<option value="10Y1001A1001A47J">SE4</option>
</optgroup>
<optgroup label="Denmark">
<option value="10YDK-1--------W">DK1</option>
<option value="10YDK-2--------M">DK2</option>
</optgroup>
<option value="10YAT-APG------L">Austria</option>
<option value="10YBE----------2">Belgium</option>
<option value="10YCZ-CEPS-----N">Czech Republic</option>
<option value="10Y1001A1001A39I">Estonia</option>
<option value="10YFI-1--------U">Finland</option>
<option value="10YFR-RTE------C">France</option>
<option value="10Y1001A1001A83F">Germany</option>
<option value="10YGB----------A">Great Britain</option>
<option value="10YLV-1001A00074">Latvia</option>
<option value="10YLT-1001A0008Q">Lithuania</option>
<option value="10YNL----------L">Netherland</option>
<option value="10YPL-AREA-----S">Poland</option>
<option value="10YCH-SWISSGRIDZ">Switzerland</option>
</select>
</div>
<div class="my-1">
<div class="flex">
<div class="w-1/2">
Currency<br/>
<select name="pc" bind:value={configuration.p.c} class="in-f w-full">
<option value="NOK">NOK</option>
<option value="SEK">SEK</option>
<option value="DKK">DKK</option>
<option value="EUR">EUR</option>
</select>
</div>
<div class="w-1/2">
Multiplier<br/>
<input name="pm" bind:value={configuration.p.m} type="number" min="0.001" max="1000" step="0.001" class="in-l tr w-full"/>
</div>
</div>
</div>
<div class="my-1">
<label><input type="checkbox" name="pe" value="true" bind:checked={configuration.p.e} class="rounded mb-1"/> Enable price fetch from remote server</label>
{#if configuration.p.e && sysinfo.chip != 'esp8266'}
<br/><input name="pt" bind:value={configuration.p.t} type="text" class="in-s" placeholder="ENTSO-E API key, optional, read docs"/>
{/if}
</div>
<div class="my-1">
Security<br/>
<select name="gs" bind:value={configuration.g.s} class="in-s">
<option value={0}>None</option>
<option value={1}>Only configuration</option>
<option value={2}>Everything</option>
</select>
</div>
{#if configuration.g.s > 0}
<div class="my-1">
Username<br/>
<input name="gu" bind:value={configuration.g.u} type="text" class="in-s"/>
</div>
<div class="my-1">
Password<br/>
<input name="gp" bind:value={configuration.g.p} type="password" class="in-s"/>
</div>
{/if}
</div>
<div class="cnt">
<strong class="text-sm">Meter</strong>
<a href="https://github.com/gskjold/AmsToMqttBridge/wiki/Meter-configuration" target="_blank" class="float-right"><HelpIcon/></a>
<input type="hidden" name="m" value="true"/>
<div class="my-1">
<span>Serial configuration</span>
<div class="flex">
<select name="mb" bind:value={configuration.m.b} class="in-f">
<option value={0} disabled={configuration.m.b != 0}>Autodetect</option>
<option value={2400}>2400</option>
<option value={4800}>4800</option>
<option value={9600}>9600</option>
<option value={19200}>19200</option>
<option value={38400}>38400</option>
<option value={57600}>57600</option>
<option value={115200}>115200</option>
</select>
<select name="mp" bind:value={configuration.m.p} class="in-l" disabled={configuration.m.b == 0}>
<option value={0} disabled={configuration.m.b != 0}>-</option>
<option value={2}>7N1</option>
<option value={3}>8N1</option>
<option value={10}>7E1</option>
<option value={11}>8E1</option>
</select>
<label class="mt-2 ml-3 whitespace-nowrap"><input name="mi" value="true" bind:checked={configuration.m.i} type="checkbox" class="rounded mb-1"/> inverted</label>
</div>
</div>
<div class="my-1">
Voltage<br/>
<select name="md" bind:value={configuration.m.d} class="in-s">
<option value={0}></option>
<option value={1}>230V (IT/TT)</option>
<option value={2}>400V (TN)</option>
</select>
</div>
<div class="my-1 flex">
<div class="mx-1">
Main fuse<br/>
<label class="flex">
<input name="mf" bind:value={configuration.m.f} type="number" min="5" max="65535" class="in-f tr w-full"/>
<span class="in-post">A</span>
</label>
</div>
<div class="mx-1">
Production<br/>
<label class="flex">
<input name="mr" bind:value={configuration.m.r} type="number" min="0" max="65535" class="in-f tr w-full"/>
<span class="in-post">kWp</span>
</label>
</div>
</div>
<div class="my-1">
</div>
<div class="my-1">
<label><input type="checkbox" name="me" value="true" bind:checked={configuration.m.e.e} class="rounded mb-1"/> Meter is encrypted</label>
{#if configuration.m.e.e}
<br/><input name="mek" bind:value={configuration.m.e.k} type="text" class="in-s"/>
{/if}
</div>
{#if configuration.m.e.e}
<div class="my-1">
Authentication key<br/>
<input name="mea" bind:value={configuration.m.e.a} type="text" class="in-s"/>
</div>
{/if}
<label><input type="checkbox" name="mm" value="true" bind:checked={configuration.m.m.e} class="rounded mb-1"/> Multipliers</label>
{#if configuration.m.m.e}
<div class="flex my-1">
<div class="w-1/4">
Watt<br/>
<input name="mmw" bind:value={configuration.m.m.w} type="number" min="0.00" max="655.35" step="0.01" class="in-f tr w-full"/>
</div>
<div class="w-1/4">
Volt<br/>
<input name="mmv" bind:value={configuration.m.m.v} type="number" min="0.00" max="655.35" step="0.01" class="in-m tr w-full"/>
</div>
<div class="w-1/4">
Amp<br/>
<input name="mma" bind:value={configuration.m.m.a} type="number" min="0.00" max="655.35" step="0.01" class="in-m tr w-full"/>
</div>
<div class="w-1/4">
kWh<br/>
<input name="mmc" bind:value={configuration.m.m.c} type="number" min="0.00" max="655.35" step="0.01" class="in-l tr w-full"/>
</div>
</div>
{/if}
</div>
<div class="cnt">
<strong class="text-sm">WiFi</strong>
<a href="https://github.com/gskjold/AmsToMqttBridge/wiki/WiFi-configuration" target="_blank" class="float-right"><HelpIcon/></a>
<input type="hidden" name="w" value="true"/>
<div class="my-1">
SSID<br/>
<input name="ws" bind:value={configuration.w.s} type="text" class="in-s"/>
</div>
<div class="my-1">
Password<br/>
<input name="wp" bind:value={configuration.w.p} type="password" class="in-s"/>
</div>
<div class="my-1 flex">
<div class="w-1/2">
Power saving<br/>
<select name="wz" bind:value={configuration.w.z} class="in-s">
<option value={255}>Default</option>
<option value={0}>Off</option>
<option value={1}>Minimum</option>
<option value={2}>Maximum</option>
</select>
</div>
<div class="ml-2 w-1/2">
Power<br/>
<div class="flex">
<input name="ww" bind:value={configuration.w.w} type="number" min="0" max="20.5" step="0.5" class="in-f tr w-full"/>
<span class="in-post">dBm</span>
</div>
</div>
</div>
<div class="my-3">
<label><input type="checkbox" name="wa" value="true" bind:checked={configuration.w.a} class="rounded mb-1"/> Auto reboot on connection problem</label>
</div>
</div>
<div class="cnt">
<strong class="text-sm">Network</strong>
<a href="https://github.com/gskjold/AmsToMqttBridge/wiki/Network-configuration" target="_blank" class="float-right"><HelpIcon/></a>
<div class="my-1">
IP<br/>
<div class="flex">
<select name="nm" bind:value={configuration.n.m} class="in-f">
<option value="dhcp">DHCP</option>
<option value="static">Static</option>
</select>
<input name="ni" bind:value={configuration.n.i} type="text" class="in-m w-full" disabled={configuration.n.m == 'dhcp'}/>
<select name="ns" bind:value={configuration.n.s} class="in-l" disabled={configuration.n.m == 'dhcp'}>
<option value="255.255.255.0">/24</option>
<option value="255.255.0.0">/16</option>
<option value="255.0.0.0">/8</option>
</select>
</div>
</div>
{#if configuration.n.m == 'static'}
<div class="my-1">
Gateway<br/>
<input name="ng" bind:value={configuration.n.g} type="text" class="in-s"/>
</div>
<div class="my-1">
DNS<br/>
<div class="flex">
<input name="nd1" bind:value={configuration.n.d1} type="text" class="in-f w-full"/>
<input name="nd2" bind:value={configuration.n.d2} type="text" class="in-l w-full"/>
</div>
</div>
{/if}
<div class="my-1">
<label><input name="nd" value="true" bind:checked={configuration.n.d} type="checkbox" class="rounded mb-1"/> enable mDNS</label>
</div>
<input type="hidden" name="ntp" value="true"/>
<div class="my-1">
NTP <label class="ml-4"><input name="ntpd" value="true" bind:checked={configuration.n.h} type="checkbox" class="rounded mb-1"/> obtain from DHCP</label><br/>
<div class="flex">
<input name="ntph" bind:value={configuration.n.n1} type="text" class="in-s"/>
</div>
</div>
</div>
<div class="cnt">
<strong class="text-sm">MQTT</strong>
<a href="https://github.com/gskjold/AmsToMqttBridge/wiki/MQTT-configuration" target="_blank" class="float-right"><HelpIcon/></a>
<input type="hidden" name="q" value="true"/>
<div class="my-1">
Server
{#if sysinfo.chip != 'esp8266'}
<label class="float-right mr-3"><input type="checkbox" name="qs" value="true" bind:checked={configuration.q.s.e} class="rounded mb-1" on:change={updateMqttPort}/> SSL</label>
{/if}
<br/>
<div class="flex">
<input name="qh" bind:value={configuration.q.h} type="text" class="in-f w-3/4"/>
<input name="qp" bind:value={configuration.q.p} type="number" min="1024" max="65535" class="in-l tr w-1/4"/>
</div>
</div>
{#if configuration.q.s.e}
<div class="my-1">
<div>
<Link to="/mqtt-ca">
{#if configuration.q.s.c}
<Badge color="green" text="CA OK" title="Click here to replace CA"/>
{:else}
<Badge color="blue" text="Upload CA" title="Click here to upload CA"/>
{/if}
</Link>
<Link to="/mqtt-cert">
{#if configuration.q.s.r}
<Badge color="green" text="Cert OK" title="Click here to replace certificate"/>
{:else}
<Badge color="blue" text="Upload cert" title="Click here to upload certificate"/>
{/if}
</Link>
<Link to="/mqtt-key">
{#if configuration.q.s.k}
<Badge color="green" text="Key OK" title="Click here to replace key"/>
{:else}
<Badge color="blue" text="Upload key" title="Click here to upload key"/>
{/if}
</Link>
</div>
</div>
{/if}
<div class="my-1">
Username<br/>
<input name="qu" bind:value={configuration.q.u} type="text" class="in-s"/>
</div>
<div class="my-1">
Password<br/>
<input name="qa" bind:value={configuration.q.a} type="password" class="in-s"/>
</div>
<div class="my-1 flex">
<div>
Client ID<br/>
<input name="qc" bind:value={configuration.q.c} type="text" class="in-f w-full"/>
</div>
<div>
Payload<br/>
<select name="qm" bind:value={configuration.q.m} class="in-l">
<option value={0}>JSON</option>
<option value={1}>Raw (minimal)</option>
<option value={2}>Raw (full)</option>
<option value={3}>Domoticz</option>
<option value={4}>HomeAssistant</option>
<option value={255}>HEX dump</option>
</select>
</div>
</div>
<div class="my-1">
Publish topic<br/>
<input name="qb" bind:value={configuration.q.b} type="text" class="in-s"/>
</div>
</div>
{#if configuration.q.m == 3}
<div class="cnt">
<strong class="text-sm">Domoticz</strong>
<a href="https://github.com/gskjold/AmsToMqttBridge/wiki/MQTT-configuration#domoticz" target="_blank" class="float-right"><HelpIcon/></a>
<input type="hidden" name="o" value="true"/>
<div class="my-1 flex">
<div class="w-1/2">
Electricity IDX<br/>
<input name="oe" bind:value={configuration.o.e} type="text" class="in-f tr w-full"/>
</div>
<div class="w-1/2">
Current IDX<br/>
<input name="oc" bind:value={configuration.o.c} type="text" class="in-l tr w-full"/>
</div>
</div>
<div class="my-1">
Voltage IDX: L1, L2 & L3
<div class="flex">
<input name="ou1" bind:value={configuration.o.u1} type="text" class="in-f tr w-1/3"/>
<input name="ou2" bind:value={configuration.o.u2} type="text" class="in-m tr w-1/3"/>
<input name="ou2" bind:value={configuration.o.u3} type="text" class="in-l tr w-1/3"/>
</div>
</div>
</div>
{/if}
{#if configuration.p.r.startsWith("10YNO") || configuration.p.r == '10Y1001A1001A48H'}
<div class="cnt">
<strong class="text-sm">Tariff thresholds</strong>
<a href="https://github.com/gskjold/AmsToMqttBridge/wiki/Threshold-configuration" target="_blank" class="float-right"><HelpIcon/></a>
<input type="hidden" name="t" value="true"/>
<div class="flex flex-wrap my-1">
<label class="flex w-40 m-1">
<span class="in-pre">1</span>
<input name="t0" bind:value={configuration.t.t[0]} type="number" min="0" max="255" class="in-txt w-full"/>
<span class="in-post">kWh</span>
</label>
<label class="flex w-40 m-1">
<span class="in-pre">2</span>
<input name="t1" bind:value={configuration.t.t[1]} type="number" min="0" max="255" class="in-txt w-full"/>
<span class="in-post">kWh</span>
</label>
<label class="flex w-40 m-1">
<span class="in-pre">3</span>
<input name="t2" bind:value={configuration.t.t[2]} type="number" min="0" max="255" class="in-txt w-full"/>
<span class="in-post">kWh</span>
</label>
<label class="flex w-40 m-1">
<span class="in-pre">4</span>
<input name="t3" bind:value={configuration.t.t[3]} type="number" min="0" max="255" class="in-txt w-full"/>
<span class="in-post">kWh</span>
</label>
<label class="flex w-40 m-1">
<span class="in-pre">5</span>
<input name="t4" bind:value={configuration.t.t[4]} type="number" min="0" max="255" class="in-txt w-full"/>
<span class="in-post">kWh</span>
</label>
<label class="flex w-40 m-1">
<span class="in-pre">6</span>
<input name="t5" bind:value={configuration.t.t[5]} type="number" min="0" max="255" class="in-txt w-full"/>
<span class="in-post">kWh</span>
</label>
<label class="flex w-40 m-1">
<span class="in-pre">7</span>
<input name="t6" bind:value={configuration.t.t[6]} type="number" min="0" max="255" class="in-txt w-full"/>
<span class="in-post">kWh</span>
</label>
<label class="flex w-40 m-1">
<span class="in-pre">8</span>
<input name="t7" bind:value={configuration.t.t[7]} type="number" min="0" max="255" class="in-txt w-full"/>
<span class="in-post">kWh</span>
</label>
<label class="flex w-40 m-1">
<span class="in-pre">9</span>
<input name="t8" bind:value={configuration.t.t[8]} type="number" min="0" max="255" class="in-txt w-full"/>
<span class="in-post">kWh</span>
</label>
</div>
<label class="flex m-1">
<span class="in-pre">Average of</span>
<input name="th" bind:value={configuration.t.h} type="number" min="0" max="255" class="in-txt tr w-full"/>
<span class="in-post">hours</span>
</label>
</div>
{/if}
<div class="cnt">
<strong class="text-sm">User interface</strong>
<input type="hidden" name="u" value="true"/>
<div class="flex flex-wrap">
<div class="w-1/2">
Import gauge<br/>
<select name="ui" bind:value={configuration.u.i} class="in-s">
<option value={0}>Hide</option>
<option value={1}>Show</option>
<option value={2}>Dynamic</option>
</select>
</div>
<div class="w-1/2">
Export gauge<br/>
<select name="ue" bind:value={configuration.u.e} class="in-s">
<option value={0}>Hide</option>
<option value={1}>Show</option>
<option value={2}>Dynamic</option>
</select>
</div>
<div class="w-1/2">
Voltage<br/>
<select name="uv" bind:value={configuration.u.v} class="in-s">
<option value={0}>Hide</option>
<option value={1}>Show</option>
<option value={2}>Dynamic</option>
</select>
</div>
<div class="w-1/2">
Amperage<br/>
<select name="ua" bind:value={configuration.u.a} class="in-s">
<option value={0}>Hide</option>
<option value={1}>Show</option>
<option value={2}>Dynamic</option>
</select>
</div>
<div class="w-1/2">
Reactive<br/>
<select name="ur" bind:value={configuration.u.r} class="in-s">
<option value={0}>Hide</option>
<option value={1}>Show</option>
<option value={2}>Dynamic</option>
</select>
</div>
<div class="w-1/2">
Realtime<br/>
<select name="uc" bind:value={configuration.u.c} class="in-s">
<option value={0}>Hide</option>
<option value={1}>Show</option>
<option value={2}>Dynamic</option>
</select>
</div>
<div class="w-1/2">
Peaks<br/>
<select name="ut" bind:value={configuration.u.t} class="in-s">
<option value={0}>Hide</option>
<option value={1}>Show</option>
<option value={2}>Dynamic</option>
</select>
</div>
<div class="w-1/2">
Price<br/>
<select name="up" bind:value={configuration.u.p} class="in-s">
<option value={0}>Hide</option>
<option value={1}>Show</option>
<option value={2}>Dynamic</option>
</select>
</div>
<div class="w-1/2">
Day plot<br/>
<select name="ud" bind:value={configuration.u.d} class="in-s">
<option value={0}>Hide</option>
<option value={1}>Show</option>
<option value={2}>Dynamic</option>
</select>
</div>
<div class="w-1/2">
Month plot<br/>
<select name="um" bind:value={configuration.u.m} class="in-s">
<option value={0}>Hide</option>
<option value={1}>Show</option>
<option value={2}>Dynamic</option>
</select>
</div>
<div class="w-1/2">
Temperature plot<br/>
<select name="us" bind:value={configuration.u.s} class="in-s">
<option value={0}>Hide</option>
<option value={1}>Show</option>
<option value={2}>Dynamic</option>
</select>
</div>
</div>
</div>
{#if sysinfo.board > 20 || sysinfo.chip == 'esp8266'}
<div class="cnt">
<strong class="text-sm">Hardware</strong>
<a href="https://github.com/gskjold/AmsToMqttBridge/wiki/GPIO-configuration" target="_blank" class="float-right"><HelpIcon/></a>
{#if sysinfo.board > 20}
<input type="hidden" name="i" value="true"/>
<div class="flex flex-wrap">
<div class="w-1/3">
HAN<br/>
<select name="ih" bind:value={configuration.i.h} class="in-f w-full">
<UartSelectOptions chip={sysinfo.chip}/>
</select>
</div>
<div class="w-1/3">
AP button<br/>
<input name="ia" bind:value={configuration.i.a} type="number" min="0" max={sysinfo.chip == 'esp8266' ? 16 : sysinfo.chip == 'esp32s2' ? 44 : 39} class="in-m tr w-full"/>
</div>
<div class="w-1/3">
LED<label class="ml-4"><input name="ili" value="true" bind:checked={configuration.i.l.i} type="checkbox" class="rounded mb-1"/> inv</label><br/>
<div class="flex">
<input name="ilp" bind:value={configuration.i.l.p} type="number" min="0" max={sysinfo.chip == 'esp8266' ? 16 : sysinfo.chip == 'esp32s2' ? 44 : 39} class="in-l tr w-full"/>
</div>
</div>
<div class="w-full">
RGB<label class="ml-4"><input name="iri" value="true" bind:checked={configuration.i.r.i} type="checkbox" class="rounded mb-1"/> inverted</label><br/>
<div class="flex">
<input name="irr" bind:value={configuration.i.r.r} type="number" min="0" max={sysinfo.chip == 'esp8266' ? 16 : sysinfo.chip == 'esp32s2' ? 44 : 39} class="in-f tr w-1/3"/>
<input name="irg" bind:value={configuration.i.r.g} type="number" min="0" max={sysinfo.chip == 'esp8266' ? 16 : sysinfo.chip == 'esp32s2' ? 44 : 39} class="in-m tr w-1/3"/>
<input name="irb" bind:value={configuration.i.r.b} type="number" min="0" max={sysinfo.chip == 'esp8266' ? 16 : sysinfo.chip == 'esp32s2' ? 44 : 39} class="in-l tr w-1/3"/>
</div>
</div>
<div class="my-1 w-1/3">
Temperature<br/>
<input name="itd" bind:value={configuration.i.t.d} type="number" min="0" max={sysinfo.chip == 'esp8266' ? 16 : sysinfo.chip == 'esp32s2' ? 44 : 39} class="in-f tr w-full"/>
</div>
<div class="my-1 pr-1 w-1/3">
Analog temp<br/>
<input name="ita" bind:value={configuration.i.t.a} type="number" min="0" max={sysinfo.chip == 'esp8266' ? 16 : sysinfo.chip == 'esp32s2' ? 44 : 39} class="in-l tr w-full"/>
</div>
{#if sysinfo.chip != 'esp8266'}
<div class="my-1 pl-1 w-1/3">
Vcc<br/>
<input name="ivp" bind:value={configuration.i.v.p} type="number" min="0" max={sysinfo.chip == 'esp8266' ? 16 : sysinfo.chip == 'esp32s2' ? 44 : 39} class="in-s tr w-full"/>
</div>
{/if}
{#if configuration.i.v.p > 0}
<div class="my-1">
Voltage divider<br/>
<div class="flex">
<input name="ivdv" bind:value={configuration.i.v.d.v} type="number" min="0" max="65535" class="in-f tr w-full" placeholder="VCC"/>
<input name="ivdg" bind:value={configuration.i.v.d.g} type="number" min="0" max="65535" class="in-l tr w-full" placeholder="GND"/>
</div>
</div>
{/if}
</div>
{/if}
{#if sysinfo.chip == 'esp8266'}
<input type="hidden" name="iv" value="true"/>
<div class="my-1 flex flex-wrap">
<div class="w-1/3">
Vcc offset<br/>
<input name="ivo" bind:value={configuration.i.v.o} type="number" min="0.0" max="3.5" step="0.01" class="in-f tr w-full"/>
</div>
<div class="w-1/3 pr-1">
Multiplier<br/>
<input name="ivm" bind:value={configuration.i.v.m} type="number" min="0.1" max="10" step="0.01" class="in-l tr w-full"/>
</div>
{#if sysinfo.board == 2 || sysinfo.board == 100}
<div class="w-1/3 pl-1">
Boot limit<br/>
<input name="ivb" bind:value={configuration.i.v.b} type="number" min="2.5" max="3.5" step="0.1" class="in-s tr w-full"/>
</div>
{/if}
</div>
{/if}
</div>
{/if}
<div class="cnt">
<strong class="text-sm">Debugging</strong>
<a href="https://amsleser.no/blog/post/24-telnet-debug" target="_blank" class="float-right"><HelpIcon/></a>
<input type="hidden" name="d" value="true"/>
<div class="mt-3">
<label><input type="checkbox" name="ds" value="true" bind:checked={configuration.d.s} class="rounded mb-1"/> Enable debugging</label>
</div>
{#if configuration.d.s}
<div class="bd-red">Debug can cause sudden reboots. Do not leave on!</div>
<div class="my-1">
<label><input type="checkbox" name="dt" value="true" bind:checked={configuration.d.t} class="rounded mb-1"/> Enable telnet</label>
</div>
{#if configuration.d.t}
<div class="bd-red">Telnet is unsafe and should be off when not in use</div>
{/if}
<div class="my-1">
<select name="dl" bind:value={configuration.d.l} class="in-s">
<option value={1}>Verbose</option>
<option value={2}>Debug</option>
<option value={3}>Info</option>
<option value={4}>Warning</option>
</select>
</div>
{/if}
</div>
</div>
<div class="grid grid-cols-3">
<div>
<button type="button" on:click={factoryReset} class="py-2 px-4 rounded bg-red-500 text-white ml-2">Factory reset</button>
</div>
<div class="text-center">
<button type="button" on:click={askReboot} class="py-2 px-4 rounded bg-yellow-500 text-white">Reboot</button>
</div>
<div class="text-right">
<button type="submit" class="btn-pri">Save</button>
</div>
</div>
</form>
<Mask active={loading} message="Loading configuration"/>
<Mask active={saving} message="Saving configuration"/>
<Mask active={isFactoryReset} message="Device have been factory reset and switched to AP mode"/>

View File

@@ -1,10 +0,0 @@
import { writable } from 'svelte/store';
let configuration = {};
export const configurationStore = writable(configuration);
export async function getConfiguration() {
const response = await fetch("/configuration.json");
configuration = (await response.json())
configurationStore.set(configuration);
};

View File

@@ -1,54 +0,0 @@
<script>
import { sysinfoStore } from './DataStores.js';
import Mask from './Mask.svelte'
import { navigate } from 'svelte-navigator';
export let sysinfo = {}
let loadingOrSaving = false;
async function handleSubmit(e) {
loadingOrSaving = true;
const formData = new FormData(e.target)
const data = new URLSearchParams()
for (let field of formData) {
const [key, value] = field
data.append(key, value)
}
const response = await fetch('/save', {
method: 'POST',
body: data
});
let res = (await response.json())
loadingOrSaving = false;
sysinfoStore.update(s => {
s.fwconsent = formData['sf'] === true ? 1 : formData['sf'] === false ? 2 : 0;
s.booting = res.reboot;
return s;
});
navigate("/");
}
</script>
<div class="grid xl:grid-cols-3 lg:grid-cols-2">
<div class="cnt">
<form on:submit|preventDefault={handleSubmit} autocomplete="off">
<div>
Various permissions we need to do stuff:
</div>
<hr/>
<div class="my-3">
Enable one-click upgrade? (implies data collection)<br/>
<a href="https://github.com/gskjold/AmsToMqttBridge/wiki/Data-collection-on-one-click-firmware-upgrade" target="_blank" class="text-blue-600 hover:text-blue-800">Read more</a><br/>
<label><input type="radio" name="sf" value={1} checked={sysinfo.fwconsent === 1} class="rounded m-2" required/> Yes</label><label><input type="radio" name="sf" value={2} checked={sysinfo.fwconsent === 2} class="rounded m-2" required/> No</label><br/>
</div>
<div class="my-3">
<button type="submit" class="btn-pri">Save</button>
</div>
</form>
</div>
</div>
<Mask active={loadingOrSaving} message="Saving preferences"/>

View File

@@ -1,10 +0,0 @@
<script>
let europe = ["Amsterdam","Athens","Belfast","Berlin","Bratislava","Brussels","Bucharest","Budapest","Copenhagen","Dublin",
"Helsinki","Lisbon","Ljubljana","London","Luxembourg","Madrid","Malta","Nicosia","Oslo","Paris","Prague","Riga","Rome",
"Sofia","Stockholm","Tallinn","Vienna","Vilnius","Warsaw","Zagreb","Zurich"];
</script>
<option>GMT</option>
{#each europe as c}
<option>Europe/{c}</option>
{/each}

View File

@@ -1,103 +0,0 @@
<script>
import { pricesStore, dayPlotStore, monthPlotStore, temperaturesStore } from './DataStores.js';
import { metertype, uiVisibility } from './Helpers.js';
import PowerGauge from './PowerGauge.svelte';
import VoltPlot from './VoltPlot.svelte';
import AmpPlot from './AmpPlot.svelte';
import ReactiveData from './ReactiveData.svelte';
import AccountingData from './AccountingData.svelte';
import PricePlot from './PricePlot.svelte';
import DayPlot from './DayPlot.svelte';
import MonthPlot from './MonthPlot.svelte';
import TemperaturePlot from './TemperaturePlot.svelte';
import TariffPeakChart from './TariffPeakChart.svelte';
export let data = {}
export let sysinfo = {}
let prices = {}
let dayPlot = {}
let monthPlot = {}
let temperatures = {};
pricesStore.subscribe(update => {
prices = update;
});
dayPlotStore.subscribe(update => {
dayPlot = update;
});
monthPlotStore.subscribe(update => {
monthPlot = update;
});
temperaturesStore.subscribe(update => {
temperatures = update;
});
</script>
<div class="grid xl:grid-cols-6 lg:grid-cols-4 md:grid-cols-3 sm:grid-cols-2">
{#if uiVisibility(sysinfo.ui.i, data.i)}
<div class="cnt">
<div class="grid grid-cols-2">
<div class="col-span-2">
<PowerGauge val={data.i ? data.i : 0} max={data.im} unit="W" label="Import" sub={data.p} subunit={prices.currency}/>
</div>
<div>{data.mt ? metertype(data.mt) : '-'}</div>
<div class="text-right">{data.ic ? data.ic.toFixed(1) : '-'} kWh</div>
</div>
</div>
{/if}
{#if uiVisibility(sysinfo.ui.e, data.om || data.e > 0)}
<div class="cnt">
<div class="grid grid-cols-2">
<div class="col-span-2">
<PowerGauge val={data.e ? data.e : 0} max={data.om ? data.om : 10000} unit="W" label="Export"/>
</div>
<div></div>
<div class="text-right">{data.ec ? data.ec.toFixed(1) : '-'} kWh</div>
</div>
</div>
{/if}
{#if uiVisibility(sysinfo.ui.v, data.u1 > 100 || data.u2 > 100 || data.u3 > 100)}
<div class="cnt">
<VoltPlot u1={data.u1} u2={data.u2} u3={data.u3} ds={data.ds}/>
</div>
{/if}
{#if uiVisibility(sysinfo.ui.a, data.i1 > 0.01 || data.i2 > 0.01 || data.i3 > 0.01)}
<div class="cnt">
<AmpPlot u1={data.u1} u2={data.u2} u3={data.u3} i1={data.i1} i2={data.i2} i3={data.i3} max={data.mf ? data.mf : 32}/>
</div>
{/if}
{#if uiVisibility(sysinfo.ui.r, data.ri > 0 || data.re > 0 || data.ric > 0 || data.rec > 0)}
<div class="cnt">
<ReactiveData importInstant={data.ri} exportInstant={data.re} importTotal={data.ric} exportTotal={data.rec}/>
</div>
{/if}
{#if uiVisibility(sysinfo.ui.c, data.ea)}
<div class="cnt">
<AccountingData data={data.ea} currency={prices.currency}/>
</div>
{/if}
{#if data && data.pr && (data.pr.startsWith("10YNO") || data.pr == '10Y1001A1001A48H')}
<div class="cnt h-64">
<TariffPeakChart />
</div>
{/if}
{#if uiVisibility(sysinfo.ui.p, (typeof data.p == "number") && !Number.isNaN(data.p))}
<div class="cnt xl:col-span-6 lg:col-span-4 md:col-span-3 sm:col-span-2 h-64">
<PricePlot json={prices}/>
</div>
{/if}
{#if uiVisibility(sysinfo.ui.d, dayPlot)}
<div class="cnt xl:col-span-6 lg:col-span-4 md:col-span-3 sm:col-span-2 h-64">
<DayPlot json={dayPlot} />
</div>
{/if}
{#if uiVisibility(sysinfo.ui.m, monthPlot)}
<div class="cnt xl:col-span-6 lg:col-span-4 md:col-span-3 sm:col-span-2 h-64">
<MonthPlot json={monthPlot} />
</div>
{/if}
{#if uiVisibility(sysinfo.ui.s, data.t && data.t != -127 && temperatures.c > 1)}
<div class="cnt xl:col-span-6 lg:col-span-4 md:col-span-3 sm:col-span-2 h-64">
<TemperaturePlot json={temperatures} />
</div>
{/if}
</div>

View File

@@ -1,183 +0,0 @@
import { readable, writable } from 'svelte/store';
import { isBusPowered } from './Helpers';
async function fetchWithTimeout(resource, options = {}) {
const { timeout = 8000 } = options;
const controller = new AbortController();
const id = setTimeout(() => controller.abort(), timeout);
const response = await fetch(resource, {
...options,
signal: controller.signal
});
clearTimeout(id);
return response;
}
let sysinfo = {
version: '',
chip: '',
mac: null,
apmac: null,
vndcfg: null,
usrcfg: null,
fwconsent: null,
booting: false,
upgrading: false,
ui: {},
security: 0
};
export const sysinfoStore = writable(sysinfo);
export async function getSysinfo() {
const response = await fetchWithTimeout("/sysinfo.json?t=" + Math.floor(Date.now() / 1000));
sysinfo = (await response.json())
sysinfoStore.set(sysinfo);
};
let tries = 0;
let lastTemp = -127;
let lastPrice = null;
let data = {};
export const dataStore = readable(data, (set) => {
let timeout;
async function getData() {
fetchWithTimeout("/data.json")
.then((res) => res.json())
.then((data) => {
set(data);
if(lastTemp != data.t) {
lastTemp = data.t;
setTimeout(getTemperatures, 2000);
}
if(lastPrice != data.p) {
lastPrice = data.p;
setTimeout(getPrices, 4000);
}
if(sysinfo.upgrading) {
window.location.reload();
} else if(!sysinfo || !sysinfo.chip || sysinfo.booting || (tries > 1 && !isBusPowered(sysinfo.board))) {
getSysinfo();
if(dayPlotTimeout) clearTimeout(dayPlotTimeout);
dayPlotTimeout = setTimeout(getDayPlot, 2000);
if(monthPlotTimeout) clearTimeout(monthPlotTimeout);
monthPlotTimeout = setTimeout(getMonthPlot, 3000);
}
let to = 5000;
if(isBusPowered(sysinfo.board) && data.v > 2.5) {
let diff = (3.3 - Math.min(3.3, data.v));
if(diff > 0) {
to = Math.max(diff, 0.1) * 10 * 5000;
}
}
if(to > 5000) console.log("Scheduling next data fetch in " + to + "ms");
if(timeout) clearTimeout(timeout);
timeout = setTimeout(getData, to);
tries = 0;
})
.catch((err) => {
tries++;
if(tries > 3) {
set({
em: 3,
hm: 0,
wm: 0,
mm: 0
});
timeout = setTimeout(getData, 15000);
} else {
timeout = setTimeout(getData, isBusPowered(sysinfo.board) ? 10000 : 5000);
}
});
}
getData();
return function stop() {
clearTimeout(timeout);
}
});
let prices = {};
export const pricesStore = writable(prices);
export async function getPrices() {
const response = await fetchWithTimeout("/energyprice.json");
prices = (await response.json())
pricesStore.set(prices);
}
let dayPlot = {};
let dayPlotTimeout;
export async function getDayPlot() {
if(dayPlotTimeout) {
clearTimeout(dayPlotTimeout);
dayPlotTimeout = 0;
}
const response = await fetchWithTimeout("/dayplot.json");
dayPlot = (await response.json())
dayPlotStore.set(dayPlot);
let date = new Date();
dayPlotTimeout = setTimeout(getDayPlot, ((60-date.getMinutes())*60000)+20)
}
export const dayPlotStore = writable(dayPlot, (set) => {
getDayPlot();
return function stop() {}
});
let monthPlot = {};
let monthPlotTimeout;
export async function getMonthPlot() {
if(monthPlotTimeout) {
clearTimeout(monthPlotTimeout);
monthPlotTimeout = 0;
}
const response = await fetchWithTimeout("/monthplot.json");
monthPlot = (await response.json())
monthPlotStore.set(monthPlot);
let date = new Date();
monthPlotTimeout = setTimeout(getMonthPlot, ((24-date.getHours())*3600000)+40)
}
export const monthPlotStore = writable(monthPlot, (set) => {
getMonthPlot();
return function stop() {}
});
let temperatures = {};
export async function getTemperatures() {
const response = await fetchWithTimeout("/temperature.json");
temperatures = (await response.json())
temperaturesStore.set(temperatures);
}
export const temperaturesStore = writable(temperatures, (set) => {
getTemperatures();
return function stop() {}
});
let tariff = {};
let tariffTimeout;
export async function getTariff() {
if(tariffTimeout) {
clearTimeout(tariffTimeout);
tariffTimeout = 0;
}
const response = await fetchWithTimeout("/tariff.json");
tariff = (await response.json())
tariffStore.set(tariff);
let date = new Date();
tariffTimeout = setTimeout(getTariff, ((60-date.getMinutes())*60000)+30)
}
export const tariffStore = writable(tariff, (set) => {
return function stop() {}
});
let releases = [];
export const gitHubReleaseStore = writable(releases);
export async function getGitHubReleases() {
const response = await fetchWithTimeout("https://api.github.com/repos/gskjold/AmsToMqttBridge/releases");
releases = (await response.json())
gitHubReleaseStore.set(releases);
};

View File

@@ -1,101 +0,0 @@
<script>
import { zeropad } from './Helpers.js';
import BarChart from './BarChart.svelte';
export let json;
let config = {};
let max = 0;
let min = 0;
$: {
let i = 0;
let yTicks = [];
let xTicks = [];
let points = [];
let cur = new Date();
let offset = -cur.getTimezoneOffset()/60;
for(i = cur.getUTCHours(); i<24; i++) {
let imp = json["i"+zeropad(i)];
let exp = json["e"+zeropad(i)];
if(imp === undefined) imp = 0;
if(exp === undefined) exp = 0;
xTicks.push({
label: zeropad((i+offset)%24)
});
points.push({
label: imp.toFixed(1),
value: imp*10,
label2: exp.toFixed(1),
value2: exp*10,
color: '#7c3aed'
});
min = Math.max(min, exp*10);
max = Math.max(max, imp*10);
};
for(i = 0; i < cur.getUTCHours(); i++) {
let imp = json["i"+zeropad(i)];
let exp = json["e"+zeropad(i)];
if(imp === undefined) imp = 0;
if(exp === undefined) exp = 0;
xTicks.push({
label: zeropad((i+offset)%24)
});
points.push({
label: imp.toFixed(1),
value: imp*10,
label2: exp.toFixed(1),
value2: exp*10,
color: '#7c3aed'
});
min = Math.max(min, exp*10);
max = Math.max(max, imp*10);
};
let boundary = Math.ceil(Math.max(min, max));
max = boundary;
min = min == 0 ? 0 : boundary*-1;
if(min < 0) {
let yTickDistDown = min/4;
for(i = 1; i < 5; i++) {
let val = (yTickDistDown*i);
yTicks.push({
value: val,
label: (val/10).toFixed(1)
});
}
}
let yTickDistUp = max/4;
for(i = 0; i < 5; i++) {
let val = (yTickDistUp*i);
yTicks.push({
value: val,
label: (val/10).toFixed(1)
});
}
config = {
title: "Energy use last 24 hours (kWh)",
height: 226,
width: 1520,
padding: { top: 20, right: 15, bottom: 20, left: 35 },
y: {
min: min,
max: max,
ticks: yTicks
},
x: {
ticks: xTicks
},
points: points
};
};
</script>
<BarChart config={config} />

View File

@@ -1,6 +0,0 @@
<script></script>
<!-- Heroicons -->
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-6 h-6">
<path stroke-linecap="round" stroke-linejoin="round" d="M9 8.25H7.5a2.25 2.25 0 00-2.25 2.25v9a2.25 2.25 0 002.25 2.25h9a2.25 2.25 0 002.25-2.25v-9a2.25 2.25 0 00-2.25-2.25H15M9 12l3 3m0 0l3-3m-3 3V2.25" />
</svg>

View File

@@ -1,22 +0,0 @@
<script>
import Mask from "./Mask.svelte";
export let action;
export let title;
let uploading = false;
</script>
<div class="grid xl:grid-cols-4 lg:grid-cols-2 md:grid-cols-2">
<div class="cnt">
<strong>Upload {title}</strong>
<p class="mb-4">Select a suitable file and click upload</p>
<form action="{action}" enctype="multipart/form-data" method="post" on:submit={() => uploading=true} autocomplete="off">
<input name="file" type="file">
<div class="w-full text-right mt-4">
<button type="submit" class="btn-pri">Upload</button>
</div>
</form>
</div>
</div>
<Mask active={uploading} message="Uploading file, please wait"/>

View File

@@ -1,7 +0,0 @@
<script></script>
<!-- Heroicons -->
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-6 h-6">
<path stroke-linecap="round" stroke-linejoin="round" d="M10.343 3.94c.09-.542.56-.94 1.11-.94h1.093c.55 0 1.02.398 1.11.94l.149.894c.07.424.384.764.78.93.398.164.855.142 1.205-.108l.737-.527a1.125 1.125 0 011.45.12l.773.774c.39.389.44 1.002.12 1.45l-.527.737c-.25.35-.272.806-.107 1.204.165.397.505.71.93.78l.893.15c.543.09.94.56.94 1.109v1.094c0 .55-.397 1.02-.94 1.11l-.893.149c-.425.07-.765.383-.93.78-.165.398-.143.854.107 1.204l.527.738c.32.447.269 1.06-.12 1.45l-.774.773a1.125 1.125 0 01-1.449.12l-.738-.527c-.35-.25-.806-.272-1.203-.107-.397.165-.71.505-.781.929l-.149.894c-.09.542-.56.94-1.11.94h-1.094c-.55 0-1.019-.398-1.11-.94l-.148-.894c-.071-.424-.384-.764-.781-.93-.398-.164-.854-.142-1.204.108l-.738.527c-.447.32-1.06.269-1.45-.12l-.773-.774a1.125 1.125 0 01-.12-1.45l.527-.737c.25-.35.273-.806.108-1.204-.165-.397-.505-.71-.93-.78l-.894-.15c-.542-.09-.94-.56-.94-1.109v-1.094c0-.55.398-1.02.94-1.11l.894-.149c.424-.07.765-.383.93-.78.165-.398.143-.854-.107-1.204l-.527-.738a1.125 1.125 0 01.12-1.45l.773-.773a1.125 1.125 0 011.45-.12l.737.527c.35.25.807.272 1.204.107.397-.165.71-.505.78-.929l.15-.894z" />
<path stroke-linecap="round" stroke-linejoin="round" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
</svg>

View File

@@ -1,100 +0,0 @@
<script>
import { Link } from "svelte-navigator";
import { sysinfoStore, getGitHubReleases, gitHubReleaseStore } from './DataStores.js';
import { upgrade, getNextVersion } from './UpgradeHelper';
import { boardtype, hanError, mqttError, priceError, isBusPowered } from './Helpers.js';
import AmsleserSvg from "./../assets/favicon.svg";
import GitHubLogo from './../assets/github.svg';
import Uptime from "./Uptime.svelte";
import Badge from './Badge.svelte';
import Clock from './Clock.svelte';
import GearIcon from './GearIcon.svelte';
import InfoIcon from "./InfoIcon.svelte";
import HelpIcon from "./HelpIcon.svelte";
import DownloadIcon from "./DownloadIcon.svelte";
export let data = {}
let sysinfo = {}
let nextVersion = {};
function askUpgrade() {
if(confirm('Do you want to upgrade this device to ' + nextVersion.tag_name + '?')) {
if(!isBusPowered(sysinfo.board) || confirm('WARNING: ' + boardtype(sysinfo.chip, sysinfo.board) + ' must be connected to an external power supply during firmware upgrade. Failure to do so may cause power-down during upload resulting in non-functioning unit.')) {
sysinfoStore.update(s => {
s.upgrading = true;
return s;
});
upgrade(nextVersion);
}
}
}
sysinfoStore.subscribe(update => {
sysinfo = update;
if(update.fwconsent === 1) {
getGitHubReleases();
}
});
gitHubReleaseStore.subscribe(releases => {
nextVersion = getNextVersion(sysinfo.version, releases);
});
</script>
<nav class="bg-violet-600 p-1 rounded-md mx-2">
<div class="flex flex-wrap space-x-4 text-sm text-gray-300">
<div class="flex text-lg text-gray-100 p-2">
<Link to="/">AMS reader <span>{sysinfo.version}</span></Link>
</div>
<div class="flex-none my-auto p-2 flex space-x-4">
<div class="flex-none my-auto"><Uptime epoch={data.u}/></div>
{#if data.t > -50}
<div class="flex-none my-auto">{ data.t > -50 ? data.t.toFixed(1) : '-' }&deg;C</div>
{/if}
<div class="flex-none my-auto">Free mem: {data.m ? (data.m/1000).toFixed(1) : '-'}kb</div>
</div>
<div class="flex-auto flex-wrap my-auto justify-center p-2">
<Badge title="ESP" text={sysinfo.booting ? 'Booting' : data.v > 2.0 ? data.v.toFixed(2)+"V" : "ESP"} color={sysinfo.booting ? 'yellow' : data.em === 1 ? 'green' : data.em === 2 ? 'yellow' : data.em === 3 ? 'red' : 'gray'}/>
<Badge title="HAN" text="HAN" color={sysinfo.booting ? 'gray' : data.hm === 1 ? 'green' : data.hm === 2 ? 'yellow' : data.hm === 3 ? 'red' : 'gray'}/>
<Badge title="WiFi" text={data.r ? data.r.toFixed(0)+"dBm" : "WiFi"} color={sysinfo.booting ? 'gray' : data.wm === 1 ? 'green' : data.wm === 2 ? 'yellow' : data.wm === 3 ? 'red' : 'gray'}/>
<Badge title="MQTT" text="MQTT" color={sysinfo.booting ? 'gray' : data.mm === 1 ? 'green' : data.mm === 2 ? 'yellow' : data.mm === 3 ? 'red' : 'gray'}/>
</div>
{#if data.he < 0 || data.he > 0}
<div class="bd-red">{ 'HAN: ' + hanError(data.he) }</div>
{/if}
{#if data.me < 0}
<div class="bd-red">{ 'MQTT: ' + mqttError(data.me) }</div>
{/if}
{#if data.ee > 0 || data.ee < 0}
<div class="bd-red">{ 'PriceAPI: ' + priceError(data.ee) }</div>
{/if}
<div class="flex-auto p-2 flex flex-row-reverse flex-wrap">
<div class="flex-none">
<a class="float-right" href='https://github.com/gskjold/AmsToMqttBridge' target='_blank' rel="noreferrer" aria-label="GitHub"><img class="gh-logo" src={GitHubLogo} alt="GitHub repo"/></a>
</div>
<div class="flex-none my-auto px-2">
<Clock timestamp={ data.c ? new Date(data.c * 1000) : new Date(0) } />
</div>
{#if sysinfo.vndcfg && sysinfo.usrcfg}
<div class="flex-none px-1 mt-1" title="Configuration">
<Link to="/configuration"><GearIcon/></Link>
</div>
<div class="flex-none px-1 mt-1" title="Device information">
<Link to="/status"><InfoIcon/></Link>
</div>
{/if}
<div class="flex-none px-1 mt-1" title="Documentation">
<a href="https://github.com/gskjold/AmsToMqttBridge/wiki" target='_blank' rel="noreferrer"><HelpIcon/></a>
</div>
{#if sysinfo.fwconsent === 1 && nextVersion}
<div class="flex-none mr-3 text-yellow-500" title="New version: {nextVersion.tag_name}">
{#if sysinfo.security == 0 || data.a}
<button on:click={askUpgrade} class="flex"><span class="mt-1">New version: {nextVersion.tag_name}</span> <DownloadIcon/></button>
{:else}
<span>New version: {nextVersion.tag_name}</span>
{/if}
</div>
{/if}
</div>
</div>
</nav>

View File

@@ -1,6 +0,0 @@
<script></script>
<!-- Heroicons -->
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-6 h-6">
<path stroke-linecap="round" stroke-linejoin="round" d="M9.879 7.519c1.171-1.025 3.071-1.025 4.242 0 1.172 1.025 1.172 2.687 0 3.712-.203.179-.43.326-.67.442-.745.361-1.45.999-1.45 1.827v.75M21 12a9 9 0 11-18 0 9 9 0 0118 0zm-9 5.25h.008v.008H12v-.008z" />
</svg>

View File

@@ -1,160 +0,0 @@
export let monthnames = ["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"];
export function voltcol(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';
};
export function ampcol(pct) {
if(pct > 90) return '#d90000';
else if(pct > 85) return'#e32100';
else if(pct > 80) return '#ffb800';
else if(pct > 75) return '#dcd800';
else return '#32d900';
};
export function metertype(mt) {
switch(mt) {
case 1:
return "Aidon";
case 2:
return "Kaifa";
case 3:
return "Kamstrup";
case 8:
return "Iskra";
case 9:
return "Landis+Gyr";
case 10:
return "Sagemcom";
default:
return "";
}
}
export function zeropad(num) {
num = num.toString();
while (num.length < 2) num = "0" + num;
return num;
}
export function boardtype(c, b) {
switch(b) {
case 5:
switch(c) {
case 'esp8266':
return "Pow-K (GPIO12)";
case 'esp32s2':
return "Pow-K+";
}
case 7:
switch(c) {
case 'esp8266':
return "Pow-U (GPIO12)";
case 'esp32s2':
return "Pow-U+";
}
case 6:
return "Pow-P1";
case 51:
return "Wemos S2 mini";
case 50:
return "Generic ESP32-S2";
case 201:
return "Wemos LOLIN D32";
case 202:
return "Adafruit HUZZAH32";
case 203:
return "DevKitC";
case 200:
return "Generic ESP32";
case 2:
return "HAN Reader 2.0 by Max Spencer";
case 0:
return "Custom hardware by Roar Fredriksen";
case 1:
return "Kamstrup module by Egil Opsahl"
case 3:
return "Pow-K (UART0)";
case 4:
return "Pow-U (UART0)";
case 101:
return "Wemos D1 mini";
case 100:
return "Generic ESP8266";
}
}
export function hanError(err) {
switch(err) {
case -1: return "Parse error";
case -2: return "Incomplete data received";
case -3: return "Payload boundry flag missing";
case -4: return "Header checksum error";
case -5: return "Footer checksum error";
case -9: return "Unknown data received, check meter config";
case -41: return "Frame length not equal";
case -51: return "Authentication failed";
case -52: return "Decryption failed";
case -53: return "Encryption key invalid";
case 90: return "No HAN data received last 30s";
case 98: return "Exception in code, debugging necessary";
case 99: return "Autodetection failed";
}
if(err < 0) return "Unspecified error "+err;
return "";
}
export function mqttError(err) {
switch(err) {
case -3: return "Connection failed";
case -4: return "Network timeout";
case -10: return "Connection denied";
case -11: return "Failed to subscribe";
case -13: return "Connection lost";
}
if(err < 0) return "Unspecified error "+err;
return "";
}
export function priceError(err) {
switch(err) {
case 401:
case 403:
return "Unauthorized, check API key";
case 404:
return "Price unavailable, not found";
case 500:
return "Internal server error";
case -2: return "Incomplete data received";
case -3: return "Invalid data, tag missing";
case -51: return "Authentication failed";
case -52: return "Decryption failed";
case -53: return "Encryption key invalid";
}
if(err < 0) return "Unspecified error "+err;
return "";
}
export function isBusPowered(boardType) {
switch(boardType) {
case 2:
case 4:
case 7:
return true;
}
return false;
}
export function uiVisibility(choice, state) {
return choice == 1 || (choice == 2 && state);
}

View File

@@ -1,6 +0,0 @@
<script></script>
<!-- Heroicons -->
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-6 h-6">
<path stroke-linecap="round" stroke-linejoin="round" d="M11.25 11.25l.041-.02a.75.75 0 011.063.852l-.708 2.836a.75.75 0 001.063.853l.041-.021M21 12a9 9 0 11-18 0 9 9 0 0118 0zm-9-3.75h.008v.008H12V8.25z" />
</svg>

View File

@@ -1,14 +0,0 @@
<script>
export let active;
export let message;
</script>
{#if active}
<div class="z-50" aria-modal="true">
<div class="fixed inset-0 bg-gray-500 bg-opacity-50 flex items-center justify-center">
{#if message}
<div class="bg-white m-2 p-3 rounded-md shadow-lg pb-4 text-gray-700 w-96">{message}</div>
{/if}
</div>
</div>
{/if}

View File

@@ -1,103 +0,0 @@
<script>
import { zeropad } from './Helpers.js';
import BarChart from './BarChart.svelte';
export let json;
let config = {};
let max = 0;
let min = 0;
$: {
let i = 0;
let yTicks = [];
let xTicks = [];
let points = [];
let cur = new Date();
let lm = new Date();
lm.setDate(0);
for(i = cur.getDate(); i<=lm.getDate(); i++) {
let imp = json["i"+zeropad(i)];
let exp = json["e"+zeropad(i)];
if(imp === undefined) imp = 0;
if(exp === undefined) exp = 0;
xTicks.push({
label: zeropad(i)
});
points.push({
label: imp.toFixed(0),
value: imp,
label2: exp.toFixed(0),
value2: exp,
color: '#7c3aed'
});
min = Math.max(min, exp);
max = Math.max(max, imp);
}
for(i = 1; i < cur.getDate(); i++) {
let imp = json["i"+zeropad(i)];
let exp = json["e"+zeropad(i)];
if(imp === undefined) imp = 0;
if(exp === undefined) exp = 0;
xTicks.push({
label: zeropad(i)
});
points.push({
label: imp.toFixed(0),
value: imp,
label2: exp.toFixed(0),
value2: exp,
color: '#7c3aed'
});
min = Math.max(min, exp);
max = Math.max(max, imp);
}
let boundary = Math.ceil(Math.max(min, max)/10)*10;
max = boundary;
min = min == 0 ? 0 : boundary*-1;
if(min < 0) {
let yTickDistDown = min/4;
for(i = 0; i < 5; i++) {
let val = (yTickDistDown*i);
yTicks.push({
value: val,
label: val.toFixed(0)
});
}
}
let yTickDistUp = max/4;
for(i = 0; i < 5; i++) {
let val = (yTickDistUp*i);
yTicks.push({
value: val,
label: val.toFixed(0)
});
}
config = {
title: "Energy use last month (kWh)",
height: 226,
width: 1520,
padding: { top: 20, right: 15, bottom: 20, left: 35 },
y: {
min: min,
max: max,
ticks: yTicks
},
x: {
ticks: xTicks
},
points: points
};
};
</script>
<BarChart config={config} />

View File

@@ -1,26 +0,0 @@
<script>
import PowerGaugeSvg from './PowerGaugeSvg.svelte';
import { ampcol } from './Helpers.js';
export let val;
export let max;
export let unit;
export let label;
export let sub = "";
export let subunit = "";
</script>
<div class="pl-root">
<PowerGaugeSvg pct={val/max * 100} color={ampcol(val/max * 100)}/>
<span class="pl-ov">
<span class="pl-lab">{label}</span>
<br/>
<span class="pl-val">{val}</span>
<span class="pl-unt">{unit}</span>
{#if sub}
<br/>
<span class="pl-sub">{sub}</span>
<span class="pl-snt">{subunit}/kWh</span>
{/if}
</span>
</div>

View File

@@ -1,31 +0,0 @@
<script>
export let pct = 0;
export let color = "red";
function polarToCartesian(centerX, centerY, radius, angleInDegrees) {
var angleInRadians = (angleInDegrees-90) * Math.PI / 180.0;
return {
x: centerX + (radius * Math.cos(angleInRadians)),
y: centerY + (radius * Math.sin(angleInRadians))
};
}
function describeArc(x, y, radius, startAngle, endAngle){
var start = polarToCartesian(x, y, radius, endAngle);
var end = polarToCartesian(x, y, radius, startAngle);
var arcSweep = endAngle - startAngle <= 180 ? "0" : "1";
var d = [
"M", start.x, start.y,
"A", radius, radius, 0, arcSweep, 0, end.x, end.y
].join(" ");
return d;
}
</script>
<svg viewBox="0 0 300 300" xmlns="http://www.w3.org/2000/svg" height="100%">
<path d="{ describeArc(150, 150, 115, 210, 510) }" stroke="#eee" fill="none" stroke-width="55"/>
<path d="{ describeArc(150, 150, 115, 210, 210 + (300*pct/100)) }" stroke={color} fill="none" stroke-width="55"/>
</svg>

View File

@@ -1,97 +0,0 @@
<script>
import { zeropad } from './Helpers.js';
import BarChart from './BarChart.svelte';
export let json;
let config = {};
let max = 0;
let min = 0;
$: {
let hour = new Date().getUTCHours();
let i = 0;
let val = 0;
let h = 0;
let d = json["20"] == null ? 2 : 1;
let yTicks = [];
let xTicks = [];
let points = [];
let cur = new Date();
for(i = hour; i<24; i++) {
cur.setUTCHours(i);
val = json[zeropad(h++)];
if(val == null) break;
xTicks.push({
label: zeropad(cur.getHours())
});
points.push({
label: val > 0 ? val.toFixed(d) : '',
value: val > 0 ? Math.abs(val*100) : 0,
label2: val < 0 ? val.toFixed(d) : '',
value2: val < 0 ? Math.abs(val*100) : 0,
color: '#7c3aed'
});
min = Math.min(min, val*100);
max = Math.max(max, val*100);
};
for(i = 0; i < 24; i++) {
cur.setUTCHours(i);
val = json[zeropad(h++)];
if(val == null) break;
xTicks.push({
label: zeropad(cur.getHours())
});
points.push({
label: val > 0 ? val.toFixed(d) : '',
value: val > 0 ? Math.abs(val*100) : 0,
label2: val < 0 ? val.toFixed(d) : '',
value2: val < 0 ? Math.abs(val*100) : 0,
color: '#7c3aed'
});
min = Math.min(min, val*100);
max = Math.max(max, val*100);
};
max = Math.ceil(max);
min = Math.floor(min);
if(min < 0) {
let yTickDistDown = min/4;
for(i = 1; i < 5; i++) {
let val = (yTickDistDown*i);
yTicks.push({
value: val,
label: (val/100).toFixed(2)
});
}
}
let yTickDistUp = max/4;
for(i = 0; i < 5; i++) {
let val = (yTickDistUp*i);
yTicks.push({
value: val,
label: (val/100).toFixed(2)
});
}
config = {
title: "Future energy price (" + json.currency + ")",
padding: { top: 20, right: 15, bottom: 20, left: 35 },
y: {
min: min,
max: max,
ticks: yTicks
},
x: {
ticks: xTicks
},
points: points
};
};
</script>
<a href="https://transparency.entsoe.eu/" target="_blank" class="text-xs float-right z-40">Provided by ENTSO-E</a>
<BarChart config={config} />

View File

@@ -1,24 +0,0 @@
<script>
export let importInstant;
export let exportInstant;
export let importTotal;
export let exportTotal;
</script>
<div class="mx-2">
<strong class="text-sm">Reactive</strong>
<div class="grid grid-cols-2 mt-4">
<div>Instant in</div>
<div class="text-right">{typeof importInstant !== 'undefined' ? importInstant.toFixed(0) : '-'} VAr</div>
<div>Instant out</div>
<div class="text-right">{typeof exportInstant !== 'undefined' ? exportInstant.toFixed(0) : '-'} VAr</div>
</div>
<div class="grid grid-cols-2 mt-4">
<div>Total in</div>
<div class="text-right">{typeof importTotal !== 'undefined' ? importTotal.toFixed(1) : '-'} kVArh</div>
<div>Total out</div>
<div class="text-right">{typeof exportTotal !== 'undefined' ? exportTotal.toFixed(1) : '-'} kVArh</div>
</div>
</div>

View File

@@ -1,121 +0,0 @@
<script>
import { sysinfoStore } from './DataStores.js';
import Mask from './Mask.svelte'
export let sysinfo = {}
let staticIp = false;
let loadingOrSaving = false;
let tries = 0;
function scanForDevice() {
var url = "";
tries++;
if(sysinfo.net.ip && tries%3 == 0) {
url = "http://" + sysinfo.net.ip;
} else if(sysinfo.hostname && tries%3 == 1) {
url = "http://" + sysinfo.hostname;
} else if(sysinfo.hostname && tries%3 == 2) {
url = "http://" + sysinfo.hostname + ".local";
} else {
url = "";
}
if(console) console.log("Trying url " + url);
var retry = function() {
setTimeout(scanForDevice, 1000);
};
var xhr = new XMLHttpRequest();
xhr.timeout = 5000;
xhr.addEventListener('abort', retry);
xhr.addEventListener('error', retry);
xhr.addEventListener('timeout', retry);
xhr.addEventListener('load', function(e) {
window.location.href = url ? url : "/";
});
xhr.open("GET", url + "/is-alive", true);
xhr.send();
};
async function handleSubmit(e) {
loadingOrSaving = true;
const formData = new FormData(e.target);
let hostname = sysinfo.hostname;
const data = new URLSearchParams();
for (let field of formData) {
const [key, value] = field;
data.append(key, value)
if(key == 'sh') hostname = value;
}
const response = await fetch('/save', {
method: 'POST',
body: data
});
let res = (await response.json())
loadingOrSaving = false;
sysinfoStore.update(s => {
s.hostname = hostname;
s.usrcfg = res.success;
s.booting = res.reboot;
setTimeout(scanForDevice, 5000);
return s;
});
}
</script>
<div class="grid xl:grid-cols-4 lg:grid-cols-3 md:grid-cols-2">
<div class="cnt">
<form on:submit|preventDefault={handleSubmit} autocomplete="off">
<input type="hidden" name="s" value="true"/>
<strong class="text-sm">Setup</strong>
<div class="my-3">
SSID<br/>
<input name="ss" type="text" class="in-s"/>
</div>
<div class="my-3">
PSK<br/>
<input name="sp" type="password" class="in-s"/>
</div>
<div>
Hostname:
<input name="sh" bind:value={sysinfo.hostname} type="text" class="in-s" maxlength="32" pattern="[a-z0-9_-]+" placeholder="Optional, ex.: ams-reader"/>
</div>
<div class="my-3">
<label><input type="checkbox" name="sm" value="static" class="rounded mb-1" bind:checked={staticIp} /> Static IP</label>
{#if staticIp}
<br/>
<div class="flex">
<input name="si" type="text" class="in-f w-full" required={staticIp}/>
<select name="su" class="in-l" required={staticIp}>
<option value="255.255.255.0">/24</option>
<option value="255.255.0.0">/16</option>
<option value="255.0.0.0">/8</option>
</select>
</div>
{/if}
</div>
{#if staticIp}
<div class="my-3 flex">
<div>
Gateway<br/>
<input name="sg" type="text" class="in-f w-full"/>
</div>
<div>
DNS<br/>
<input name="sd" type="text" class="in-l w-full"/>
</div>
</div>
{/if}
<div class="my-3">
<button type="submit" class="btn-pri">Save</button>
</div>
</form>
</div>
</div>
<Mask active={loadingOrSaving} message="Saving your configuration to the device"/>

View File

@@ -1,188 +0,0 @@
<script>
import { metertype, boardtype, isBusPowered } from './Helpers.js';
import { getSysinfo, gitHubReleaseStore, sysinfoStore } from './DataStores.js';
import { upgrade, getNextVersion } from './UpgradeHelper';
import DownloadIcon from './DownloadIcon.svelte';
import { Link } from 'svelte-navigator';
import Mask from './Mask.svelte';
export let data;
export let sysinfo;
let nextVersion = {};
gitHubReleaseStore.subscribe(releases => {
nextVersion = getNextVersion(sysinfo.version, releases);
if(!nextVersion) {
nextVersion = releases[0];
}
});
function askUpgrade() {
if(confirm('Do you want to upgrade this device to ' + nextVersion.tag_name + '?')) {
if((sysinfo.board != 2 && sysinfo.board != 4 && sysinfo.board != 7) || confirm('WARNING: ' + boardtype(sysinfo.chip, sysinfo.board) + ' must be connected to an external power supply during firmware upgrade. Failure to do so may cause power-down during upload resulting in non-functioning unit.')) {
sysinfoStore.update(s => {
s.upgrading = true;
return s;
});
upgrade(nextVersion);
}
}
}
async function reboot() {
const response = await fetch('/reboot', {
method: 'POST'
});
let res = (await response.json())
}
const askReboot = function() {
if(confirm('Are you sure you want to reboot the device?')) {
sysinfoStore.update(s => {
s.booting = true;
return s;
});
reboot();
}
}
let firmwareFileInput;
let firmwareFiles = [];
let firmwareUploading = false;
let configFileInput;
let configFiles = [];
let configUploading = false;
getSysinfo();
</script>
<div class="grid xl:grid-cols-5 lg:grid-cols-3 md:grid-cols-2">
<div class="cnt">
<strong class="text-sm">Device information</strong>
<div class="my-2">
Chip: {sysinfo.chip}
</div>
<div class="my-2">
Device: {boardtype(sysinfo.chip, sysinfo.board)}
</div>
<div class="my-2">
MAC: {sysinfo.mac}
</div>
{#if sysinfo.apmac && sysinfo.apmac != sysinfo.mac}
<div class="my-2">
AP MAC: {sysinfo.apmac}
</div>
{/if}
<div class="my-2">
<Link to="/consent">
<span class="text-xs py-1 px-2 rounded bg-blue-500 text-white mr-3 ">Update consents</span>
</Link>
<button on:click={askReboot} class="text-xs py-1 px-2 rounded bg-yellow-500 text-white mr-3 float-right">Reboot</button>
</div>
</div>
{#if sysinfo.meter}
<div class="cnt">
<strong class="text-sm">Meter</strong>
<div class="my-2">
Manufacturer: {metertype(sysinfo.meter.mfg)}
</div>
<div class="my-2">
Model: {sysinfo.meter.model}
</div>
<div class="my-2">
ID: {sysinfo.meter.id}
</div>
</div>
{/if}
{#if sysinfo.net}
<div class="cnt">
<strong class="text-sm">Network</strong>
<div class="my-2">
IP: {sysinfo.net.ip}
</div>
<div class="my-2">
Mask: {sysinfo.net.mask}
</div>
<div class="my-2">
Gateway: {sysinfo.net.gw}
</div>
<div class="my-2">
DNS: {sysinfo.net.dns1} {#if sysinfo.net.dns2 && sysinfo.net.dns2 != '0.0.0.0'}/ {sysinfo.net.dns2}{/if}
</div>
</div>
{/if}
<div class="cnt">
<strong class="text-sm">Firmware</strong>
<div class="my-2">
Installed version: {sysinfo.version}
</div>
{#if nextVersion}
<div class="my-2 flex">
Latest version:
<a href={nextVersion.html_url} class="ml-2 text-blue-600 hover:text-blue-800" target='_blank' rel="noreferrer">{nextVersion.tag_name}</a>
{#if (sysinfo.security == 0 || data.a) && sysinfo.fwconsent === 1 && nextVersion && nextVersion.tag_name}
<div class="flex-none ml-2 text-green-500" title="Install this version">
<button on:click={askUpgrade}><DownloadIcon/></button>
</div>
{/if}
</div>
{#if sysinfo.fwconsent === 2}
<div class="my-2">
<div class="bd-ylo">You have disabled one-click firmware upgrade, link to self-upgrade is disabled</div>
</div>
{/if}
{/if}
{#if (sysinfo.security == 0 || data.a) && isBusPowered(sysinfo.board) }
<div class="bd-red">
{boardtype(sysinfo.chip, sysinfo.board)} must be connected to an external power supply during firmware upgrade. Failure to do so may cause power-down during upload resulting in non-functioning unit.
</div>
{/if}
{#if sysinfo.security == 0 || data.a}
<div class="my-2 flex">
<form action="/firmware" enctype="multipart/form-data" method="post" on:submit={() => firmwareUploading=true} autocomplete="off">
<input style="display:none" name="file" type="file" accept=".bin" bind:this={firmwareFileInput} bind:files={firmwareFiles}>
{#if firmwareFiles.length == 0}
<button type="button" on:click={()=>{firmwareFileInput.click();}} class="text-xs py-1 px-2 rounded bg-blue-500 text-white float-right mr-3">Select firmware file for upgrade</button>
{:else}
{firmwareFiles[0].name}
<button type="submit" class="ml-2 text-xs py-1 px-2 rounded bg-blue-500 text-white float-right mr-3">Upload</button>
{/if}
</form>
</div>
{/if}
</div>
{#if sysinfo.security == 0 || data.a}
<div class="cnt">
<strong class="text-sm">Configuration</strong>
<form method="get" action="/configfile.cfg" autocomplete="off">
<div class="grid grid-cols-2">
<label class="my-1 mx-3"><input type="checkbox" class="rounded" name="iw" value="true" checked/> WiFi</label>
<label class="my-1 mx-3"><input type="checkbox" class="rounded" name="im" value="true" checked/> MQTT</label>
<label class="my-1 mx-3"><input type="checkbox" class="rounded" name="ie" value="true" checked/> Web</label>
<label class="my-1 mx-3"><input type="checkbox" class="rounded" name="it" value="true" checked/> Meter</label>
<label class="my-1 mx-3"><input type="checkbox" class="rounded" name="ih" value="true" checked/> Thresholds</label>
<label class="my-1 mx-3"><input type="checkbox" class="rounded" name="ig" value="true" checked/> GPIO</label>
<label class="my-1 mx-3"><input type="checkbox" class="rounded" name="id" value="true" checked/> Domoticz</label>
<label class="my-1 mx-3"><input type="checkbox" class="rounded" name="in" value="true" checked/> NTP</label>
<label class="my-1 mx-3"><input type="checkbox" class="rounded" name="is" value="true" checked/> Price API</label>
<label class="my-1 mx-3 col-span-2"><input type="checkbox" class="rounded" name="ic" value="true"/> Include Secrets<br/><small>(SSID, PSK, passwords and tokens)</small></label>
</div>
{#if configFiles.length == 0}
<button type="submit" class="ml-2 text-xs py-1 px-2 rounded bg-blue-500 text-white float-right mr-3">Download</button>
{/if}
</form>
<form action="/configfile" enctype="multipart/form-data" method="post" on:submit={() => configUploading=true} autocomplete="off">
<input style="display:none" name="file" type="file" accept=".cfg" bind:this={configFileInput} bind:files={configFiles}>
{#if configFiles.length == 0}
<button type="button" on:click={()=>{configFileInput.click();}} class="text-xs py-1 px-2 rounded bg-blue-500 text-white mr-3">Select file...</button>
{:else}
{configFiles[0].name}
<button type="submit" class="ml-2 text-xs py-1 px-2 rounded bg-blue-500 text-white mr-3">Upload</button>
{/if}
</form>
</div>
{/if}
</div>
<Mask active={firmwareUploading} message="Uploading firmware, please wait"/>
<Mask active={configUploading} message="Uploading configuration, please wait"/>

View File

@@ -1,88 +0,0 @@
<script>
import { monthnames, zeropad } from './Helpers.js';
import BarChart from './BarChart.svelte';
import { tariffStore, getTariff } from './DataStores';
let config = {};
let max = 0;
let min = 0;
let tariffData;
tariffStore.subscribe(update => {
tariffData = update;
});
getTariff();
$: {
let i = 0;
let yTicks = [];
let xTicks = [];
let points = [];
yTicks.push({
value: 0,
label: 0
});
if(tariffData && tariffData.p) {
for(i = 0; i < tariffData.p.length; i++) {
let peak = tariffData.p[i];
points.push({
label: peak.v.toFixed(2),
value: peak.v,
color: '#7c3aed'
});
xTicks.push({
label: peak.d > 0 ? zeropad(peak.d) + "." + monthnames[new Date().getMonth()] : "-"
})
max = Math.max(max, peak.v);
}
}
if(tariffData && tariffData.t) {
for(i = 0; i < tariffData.t.length; i++) {
let val = tariffData.t[i];
if(val >= max) break;
yTicks.push({
value: val,
label: val
});
}
yTicks.push({
label: tariffData.m.toFixed(1),
align: 'right',
color: 'green',
value: tariffData.m,
});
}
if(tariffData && tariffData.c) {
yTicks.push({
label: tariffData.c.toFixed(0),
color: 'orange',
value: tariffData.c,
});
max = Math.max(max, tariffData.c);
}
max = Math.ceil(max);
config = {
title: "Tariff peaks",
padding: { top: 20, right: 35, bottom: 20, left: 35 },
y: {
min: min,
max: max,
ticks: yTicks
},
x: {
ticks: xTicks
},
points: points
};
}
</script>
<BarChart config={config} />

View File

@@ -1,66 +0,0 @@
<script>
import BarChart from './BarChart.svelte';
export let json;
let config = {};
let max = 0;
let min = 0;
$: {
let i = 0;
let val = 0;
let yTicks = [];
let xTicks = [];
let points = [];
if(json.s) {
json.s.forEach((obj, i) => {
var name = obj.n ? obj.n : obj.a;
val = obj.v;
if(val == -127) val = 0;
xTicks.push({
label: name.slice(-4)
});
points.push({
label: val.toFixed(1),
value: val,
color: '#7c3aed'
});
min = Math.min(min, val);
max = Math.max(max, val);
});
}
max = Math.ceil(max);
min = Math.floor(min);
let range = max;
if(min < 0) range += Math.abs(min);
let yTickDist = range/4;
for(i = 0; i < 5; i++) {
val = min + (yTickDist*i);
yTicks.push({
value: val,
label: val.toFixed(1)
});
}
config = {
title: "Temperature sensors (°C)",
height: 226,
width: 1520,
padding: { top: 20, right: 15, bottom: 20, left: 35 },
y: {
min: min,
max: max,
ticks: yTicks
},
x: {
ticks: xTicks
},
points: points
};
};
</script>
<BarChart config={config} />

View File

@@ -1,58 +0,0 @@
<script>
export let chip;
</script>
<option value={3}>UART0</option>
{#if chip == 'esp8266'}
<option value={113}>UART2</option>
{/if}
{#if chip == 'esp32' || chip == 'esp32solo'}
<option value={9}>UART1</option>
<option value={16}>UART2</option>
{/if}
{#if chip == 'esp32s2'}
<option value={18}>UART1</option>
{/if}
<option value={4}>GPIO4</option>
<option value={5}>GPIO5</option>
{#if chip.startsWith('esp32')}
<option value={6}>GPIO6</option>
<option value={7}>GPIO7</option>
<option value={8}>GPIO8</option>
{/if}
{#if chip == 'esp8266'}
<option value={9}>GPIO9</option>
{/if}
<option value={10}>GPIO10</option>
{#if chip.startsWith('esp32')}
<option value={11}>GPIO11</option>
{/if}
<option value={12}>GPIO12</option>
<option value={13}>GPIO13</option>
<option value={14}>GPIO14</option>
<option value={15}>GPIO15</option>
{#if chip.startsWith('esp32')}
<option value={17}>GPIO17</option>
{#if chip != 'esp32s2'}
<option value={18}>GPIO18</option>
{/if}
<option value={19}>GPIO19</option>
<option value={21}>GPIO21</option>
<option value={22}>GPIO22</option>
<option value={23}>GPIO23</option>
<option value={25}>GPIO25</option>
<option value={32}>GPIO32</option>
<option value={33}>GPIO33</option>
<option value={34}>GPIO34</option>
<option value={35}>GPIO35</option>
<option value={36}>GPIO36</option>
<option value={39}>GPIO39</option>
{/if}
{#if chip == 'esp32s2'}
<option value={40}>GPIO40</option>
<option value={41}>GPIO41</option>
<option value={42}>GPIO42</option>
<option value={43}>GPIO43</option>
<option value={44}>GPIO44</option>
{/if}

View File

@@ -1,63 +0,0 @@
export async function upgrade(version) {
const data = new URLSearchParams()
data.append('version', version.tag_name);
const response = await fetch('/upgrade', {
method: 'POST',
body: data
});
let res = (await response.json())
}
export function getNextVersion(currentVersion, releases) {
if(/^v\d{1,2}\.\d{1,2}\.\d{1,2}$/.test(currentVersion)) {
var v = currentVersion.substring(1).split('.');
var v_major = parseInt(v[0]);
var v_minor = parseInt(v[1]);
var v_patch = parseInt(v[2]);
releases.reverse();
var next_patch;
var next_minor;
var next_major;
for(var i = 0; i < releases.length; i++) {
var release = releases[i];
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;
}
} 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) {
next_major = release;
}
} else {
next_major = release;
}
}
};
if(next_minor) {
return next_minor;
} else if(next_major) {
return next_major;
} else if(next_patch) {
return next_patch;
}
return false;
} else {
return releases[0];
}
}

View File

@@ -1,29 +0,0 @@
<script>
export let epoch;
let days = 0;
let hours = 0;
let minutes = 0;
$: {
days = Math.floor(epoch/86400);
hours = Math.floor(epoch/3600);
minutes = Math.floor(epoch/60);
}
</script>
{#if epoch}
Up
{#if days > 1}
{days} days
{:else if days > 0}
{days} day
{:else if hours > 1}
{hours} hours
{:else if hours > 0}
{hours} hour
{:else if minutes > 1}
{minutes} minutes
{:else if minutes > 0}
{minutes} minute
{:else}
{epoch} seconds
{/if}
{/if}

View File

@@ -1,65 +0,0 @@
<script>
import { sysinfoStore } from './DataStores.js';
import BoardTypeSelectOptions from './BoardTypeSelectOptions.svelte';
import UartSelectOptions from './UartSelectOptions.svelte';
import Mask from './Mask.svelte'
import { navigate } from 'svelte-navigator';
export let sysinfo = {}
let loadingOrSaving = false;
async function handleSubmit(e) {
loadingOrSaving = true;
const formData = new FormData(e.target)
const data = new URLSearchParams()
for (let field of formData) {
const [key, value] = field
data.append(key, value)
}
const response = await fetch('/save', {
method: 'POST',
body: data
});
let res = (await response.json())
loadingOrSaving = false;
sysinfoStore.update(s => {
s.vndcfg = res.success;
s.booting = res.reboot;
return s;
});
navigate(sysinfo.usrcfg ? "/" : "/setup");
}
</script>
<div class="grid xl:grid-cols-4 lg:grid-cols-3 md:grid-cols-2">
<div class="cnt">
<form on:submit|preventDefault={handleSubmit} autocomplete="off">
<input type="hidden" name="v" value="true"/>
<strong class="text-sm">Initial configuration</strong>
<div class="my-3">
Board type<br/>
<select name="vb" bind:value={sysinfo.board} class="in-s">
<BoardTypeSelectOptions chip={sysinfo.chip}/>
</select>
</div>
{#if sysinfo.board && sysinfo.board > 20}
<div class="my-3">
HAN GPIO<br/>
<select name="vh" class="in-s">
<UartSelectOptions chip={sysinfo.chip}/>
</select>
</div>
{/if}
<div class="my-3">
<label><input type="checkbox" name="vr" value="true" class="rounded mb-1" checked /> Clear all other configuration</label>
</div>
<div class="my-3">
<button type="submit" class="btn-pri">Save</button>
</div>
<span class="clear-both">&nbsp;</span>
</form>
</div>
</div>
<Mask active={loadingOrSaving} message="Saving device configuration" />

View File

@@ -1,59 +0,0 @@
<script>
import BarChart from './BarChart.svelte';
import { voltcol } from './Helpers.js';
export let u1;
export let u2;
export let u3;
export let ds;
let min = 200;
let max = 260;
let config = {};
$: {
let xTicks = [];
let points = [];
if(u1 > 0) {
xTicks.push({ label: ds === 1 ? 'L1-L2' : 'L1' });
points.push({
label: u1 ? u1.toFixed(0) + 'V' : '-',
value: u1 ? u1 : 0,
color: voltcol(u1 ? (u1-min)/(max-min)*100 : 0)
});
}
if(u2 > 0) {
xTicks.push({ label: ds === 1 ? 'L1-L3' : 'L2' });
points.push({
label: u2 ? u2.toFixed(0) + 'V' : '-',
value: u2 ? u2 : 0,
color: voltcol(u2 ? (u2-min)/(max-min)*100 : 0)
});
}
if(u3 > 0) {
xTicks.push({ label: ds === 1 ? 'L2-L3' : 'L3' });
points.push({
label: u3 ? u3.toFixed(0) + 'V' : '-',
value: u3 ? u3 : 0,
color: voltcol(u3 ? (u3-min)/(max-min)*100 : 0)
});
}
config = {
padding: { top: 20, right: 15, bottom: 20, left: 35 },
y: {
min: min,
max: max,
ticks: [
{ value: 207, label: '-10%' },
{ value: 230, label: '230v' },
{ value: 253, label: '+10%' }
]
},
x: {
ticks: xTicks
},
points: points
};
}
</script>
<BarChart config={config} />

View File

@@ -1,9 +0,0 @@
import "./app.postcss";
import App from "./App.svelte";
const app = new App({
target: document.getElementById("app"),
});
export default app;

View File

@@ -1,2 +0,0 @@
/// <reference types="svelte" />
/// <reference types="vite/client" />

View File

@@ -1,11 +0,0 @@
import preprocess from "svelte-preprocess";
const config = {
preprocess: [
preprocess({
postcss: true,
}),
],
};
export default config;

View File

@@ -1,13 +0,0 @@
const config = {
content: ["./index.html","./src/**/*.{html,js,svelte,ts}"],
theme: {
extend: {},
},
plugins: [
require('@tailwindcss/forms')
],
};
module.exports = config;

View File

@@ -1,34 +0,0 @@
import { defineConfig } from 'vite'
import { svelte } from '@sveltejs/vite-plugin-svelte'
// https://vitejs.dev/config/
export default defineConfig({
build: {
outDir: 'dist',
assetsDir: '.',
rollupOptions: {
output: {
assetFileNames: '[name][extname]',
chunkFileNames: '[name].js',
entryFileNames: '[name].js'
}
}
},
plugins: [svelte()],
server: {
proxy: {
"/data.json": "http://192.168.233.229",
"/energyprice.json": "http://192.168.233.229",
"/dayplot.json": "http://192.168.233.229",
"/monthplot.json": "http://192.168.233.229",
"/temperature.json": "http://192.168.233.229",
"/sysinfo.json": "http://192.168.233.229",
"/configuration.json": "http://192.168.233.229",
"/tariff.json": "http://192.168.233.229",
"/save": "http://192.168.233.229",
"/reboot": "http://192.168.233.229",
"/configfile": "http://192.168.233.229",
"/upgrade": "http://192.168.233.229"
}
}
})

View File

@@ -1,2 +0,0 @@
html/*.h
json/*.h

View File

@@ -1,17 +0,0 @@
static const char HEADER_CACHE_CONTROL[] PROGMEM = "Cache-Control";
static const char HEADER_PRAGMA[] PROGMEM = "Pragma";
static const char HEADER_EXPIRES[] PROGMEM = "Expires";
static const char HEADER_AUTHENTICATE[] PROGMEM = "WWW-Authenticate";
static const char HEADER_LOCATION[] PROGMEM = "Location";
static const char CACHE_CONTROL_NO_CACHE[] PROGMEM = "no-cache, no-store, must-revalidate";
static const char CACHE_1HR[] PROGMEM = "public, max-age=3600";
static const char PRAGMA_NO_CACHE[] PROGMEM = "no-cache";
static const char EXPIRES_OFF[] PROGMEM = "-1";
static const char AUTHENTICATE_BASIC[] PROGMEM = "Basic realm=\"Secure Area\"";
static const char MIME_PLAIN[] PROGMEM = "text/plain";
static const char MIME_HTML[] PROGMEM = "text/html";
static const char MIME_JSON[] PROGMEM = "application/json";
static const char MIME_CSS[] PROGMEM = "text/css";
static const char MIME_JS[] PROGMEM = "text/javascript";

View File

@@ -1,118 +0,0 @@
#ifndef _AMSWEBSERVER_h
#define _AMSWEBSERVER_h
#include "Arduino.h"
#include <MQTT.h>
#include "AmsConfiguration.h"
#include "HwTools.h"
#include "AmsData.h"
#include "AmsStorage.h"
#include "AmsDataStorage.h"
#include "EnergyAccounting.h"
#include "Uptime.h"
#include "RemoteDebug.h"
#include "EntsoeApi.h"
#if defined(ESP8266)
#include <ESP8266WiFi.h>
#include <ESP8266WebServer.h>
#include <ESP8266HTTPClient.h>
#include <ESP8266httpUpdate.h>
#elif defined(ESP32) // ARDUINO_ARCH_ESP32
#include <WiFi.h>
#include <WebServer.h>
#include <HTTPClient.h>
#include <HTTPUpdate.h>
#else
#warning "Unsupported board type"
#endif
#include "LittleFS.h"
class AmsWebServer {
public:
AmsWebServer(uint8_t* buf, RemoteDebug* Debug, HwTools* hw);
void setup(AmsConfiguration*, GpioConfig*, MeterConfig*, AmsData*, AmsDataStorage*, EnergyAccounting*);
void loop();
void setMqtt(MQTTClient* mqtt);
void setTimezone(Timezone* tz);
void setMqttEnabled(bool);
void setEntsoeApi(EntsoeApi* eapi);
void setPriceRegion(String);
private:
RemoteDebug* debugger;
bool mqttEnabled = false;
int maxPwr = 0;
HwTools* hw;
Timezone* tz;
EntsoeApi* eapi = NULL;
AmsConfiguration* config;
GpioConfig* gpioConfig;
MeterConfig* meterConfig;
WebConfig webConfig;
AmsData* meterState;
AmsDataStorage* ds;
EnergyAccounting* ea = NULL;
MQTTClient* mqtt = NULL;
bool uploading = false;
File file;
bool performRestart = false;
bool performUpgrade = false;
bool rebootForUpgrade = false;
String priceRegion = "";
#if defined(AMS2MQTT_FIRMWARE_URL)
String customFirmwareUrl = AMS2MQTT_FIRMWARE_URL;
#else
String customFirmwareUrl;
#endif
static const uint16_t BufferSize = 2048;
char* buf;
#if defined(ESP8266)
ESP8266WebServer server;
#elif defined(ESP32)
WebServer server;
#endif
bool checkSecurity(byte level, bool send401 = true);
void indexHtml();
void indexJs();
void indexCss();
void githubSvg();
void faviconSvg();
void sysinfoJson();
void dataJson();
void dayplotJson();
void monthplotJson();
void energyPriceJson();
void temperatureJson();
void tariffJson();
void configurationJson();
void handleSave();
void reboot();
void upgrade();
void firmwareHtml();
void firmwarePost();
void firmwareUpload();
void isAliveCheck();
void mqttCaUpload();
void mqttCertUpload();
void mqttKeyUpload();
HTTPUpload& uploadFile(const char* path);
void configFileDownload();
void configFileUpload();
void factoryResetPost();
void notFound();
void redirectToMain();
void robotstxt();
};
#endif

View File

@@ -1,5 +0,0 @@
"d": {
"s": %s,
"t": %s,
"l": %d
},

View File

@@ -1,7 +0,0 @@
"o": {
"e" : %d,
"c" : %d,
"u1" : %d,
"u2" : %d,
"u3" : %d
}

View File

@@ -1,7 +0,0 @@
"g": {
"t": "%s",
"h": "%s",
"s": %d,
"u": "%s",
"p": "%s"
},

View File

@@ -1,28 +0,0 @@
"i": {
"h": %s,
"a": %s,
"l": {
"p": %s,
"i": %s
},
"r": {
"r": %s,
"g": %s,
"b": %s,
"i": %s
},
"t": {
"d": %s,
"a": %s
},
"v": {
"p": %s,
"o": %.2f,
"m": %.3f,
"d": {
"v": %d,
"g": %d
},
"b": %.1f
}
},

View File

@@ -1,20 +0,0 @@
"m": {
"b": %d,
"p": %d,
"i": %s,
"d": %d,
"f": %d,
"r": %d,
"e": {
"e": %s,
"k": "%s",
"a": "%s"
},
"m": {
"e": %s,
"w": %.3f,
"v": %.3f,
"a": %.3f,
"c": %.3f
}
},

View File

@@ -1,15 +0,0 @@
"q": {
"h": "%s",
"p": %d,
"u": "%s",
"a": "%s",
"c": "%s",
"b": "%s",
"m": %d,
"s": {
"e": %s,
"c": %s,
"r": %s,
"k": %s
}
},

View File

@@ -1,11 +0,0 @@
"n": {
"m": "%s",
"i": "%s",
"s": "%s",
"g": "%s",
"d1": "%s",
"d2": "%s",
"d": %s,
"n1": "%s",
"h": %s
},

View File

@@ -1,7 +0,0 @@
"p": {
"e": %s,
"t": "%s",
"r": "%s",
"c": "%s",
"m": %.3f
},

View File

@@ -1,15 +0,0 @@
"t": {
"t": [
%d,
%d,
%d,
%d,
%d,
%d,
%d,
%d,
%d,
%d
],
"h": %d
},

View File

@@ -1,13 +0,0 @@
"u": {
"i": %d,
"e": %d,
"v": %d,
"a": %d,
"r": %d,
"c": %d,
"t": %d,
"p": %d,
"d": %d,
"m": %d,
"s": %d
},

View File

@@ -1,7 +0,0 @@
"w": {
"s": "%s",
"p": "%s",
"w": %.1f,
"z": %d,
"a": %s
},

View File

@@ -1,64 +0,0 @@
{
"im" : %d,
"om" : %d,
"mf" : %d,
"i" : %d,
"e" : %d,
"ri" : %d,
"re" : %d,
"ic" : %.3f,
"ec" : %.3f,
"ric" : %.3f,
"rec" : %.3f,
"u1" : %.2f,
"u2" : %.2f,
"u3" : %.2f,
"i1" : %.2f,
"i2" : %.2f,
"i3" : %.2f,
"f" : %.2f,
"f1" : %.2f,
"f2" : %.2f,
"f3" : %.2f,
"v" : %.3f,
"r" : %d,
"t" : %.2f,
"u" : %lu,
"m" : %lu,
"em" : %d,
"hm" : %d,
"wm" : %d,
"mm" : %d,
"me" : %d,
"p" : %s,
"mt" : %d,
"ds" : %d,
"ea" : {
"x" : %.1f,
"p" : [ %s ],
"t" : %d,
"h" : {
"u" : %.2f,
"c" : %.2f,
"p" : %.2f,
"i" : %.2f
},
"d" : {
"u" : %.2f,
"c" : %.2f,
"p" : %.2f,
"i" : %.2f
},
"m" : {
"u" : %.2f,
"c" : %.2f,
"p" : %.2f,
"i" : %.2f
}
},
"pr" : "%s",
"he" : %d,
"ee" : %d,
"c" : %lu,
"a" : %s
}

View File

@@ -1,50 +0,0 @@
{
"i00" : %.2f,
"i01" : %.2f,
"i02" : %.2f,
"i03" : %.2f,
"i04" : %.2f,
"i05" : %.2f,
"i06" : %.2f,
"i07" : %.2f,
"i08" : %.2f,
"i09" : %.2f,
"i10" : %.2f,
"i11" : %.2f,
"i12" : %.2f,
"i13" : %.2f,
"i14" : %.2f,
"i15" : %.2f,
"i16" : %.2f,
"i17" : %.2f,
"i18" : %.2f,
"i19" : %.2f,
"i20" : %.2f,
"i21" : %.2f,
"i22" : %.2f,
"i23" : %.2f,
"e00" : %.2f,
"e01" : %.2f,
"e02" : %.2f,
"e03" : %.2f,
"e04" : %.2f,
"e05" : %.2f,
"e06" : %.2f,
"e07" : %.2f,
"e08" : %.2f,
"e09" : %.2f,
"e10" : %.2f,
"e11" : %.2f,
"e12" : %.2f,
"e13" : %.2f,
"e14" : %.2f,
"e15" : %.2f,
"e16" : %.2f,
"e17" : %.2f,
"e18" : %.2f,
"e19" : %.2f,
"e20" : %.2f,
"e21" : %.2f,
"e22" : %.2f,
"e23" : %.2f
}

View File

@@ -1,39 +0,0 @@
{
"currency" : "%s",
"00" : %s,
"01" : %s,
"02" : %s,
"03" : %s,
"04" : %s,
"05" : %s,
"06" : %s,
"07" : %s,
"08" : %s,
"09" : %s,
"10" : %s,
"11" : %s,
"12" : %s,
"13" : %s,
"14" : %s,
"15" : %s,
"16" : %s,
"17" : %s,
"18" : %s,
"19" : %s,
"20" : %s,
"21" : %s,
"22" : %s,
"23" : %s,
"24" : %s,
"25" : %s,
"26" : %s,
"27" : %s,
"28" : %s,
"29" : %s,
"30" : %s,
"31" : %s,
"32" : %s,
"33" : %s,
"34" : %s,
"35" : %s
}

View File

@@ -1,11 +0,0 @@
<html>
<form action="/firmware" enctype="multipart/form-data" method="post" autocomplete="off">
File: <input name="file" type="file" accept=".bin">
<button type="submit" class="">Upload</button>
</form>
or<br/><br/>
<form action="/firmware" method="post" autocomplete="off">
URL: <input name="url" type="text"/>
<button type="submit" class="">Install</button>
</form>
</html>

View File

@@ -1,64 +0,0 @@
{
"i01" : %.2f,
"i02" : %.2f,
"i03" : %.2f,
"i04" : %.2f,
"i05" : %.2f,
"i06" : %.2f,
"i07" : %.2f,
"i08" : %.2f,
"i09" : %.2f,
"i10" : %.2f,
"i11" : %.2f,
"i12" : %.2f,
"i13" : %.2f,
"i14" : %.2f,
"i15" : %.2f,
"i16" : %.2f,
"i17" : %.2f,
"i18" : %.2f,
"i19" : %.2f,
"i20" : %.2f,
"i21" : %.2f,
"i22" : %.2f,
"i23" : %.2f,
"i24" : %.2f,
"i25" : %.2f,
"i26" : %.2f,
"i27" : %.2f,
"i28" : %.2f,
"i29" : %.2f,
"i30" : %.2f,
"i31" : %.2f,
"e01" : %.2f,
"e02" : %.2f,
"e03" : %.2f,
"e04" : %.2f,
"e05" : %.2f,
"e06" : %.2f,
"e07" : %.2f,
"e08" : %.2f,
"e09" : %.2f,
"e10" : %.2f,
"e11" : %.2f,
"e12" : %.2f,
"e13" : %.2f,
"e14" : %.2f,
"e15" : %.2f,
"e16" : %.2f,
"e17" : %.2f,
"e18" : %.2f,
"e19" : %.2f,
"e20" : %.2f,
"e21" : %.2f,
"e22" : %.2f,
"e23" : %.2f,
"e24" : %.2f,
"e25" : %.2f,
"e26" : %.2f,
"e27" : %.2f,
"e28" : %.2f,
"e29" : %.2f,
"e30" : %.2f,
"e31" : %.2f
}

View File

@@ -1,4 +0,0 @@
{
"d": %d,
"v": %.2f
}

View File

@@ -1,5 +0,0 @@
{
"success": %s,
"message": "%s",
"reboot": %s
}

View File

@@ -1,40 +0,0 @@
{
"version": "%s",
"chip": "%s",
"chipId": "%s",
"mac": "%s",
"apmac": "%s",
"board": %d,
"vndcfg": %s,
"usrcfg": %s,
"fwconsent": %d,
"hostname": "%s",
"booting": %s,
"upgrading": %s,
"net": {
"ip": "%s",
"mask": "%s",
"gw": "%s",
"dns1": "%s",
"dns2": "%s"
},
"meter": {
"mfg": %d,
"model": "%s",
"id": "%s"
},
"ui": {
"i": %d,
"e": %d,
"v": %d,
"a": %d,
"r": %d,
"c": %d,
"t": %d,
"p": %d,
"d": %d,
"m": %d,
"s": %d
},
"security": %d
}

View File

@@ -1,17 +0,0 @@
{
"t": [
%d,
%d,
%d,
%d,
%d,
%d,
%d,
%d,
%d,
%d
],
"p": [ %s ],
"c": %d,
"m": %.2f
}

View File

@@ -1,7 +0,0 @@
{
"i" : %d,
"a" : "%s",
"n" : "%s",
"c" : %d,
"v" : %.1f
},

View File

@@ -1,82 +0,0 @@
import os
import re
import shutil
import subprocess
try:
from css_html_js_minify import html_minify, js_minify, css_minify
except:
from SCons.Script import (
ARGUMENTS,
COMMAND_LINE_TARGETS,
DefaultEnvironment,
)
env = DefaultEnvironment()
env.Execute(
env.VerboseAction(
'$PYTHONEXE -m pip install "css_html_js_minify" ',
"Installing Python dependencies",
)
)
try:
from css_html_js_minify import html_minify, js_minify, css_minify
except:
print("WARN: Unable to load minifier")
srcroot = "lib/SvelteUi/include/html"
version = os.environ.get('GITHUB_TAG')
if version == None:
try:
result = subprocess.run(['git','rev-parse','--short','HEAD'], capture_output=True, check=False)
if result.returncode == 0:
version = result.stdout.decode('utf-8').strip()
else:
version = "SNAPSHOT"
except:
version = "SNAPSHOT"
if os.path.exists(srcroot):
shutil.rmtree(srcroot)
os.mkdir(srcroot)
else:
os.mkdir(srcroot)
for webroot in ["lib/SvelteUi/app/dist", "lib/SvelteUi/json"]:
for filename in os.listdir(webroot):
basename = re.sub("[^0-9a-zA-Z]+", "_", filename)
srcfile = webroot + "/" + filename
dstfile = srcroot + "/" + basename + ".h"
varname = basename.upper()
with open(srcfile, encoding="utf-8") as f:
content = f.read()
content = content.replace("index.js", "index-"+version+".js")
content = content.replace("index.css", "index-"+version+".css")
try:
if filename.endswith(".html"):
content = html_minify(content)
elif filename.endswith(".css"):
content = css_minify(content)
elif filename.endswith(".json"):
content = js_minify(content)
except:
print("WARN: Unable to minify")
with open(dstfile, "w") as dst:
dst.write("static const char ")
dst.write(varname)
dst.write("[] PROGMEM = R\"==\"==(")
dst.write(content)
dst.write(")==\"==\";\n")
dst.write("const int ");
dst.write(varname)
dst.write("_LEN PROGMEM = ");
dst.write(str(len(content)))
dst.write(";");

File diff suppressed because it is too large Load Diff

View File

@@ -2,74 +2,63 @@
extra_configs = platformio-user.ini
[common]
lib_deps = EEPROM, LittleFS, DNSServer, 256dpi/MQTT@2.5.0, OneWireNg@0.10.0, DallasTemperature@3.9.1, EspSoftwareSerial@6.14.1, https://github.com/gskjold/RemoteDebug.git, Time@1.6.1, Timezone@1.2.4, AmsConfiguration, AmsData, AmsDataStorage, HwTools, Uptime, AmsDecoder, EntsoePriceApi, EnergyAccounting, RawMqttHandler, JsonMqttHandler, DomoticzMqttHandler, HomeAssistantMqttHandler, SvelteUi
lib_deps = Timezone@1.2.4, 256dpi/MQTT@2.5.0, OneWireNg@0.10.0, DallasTemperature@3.9.1, EspSoftwareSerial@6.14.1, https://github.com/gskjold/RemoteDebug.git, Time@1.6.1
lib_ignore = OneWire
extra_scripts =
pre:scripts/addversion.py
lib/JsonMqttHandler/scripts/generate_includes.py
lib/DomoticzMqttHandler/scripts/generate_includes.py
lib/HomeAssistantMqttHandler/scripts/generate_includes.py
lib/SvelteUi/scripts/generate_includes.py
build_flags =
-D WEBSOCKET_DISABLED=1
-D NO_AMS2MQTT_PRICE_KEY
-D NO_AMS2MQTT_PRICE_AUTHENTICATION
-fexceptions
[esp32]
lib_deps = WiFi, ESPmDNS, WiFiClientSecure, HTTPClient, FS, Update, HTTPUpdate, WebServer, ${common.lib_deps}
[env:esp8266]
platform = espressif8266@3.2.0
framework = arduino
board = esp12e
board_build.ldscript = eagle.flash.4m2m.ld
build_flags = ${common.build_flags}
lib_ldf_mode = off
lib_compat_mode = off
lib_deps = ESP8266WiFi, ESP8266mDNS, ESP8266WebServer, ESP8266HTTPClient, ESP8266httpUpdate, ${common.lib_deps}
build_flags = -D WEBSOCKET_DISABLED=1
lib_deps = ${common.lib_deps}
lib_ignore = ${common.lib_ignore}
extra_scripts = ${common.extra_scripts}
extra_scripts =
pre:scripts/addversion.py
scripts/makeweb.py
# Sticking to v2.0.3 because of #298
[env:esp32]
platform = https://github.com/tasmota/platform-espressif32/releases/download/v2.0.5.3/platform-espressif32-2.0.5.3.zip
platform = https://github.com/tasmota/platform-espressif32/releases/download/v2.0.3/platform-espressif32-2.0.3.zip
framework = arduino
board = esp32dev
board_build.f_cpu = 160000000L
build_flags = ${common.build_flags}
lib_ldf_mode = off
lib_compat_mode = off
lib_deps = ${esp32.lib_deps}
build_flags = -D WEBSOCKET_DISABLED=1 -fexceptions
lib_deps = ${common.lib_deps}
lib_ignore = ${common.lib_ignore}
extra_scripts = ${common.extra_scripts}
extra_scripts =
pre:scripts/addversion.py
scripts/makeweb.py
# Tasmota has pre-built platform for C3, S2, S3 and Solo, more information at:
# https://github.com/Jason2866/esp32-arduino-lib-builder
[env:esp32s2]
platform = https://github.com/tasmota/platform-espressif32/releases/download/v2.0.5.3/platform-espressif32-2.0.5.3.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.3
framework = arduino
board = esp32-s2-saola-1
board = esp32dev
board_build.mcu = esp32s2
board_build.variant = esp32s2
board_build.flash_mode = qio
board_build.f_cpu = 160000000L
board_build.f_flash = 40000000L
build_flags = ${common.build_flags}
lib_ldf_mode = off
lib_compat_mode = off
lib_deps = ${esp32.lib_deps}
build_flags = -D WEBSOCKET_DISABLED=1
lib_deps = ${common.lib_deps}
lib_ignore = ${common.lib_ignore}
extra_scripts = ${common.extra_scripts}
extra_scripts =
pre:scripts/addversion.py
scripts/makeweb.py
[env:esp32solo]
platform = https://github.com/tasmota/platform-espressif32/releases/download/v2.0.5.3/platform-espressif32-2.0.5.3.zip
platform = https://github.com/tasmota/platform-espressif32/releases/download/v.2.0.3/platform-espressif32-solo1-v.2.0.3.zip
framework = arduino
board = esp32-solo1
board = esp32dev
board_build.f_cpu = 160000000L
build_flags = ${common.build_flags} -DFRAMEWORK_ARDUINO_SOLO1
lib_ldf_mode = off
lib_compat_mode = off
lib_deps = ${esp32.lib_deps}
build_flags = -D WEBSOCKET_DISABLED=1 -fexceptions
lib_deps = ${common.lib_deps}
lib_ignore = ${common.lib_ignore}
extra_scripts = ${common.extra_scripts}
extra_scripts =
pre:scripts/addversion.py
scripts/makeweb.py

View File

@@ -2,7 +2,7 @@ import os
import subprocess
from time import time
FILENAME_VERSION_H = 'lib/AmsConfiguration/include/version.h'
FILENAME_VERSION_H = 'src/version.h'
version = os.environ.get('GITHUB_TAG')
if version == None:
try:

View File

@@ -25,8 +25,8 @@ except:
print("WARN: Unable to load minifier")
webroot = "lib/ClassicUi/html"
srcroot = "lib/ClassicUi/include/root"
webroot = "web"
srcroot = "src/web/root"
version = os.environ.get('GITHUB_TAG')
if version == None:

View File

@@ -1,18 +1,12 @@
#include "AmsConfiguration.h"
bool AmsConfiguration::getSystemConfig(SystemConfig& config) {
EEPROM.begin(EEPROM_SIZE);
uint8_t configVersion = EEPROM.read(EEPROM_CONFIG_ADDRESS);
if(configVersion == EEPROM_CHECK_SUM || configVersion == EEPROM_CLEARED_INDICATOR) {
if(hasConfig()) {
EEPROM.begin(EEPROM_SIZE);
EEPROM.get(CONFIG_SYSTEM_START, config);
EEPROM.end();
return true;
} else {
config.boardType = 0xFF;
config.vendorConfigured = false;
config.userConfigured = false;
config.dataCollectionConsent = 0;
strcpy(config.country, "");
return false;
}
}
@@ -54,8 +48,6 @@ bool AmsConfiguration::setWiFiConfig(WiFiConfig& config) {
wifiChanged |= strcmp(config.hostname, existing.hostname) != 0;
wifiChanged |= config.power != existing.power;
wifiChanged |= config.sleep != existing.sleep;
wifiChanged |= config.mode != existing.mode;
wifiChanged |= config.autoreboot != existing.autoreboot;
} else {
wifiChanged = true;
}
@@ -218,8 +210,8 @@ bool AmsConfiguration::setMeterConfig(MeterConfig& config) {
}
void AmsConfiguration::clearMeter(MeterConfig& config) {
config.baud = 0;
config.parity = 0;
config.baud = 2400;
config.parity = 11; // 8E1
config.invert = false;
config.distributionSystem = 0;
config.mainFuse = 0;
@@ -230,8 +222,6 @@ void AmsConfiguration::clearMeter(MeterConfig& config) {
config.voltageMultiplier = 0;
config.amperageMultiplier = 0;
config.accumulatedMultiplier = 0;
config.source = 1; // Serial
config.parser = 0; // Auto
}
bool AmsConfiguration::isMeterChanged() {
@@ -440,8 +430,9 @@ bool AmsConfiguration::setNtpConfig(NtpConfig& config) {
}
}
ntpChanged |= config.dhcp != existing.dhcp;
ntpChanged |= config.offset != existing.offset;
ntpChanged |= config.summerOffset != existing.summerOffset;
ntpChanged |= strcmp(config.server, existing.server) != 0;
ntpChanged |= strcmp(config.timezone, existing.timezone) != 0;
} else {
ntpChanged = true;
}
@@ -463,8 +454,9 @@ void AmsConfiguration::ackNtpChange() {
void AmsConfiguration::clearNtp(NtpConfig& config) {
config.enable = true;
config.dhcp = true;
config.offset = 360;
config.summerOffset = 360;
strcpy(config.server, "pool.ntp.org");
strcpy(config.timezone, "Europe/Oslo");
}
bool AmsConfiguration::getEntsoeConfig(EntsoeConfig& config) {
@@ -488,7 +480,6 @@ bool AmsConfiguration::setEntsoeConfig(EntsoeConfig& config) {
entsoeChanged |= strcmp(config.area, existing.area) != 0;
entsoeChanged |= strcmp(config.currency, existing.currency) != 0;
entsoeChanged |= config.multiplier != existing.multiplier;
entsoeChanged |= config.enabled != existing.enabled;
} else {
entsoeChanged = true;
}
@@ -549,6 +540,7 @@ bool AmsConfiguration::setEnergyAccountingConfig(EnergyAccountingConfig& config)
bool ret = EEPROM.commit();
EEPROM.end();
return ret;
}
void AmsConfiguration::clearEnergyAccountingConfig(EnergyAccountingConfig& config) {
@@ -573,53 +565,9 @@ void AmsConfiguration::ackEnergyAccountingChange() {
energyAccountingChanged = false;
}
bool AmsConfiguration::getUiConfig(UiConfig& config) {
if(hasConfig()) {
EEPROM.begin(EEPROM_SIZE);
EEPROM.get(CONFIG_UI_START, config);
if(config.showImport > 2) clearUiConfig(config); // Must be wrong
EEPROM.end();
return true;
} else {
clearUiConfig(config);
return false;
}
}
bool AmsConfiguration::setUiConfig(UiConfig& config) {
EEPROM.begin(EEPROM_SIZE);
EEPROM.put(CONFIG_UI_START, config);
bool ret = EEPROM.commit();
EEPROM.end();
return ret;
}
void AmsConfiguration::clearUiConfig(UiConfig& config) {
// 1 = Always, 2 = If value present, 0 = Hidden
config.showImport = 1;
config.showExport = 2;
config.showVoltage = 2;
config.showAmperage = 2;
config.showReactive = 0;
config.showRealtime = 1;
config.showPeaks = 2;
config.showPricePlot = 2;
config.showDayPlot = 1;
config.showMonthPlot = 1;
config.showTemperaturePlot = 2;
}
void AmsConfiguration::clear() {
EEPROM.begin(EEPROM_SIZE);
SystemConfig sys;
EEPROM.get(CONFIG_SYSTEM_START, sys);
sys.userConfigured = false;
sys.dataCollectionConsent = 0;
strcpy(sys.country, "");
EEPROM.put(CONFIG_SYSTEM_START, sys);
MeterConfig meter;
clearMeter(meter);
EEPROM.put(CONFIG_METER_START, meter);
@@ -652,15 +600,7 @@ void AmsConfiguration::clear() {
clearEnergyAccountingConfig(eac);
EEPROM.put(CONFIG_ENERGYACCOUNTING_START, eac);
DebugConfig debug;
clearDebug(debug);
EEPROM.put(CONFIG_DEBUG_START, debug);
UiConfig ui;
clearUiConfig(ui);
EEPROM.put(CONFIG_UI_START, ui);
EEPROM.put(EEPROM_CONFIG_ADDRESS, EEPROM_CLEARED_INDICATOR);
EEPROM.put(EEPROM_CONFIG_ADDRESS, -1);
EEPROM.commit();
EEPROM.end();
}
@@ -679,6 +619,22 @@ bool AmsConfiguration::hasConfig() {
}
} else {
switch(configVersion) {
case 86:
configVersion = -1; // Prevent loop
if(relocateConfig86()) {
configVersion = 87;
} else {
configVersion = 0;
return false;
}
case 87:
configVersion = -1; // Prevent loop
if(relocateConfig87()) {
configVersion = 88;
} else {
configVersion = 0;
return false;
}
case 90:
configVersion = -1; // Prevent loop
if(relocateConfig90()) {
@@ -727,22 +683,6 @@ bool AmsConfiguration::hasConfig() {
configVersion = 0;
return false;
}
case 96:
configVersion = -1; // Prevent loop
if(relocateConfig96()) {
configVersion = 100;
} else {
configVersion = 0;
return false;
}
case 100:
configVersion = -1; // Prevent loop
if(relocateConfig100()) {
configVersion = 101;
} else {
configVersion = 0;
return false;
}
case EEPROM_CHECK_SUM:
return true;
default:
@@ -795,6 +735,51 @@ void AmsConfiguration::saveTempSensors() {
}
}
bool AmsConfiguration::relocateConfig86() {
MqttConfig86 mqtt86;
MqttConfig mqtt;
EEPROM.begin(EEPROM_SIZE);
EEPROM.get(CONFIG_MQTT_START_86, mqtt86);
strcpy(mqtt.host, mqtt86.host);
mqtt.port = mqtt86.port;
strcpy(mqtt.clientId, mqtt86.clientId);
strcpy(mqtt.publishTopic, mqtt86.publishTopic);
strcpy(mqtt.subscribeTopic, mqtt86.subscribeTopic);
strcpy(mqtt.username, mqtt86.username);
strcpy(mqtt.password, mqtt86.password);
mqtt.payloadFormat = mqtt86.payloadFormat;
mqtt.ssl = mqtt86.ssl;
EEPROM.put(CONFIG_MQTT_START, mqtt);
EEPROM.put(EEPROM_CONFIG_ADDRESS, 87);
bool ret = EEPROM.commit();
EEPROM.end();
return ret;
}
bool AmsConfiguration::relocateConfig87() {
MeterConfig87 meter87 = {0,0,0,0,0,0,0};
MeterConfig meter;
EEPROM.begin(EEPROM_SIZE);
EEPROM.get(CONFIG_METER_START_87, meter87);
if(meter87.type < 5) {
meter.baud = 2400;
meter.parity = meter87.type == 3 || meter87.type == 4 ? 3 : 11;
meter.invert = false;
} else {
meter.baud = 115200;
meter.parity = 3;
meter.invert = meter87.type == 6;
}
meter.distributionSystem = meter87.distributionSystem;
meter.mainFuse = meter87.mainFuse;
meter.productionCapacity = meter87.productionCapacity;
EEPROM.put(CONFIG_METER_START, meter);
EEPROM.put(EEPROM_CONFIG_ADDRESS, 88);
bool ret = EEPROM.commit();
EEPROM.end();
return ret;
}
bool AmsConfiguration::relocateConfig90() {
EntsoeConfig entsoe;
EEPROM.begin(EEPROM_SIZE);
@@ -892,112 +877,6 @@ bool AmsConfiguration::relocateConfig95() {
return ret;
}
bool AmsConfiguration::relocateConfig96() {
EEPROM.begin(EEPROM_SIZE);
SystemConfig sys;
EEPROM.get(CONFIG_SYSTEM_START, sys);
MeterConfig meter;
EEPROM.get(CONFIG_METER_START, meter);
meter.source = 1; // Serial
meter.parser = 0; // Auto
EEPROM.put(CONFIG_METER_START, meter);
#if defined(ESP8266)
GpioConfig gpio;
EEPROM.get(CONFIG_GPIO_START, gpio);
switch(sys.boardType) {
case 3: // Pow UART0 -- Now Pow-K UART0
case 4: // Pow GPIO12 -- Now Pow-U UART0
case 5: // Pow-K+ -- Now also Pow-K GPIO12
case 7: // Pow-U+ -- Now also Pow-U GPIO12
if(meter.baud == 2400 && meter.parity == 3) { // 3 == 8N1, assuming Pow-K
if(gpio.hanPin == 3) { // UART0
sys.boardType = 3;
} else if(gpio.hanPin == 12) {
sys.boardType = 5;
}
} else { // Assuming Pow-U
if(gpio.hanPin == 3) { // UART0
sys.boardType = 4;
} else if(gpio.hanPin == 12) {
sys.boardType = 7;
}
}
break;
}
#endif
sys.vendorConfigured = true;
sys.userConfigured = true;
sys.dataCollectionConsent = 0;
strcpy(sys.country, "");
EEPROM.put(CONFIG_SYSTEM_START, sys);
WiFiConfig wifi;
EEPROM.get(CONFIG_WIFI_START, wifi);
wifi.mode = 1; // WIFI_STA
wifi.autoreboot = true;
EEPROM.put(CONFIG_WIFI_START, wifi);
NtpConfig ntp;
NtpConfig96 ntp96;
EEPROM.get(CONFIG_NTP_START, ntp96);
ntp.enable = ntp96.enable;
ntp.dhcp = ntp96.dhcp;
if(ntp96.offset == 360 && ntp96.summerOffset == 360) {
strcpy(ntp.timezone, "Europe/Oslo");
} else {
strcpy(ntp.timezone, "GMT");
}
strcpy(ntp.server, ntp96.server);
EEPROM.put(CONFIG_NTP_START, ntp);
EntsoeConfig entsoe;
EEPROM.get(CONFIG_ENTSOE_START, entsoe);
entsoe.enabled = strlen(entsoe.token) > 0;
EEPROM.put(CONFIG_ENTSOE_START, entsoe);
EEPROM.put(EEPROM_CONFIG_ADDRESS, 100);
bool ret = EEPROM.commit();
EEPROM.end();
return ret;
}
bool AmsConfiguration::relocateConfig100() {
EEPROM.begin(EEPROM_SIZE);
MeterConfig100 meter100;
EEPROM.get(CONFIG_METER_START, meter100);
MeterConfig meter;
meter.baud = meter100.baud;
meter.parity = meter100.parity;
meter.invert = meter100.invert;
meter.distributionSystem = meter100.distributionSystem;
meter.mainFuse = meter100.mainFuse;
meter.productionCapacity = meter100.productionCapacity;
memcpy(meter.encryptionKey, meter100.encryptionKey, 16);
memcpy(meter.authenticationKey, meter100.authenticationKey, 16);
meter.wattageMultiplier = meter100.wattageMultiplier;
meter.voltageMultiplier = meter100.voltageMultiplier;
meter.amperageMultiplier = meter100.amperageMultiplier;
meter.accumulatedMultiplier = meter100.accumulatedMultiplier;
meter.source = meter100.source;
meter.parser = meter100.parser;
EEPROM.put(CONFIG_METER_START, meter);
UiConfig ui;
clearUiConfig(ui);
EEPROM.put(CONFIG_UI_START, ui);
EEPROM.put(EEPROM_CONFIG_ADDRESS, 101);
bool ret = EEPROM.commit();
EEPROM.end();
return ret;
}
bool AmsConfiguration::save() {
EEPROM.begin(EEPROM_SIZE);
EEPROM.put(EEPROM_CONFIG_ADDRESS, EEPROM_CHECK_SUM);
@@ -1201,7 +1080,8 @@ void AmsConfiguration::print(Print* debugger)
debugger->println("--NTP configuration--");
debugger->printf("Enabled: %s\r\n", ntp.enable ? "Yes" : "No");
if(ntp.enable) {
debugger->printf("Timezone: %s\r\n", ntp.timezone);
debugger->printf("Offset: %i\r\n", ntp.offset);
debugger->printf("Summer offset: %i\r\n", ntp.summerOffset);
debugger->printf("Server: %s\r\n", ntp.server);
debugger->printf("DHCP: %s\r\n", ntp.dhcp ? "Yes" : "No");
}
@@ -1212,12 +1092,12 @@ void AmsConfiguration::print(Print* debugger)
EntsoeConfig entsoe;
if(getEntsoeConfig(entsoe)) {
if(strlen(entsoe.area) > 0) {
debugger->println("--ENTSO-E configuration--");
debugger->println("--ENTSO-E configuration--");
debugger->printf("Token: %s\r\n", entsoe.token);
if(strlen(entsoe.token) > 0) {
debugger->printf("Area: %s\r\n", entsoe.area);
debugger->printf("Currency: %s\r\n", entsoe.currency);
debugger->printf("Multiplier: %f\r\n", entsoe.multiplier / 1000.0);
debugger->printf("Token: %s\r\n", entsoe.token);
}
debugger->println("");
delay(10);

View File

@@ -4,14 +4,12 @@
#include "Arduino.h"
#define EEPROM_SIZE 1024*3
#define EEPROM_CHECK_SUM 101 // Used to check if config is stored. Change if structure changes
#define EEPROM_CLEARED_INDICATOR 0xFC
#define EEPROM_CHECK_SUM 96 // 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_METER_START 32
#define CONFIG_UI_START 248
#define CONFIG_GPIO_START 266
#define CONFIG_ENTSOE_START 290
#define CONFIG_WIFI_START 360
@@ -31,11 +29,7 @@
struct SystemConfig {
uint8_t boardType;
bool vendorConfigured;
bool userConfigured;
uint8_t dataCollectionConsent; // 0 = unknown, 1 = accepted, 2 = declined
char country[2];
}; // 6
}; // 1
struct WiFiConfig91 {
char ssid[32];
@@ -61,9 +55,7 @@ struct WiFiConfig {
bool mdns;
uint8_t power;
uint8_t sleep;
uint8_t mode;
bool autoreboot;
}; // 213
}; // 211
struct MqttConfig86 {
char host[128];
@@ -96,23 +88,6 @@ struct WebConfig {
}; // 129
struct MeterConfig {
uint32_t baud;
uint8_t parity;
bool invert;
uint8_t distributionSystem;
uint16_t mainFuse;
uint16_t productionCapacity;
uint8_t encryptionKey[16];
uint8_t authenticationKey[16];
uint32_t wattageMultiplier;
uint32_t voltageMultiplier;
uint32_t amperageMultiplier;
uint32_t accumulatedMultiplier;
uint8_t source;
uint8_t parser;
}; // 52
struct MeterConfig100 {
uint32_t baud;
uint8_t parity;
bool invert;
@@ -190,13 +165,6 @@ struct DomoticzConfig {
}; // 10
struct NtpConfig {
bool enable;
bool dhcp;
char server[64];
char timezone[32];
}; // 98
struct NtpConfig96 {
bool enable;
bool dhcp;
int16_t offset;
@@ -209,7 +177,6 @@ struct EntsoeConfig {
char area[17];
char currency[4];
uint32_t multiplier;
bool enabled;
}; // 62
struct EnergyAccountingConfig {
@@ -217,20 +184,6 @@ struct EnergyAccountingConfig {
uint8_t hours;
}; // 11
struct UiConfig {
uint8_t showImport;
uint8_t showExport;
uint8_t showVoltage;
uint8_t showAmperage;
uint8_t showReactive;
uint8_t showRealtime;
uint8_t showPeaks;
uint8_t showPricePlot;
uint8_t showDayPlot;
uint8_t showMonthPlot;
uint8_t showTemperaturePlot;
}; // 11
struct TempSensorConfig {
uint8_t address[8];
char name[16];
@@ -307,10 +260,6 @@ public:
bool isEnergyAccountingChanged();
void ackEnergyAccountingChange();
bool getUiConfig(UiConfig&);
bool setUiConfig(UiConfig&);
void clearUiConfig(UiConfig&);
void loadTempSensors();
void saveTempSensors();
uint8_t getTempSensorCount();
@@ -331,14 +280,14 @@ private:
uint8_t tempSensorCount = 0;
TempSensorConfig** tempSensors = NULL;
bool relocateConfig86(); // 1.5.0
bool relocateConfig87(); // 1.5.4
bool relocateConfig90(); // 2.0.0
bool relocateConfig91(); // 2.0.2
bool relocateConfig92(); // 2.0.3
bool relocateConfig93(); // 2.1.0
bool relocateConfig94(); // 2.1.0
bool relocateConfig95(); // 2.1.4
bool relocateConfig96(); // 2.1.14
bool relocateConfig100(); // 2.2-dev
bool relocateConfig94(); // 2.1.4
bool relocateConfig95(); // 2.1.13
void saveToFs();
bool loadFromFs(uint8_t version);

View File

@@ -64,6 +64,7 @@ void AmsData::apply(AmsData& other) {
this->meterType = other.getMeterType();
this->meterModel = other.getMeterModel();
this->reactiveImportPower = other.getReactiveImportPower();
this->activeExportPower = other.getActiveExportPower();
this->reactiveExportPower = other.getReactiveExportPower();
this->l1current = other.getL1Current();
this->l2current = other.getL2Current();
@@ -73,13 +74,9 @@ void AmsData::apply(AmsData& other) {
this->l3voltage = other.getL3Voltage();
this->threePhase = other.isThreePhase();
this->twoPhase = other.isTwoPhase();
case 1:
this->activeImportPower = other.getActiveImportPower();
}
// Moved outside switch to handle meters alternating between sending active and accumulated values
if(other.getListType() == 1 || (other.getActiveImportPower() > 0 || other.getActiveExportPower() > 0))
this->activeImportPower = other.getActiveImportPower();
if(other.getListType() == 2 || (other.getActiveImportPower() > 0 || other.getActiveExportPower() > 0))
this->activeExportPower = other.getActiveExportPower();
}
unsigned long AmsData::getLastUpdateMillis() {
@@ -217,16 +214,3 @@ bool AmsData::isThreePhase() {
bool AmsData::isTwoPhase() {
return this->twoPhase;
}
int8_t AmsData::getLastError() {
return lastErrorCount > 3 ? lastError : 0;
}
void AmsData::setLastError(int8_t lastError) {
this->lastError = lastError;
if(lastError == 0) {
lastErrorCount = 0;
} else {
lastErrorCount++;
}
}

View File

@@ -12,6 +12,7 @@ enum AmsType {
AmsTypeIskra = 0x08,
AmsTypeLandisGyr = 0x09,
AmsTypeSagemcom = 0x0A,
AmsTypeLng = 0x0B,
AmsTypeCustom = 0x88,
AmsTypeUnknown = 0xFF
};
@@ -69,9 +70,6 @@ public:
bool isThreePhase();
bool isTwoPhase();
int8_t getLastError();
void setLastError(int8_t);
protected:
unsigned long lastUpdateMillis = 0;
unsigned long lastList2 = 0;
@@ -86,9 +84,6 @@ protected:
float powerFactor = 0, l1PowerFactor = 0, l2PowerFactor = 0, l3PowerFactor = 0;
double activeImportCounter = 0, reactiveImportCounter = 0, activeExportCounter = 0, reactiveExportCounter = 0;
bool threePhase = false, twoPhase = false, counterEstimated = false;
int8_t lastError = 0x00;
uint8_t lastErrorCount = 0;
};
#endif

View File

@@ -5,10 +5,8 @@
#include "version.h"
AmsDataStorage::AmsDataStorage(RemoteDebug* debugger) {
day.version = 5;
day.accuracy = 1;
month.version = 6;
month.accuracy = 1;
day.version = 4;
month.version = 5;
this->debugger = debugger;
}
@@ -239,160 +237,44 @@ bool AmsDataStorage::update(AmsData* data) {
return ret;
}
void AmsDataStorage::setHourImport(uint8_t hour, uint32_t val) {
void AmsDataStorage::setHourImport(uint8_t hour, int32_t val) {
if(hour < 0 || hour > 24) return;
uint8_t accuracy = day.accuracy;
uint32_t update = val / pow(10, accuracy);
while(update > UINT16_MAX) {
accuracy++;
update = val / pow(10, accuracy);
}
if(accuracy != day.accuracy) {
setDayAccuracy(accuracy);
}
day.hImport[hour] = update;
uint32_t max = 0;
for(uint8_t i = 0; i < 24; i++) {
if(day.hImport[i] > max)
max = day.hImport[i];
if(day.hExport[i] > max)
max = day.hExport[i];
}
while(max < UINT16_MAX/10 && accuracy > 0) {
accuracy--;
max = max*10;
}
if(accuracy != day.accuracy) {
setDayAccuracy(accuracy);
}
day.hImport[hour] = val / 10;
}
uint32_t AmsDataStorage::getHourImport(uint8_t hour) {
int32_t AmsDataStorage::getHourImport(uint8_t hour) {
if(hour < 0 || hour > 24) return 0;
return day.hImport[hour] * pow(10, day.accuracy);
return day.hImport[hour] * 10;
}
void AmsDataStorage::setHourExport(uint8_t hour, uint32_t val) {
void AmsDataStorage::setHourExport(uint8_t hour, int32_t val) {
if(hour < 0 || hour > 24) return;
uint8_t accuracy = day.accuracy;
uint32_t update = val / pow(10, accuracy);
while(update > UINT16_MAX) {
accuracy++;
update = val / pow(10, accuracy);
}
if(accuracy != day.accuracy) {
setDayAccuracy(accuracy);
}
day.hExport[hour] = update;
uint32_t max = 0;
for(uint8_t i = 0; i < 24; i++) {
if(day.hImport[i] > max)
max = day.hImport[i];
if(day.hExport[i] > max)
max = day.hExport[i];
}
while(max < UINT16_MAX/10 && accuracy > 0) {
accuracy--;
max = max*10;
}
if(accuracy != day.accuracy) {
setDayAccuracy(accuracy);
}
day.hExport[hour] = val / 10;
}
uint32_t AmsDataStorage::getHourExport(uint8_t hour) {
int32_t AmsDataStorage::getHourExport(uint8_t hour) {
if(hour < 0 || hour > 24) return 0;
return day.hExport[hour] * pow(10, day.accuracy);
return day.hExport[hour] * 10;
}
void AmsDataStorage::setDayImport(uint8_t day, uint32_t val) {
void AmsDataStorage::setDayImport(uint8_t day, int32_t val) {
if(day < 1 || day > 31) return;
uint8_t accuracy = month.accuracy;
uint32_t update = val / pow(10, accuracy);
while(update > UINT16_MAX) {
accuracy++;
update = val / pow(10, accuracy);
}
if(accuracy != month.accuracy) {
setMonthAccuracy(accuracy);
}
month.dImport[day-1] = update;
uint32_t max = 0;
for(uint8_t i = 0; i < 31; i++) {
if(month.dImport[i] > max)
max = month.dImport[i];
if(month.dExport[i] > max)
max = month.dExport[i];
}
while(max < UINT16_MAX/10 && accuracy > 0) {
accuracy--;
max = max*10;
}
if(accuracy != month.accuracy) {
setMonthAccuracy(accuracy);
}
month.dImport[day-1] = val / 10;
}
uint32_t AmsDataStorage::getDayImport(uint8_t day) {
int32_t AmsDataStorage::getDayImport(uint8_t day) {
if(day < 1 || day > 31) return 0;
return (month.dImport[day-1] * pow(10, month.accuracy));
return (month.dImport[day-1] * 10);
}
void AmsDataStorage::setDayExport(uint8_t day, uint32_t val) {
void AmsDataStorage::setDayExport(uint8_t day, int32_t val) {
if(day < 1 || day > 31) return;
uint8_t accuracy = month.accuracy;
uint32_t update = val / pow(10, accuracy);
while(update > UINT16_MAX) {
accuracy++;
update = val / pow(10, accuracy);
}
if(accuracy != month.accuracy) {
setMonthAccuracy(accuracy);
}
month.dExport[day-1] = update;
uint32_t max = 0;
for(uint8_t i = 0; i < 31; i++) {
if(month.dImport[i] > max)
max = month.dImport[i];
if(month.dExport[i] > max)
max = month.dExport[i];
}
while(max < UINT16_MAX/10 && accuracy > 0) {
accuracy--;
max = max*10;
}
if(accuracy != month.accuracy) {
setMonthAccuracy(accuracy);
}
month.dExport[day-1] = val / 10;
}
uint32_t AmsDataStorage::getDayExport(uint8_t day) {
int32_t AmsDataStorage::getDayExport(uint8_t day) {
if(day < 1 || day > 31) return 0;
return (month.dExport[day-1] * pow(10, month.accuracy));
return (month.dExport[day-1] * 10);
}
bool AmsDataStorage::load() {
@@ -466,74 +348,31 @@ MonthDataPoints AmsDataStorage::getMonthData() {
}
bool AmsDataStorage::setDayData(DayDataPoints& day) {
if(day.version == 5) {
if(day.version == 4) {
this->day = day;
return true;
} else if(day.version == 4) {
this->day = day;
this->day.accuracy = 1;
this->day.version = 5;
return true;
} else if(day.version == 3) {
this->day = day;
for(uint8_t i = 0; i < 24; i++) this->day.hExport[i] = 0;
this->day.accuracy = 1;
this->day.version = 5;
this->day.version = 4;
return true;
}
return false;
}
bool AmsDataStorage::setMonthData(MonthDataPoints& month) {
if(month.version == 6) {
if(month.version == 5) {
this->month = month;
return true;
} else if(month.version == 5) {
this->month = month;
this->month.accuracy = 1;
this->month.version = 6;
return true;
} else if(month.version == 4) {
this->month = month;
for(uint8_t i = 0; i < 31; i++) this->month.dExport[i] = 0;
this->month.accuracy = 1;
this->month.version = 6;
this->month.version = 5;
return true;
}
return false;
}
uint8_t AmsDataStorage::getDayAccuracy() {
return day.accuracy;
}
void AmsDataStorage::setDayAccuracy(uint8_t accuracy) {
if(day.accuracy != accuracy) {
uint16_t multiplier = pow(10, day.accuracy)/pow(10, accuracy);
for(uint8_t i = 0; i < 24; i++) {
day.hImport[i] = day.hImport[i] * multiplier;
day.hExport[i] = day.hExport[i] * multiplier;
}
day.accuracy = accuracy;
}
}
uint8_t AmsDataStorage::getMonthAccuracy() {
return month.accuracy;
}
void AmsDataStorage::setMonthAccuracy(uint8_t accuracy) {
if(month.accuracy != accuracy) {
uint16_t multiplier = pow(10, month.accuracy)/pow(10, accuracy);
for(uint8_t i = 0; i < 31; i++) {
month.dImport[i] = month.dImport[i] * multiplier;
month.dExport[i] = month.dExport[i] * multiplier;
}
month.accuracy = accuracy;
}
month.accuracy = accuracy;
}
bool AmsDataStorage::isHappy() {
return isDayHappy() && isMonthHappy();
}

Some files were not shown because too many files have changed in this diff Show More