Compare commits

..

28 Commits

Author SHA1 Message Date
Gunnar Skjold
fb1d343ee3 Removed debugging 2023-01-17 19:03:07 +01:00
Gunnar Skjold
1f3c32e80a Adding secrets to build 2023-01-17 18:57:11 +01:00
Gunnar Skjold
43fbca7099 Update release build 2023-01-17 18:42:09 +01:00
Gunnar Skjold
7a36082564 Trying stuff to get build working 2023-01-17 18:34:47 +01:00
Gunnar Skjold
fb2cfdfe01 Trying to fix build 2023-01-17 18:28:08 +01:00
Gunnar Skjold
6c3dca9344 Updated build 2023-01-17 18:15:32 +01:00
Gunnar Skjold
cce5d75fd7 Updated build 2023-01-17 18:13:54 +01:00
Gunnar Skjold
8fd411c1d6 Updated build 2023-01-17 18:13:20 +01:00
Gunnar Skjold
508b2e6c45 Icon for safari 2023-01-17 17:51:50 +01:00
Gunnar Skjold
af630615db Changes after testing 2023-01-17 17:49:01 +01:00
Gunnar Skjold
222a4f13e2 Changes after testing 2023-01-17 17:48:42 +01:00
Gunnar Skjold
3bc6c75c5a Some more changes for L&G 2023-01-15 11:20:28 +01:00
Gunnar Skjold
dbb3eac709 Some changes for L&G 2023-01-15 10:56:46 +01:00
Gunnar Skjold
1cee48eab4 Some design changes 2023-01-14 09:34:28 +01:00
Gunnar Skjold
c28752a00a Fixed firmware upgrade 2023-01-12 21:24:36 +01:00
Gunnar Skjold
365061df29 Some changes after testing 2023-01-12 20:17:37 +01:00
Gunnar Skjold
870617f780 More changes for v2.2 2023-01-11 20:41:27 +01:00
Gunnar Skjold
4972b980ba Changes for v2.2 2023-01-06 14:34:07 +01:00
Gunnar Skjold
51c70abda3 Merge branch 'master' into dev-v2.2 2023-01-04 18:55:38 +01:00
Gunnar Skjold
8d448533c7 Fixed config export for esp32 2022-12-19 18:36:04 +01:00
Gunnar Skjold
43cb9a0000 Fixed DSMR CRC check 2022-12-19 18:14:12 +01:00
Gunnar Skjold
d49b7eac09 Minor change 2022-12-19 16:49:40 +01:00
Gunnar Skjold
8f057e687c Merge pull request #387 from lassebm/lg-e360-fixes
Landis+Gyr E360 fixes
2022-12-19 16:45:52 +01:00
Lasse Bang Mikkelsen
5b4f680114 Align Landis+Gyr naming 2022-12-17 12:38:18 +01:00
Lasse Bang Mikkelsen
fabdfbadf4 Add support for Landis+Gyr meters using "LGF" manufacturer ID 2022-12-16 20:20:40 +01:00
Lasse Bang Mikkelsen
afa47ea633 Use list type 4 when phase active import/export power is available 2022-12-16 20:19:15 +01:00
Gunnar Skjold
461d76e651 Merge branch 'master' into dev-v2.2 2022-12-15 18:32:36 +01:00
Gunnar Skjold
c40e20c8e9 Extract active import/export per phase from DSMR 2022-12-15 18:25:13 +01:00
52 changed files with 1195 additions and 312 deletions

View File

@@ -8,6 +8,7 @@ on:
- scripts/**
- web/**
- platformio.ini
- .github/workflows/**
branches:
- '*'
tags:
@@ -22,6 +23,12 @@ 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:
@@ -40,6 +47,18 @@ 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,6 +23,12 @@ 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:
@@ -41,12 +47,39 @@ 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: Build ESP8266 firmware
run: pio run -e esp8266
- name: Create ESP8266 zip file
run: /bin/sh scripts/esp8266/mkzip.sh
- name: Build ESP32 firmware
run: pio run -e esp32
- name: Create ESP32 zip file
run: /bin/sh scripts/esp32/mkzip.sh
- name: Build ESP32-S2 firmware
run: pio run -e esp32s2
- name: Create ESP32-S2 zip file
run: /bin/sh scripts/esp32s2/mkzip.sh
- name: Build ESP32-SOLO firmware
run: pio run -e esp32solo
- name: Create ESP32-SOLO zip file
run: /bin/sh scripts/esp32solo/mkzip.sh
- name: Create Release
id: create_release
uses: actions/create-release@v1.0.0

View File

@@ -4,13 +4,14 @@
#include "Arduino.h"
#define EEPROM_SIZE 1024*3
#define EEPROM_CHECK_SUM 100 // Used to check if config is stored. Change if structure changes
#define EEPROM_CHECK_SUM 101 // Used to check if config is stored. Change if structure changes
#define EEPROM_CLEARED_INDICATOR 0xFC
#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
@@ -95,6 +96,23 @@ 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;
@@ -199,6 +217,20 @@ 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];
@@ -275,6 +307,10 @@ public:
bool isEnergyAccountingChanged();
void ackEnergyAccountingChange();
bool getUiConfig(UiConfig&);
bool setUiConfig(UiConfig&);
void clearUiConfig(UiConfig&);
void loadTempSensors();
void saveTempSensors();
uint8_t getTempSensorCount();
@@ -302,6 +338,7 @@ private:
bool relocateConfig94(); // 2.1.0
bool relocateConfig95(); // 2.1.4
bool relocateConfig96(); // 2.1.14
bool relocateConfig100(); // 2.2-dev
void saveToFs();
bool loadFromFs(uint8_t version);

View File

@@ -230,6 +230,8 @@ 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() {
@@ -547,7 +549,6 @@ bool AmsConfiguration::setEnergyAccountingConfig(EnergyAccountingConfig& config)
bool ret = EEPROM.commit();
EEPROM.end();
return ret;
}
void AmsConfiguration::clearEnergyAccountingConfig(EnergyAccountingConfig& config) {
@@ -572,6 +573,42 @@ 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);
@@ -619,6 +656,10 @@ void AmsConfiguration::clear() {
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.commit();
EEPROM.end();
@@ -694,6 +735,14 @@ bool AmsConfiguration::hasConfig() {
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:
@@ -848,9 +897,13 @@ bool AmsConfiguration::relocateConfig96() {
SystemConfig sys;
EEPROM.get(CONFIG_SYSTEM_START, sys);
#if defined(ESP8266)
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);
@@ -912,6 +965,39 @@ bool AmsConfiguration::relocateConfig96() {
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);

View File

@@ -10,9 +10,8 @@ enum AmsType {
AmsTypeKaifa = 0x02,
AmsTypeKamstrup = 0x03,
AmsTypeIskra = 0x08,
AmsTypeLandis = 0x09,
AmsTypeLandisGyr = 0x09,
AmsTypeSagemcom = 0x0A,
AmsTypeLng = 0x0B,
AmsTypeCustom = 0x88,
AmsTypeUnknown = 0xFF
};

View File

@@ -64,7 +64,6 @@ 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();
@@ -74,9 +73,13 @@ 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() {

View File

@@ -12,7 +12,8 @@ struct DayDataPoints {
uint32_t activeImport;
uint32_t activeExport;
uint16_t hExport[24];
}; // 112 bytes
uint8_t accuracy;
}; // 113 bytes
struct MonthDataPoints {
uint8_t version;
@@ -21,17 +22,18 @@ struct MonthDataPoints {
uint32_t activeImport;
uint32_t activeExport;
uint16_t dExport[31];
}; // 141 bytes
uint8_t accuracy;
}; // 142 bytes
class AmsDataStorage {
public:
AmsDataStorage(RemoteDebug*);
void setTimezone(Timezone*);
bool update(AmsData*);
int32_t getHourImport(uint8_t);
int32_t getHourExport(uint8_t);
int32_t getDayImport(uint8_t);
int32_t getDayExport(uint8_t);
uint32_t getHourImport(uint8_t);
uint32_t getHourExport(uint8_t);
uint32_t getDayImport(uint8_t);
uint32_t getDayExport(uint8_t);
bool load();
bool save();
@@ -40,6 +42,11 @@ public:
MonthDataPoints getMonthData();
bool setMonthData(MonthDataPoints&);
uint8_t getDayAccuracy();
void setDayAccuracy(uint8_t);
uint8_t getMonthAccuracy();
void setMonthAccuracy(uint8_t);
bool isHappy();
bool isDayHappy();
bool isMonthHappy();
@@ -50,19 +57,21 @@ private:
0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
10
};
MonthDataPoints month = {
0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
10
};
RemoteDebug* debugger;
void setHourImport(uint8_t, int32_t);
void setHourExport(uint8_t, int32_t);
void setDayImport(uint8_t, int32_t);
void setDayExport(uint8_t, int32_t);
void setHourImport(uint8_t, uint32_t);
void setHourExport(uint8_t, uint32_t);
void setDayImport(uint8_t, uint32_t);
void setDayExport(uint8_t, uint32_t);
};
#endif

View File

@@ -5,8 +5,10 @@
#include "version.h"
AmsDataStorage::AmsDataStorage(RemoteDebug* debugger) {
day.version = 4;
month.version = 5;
day.version = 5;
day.accuracy = 1;
month.version = 6;
month.accuracy = 1;
this->debugger = debugger;
}
@@ -237,44 +239,160 @@ bool AmsDataStorage::update(AmsData* data) {
return ret;
}
void AmsDataStorage::setHourImport(uint8_t hour, int32_t val) {
void AmsDataStorage::setHourImport(uint8_t hour, uint32_t val) {
if(hour < 0 || hour > 24) return;
day.hImport[hour] = val / 10;
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);
}
}
int32_t AmsDataStorage::getHourImport(uint8_t hour) {
uint32_t AmsDataStorage::getHourImport(uint8_t hour) {
if(hour < 0 || hour > 24) return 0;
return day.hImport[hour] * 10;
return day.hImport[hour] * pow(10, day.accuracy);
}
void AmsDataStorage::setHourExport(uint8_t hour, int32_t val) {
void AmsDataStorage::setHourExport(uint8_t hour, uint32_t val) {
if(hour < 0 || hour > 24) return;
day.hExport[hour] = val / 10;
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);
}
}
int32_t AmsDataStorage::getHourExport(uint8_t hour) {
uint32_t AmsDataStorage::getHourExport(uint8_t hour) {
if(hour < 0 || hour > 24) return 0;
return day.hExport[hour] * 10;
return day.hExport[hour] * pow(10, day.accuracy);
}
void AmsDataStorage::setDayImport(uint8_t day, int32_t val) {
void AmsDataStorage::setDayImport(uint8_t day, uint32_t val) {
if(day < 1 || day > 31) return;
month.dImport[day-1] = val / 10;
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);
}
}
int32_t AmsDataStorage::getDayImport(uint8_t day) {
uint32_t AmsDataStorage::getDayImport(uint8_t day) {
if(day < 1 || day > 31) return 0;
return (month.dImport[day-1] * 10);
return (month.dImport[day-1] * pow(10, month.accuracy));
}
void AmsDataStorage::setDayExport(uint8_t day, int32_t val) {
void AmsDataStorage::setDayExport(uint8_t day, uint32_t val) {
if(day < 1 || day > 31) return;
month.dExport[day-1] = val / 10;
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);
}
}
int32_t AmsDataStorage::getDayExport(uint8_t day) {
uint32_t AmsDataStorage::getDayExport(uint8_t day) {
if(day < 1 || day > 31) return 0;
return (month.dExport[day-1] * 10);
return (month.dExport[day-1] * pow(10, month.accuracy));
}
bool AmsDataStorage::load() {
@@ -348,31 +466,74 @@ MonthDataPoints AmsDataStorage::getMonthData() {
}
bool AmsDataStorage::setDayData(DayDataPoints& day) {
if(day.version == 4) {
if(day.version == 5) {
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.version = 4;
this->day.accuracy = 1;
this->day.version = 5;
return true;
}
return false;
}
bool AmsDataStorage::setMonthData(MonthDataPoints& month) {
if(month.version == 5) {
if(month.version == 6) {
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.version = 5;
this->month.accuracy = 1;
this->month.version = 6;
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();
}

View File

@@ -4,6 +4,7 @@
#include "Arduino.h"
#include <stdint.h>
uint16_t crc16(const uint8_t* p, int len);
uint16_t crc16_x25(const uint8_t* p, int len);
#endif

View File

@@ -1,4 +1,7 @@
#include "DsmrParser.h"
#include "crc.h"
#include "hexutils.h"
#include "lwip/def.h"
int8_t DSMRParser::parse(uint8_t *buf, DataParserContext &ctx, bool verified) {
uint16_t crcPos = 0;
@@ -14,8 +17,13 @@ int8_t DSMRParser::parse(uint8_t *buf, DataParserContext &ctx, bool verified) {
if(!reachedEnd) return DATA_PARSE_INCOMPLETE;
buf[ctx.length+1] = '\0';
if(crcPos > 0) {
// TODO: CRC
Serial.printf("CRC: %s\n", buf+crcPos);
uint16_t crc_calc = crc16(buf, crcPos);
uint16_t crc = 0x0000;
fromHex((uint8_t*) &crc, String((char*) buf+crcPos), 2);
crc = ntohs(crc);
if(crc != crc_calc)
return DATA_PARSE_FOOTER_CHECKSUM_ERROR;
}
return DATA_PARSE_OK;
}

View File

@@ -10,3 +10,20 @@ uint16_t crc16_x25(const uint8_t* p, int len)
return (~crc << 8) | (~crc >> 8 & 0xff);
}
uint16_t crc16 (const uint8_t *p, int len) {
uint16_t crc = 0;
while (len--) {
int i;
crc ^= *p++;
for (i = 0 ; i < 8 ; ++i) {
if (crc & 1)
crc = (crc >> 1) ^ 0xa001;
else
crc = (crc >> 1);
}
}
return crc;
}

View File

@@ -854,7 +854,7 @@ var fetch = function() {
$('.jmt').html("Iskra");
break;
case 9:
$('.jmt').html("Landis");
$('.jmt').html("Landis+Gyr");
break;
case 10:
$('.jmt').html("Sagemcom");

View File

@@ -329,8 +329,8 @@ void AmsWebServer::configMeterHtml() {
case AmsTypeIskra:
manufacturer = F("Iskra");
break;
case AmsTypeLandis:
manufacturer = F("Landis + Gyro");
case AmsTypeLandisGyr:
manufacturer = F("Landis+Gyr");
break;
case AmsTypeSagemcom:
manufacturer = F("Sagemcom");

View File

@@ -37,8 +37,7 @@ private:
HTTPClient http;
uint8_t currentDay = 0, currentHour = 0;
uint32_t tomorrowFetchMillis = 36000000; // Number of ms before midnight. Default fetch 10hrs before midnight (14:00 CE(S)T)
uint64_t midnightMillis = 0;
uint8_t tomorrowFetchMinute = 15; // How many minutes over 13:00 should it fetch prices
uint64_t lastTodayFetch = 0;
uint64_t lastTomorrowFetch = 0;
uint64_t lastCurrencyFetch = 0;
@@ -60,7 +59,7 @@ private:
PricesContainer* fetchPrices(time_t);
bool retrieve(const char* url, Stream* doc);
float getCurrencyMultiplier(const char* from, const char* to);
float getCurrencyMultiplier(const char* from, const char* to, time_t t);
void printD(String fmt, ...);
void printE(String fmt, ...);

View File

@@ -21,7 +21,7 @@ EntsoeApi::EntsoeApi(RemoteDebug* Debug) {
TimeChangeRule CET = {"CET ", Last, Sun, Oct, 3, 60};
tz = new Timezone(CEST, CET);
tomorrowFetchMillis = 36000000 + (random(1800) * 1000); // Random between 13:30 and 14:00
tomorrowFetchMinute = 15 + random(45); // Random between 13:15 and 14:00
}
void EntsoeApi::setup(EntsoeConfig& config) {
@@ -96,7 +96,7 @@ float EntsoeApi::getValueForHour(time_t cur, int8_t hour) {
} else {
return ENTSOE_NO_VALUE;
}
float mult = getCurrencyMultiplier(tomorrow->currency, config->currency);
float mult = getCurrencyMultiplier(tomorrow->currency, config->currency, cur);
if(mult == 0) return ENTSOE_NO_VALUE;
multiplier *= mult;
} else if(pos >= 0) {
@@ -112,7 +112,7 @@ float EntsoeApi::getValueForHour(time_t cur, int8_t hour) {
} else {
return ENTSOE_NO_VALUE;
}
float mult = getCurrencyMultiplier(today->currency, config->currency);
float mult = getCurrencyMultiplier(today->currency, config->currency, cur);
if(mult == 0) return ENTSOE_NO_VALUE;
multiplier *= mult;
}
@@ -138,21 +138,15 @@ bool EntsoeApi::loop() {
if(strlen(config->currency) == 0)
return false;
bool ret = false;
tmElements_t tm;
breakTime(tz->toLocal(t), tm);
if(currentHour != tm.Hour) {
currentHour = tm.Hour;
ret = today != NULL; // Only trigger MQTT publish if we have todays prices.
}
if(midnightMillis == 0) {
uint32_t curDayMillis = (((((tm.Hour * 60) + tm.Minute) * 60) + tm.Second) * 1000);
midnightMillis = now + (SECS_PER_DAY * 1000) - curDayMillis;
if(debugger->isActive(RemoteDebug::INFO)) debugger->printf("(EntsoeApi) Setting midnight millis %llu\n", midnightMillis);
if(currentDay == 0) {
currentDay = tm.Day;
return false;
} else if(now > midnightMillis && 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) {
@@ -160,38 +154,38 @@ bool EntsoeApi::loop() {
tomorrow = NULL;
}
currentDay = tm.Day;
midnightMillis = 0; // Force new midnight millis calculation
return true;
} else {
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;
}
// Prices for next day are published at 13:00 CE(S)T, but to avoid heavy server traffic at that time, we will
// fetch 1 hr after that (with some random delay) and retry every 15 minutes
if(tomorrow == NULL
&& midnightMillis - now < tomorrowFetchMillis
&& (lastTomorrowFetch == 0 || now - lastTomorrowFetch > 900000)
) {
try {
breakTime(t+SECS_PER_DAY, tm); // Break UTC tomorrow to find UTC midnight
lastTomorrowFetch = now;
tomorrow = fetchPrices(t+SECS_PER_DAY);
} catch(const std::exception& e) {
if(lastError == 0) lastError = 900;
tomorrow = NULL;
}
return tomorrow != NULL;
}
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.
}
return ret;
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) {
@@ -235,7 +229,7 @@ bool EntsoeApi::retrieve(const char* url, Stream* doc) {
return false;
}
float EntsoeApi::getCurrencyMultiplier(const char* from, const char* to) {
float EntsoeApi::getCurrencyMultiplier(const char* from, const char* to, time_t t) {
if(strcmp(from, to) == 0)
return 1.00;
@@ -272,7 +266,9 @@ float EntsoeApi::getCurrencyMultiplier(const char* from, const char* to) {
return 0;
}
if(debugger->isActive(RemoteDebug::DEBUG)) debugger->printf("(EntsoeApi) Resulting currency multiplier: %.4f\n", currencyMultiplier);
lastCurrencyFetch = midnightMillis;
tmElements_t tm;
breakTime(t, tm);
lastCurrencyFetch = now + (SECS_PER_DAY * 1000) - (((((tm.Hour * 60) + tm.Minute) * 60) + tm.Second) * 1000);
}
return currencyMultiplier;
}

View File

@@ -22,12 +22,12 @@ HomeAssistantSensor HA_SENSORS[HA_SENSOR_COUNT] PROGMEM = {
{"L1 active import", "/power", "P1", "W", "power", "\"measurement\""},
{"L2 active import", "/power", "P2", "W", "power", "\"measurement\""},
{"L3 active import", "/power", "P3", "W", "power", "\"measurement\""},
{"Reactive import", "/power", "Q", "VAr", "reactive_power", "\"measurement\""},
{"Reactive import", "/power", "Q", "var", "reactive_power", "\"measurement\""},
{"Active export", "/power", "PO", "W", "power", "\"measurement\""},
{"L1 active export", "/power", "PO1", "W", "power", "\"measurement\""},
{"L2 active export", "/power", "PO2", "W", "power", "\"measurement\""},
{"L3 active export", "/power", "PO3", "W", "power", "\"measurement\""},
{"Reactive export", "/power", "QO", "VAr", "reactive_power", "\"measurement\""},
{"Reactive export", "/power", "QO", "var", "reactive_power", "\"measurement\""},
{"L1 current", "/power", "I1", "A", "current", "\"measurement\""},
{"L2 current", "/power", "I2", "A", "current", "\"measurement\""},
{"L3 current", "/power", "I3", "A", "current", "\"measurement\""},
@@ -36,12 +36,12 @@ HomeAssistantSensor HA_SENSORS[HA_SENSOR_COUNT] PROGMEM = {
{"L3 voltage", "/power", "U3", "V", "voltage", "\"measurement\""},
{"Accumulated active import", "/energy", "tPI", "kWh", "energy", "\"total_increasing\""},
{"Accumulated active export", "/energy", "tPO", "kWh", "energy", "\"total_increasing\""},
{"Accumulated reactive import","/energy", "tQI", "kVArh","energy", "\"total_increasing\""},
{"Accumulated reactive export","/energy", "tQO", "kVArh","energy", "\"total_increasing\""},
{"Power factor", "/power", "PF", "", "power_factor", "\"measurement\""},
{"L1 power factor", "/power", "PF1", "", "power_factor", "\"measurement\""},
{"L2 power factor", "/power", "PF2", "", "power_factor", "\"measurement\""},
{"L3 power factor", "/power", "PF3", "", "power_factor", "\"measurement\""},
{"Accumulated reactive import","/energy", "tQI", "kvarh","energy", "\"total_increasing\""},
{"Accumulated reactive export","/energy", "tQO", "kvarh","energy", "\"total_increasing\""},
{"Power factor", "/power", "PF", "%", "power_factor", "\"measurement\""},
{"L1 power factor", "/power", "PF1", "%", "power_factor", "\"measurement\""},
{"L2 power factor", "/power", "PF2", "%", "power_factor", "\"measurement\""},
{"L3 power factor", "/power", "PF3", "%", "power_factor", "\"measurement\""},
{"Price current hour", "/prices", "prices['0']", "", "monetary", ""},
{"Price next hour", "/prices", "prices['1']", "", "monetary", ""},
{"Price in two hour", "/prices", "prices['2']", "", "monetary", ""},

View File

@@ -136,7 +136,7 @@ bool HomeAssistantMqttHandler::publishTemperatures(AmsConfiguration* config, HwT
bool HomeAssistantMqttHandler::publishPrices(EntsoeApi* eapi) {
if(topic.isEmpty() || !mqtt->connected())
return false;
if(strlen(eapi->getToken()) == 0)
if(eapi->getValueForHour(0) == ENTSOE_NO_VALUE)
return false;
time_t now = time(nullptr);
@@ -280,12 +280,19 @@ bool HomeAssistantMqttHandler::publishSystem(HwTools* hw, EntsoeApi* eapi, Energ
String uom = String(sensor.uom);
if(strncmp(sensor.devcl, "monetary", 8) == 0) {
if(eapi == NULL) continue;
uom = String(eapi->getCurrency());
if(strncmp(sensor.path, "prices", 5) == 0) {
uom = String(eapi->getCurrency()) + "/kWh";
} else {
uom = String(eapi->getCurrency());
}
}
if(strncmp(sensor.path, "peaks[", 6) == 0) {
if(peaks >= peakCount) continue;
peaks++;
}
if(strncmp(sensor.path, "temp", 4) == 0) {
if(hw->getTemperature() < 0) continue;
}
snprintf_P(json, BufferSize, HADISCOVER_JSON,
sensor.name,
topic.c_str(), sensor.topic,

View File

@@ -175,7 +175,7 @@ bool JsonMqttHandler::publishTemperatures(AmsConfiguration* config, HwTools* hw)
bool JsonMqttHandler::publishPrices(EntsoeApi* eapi) {
if(topic.isEmpty() || !mqtt->connected())
return false;
if(strlen(eapi->getToken()) == 0)
if(eapi->getValueForHour(0) == ENTSOE_NO_VALUE)
return false;
time_t now = time(nullptr);

View File

@@ -92,6 +92,7 @@ bool RawMqttHandler::publish(AmsData* data, AmsData* meterState, EnergyAccountin
}
mqtt->publish(topic + "/realtime/import/hour", String(ea->getUseThisHour(), 3));
mqtt->publish(topic + "/realtime/import/day", String(ea->getUseToday(), 2));
mqtt->publish(topic + "/realtime/import/month", String(ea->getUseThisMonth(), 1));
uint8_t peakCount = ea->getConfig()->hours;
if(peakCount > 5) peakCount = 5;
for(uint8_t i = 1; i <= peakCount; i++) {
@@ -101,6 +102,7 @@ bool RawMqttHandler::publish(AmsData* data, AmsData* meterState, EnergyAccountin
mqtt->publish(topic + "/realtime/import/monthmax", String(ea->getMonthMax(), 3), true, 0);
mqtt->publish(topic + "/realtime/export/hour", String(ea->getProducedThisHour(), 3));
mqtt->publish(topic + "/realtime/export/day", String(ea->getProducedToday(), 2));
mqtt->publish(topic + "/realtime/export/month", String(ea->getProducedThisMonth(), 1));
return true;
}
@@ -121,7 +123,7 @@ bool RawMqttHandler::publishTemperatures(AmsConfiguration* config, HwTools* hw)
bool RawMqttHandler::publishPrices(EntsoeApi* eapi) {
if(topic.isEmpty() || !mqtt->connected())
return false;
if(strcmp(eapi->getToken(), "") == 0)
if(eapi->getValueForHour(0) == ENTSOE_NO_VALUE)
return false;
time_t now = time(nullptr);

View File

@@ -3,6 +3,8 @@
<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">

View File

@@ -0,0 +1,19 @@
// 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"
}

View File

@@ -0,0 +1,11 @@
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

@@ -10,7 +10,7 @@
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;
@@ -33,7 +33,7 @@
<Router>
<Header data={data}/>
<Route path="/">
<Dashboard data={data}/>
<Dashboard data={data} sysinfo={sysinfo}/>
</Route>
<Route path="/configuration">
<ConfigurationPanel sysinfo={sysinfo}/>

View File

@@ -63,7 +63,7 @@
}
.pl-ov {
position: absolute;
top: 28%;
top: 27%;
left: 25%;
width: 50%;
text-align: center;
@@ -76,6 +76,7 @@
color: grey;
}
.pl-sub {
padding-top: 10px;
font-size: 1.0rem;
}
.pl-snt {

View File

@@ -0,0 +1,19 @@
<?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>

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@@ -18,7 +18,7 @@
if(u1 > 0) {
xTicks.push({ label: 'L1' });
points.push({
label: i1 ? i1 + 'A' : '-',
label: i1 ? (i1 > 10 ? i1.toFixed(0) : i1.toFixed(1)) + 'A' : '-',
value: i1 ? i1 : 0,
color: ampcol(i1 ? (i1)/(max)*100 : 0)
});
@@ -26,7 +26,7 @@
if(u2 > 0) {
xTicks.push({ label: 'L2' });
points.push({
label: i2 ? i2 + 'A' : '-',
label: i2 ? (i2 > 10 ? i2.toFixed(0) : i2.toFixed(1)) + 'A' : '-',
value: i2 ? i2 : 0,
color: ampcol(i2 ? (i2)/(max)*100 : 0)
});
@@ -34,7 +34,7 @@
if(u3 > 0) {
xTicks.push({ label: 'L3' });
points.push({
label: i3 ? i3 + 'A' : '-',
label: i3 ? (i3 > 10 ? i3.toFixed(0) : i3.toFixed(1)) + 'A' : '-',
value: i3 ? i3 : 0,
color: ampcol(i3 ? (i3)/(max)*100 : 0)
});

View File

@@ -7,11 +7,13 @@
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);
@@ -19,8 +21,13 @@
return (i*barWidth)+config.padding.left;
};
yScale = function(i) {
if(i > config.y.max) return heightAvailable;
let ret = heightAvailable-config.padding.bottom-((i-config.y.min)*yPerUnit);
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;
};
};
@@ -53,42 +60,45 @@
<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}"
/>
<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}"
/>
<text
y="{yScale(point.value) > yScale(0)-15 ? yScale(point.value) - 12 : 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)-15 ? point.color : 'white'}"
transform="rotate({barWidth < 25 ? 90 : 0}, {xScale(i) + (barWidth/2)}, {yScale(point.value) > yScale(0)-12 ? yScale(point.value) - 12 : yScale(point.value)+9})"
>{point.label}</text>
{#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}"
/>
<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>
<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>

View File

@@ -31,6 +31,13 @@
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
},
@@ -40,6 +47,9 @@
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 },
@@ -87,6 +97,7 @@
sysinfoStore.update(s => {
s.booting = res.reboot;
s.ui = configuration.u;
return s;
});
@@ -120,7 +131,7 @@
}
</script>
<form on:submit|preventDefault={handleSubmit}>
<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>
@@ -257,14 +268,14 @@
<div class="mx-1">
Main fuse<br/>
<label class="flex">
<input name="mf" bind:value={configuration.m.f} type="number" min="5" max="255" class="in-f tr w-full"/>
<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="255" class="in-f tr w-full"/>
<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>
@@ -289,7 +300,7 @@
{#if configuration.m.m.e}
<div class="flex my-1">
<div class="w-1/4">
Instant<br/>
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">
@@ -301,7 +312,7 @@
<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">
Acc.<br/>
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>
@@ -457,6 +468,31 @@
<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>
@@ -516,6 +552,100 @@
</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>

View File

@@ -34,7 +34,7 @@
<div class="grid xl:grid-cols-3 lg:grid-cols-2">
<div class="cnt">
<form on:submit|preventDefault={handleSubmit}>
<form on:submit|preventDefault={handleSubmit} autocomplete="off">
<div>
Various permissions we need to do stuff:
</div>

View File

@@ -1,6 +1,6 @@
<script>
import { pricesStore, dayPlotStore, monthPlotStore, temperaturesStore } from './DataStores.js';
import { metertype } from './Helpers.js';
import { metertype, uiVisibility } from './Helpers.js';
import PowerGauge from './PowerGauge.svelte';
import VoltPlot from './VoltPlot.svelte';
import AmpPlot from './AmpPlot.svelte';
@@ -13,6 +13,7 @@
import TariffPeakChart from './TariffPeakChart.svelte';
export let data = {}
export let sysinfo = {}
let prices = {}
let dayPlot = {}
let monthPlot = {}
@@ -32,6 +33,7 @@
</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">
@@ -41,7 +43,8 @@
<div class="text-right">{data.ic ? data.ic.toFixed(1) : '-'} kWh</div>
</div>
</div>
{#if data.om || data.e > 0}
{/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">
@@ -52,39 +55,47 @@
</div>
</div>
{/if}
{#if data.u1 > 100 || data.u2 > 100 || data.u3 > 100}
{#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 data.i1 > 0.01 || data.i2 > 0.01 || data.i3 > 0.01}
{#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 (typeof data.p == "number") && !Number.isNaN(data.p)}
{#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 data.t && data.t != -127 && temperatures.c > 1}
{/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>

View File

@@ -1,4 +1,5 @@
import { readable, writable } from 'svelte/store';
import { isBusPowered } from './Helpers';
async function fetchWithTimeout(resource, options = {}) {
const { timeout = 8000 } = options;
@@ -16,11 +17,14 @@ async function fetchWithTimeout(resource, options = {}) {
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);
@@ -43,18 +47,31 @@ export const dataStore = readable(data, (set) => {
set(data);
if(lastTemp != data.t) {
lastTemp = data.t;
getTemperatures();
setTimeout(getTemperatures, 2000);
}
if(lastPrice != data.p) {
lastPrice = data.p;
getPrices();
setTimeout(getPrices, 4000);
}
if(sysinfo.upgrading) {
window.location.reload();
} else if(sysinfo.booting) {
} 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);
}
timeout = setTimeout(getData, 5000);
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) => {
@@ -68,7 +85,7 @@ export const dataStore = readable(data, (set) => {
});
timeout = setTimeout(getData, 15000);
} else {
timeout = setTimeout(getData, 5000);
timeout = setTimeout(getData, isBusPowered(sysinfo.board) ? 10000 : 5000);
}
});
}
@@ -80,37 +97,49 @@ export const dataStore = readable(data, (set) => {
let prices = {};
export const pricesStore = writable(prices);
export async function getPrices(){
export async function getPrices() {
const response = await fetchWithTimeout("/energyprice.json");
prices = (await response.json())
pricesStore.set(prices);
}
let dayPlot = {};
export const dayPlotStore = readable(dayPlot, (set) => {
async function getDayPlot(){
const response = await fetchWithTimeout("/dayplot.json");
dayPlot = (await response.json())
set(dayPlot);
let date = new Date();
setTimeout(getDayPlot, (61-date.getMinutes())*60000)
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 = {};
export const monthPlotStore = readable(monthPlot, (set) => {
async function getmonthPlot(){
const response = await fetchWithTimeout("/monthplot.json");
monthPlot = (await response.json())
set(monthPlot);
let date = new Date();
setTimeout(getmonthPlot, (24-date.getHours())*3600000)
let monthPlotTimeout;
export async function getMonthPlot() {
if(monthPlotTimeout) {
clearTimeout(monthPlotTimeout);
monthPlotTimeout = 0;
}
getmonthPlot();
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() {}
});
@@ -127,12 +156,17 @@ export const temperaturesStore = writable(temperatures, (set) => {
});
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();
setTimeout(getTariff, (61-date.getMinutes())*60000)
tariffTimeout = setTimeout(getTariff, ((60-date.getMinutes())*60000)+30)
}
export const tariffStore = writable(tariff, (set) => {

View File

@@ -11,7 +11,7 @@
<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}>
<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>

View File

@@ -2,7 +2,8 @@
import { Link } from "svelte-navigator";
import { sysinfoStore, getGitHubReleases, gitHubReleaseStore } from './DataStores.js';
import { upgrade, getNextVersion } from './UpgradeHelper';
import { boardtype, hanError, mqttError, priceError } from './Helpers.js';
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';
@@ -19,7 +20,7 @@
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.')) {
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;

View File

@@ -31,7 +31,7 @@ export function metertype(mt) {
case 8:
return "Iskra";
case 9:
return "Landis";
return "Landis+Gyr";
case 10:
return "Sagemcom";
default:
@@ -143,4 +143,18 @@ export function priceError(err) {
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

@@ -76,8 +76,6 @@
});
}
console.log(yTicks);
config = {
title: "Future energy price (" + json.currency + ")",
padding: { top: 20, right: 15, bottom: 20, left: 35 },

View File

@@ -46,6 +46,7 @@
const data = new URLSearchParams();
for (let field of formData) {
const [key, value] = field;
data.append(key, value)
if(key == 'sh') hostname = value;
}
@@ -69,7 +70,7 @@
<div class="grid xl:grid-cols-4 lg:grid-cols-3 md:grid-cols-2">
<div class="cnt">
<form on:submit|preventDefault={handleSubmit}>
<form on:submit|preventDefault={handleSubmit} autocomplete="off">
<input type="hidden" name="s" value="true"/>
<strong class="text-sm">Setup</strong>
<div class="my-3">

View File

@@ -1,5 +1,5 @@
<script>
import { metertype, boardtype } from './Helpers.js';
import { metertype, boardtype, isBusPowered } from './Helpers.js';
import { getSysinfo, gitHubReleaseStore, sysinfoStore } from './DataStores.js';
import { upgrade, getNextVersion } from './UpgradeHelper';
import DownloadIcon from './DownloadIcon.svelte';
@@ -69,6 +69,11 @@
<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>
@@ -103,7 +108,7 @@
Gateway: {sysinfo.net.gw}
</div>
<div class="my-2">
DNS: {sysinfo.net.dns1} {#if sysinfo.net.dns2 != '0.0.0.0'}/ {sysinfo.net.dns2}{/if}
DNS: {sysinfo.net.dns1} {#if sysinfo.net.dns2 && sysinfo.net.dns2 != '0.0.0.0'}/ {sysinfo.net.dns2}{/if}
</div>
</div>
{/if}
@@ -128,14 +133,14 @@
</div>
{/if}
{/if}
{#if (sysinfo.security == 0 || data.a) && (sysinfo.board == 2 || sysinfo.board == 4 || sysinfo.board == 7) }
{#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}>
<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>
@@ -150,7 +155,7 @@
{#if sysinfo.security == 0 || data.a}
<div class="cnt">
<strong class="text-sm">Configuration</strong>
<form method="get" action="/configfile.cfg">
<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>
@@ -167,7 +172,7 @@
<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}>
<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>

View File

@@ -4,24 +4,26 @@
let hours = 0;
let minutes = 0;
$: {
days = Math.round(epoch/86400);
hours = Math.round(epoch/3600);
minutes = Math.round(epoch/60);
days = Math.floor(epoch/86400);
hours = Math.floor(epoch/3600);
minutes = Math.floor(epoch/60);
}
</script>
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 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

@@ -35,7 +35,7 @@
<div class="grid xl:grid-cols-4 lg:grid-cols-3 md:grid-cols-2">
<div class="cnt">
<form on:submit|preventDefault={handleSubmit}>
<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">

View File

@@ -17,7 +17,7 @@
if(u1 > 0) {
xTicks.push({ label: ds === 1 ? 'L1-L2' : 'L1' });
points.push({
label: u1 ? u1 + 'V' : '-',
label: u1 ? u1.toFixed(0) + 'V' : '-',
value: u1 ? u1 : 0,
color: voltcol(u1 ? (u1-min)/(max-min)*100 : 0)
});
@@ -25,7 +25,7 @@
if(u2 > 0) {
xTicks.push({ label: ds === 1 ? 'L1-L3' : 'L2' });
points.push({
label: u2 ? u2 + 'V' : '-',
label: u2 ? u2.toFixed(0) + 'V' : '-',
value: u2 ? u2 : 0,
color: voltcol(u2 ? (u2-min)/(max-min)*100 : 0)
});
@@ -33,7 +33,7 @@
if(u3 > 0) {
xTicks.push({ label: ds === 1 ? 'L2-L3' : 'L3' });
points.push({
label: u3 ? u3 + 'V' : '-',
label: u3 ? u3.toFixed(0) + 'V' : '-',
value: u3 ? u3 : 0,
color: voltcol(u3 ? (u3-min)/(max-min)*100 : 0)
});

View File

@@ -61,6 +61,11 @@ private:
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;
@@ -77,7 +82,7 @@ private:
void indexJs();
void indexCss();
void githubSvg();
void faviconIco();
void faviconSvg();
void sysinfoJson();
void dataJson();

View File

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

View File

@@ -25,4 +25,4 @@
},
"b": %.1f
}
}
},

View File

@@ -0,0 +1,13 @@
"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,6 +1,11 @@
<html>
<form action="/firmware" enctype="multipart/form-data" method="post">
<input name="file" type="file" accept=".bin">
<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

@@ -3,6 +3,7 @@
"chip": "%s",
"chipId": "%s",
"mac": "%s",
"apmac": "%s",
"board": %d,
"vndcfg": %s,
"usrcfg": %s,
@@ -22,5 +23,18 @@
"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

@@ -7,6 +7,7 @@
#include "html/index_css.h"
#include "html/index_js.h"
#include "html/github_svg.h"
#include "html/favicon_svg.h"
#include "html/data_json.h"
#include "html/dayplot_json.h"
#include "html/monthplot_json.h"
@@ -25,12 +26,15 @@
#include "html/conf_thresholds_json.h"
#include "html/conf_debug_json.h"
#include "html/conf_gpio_json.h"
#include "html/conf_domoticz_json.h"
#include "html/conf_ui_json.h"
#include "html/firmware_html.h"
#include "version.h"
#if defined(ESP32)
#include <esp_task_wdt.h>
#include <esp_wifi.h>
#endif
@@ -64,7 +68,7 @@ void AmsWebServer::setup(AmsConfiguration* config, GpioConfig* gpioConfig, Meter
server.on(F("/mqtt-key"), HTTP_GET, std::bind(&AmsWebServer::indexHtml, this));
server.on(F("/github.svg"), HTTP_GET, std::bind(&AmsWebServer::githubSvg, this));
server.on(F("/favicon.ico"), HTTP_GET, std::bind(&AmsWebServer::faviconIco, this));
server.on(F("/favicon.svg"), HTTP_GET, std::bind(&AmsWebServer::faviconSvg, this));
server.on(F("/sysinfo.json"), HTTP_GET, std::bind(&AmsWebServer::sysinfoJson, this));
server.on(F("/data.json"), HTTP_GET, std::bind(&AmsWebServer::dataJson, this));
server.on(F("/dayplot.json"), HTTP_GET, std::bind(&AmsWebServer::dayplotJson, this));
@@ -183,10 +187,11 @@ void AmsWebServer::githubSvg() {
server.send_P(200, "image/svg+xml", GITHUB_SVG);
}
void AmsWebServer::faviconIco() {
void AmsWebServer::faviconSvg() {
if(debugger->isActive(RemoteDebug::DEBUG)) debugger->printf("Serving /favicon.ico over http...\n");
notFound(); //TODO
server.sendHeader(HEADER_CACHE_CONTROL, CACHE_1HR);
server.send_P(200, "image/svg+xml", FAVICON_SVG);
}
void AmsWebServer::sysinfoJson() {
@@ -215,6 +220,26 @@ void AmsWebServer::sysinfoJson() {
IPAddress dns1 = WiFi.dnsIP(0);
IPAddress dns2 = WiFi.dnsIP(1);
char macStr[18] = { 0 };
char apMacStr[18] = { 0 };
uint8_t mac[6];
uint8_t apmac[6];
#if defined(ESP8266)
wifi_get_macaddr(STATION_IF, mac);
wifi_get_macaddr(SOFTAP_IF, apmac);
#elif defined(ESP32)
esp_wifi_get_mac((wifi_interface_t)ESP_IF_WIFI_STA, mac);
esp_wifi_get_mac((wifi_interface_t)ESP_IF_WIFI_AP, apmac);
#endif
sprintf(macStr, "%02X:%02X:%02X:%02X:%02X:%02X", mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]);
sprintf(apMacStr, "%02X:%02X:%02X:%02X:%02X:%02X", apmac[0], apmac[1], apmac[2], apmac[3], apmac[4], apmac[5]);
UiConfig ui;
config->getUiConfig(ui);
snprintf_P(buf, BufferSize, SYSINFO_JSON,
VERSION,
#if defined(CONFIG_IDF_TARGET_ESP32S2)
@@ -227,7 +252,8 @@ void AmsWebServer::sysinfoJson() {
"esp8266",
#endif
chipIdStr.c_str(),
WiFi.macAddress().c_str(),
macStr,
apMacStr,
sys.boardType,
sys.vendorConfigured ? "true" : "false",
sys.userConfigured ? "true" : "false",
@@ -248,6 +274,17 @@ void AmsWebServer::sysinfoJson() {
meterState->getMeterType(),
meterState->getMeterModel().c_str(),
meterState->getMeterId().c_str(),
ui.showImport,
ui.showExport,
ui.showVoltage,
ui.showAmperage,
ui.showReactive,
ui.showRealtime,
ui.showPeaks,
ui.showPricePlot,
ui.showDayPlot,
ui.showMonthPlot,
ui.showTemperaturePlot,
webConfig.security
);
@@ -727,6 +764,10 @@ void AmsWebServer::configurationJson() {
config->getEntsoeConfig(entsoe);
DebugConfig debugConfig;
config->getDebugConfig(debugConfig);
DomoticzConfig domo;
config->getDomoticzConfig(domo);
UiConfig ui;
config->getUiConfig(ui);
bool qsc = false;
bool qsr = false;
@@ -838,7 +879,7 @@ void AmsWebServer::configurationJson() {
snprintf_P(buf, BufferSize, CONF_GPIO_JSON,
gpioConfig->hanPin == 0xff ? "null" : String(gpioConfig->hanPin, 10).c_str(),
gpioConfig->apPin == 0xff ? "null" : String(gpioConfig->apPin, 10).c_str(),
gpioConfig->hanPin == 0xff ? "null" : String(gpioConfig->hanPin, 10).c_str(),
gpioConfig->ledPin == 0xff ? "null" : String(gpioConfig->ledPin, 10).c_str(),
gpioConfig->ledInverted ? "true" : "false",
gpioConfig->ledPinRed == 0xff ? "null" : String(gpioConfig->ledPinRed, 10).c_str(),
gpioConfig->ledPinGreen == 0xff ? "null" : String(gpioConfig->ledPinGreen, 10).c_str(),
@@ -854,6 +895,28 @@ void AmsWebServer::configurationJson() {
gpioConfig->vccBootLimit / 10.0
);
server.sendContent(buf);
snprintf_P(buf, BufferSize, CONF_UI_JSON,
ui.showImport,
ui.showExport,
ui.showVoltage,
ui.showAmperage,
ui.showReactive,
ui.showRealtime,
ui.showPeaks,
ui.showPricePlot,
ui.showDayPlot,
ui.showMonthPlot,
ui.showTemperaturePlot
);
server.sendContent(buf);
snprintf_P(buf, BufferSize, CONF_DOMOTICZ_JSON,
domo.elidx,
domo.cl1idx,
domo.vl1idx,
domo.vl2idx,
domo.vl3idx
);
server.sendContent(buf);
server.sendContent("}");
}
@@ -1141,14 +1204,14 @@ void AmsWebServer::handleSave() {
config->setMqttConfig(mqtt);
}
if(server.hasArg(F("dc")) && server.arg(F("dc")) == F("true")) {
if(server.hasArg(F("o")) && server.arg(F("o")) == F("true")) {
if(debugger->isActive(RemoteDebug::DEBUG)) debugger->printf(PSTR("Received Domoticz config"));
DomoticzConfig domo {
static_cast<uint16_t>(server.arg(F("elidx")).toInt()),
static_cast<uint16_t>(server.arg(F("vl1idx")).toInt()),
static_cast<uint16_t>(server.arg(F("vl2idx")).toInt()),
static_cast<uint16_t>(server.arg(F("vl3idx")).toInt()),
static_cast<uint16_t>(server.arg(F("cl1idx")).toInt())
static_cast<uint16_t>(server.arg(F("oe")).toInt()),
static_cast<uint16_t>(server.arg(F("ou1")).toInt()),
static_cast<uint16_t>(server.arg(F("ou2")).toInt()),
static_cast<uint16_t>(server.arg(F("ou3")).toInt()),
static_cast<uint16_t>(server.arg(F("oc")).toInt())
};
config->setDomoticzConfig(domo);
}
@@ -1240,6 +1303,23 @@ void AmsWebServer::handleSave() {
config->setDebugConfig(debug);
}
if(server.hasArg(F("u")) && server.arg(F("u")) == F("true")) {
UiConfig ui;
config->getUiConfig(ui);
ui.showImport = server.arg(F("ui")).toInt();
ui.showExport = server.arg(F("ue")).toInt();
ui.showVoltage = server.arg(F("uv")).toInt();
ui.showAmperage = server.arg(F("ua")).toInt();
ui.showReactive = server.arg(F("ur")).toInt();
ui.showRealtime = server.arg(F("uc")).toInt();
ui.showPeaks = server.arg(F("ut")).toInt();
ui.showPricePlot = server.arg(F("up")).toInt();
ui.showDayPlot = server.arg(F("ud")).toInt();
ui.showMonthPlot = server.arg(F("um")).toInt();
ui.showTemperaturePlot = server.arg(F("us")).toInt();
config->setUiConfig(ui);
}
if(server.hasArg(F("p")) && server.arg(F("p")) == F("true")) {
if(debugger->isActive(RemoteDebug::DEBUG)) debugger->printf(PSTR("Received price API config"));
@@ -1351,7 +1431,6 @@ void AmsWebServer::upgrade() {
server.handleClient();
delay(250);
String customFirmwareUrl = "";
if(server.hasArg(F("url"))) {
customFirmwareUrl = server.arg(F("url"));
}
@@ -1415,8 +1494,55 @@ void AmsWebServer::firmwarePost() {
if(debugger->isActive(RemoteDebug::DEBUG)) debugger->printf(PSTR("Handling firmware post..."));
if(!checkSecurity(1))
return;
if(rebootForUpgrade) {
server.send(200);
} else {
if(server.hasArg(F("url"))) {
String url = server.arg(F("url"));
if(!url.isEmpty() && (url.startsWith(F("http://")) || url.startsWith(F("https://")))) {
if(debugger->isActive(RemoteDebug::DEBUG)) debugger->printf(PSTR("Custom firmware URL was provided"));
server.send(200);
WiFiClient client;
#if defined(ESP8266)
String chipType = F("esp8266");
#elif defined(CONFIG_IDF_TARGET_ESP32S2)
String chipType = F("esp32s2");
#elif defined(ESP32)
#if defined(CONFIG_FREERTOS_UNICORE)
String chipType = F("esp32solo");
#else
String chipType = F("esp32");
#endif
#endif
#if defined(ESP8266)
ESPhttpUpdate.setFollowRedirects(HTTPC_STRICT_FOLLOW_REDIRECTS);
t_httpUpdate_return ret = ESPhttpUpdate.update(client, url, VERSION);
#elif defined(ESP32)
HTTPUpdate httpUpdate;
httpUpdate.setFollowRedirects(HTTPC_STRICT_FOLLOW_REDIRECTS);
HTTPUpdateResult ret = httpUpdate.update(client, url, String(VERSION) + "-" + chipType);
#endif
switch(ret) {
case HTTP_UPDATE_FAILED:
debugger->printf(PSTR("Update failed"));
break;
case HTTP_UPDATE_NO_UPDATES:
debugger->printf(PSTR("No Update"));
break;
case HTTP_UPDATE_OK:
debugger->printf(PSTR("Update OK"));
break;
}
server.send(200, MIME_PLAIN, "OK");
return;
}
}
server.sendHeader(HEADER_LOCATION,F("/firmware"));
server.send(303);
}
}
@@ -1425,8 +1551,12 @@ void AmsWebServer::firmwareUpload() {
return;
HTTPUpload& upload = server.upload();
if(upload.status == UPLOAD_FILE_START) {
if(upload.status == UPLOAD_FILE_START) {
String filename = upload.filename;
if(filename.isEmpty()) {
if(debugger->isActive(RemoteDebug::ERROR)) debugger->printf(PSTR("No file, falling back to post\n"));
return;
}
if(!filename.endsWith(".bin")) {
server.send(500, MIME_PLAIN, "500: couldn't create file");
} else {
@@ -1450,10 +1580,10 @@ HTTPUpload& AmsWebServer::uploadFile(const char* path) {
HTTPUpload& upload = server.upload();
if(upload.status == UPLOAD_FILE_START){
if(uploading) {
if(debugger->isActive(RemoteDebug::ERROR)) debugger->printf(PSTR("Upload already in progress"));
if(debugger->isActive(RemoteDebug::ERROR)) debugger->printf(PSTR("Upload already in progress\n"));
server.send_P(500, MIME_HTML, PSTR("<html><body><h1>Upload already in progress!</h1></body></html>"));
} else if (!LittleFS.begin()) {
if(debugger->isActive(RemoteDebug::ERROR)) debugger->printf(PSTR("An Error has occurred while mounting LittleFS"));
if(debugger->isActive(RemoteDebug::ERROR)) debugger->printf(PSTR("An Error has occurred while mounting LittleFS\n"));
server.send_P(500, MIME_HTML, PSTR("<html><body><h1>Unable to mount LittleFS!</h1></body></html>"));
} else {
uploading = true;
@@ -1491,7 +1621,7 @@ HTTPUpload& AmsWebServer::uploadFile(const char* path) {
if(debugger->isActive(RemoteDebug::ERROR)) debugger->printf(PSTR("An Error has occurred while writing file"));
snprintf_P(buf, BufferSize, RESPONSE_JSON,
"false",
"Unable to upload",
"File size does not match",
"false"
);
server.setContentLength(strlen(buf));
@@ -1499,14 +1629,18 @@ HTTPUpload& AmsWebServer::uploadFile(const char* path) {
}
}
} else if(upload.status == UPLOAD_FILE_END) {
if(debugger->isActive(RemoteDebug::DEBUG)) {
debugger->printf_P(PSTR("handleFileUpload Ended\n"));
}
if(file) {
file.flush();
file.close();
// LittleFS.end();
} else {
debugger->printf_P(PSTR("File was not valid in the end... Write error: %d, \n"), file.getWriteError());
snprintf_P(buf, BufferSize, RESPONSE_JSON,
"false",
"Unable to upload",
"Upload ended, but file is missing",
"false"
);
server.setContentLength(strlen(buf));
@@ -1830,10 +1964,11 @@ void AmsWebServer::configFileDownload() {
if(ds != NULL) {
DayDataPoints day = ds->getDayData();
server.sendContent(buf, snprintf_P(buf, BufferSize, PSTR("dayplot %d %lu %lu %d %d %d %d %d %d %d %d %d %d %d %d %d %d %d %d %d %d %d %d %d %d %d %d"),
server.sendContent(buf, snprintf_P(buf, BufferSize, PSTR("dayplot %d %lu %lu %d %d %d %d %d %d %d %d %d %d %d %d %d %d %d %d %d %d %d %d %d %d %d %d %d"),
day.version,
(int32_t) day.lastMeterReadTime,
day.activeImport,
day.accuracy,
ds->getHourImport(0),
ds->getHourImport(1),
ds->getHourImport(2),
@@ -1892,10 +2027,11 @@ void AmsWebServer::configFileDownload() {
}
MonthDataPoints month = ds->getMonthData();
server.sendContent(buf, snprintf_P(buf, BufferSize, PSTR("monthplot %d %lu %lu %d %d %d %d %d %d %d %d %d %d %d %d %d %d %d %d %d %d %d %d %d %d %d %d %d %d %d %d %d %d %d"),
server.sendContent(buf, snprintf_P(buf, BufferSize, PSTR("monthplot %d %lu %lu %d %d %d %d %d %d %d %d %d %d %d %d %d %d %d %d %d %d %d %d %d %d %d %d %d %d %d %d %d %d %d %d"),
month.version,
(int32_t) month.lastMeterReadTime,
month.activeImport,
month.accuracy,
ds->getDayImport(1),
ds->getDayImport(2),
ds->getDayImport(3),

View File

@@ -2,7 +2,7 @@
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, AmsMqttHandler, RawMqttHandler, JsonMqttHandler, DomoticzMqttHandler, HomeAssistantMqttHandler, SvelteUi
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_ignore = OneWire
extra_scripts =
pre:scripts/addversion.py
@@ -10,6 +10,11 @@ extra_scripts =
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}
@@ -19,8 +24,9 @@ platform = espressif8266@3.2.0
framework = arduino
board = esp12e
board_build.ldscript = eagle.flash.4m2m.ld
build_flags = -D WEBSOCKET_DISABLED=1 -fexceptions
build_flags = ${common.build_flags}
lib_ldf_mode = off
lib_compat_mode = off
lib_deps = ESP8266WiFi, ESP8266mDNS, ESP8266WebServer, ESP8266HTTPClient, ESP8266httpUpdate, ${common.lib_deps}
lib_ignore = ${common.lib_ignore}
extra_scripts = ${common.extra_scripts}
@@ -30,8 +36,9 @@ platform = https://github.com/tasmota/platform-espressif32/releases/download/v2.
framework = arduino
board = esp32dev
board_build.f_cpu = 160000000L
build_flags = -D WEBSOCKET_DISABLED=1 -fexceptions
build_flags = ${common.build_flags}
lib_ldf_mode = off
lib_compat_mode = off
lib_deps = ${esp32.lib_deps}
lib_ignore = ${common.lib_ignore}
extra_scripts = ${common.extra_scripts}
@@ -48,8 +55,9 @@ board_build.variant = esp32s2
board_build.flash_mode = qio
board_build.f_cpu = 160000000L
board_build.f_flash = 40000000L
build_flags = -D WEBSOCKET_DISABLED=1 -fexceptions
build_flags = ${common.build_flags}
lib_ldf_mode = off
lib_compat_mode = off
lib_deps = ${esp32.lib_deps}
lib_ignore = ${common.lib_ignore}
extra_scripts = ${common.extra_scripts}
@@ -59,8 +67,9 @@ platform = https://github.com/tasmota/platform-espressif32/releases/download/v2.
framework = arduino
board = esp32-solo1
board_build.f_cpu = 160000000L
build_flags = -D WEBSOCKET_DISABLED=1 -DFRAMEWORK_ARDUINO_SOLO1 -fexceptions
build_flags = ${common.build_flags} -DFRAMEWORK_ARDUINO_SOLO1
lib_ldf_mode = off
lib_compat_mode = off
lib_deps = ${esp32.lib_deps}
lib_ignore = ${common.lib_ignore}
extra_scripts = ${common.extra_scripts}

View File

@@ -1124,7 +1124,6 @@ void WiFi_connect() {
#endif
MDNS.end();
WiFi.persistent(false);
WiFi.disconnect(true);
WiFi.softAPdisconnect(true);
WiFi.enableAP(false);
@@ -1175,8 +1174,11 @@ void WiFi_connect() {
WiFi.hostname(wifi.hostname);
}
#endif
#if defined(ESP32)
WiFi.setScanMethod(WIFI_ALL_CHANNEL_SCAN);
WiFi.setSortMethod(WIFI_CONNECT_AP_BY_SIGNAL);
#endif
WiFi.setAutoReconnect(true);
WiFi.persistent(true);
if(WiFi.begin(wifi.ssid, wifi.psk)) {
if(wifi.sleep <= 2) {
switch(wifi.sleep) {
@@ -1463,6 +1465,7 @@ void MQTT_connect() {
#if defined(ESP8266)
if(mqttSecureClient) {
time_t epoch = time(nullptr);
debugD("Setting NTP time %lu for secure MQTT connection", epoch);
mqttSecureClient->setX509Time(epoch);
}
@@ -1535,54 +1538,62 @@ void configFileParse() {
char* buf = (char*) commonBuffer;
memset(buf, 0, 1024);
while((size = file.readBytesUntil('\n', buf, 1024)) > 0) {
for(uint16_t i = 0; i < size; i++) {
if(buf[i] < 32 || buf[i] > 126) {
memset(buf+i, 0, size-i);
debugD("Found non-ascii, shortening line from %d to %d", size, i);
size = i;
break;
}
}
if(strncmp_P(buf, PSTR("boardType "), 10) == 0) {
if(!lSys) { config.getSystemConfig(sys); lSys = true; };
sys.boardType = String(buf+10).toInt();
} else if(strncmp_P(buf, PSTR("ssid "), 5) == 0) {
if(!lWiFi) { config.getWiFiConfig(wifi); lWiFi = true; };
memcpy(wifi.ssid, buf+5, size-5);
strcpy(wifi.ssid, buf+5);
} else if(strncmp_P(buf, PSTR("psk "), 4) == 0) {
if(!lWiFi) { config.getWiFiConfig(wifi); lWiFi = true; };
memcpy(wifi.psk, buf+4, size-4);
strcpy(wifi.psk, buf+4);
} else if(strncmp_P(buf, PSTR("ip "), 3) == 0) {
if(!lWiFi) { config.getWiFiConfig(wifi); lWiFi = true; };
memcpy(wifi.ip, buf+3, size-3);
strcpy(wifi.ip, buf+3);
} else if(strncmp_P(buf, PSTR("gateway "), 8) == 0) {
if(!lWiFi) { config.getWiFiConfig(wifi); lWiFi = true; };
memcpy(wifi.gateway, buf+8, size-8);
strcpy(wifi.gateway, buf+8);
} else if(strncmp_P(buf, PSTR("subnet "), 7) == 0) {
if(!lWiFi) { config.getWiFiConfig(wifi); lWiFi = true; };
memcpy(wifi.subnet, buf+7, size-7);
strcpy(wifi.subnet, buf+7);
} else if(strncmp_P(buf, PSTR("dns1 "), 5) == 0) {
if(!lWiFi) { config.getWiFiConfig(wifi); lWiFi = true; };
memcpy(wifi.dns1, buf+5, size-5);
strcpy(wifi.dns1, buf+5);
} else if(strncmp_P(buf, PSTR("dns2 "), 5) == 0) {
if(!lWiFi) { config.getWiFiConfig(wifi); lWiFi = true; };
memcpy(wifi.dns2, buf+5, size-5);
strcpy(wifi.dns2, buf+5);
} else if(strncmp_P(buf, PSTR("hostname "), 9) == 0) {
if(!lWiFi) { config.getWiFiConfig(wifi); lWiFi = true; };
memcpy(wifi.hostname, buf+9, size-9);
strcpy(wifi.hostname, buf+9);
} else if(strncmp_P(buf, PSTR("mdns "), 5) == 0) {
if(!lWiFi) { config.getWiFiConfig(wifi); lWiFi = true; };
wifi.mdns = String(buf+5).toInt() == 1;;
} else if(strncmp_P(buf, PSTR("mqttHost "), 9) == 0) {
if(!lMqtt) { config.getMqttConfig(mqtt); lMqtt = true; };
memcpy(mqtt.host, buf+9, size-9);
strcpy(mqtt.host, buf+9);
} else if(strncmp_P(buf, PSTR("mqttPort "), 9) == 0) {
if(!lMqtt) { config.getMqttConfig(mqtt); lMqtt = true; };
mqtt.port = String(buf+9).toInt();
} else if(strncmp_P(buf, PSTR("mqttClientId "), 13) == 0) {
if(!lMqtt) { config.getMqttConfig(mqtt); lMqtt = true; };
memcpy(mqtt.clientId, buf+13, size-13);
strcpy(mqtt.clientId, buf+13);
} else if(strncmp_P(buf, PSTR("mqttPublishTopic "), 17) == 0) {
if(!lMqtt) { config.getMqttConfig(mqtt); lMqtt = true; };
memcpy(mqtt.publishTopic, buf+17, size-17);
strcpy(mqtt.publishTopic, buf+17);
} else if(strncmp_P(buf, PSTR("mqttUsername "), 13) == 0) {
if(!lMqtt) { config.getMqttConfig(mqtt); lMqtt = true; };
memcpy(mqtt.username, buf+13, size-13);
strcpy(mqtt.username, buf+13);
} else if(strncmp_P(buf, PSTR("mqttPassword "), 13) == 0) {
if(!lMqtt) { config.getMqttConfig(mqtt); lMqtt = true; };
memcpy(mqtt.password, buf+13, size-13);
strcpy(mqtt.password, buf+13);
} else if(strncmp_P(buf, PSTR("mqttPayloadFormat "), 18) == 0) {
if(!lMqtt) { config.getMqttConfig(mqtt); lMqtt = true; };
mqtt.payloadFormat = String(buf+18).toInt();
@@ -1594,10 +1605,10 @@ void configFileParse() {
web.security = String(buf+12).toInt();
} else if(strncmp_P(buf, PSTR("webUsername "), 12) == 0) {
if(!lWeb) { config.getWebConfig(web); lWeb = true; };
memcpy(web.username, buf+12, size-12);
strcpy(web.username, buf+12);
} else if(strncmp_P(buf, PSTR("webPassword "), 12) == 0) {
if(!lWeb) { config.getWebConfig(web); lWeb = true; };
memcpy(web.username, buf+12, size-12);
strcpy(web.username, buf+12);
} else if(strncmp_P(buf, PSTR("meterBaud "), 10) == 0) {
if(!lMeter) { config.getMeterConfig(meter); lMeter = true; };
meter.baud = String(buf+10).toInt();
@@ -1696,19 +1707,19 @@ void configFileParse() {
ntp.dhcp = String(buf+8).toInt() == 1;
} else if(strncmp_P(buf, PSTR("ntpServer "), 10) == 0) {
if(!lNtp) { config.getNtpConfig(ntp); lNtp = true; };
memcpy(ntp.server, buf+10, size-10);
strcpy(ntp.server, buf+10);
} else if(strncmp_P(buf, PSTR("ntpTimezone "), 12) == 0) {
if(!lNtp) { config.getNtpConfig(ntp); lNtp = true; };
memcpy(ntp.timezone, buf+12, size-12);
strcpy(ntp.timezone, buf+12);
} else if(strncmp_P(buf, PSTR("entsoeToken "), 12) == 0) {
if(!lEntsoe) { config.getEntsoeConfig(entsoe); lEntsoe = true; };
memcpy(entsoe.token, buf+12, size-12);
strcpy(entsoe.token, buf+12);
} else if(strncmp_P(buf, PSTR("entsoeArea "), 11) == 0) {
if(!lEntsoe) { config.getEntsoeConfig(entsoe); lEntsoe = true; };
memcpy(entsoe.area, buf+11, size-11);
strcpy(entsoe.area, buf+11);
} else if(strncmp_P(buf, PSTR("entsoeCurrency "), 15) == 0) {
if(!lEntsoe) { config.getEntsoeConfig(entsoe); lEntsoe = true; };
memcpy(entsoe.currency, buf+15, size-15);
strcpy(entsoe.currency, buf+15);
} else if(strncmp_P(buf, PSTR("entsoeMultiplier "), 17) == 0) {
if(!lEntsoe) { config.getEntsoeConfig(entsoe); lEntsoe = true; };
entsoe.multiplier = String(buf+17).toDouble() * 1000;
@@ -1723,20 +1734,38 @@ void configFileParse() {
eac.hours = String(pch).toInt();
} else if(strncmp_P(buf, PSTR("dayplot "), 8) == 0) {
int i = 0;
DayDataPoints day = { 4 }; // Use a version we know the multiplier of the data points
DayDataPoints day = { 0 };
char * pch = strtok (buf+8," ");
while (pch != NULL) {
int64_t val = String(pch).toInt();
if(i == 1) {
day.lastMeterReadTime = val;
} else if(i == 2) {
day.activeImport = val;
} else if(i > 2 && i < 27) {
day.hImport[i-3] = val / 10;
} else if(i == 27) {
day.activeExport = val;
} else if(i > 27 && i < 52) {
day.hExport[i-28] = val / 10;
if(day.version < 5) {
if(i == 0) {
day.version = val;
} else if(i == 1) {
day.lastMeterReadTime = val;
} else if(i == 2) {
day.activeImport = val;
} else if(i > 2 && i < 27) {
day.hImport[i-3] = val / 10;
} else if(i == 27) {
day.activeExport = val;
} else if(i > 27 && i < 52) {
day.hExport[i-28] = val / 10;
}
} else {
if(i == 1) {
day.lastMeterReadTime = val;
} else if(i == 2) {
day.activeImport = val;
} else if(i == 3) {
day.accuracy = val;
} else if(i > 3 && i < 28) {
day.hImport[i-4] = val / pow(10, day.accuracy);
} else if(i == 28) {
day.activeExport = val;
} else if(i > 28 && i < 53) {
day.hExport[i-29] = val / pow(10, day.accuracy);
}
}
pch = strtok (NULL, " ");
@@ -1746,22 +1775,39 @@ void configFileParse() {
sDs = true;
} else if(strncmp_P(buf, PSTR("monthplot "), 10) == 0) {
int i = 0;
MonthDataPoints month = { 5 }; // Use a version we know the multiplier of the data points
MonthDataPoints month = { 0 };
char * pch = strtok (buf+10," ");
while (pch != NULL) {
int64_t val = String(pch).toInt();
if(i == 1) {
month.lastMeterReadTime = val;
} else if(i == 2) {
month.activeImport = val;
} else if(i > 2 && i < 34) {
month.dImport[i-3] = val / 10;
} else if(i == 34) {
month.activeExport = val;
} else if(i > 34 && i < 66) {
month.dExport[i-35] = val / 10;
if(month.version < 6) {
if(i == 0) {
month.version = val;
} else if(i == 1) {
month.lastMeterReadTime = val;
} else if(i == 2) {
month.activeImport = val;
} else if(i > 2 && i < 34) {
month.dImport[i-3] = val / 10;
} else if(i == 34) {
month.activeExport = val;
} else if(i > 34 && i < 66) {
month.dExport[i-35] = val / 10;
}
} else {
if(i == 1) {
month.lastMeterReadTime = val;
} else if(i == 2) {
month.activeImport = val;
} else if(i == 3) {
month.accuracy = val;
} else if(i > 3 && i < 35) {
month.dImport[i-4] = val / pow(10, month.accuracy);
} else if(i == 35) {
month.activeExport = val;
} else if(i > 35 && i < 67) {
month.dExport[i-36] = val / pow(10, month.accuracy);
}
}
pch = strtok (NULL, " ");
i++;
}

View File

@@ -1,16 +1,10 @@
#include "IEC6205621.h"
#include "crc.h"
IEC6205621::IEC6205621(const char* p) {
if(strlen(p) < 16)
return;
String payload(p+1);
int crc_pos = payload.lastIndexOf("!");
String crc = payload.substring(crc_pos+1, crc_pos+5);
//uint16_t crc_calc = crc16_x25((uint8_t*) (payload.startsWith("/") ? p+1 : p), crc_pos);
//Serial.printf("CRC %s :: %04X\n", crc.c_str(), crc_calc);
lastUpdateMillis = millis();
listId = payload.substring(payload.startsWith("/") ? 1 : 0, payload.indexOf("\n"));
@@ -28,11 +22,14 @@ IEC6205621::IEC6205621(const char* p) {
meterType = AmsTypeIskra;
listId = listId.substring(0,5);
} else if(listId.startsWith("XMX")) {
meterType = AmsTypeLandis;
meterType = AmsTypeLandisGyr;
listId = listId.substring(0,6);
} else if(listId.startsWith("Ene") || listId.startsWith("EST")) {
meterType = AmsTypeSagemcom;
listId = listId.substring(0,4);
} else if(listId.startsWith("LGF")) {
meterType = AmsTypeLandisGyr;
listId = listId.substring(0,4);
} else {
meterType = AmsTypeUnknown;
listId = listId.substring(0,4);
@@ -79,6 +76,14 @@ IEC6205621::IEC6205621(const char* p) {
l1current = extractDouble(payload, "31.7.0");
l2current = extractDouble(payload, "51.7.0");
l3current = extractDouble(payload, "71.7.0");
l1activeImportPower = extractDouble(payload, "21.7.0");
l2activeImportPower = extractDouble(payload, "41.7.0");
l3activeImportPower = extractDouble(payload, "61.7.0");
l1activeExportPower = extractDouble(payload, "22.7.0");
l2activeExportPower = extractDouble(payload, "42.7.0");
l3activeExportPower = extractDouble(payload, "62.7.0");
if(l1voltage > 0 || l2voltage > 0 || l3voltage > 0)
listType = 2;
@@ -128,6 +133,9 @@ IEC6205621::IEC6205621(const char* p) {
l2current = (((activeImportPower - activeExportPower) * sqrt(3)) - (l1voltage * l1current) - (l3voltage * l3current)) / l2voltage;
}
}
if (l1activeImportPower > 0 || l2activeImportPower > 0 || l3activeImportPower > 0 || l1activeExportPower > 0 || l2activeExportPower > 0 || l3activeExportPower > 0)
listType = 4;
}
String IEC6205621::extract(String payload, String obis) {

View File

@@ -5,7 +5,7 @@
LNG::LNG(const char* payload, uint8_t useMeterType, MeterConfig* meterConfig, DataParserContext &ctx, RemoteDebug* debugger) {
LngHeader* h = (LngHeader*) payload;
if(h->tag == CosemTypeStructure && h->arrayTag == CosemTypeArray) {
meterType = AmsTypeLng;
meterType = AmsTypeLandisGyr;
this->packageTimestamp = ctx.timestamp;
uint8_t* ptr = (uint8_t*) &h[1];
@@ -26,6 +26,7 @@ LNG::LNG(const char* payload, uint8_t useMeterType, MeterConfig* meterConfig, Da
if(descriptor->obis[3] == 7) {
if(descriptor->obis[4] == 0) {
o170 = getNumber(item);
listType = listType >= 1 ? listType : 1;
if(debugger->isActive(RemoteDebug::VERBOSE)) debugger->printf(" and value %lu", o170);
}
} else if(descriptor->obis[3] == 8) {
@@ -36,9 +37,11 @@ LNG::LNG(const char* payload, uint8_t useMeterType, MeterConfig* meterConfig, Da
activeImportCounter = o180 / 1000.0;
} else if(descriptor->obis[4] == 1) {
o181 = getNumber(item);
listType = listType >= 3 ? listType : 3;
if(debugger->isActive(RemoteDebug::VERBOSE)) debugger->printf(" and value %lu", o181);
} else if(descriptor->obis[4] == 2) {
o182 = getNumber(item);
listType = listType >= 3 ? listType : 3;
if(debugger->isActive(RemoteDebug::VERBOSE)) debugger->printf(" and value %lu", o182);
}
}
@@ -46,6 +49,7 @@ LNG::LNG(const char* payload, uint8_t useMeterType, MeterConfig* meterConfig, Da
if(descriptor->obis[3] == 7) {
if(descriptor->obis[4] == 0) {
o270 = getNumber(item);
listType = listType >= 2 ? listType : 2;
if(debugger->isActive(RemoteDebug::VERBOSE)) debugger->printf(" and value %lu", o270);
}
} else if(descriptor->obis[3] == 8) {
@@ -56,9 +60,11 @@ LNG::LNG(const char* payload, uint8_t useMeterType, MeterConfig* meterConfig, Da
activeExportCounter = o280 / 1000.0;
} else if(descriptor->obis[4] == 1) {
o281 = getNumber(item);
listType = listType >= 3 ? listType : 3;
if(debugger->isActive(RemoteDebug::VERBOSE)) debugger->printf(" and value %lu", o281);
} else if(descriptor->obis[4] == 2) {
o282 = getNumber(item);
listType = listType >= 3 ? listType : 3;
if(debugger->isActive(RemoteDebug::VERBOSE)) debugger->printf(" and value %lu", o282);
}
}
@@ -69,12 +75,14 @@ LNG::LNG(const char* payload, uint8_t useMeterType, MeterConfig* meterConfig, Da
memcpy(str, item->oct.data, item->oct.length);
str[item->oct.length] = '\0';
meterId = String(str);
listType = listType >= 2 ? listType : 2;
if(debugger->isActive(RemoteDebug::VERBOSE)) debugger->printf(" and value %s (oct)", str);
} else if(descriptor->obis[4] == 1) {
char str[item->oct.length+1];
memcpy(str, item->oct.data, item->oct.length);
str[item->oct.length] = '\0';
meterModel = String(str);
listType = listType >= 2 ? listType : 2;
if(debugger->isActive(RemoteDebug::VERBOSE)) debugger->printf(" and value %s (oct)", str);
}
}
@@ -85,21 +93,18 @@ LNG::LNG(const char* payload, uint8_t useMeterType, MeterConfig* meterConfig, Da
if(o170 > 0 || o270 > 0) {
int32_t sum = o170-o270;
if(sum > 0) {
listType = listType >= 1 ? listType : 1;
activeImportPower = sum;
} else {
listType = listType >= 2 ? listType : 2;
activeExportPower = sum * -1;
listType = listType >= 2 ? listType : 2;
}
}
if(o181 > 0 || o182 > 0) {
activeImportCounter = (o181 + o182) / 1000.0;
listType = listType >= 3 ? listType : 3;
}
if(o281 > 0 || o282 > 0) {
activeExportCounter = (o281 + o282) / 1000.0;
listType = listType >= 3 ? listType : 3;
}
if((*data) == 0x09) {