Compare commits

..

94 Commits

Author SHA1 Message Date
Gunnar Skjold
39e86fa180 Show only half the labels on small screens 2023-04-21 18:55:15 +02:00
Gunnar Skjold
5d278a9d5a Splitting loop into smaller methods 2023-04-19 15:12:49 +02:00
Gunnar Skjold
61040e3e7c Crossing fingers 2023-04-18 22:06:29 +02:00
Gunnar Skjold
c4005f10a3 Some debugging changes and other optimizations 2023-04-18 11:50:44 +02:00
Gunnar Skjold
7bed5add5d Fix 2023-04-17 21:44:36 +02:00
Gunnar Skjold
18e382f21f Fix 2023-04-17 21:41:20 +02:00
Gunnar Skjold
94865b632e Some more optimization 2023-04-17 21:33:00 +02:00
Gunnar Skjold
f0461a7cdb Some adjustments 2023-04-17 11:49:22 +02:00
Gunnar Skjold
4cd6013d64 More improvements on buffer 2023-04-15 20:51:35 +02:00
Gunnar Skjold
b06dbb8d79 Buffer improvements and fixed special characters in ssid 2023-04-15 20:38:26 +02:00
Gunnar Skjold
02fe2073c2 Better error handling for price api 2023-04-15 09:30:19 +02:00
Gunnar Skjold
ec7ceafa84 General optimizations 2023-04-15 09:14:25 +02:00
Gunnar Skjold
276ac67d2e Only clear HA config if illegal characters are loaded from flash 2023-04-15 07:47:48 +02:00
Gunnar Skjold
be116d5b35 Improved firmware upgrade 2023-04-15 07:44:33 +02:00
Gunnar Skjold
9c8788225d Clear HA or domoticz config if not in use 2023-04-15 06:06:26 +02:00
Gunnar Skjold
df44b05792 Clear HA conf by default 2023-04-13 10:20:15 +02:00
Gunnar Skjold
8ee8eee6c4 Fixed laout error on realtime data 2023-04-10 14:14:20 +02:00
Gunnar Skjold
28e13b73e6 Fixed missing HA sensors 2023-04-10 14:08:41 +02:00
Gunnar Skjold
088e5645c7 Increased RX serial buff 2023-04-07 09:00:53 +02:00
Gunnar Skjold
3e3d61912d Serial debugging 2023-04-07 08:39:36 +02:00
Gunnar Skjold
b7854baa6d Some HAN serial port debugging 2023-04-07 08:36:42 +02:00
Gunnar Skjold
68bbfd6527 Load data before restoring from config file 2023-04-05 07:54:59 +02:00
Gunnar Skjold
c1ca3d0c65 Fixed month number in clock 2023-04-03 07:07:44 +02:00
Gunnar Skjold
71cac46470 Config restore loads username into password field 2023-04-03 07:06:18 +02:00
Gunnar Skjold
700f023292 Possibility to specify fixed energy price 2023-04-01 10:57:21 +02:00
Gunnar Skjold
ea43b2b632 Some changes after testing new HA features 2023-04-01 10:43:55 +02:00
Gunnar Skjold
c456e3fdf1 Fixed config loading for ha 2023-04-01 08:20:41 +02:00
Gunnar Skjold
4947f0ec7f Clear HA config in next config version 2023-04-01 08:02:12 +02:00
Gunnar Skjold
799c2f19d9 Changes after uahn merge 2023-04-01 08:00:15 +02:00
Gunnar Skjold
18dc188835 Merge pull request #481 from dbeinder/mosquito
Add HAN pullup config and mosquito board
2023-04-01 07:43:34 +02:00
Gunnar Skjold
7db745829e Merge branch 'master' into mosquito 2023-04-01 07:43:24 +02:00
Gunnar Skjold
81fedaaa6a Moved from postfix to prefix on HA sensor name tag 2023-04-01 07:38:24 +02:00
Gunnar Skjold
71e0f13f0e Various changes after testing new HA features 2023-04-01 07:31:40 +02:00
Gunnar Skjold
f214af3595 Added price API rate limit error 2023-03-31 13:57:38 +02:00
Gunnar Skjold
fb9ae2f5f6 Updated boot reason descriptions 2023-03-31 13:52:22 +02:00
Gunnar Skjold
4c73f39214 Fixed state class on sensors 2023-03-31 13:49:02 +02:00
Gunnar Skjold
1e7176af0b HA configuration 2023-03-30 14:54:10 +02:00
Gunnar Skjold
0d8c88b1fc Fixed DST 25hr day error on month plot 2023-03-30 12:25:07 +02:00
Gunnar Skjold
d189d904fe Fixed edge case where graph data sometimes goes crazy 2023-03-30 11:55:39 +02:00
Gunnar Skjold
3c86e824a2 Added boot time to status page 2023-03-30 09:44:41 +02:00
Gunnar Skjold
061f6433d6 Added another proprietary L&G 2023-03-30 09:00:12 +02:00
Gunnar Skjold
0d923e30d6 Added another proprietary L&G 2023-03-30 08:59:56 +02:00
Gunnar Skjold
51f761d25e Fixed incorrect change of HA current hour/day/month kwh state class 2023-03-29 08:53:13 +02:00
Gunnar Skjold
58fec70e7c Added detection for Kaifa through system title 2023-03-27 09:20:16 +02:00
Gunnar Skjold
dff55b4eee Remove copy/paste l2current substitution from DSMR parser 2023-03-27 08:46:41 +02:00
Gunnar Skjold
931f0d400b Only publish prices once when fetching today and tomorrow at the same time 2023-03-26 15:24:55 +02:00
Gunnar Skjold
0178dc4184 Various fixes for HA 2023-03-26 15:12:24 +02:00
Gunnar Skjold
be522b40f9 Fixed swapped voltage ranges, esp32 vs esp8266 2023-03-26 10:52:01 +02:00
Gunnar Skjold
068c55e7bb Fixed disabling encryption 2023-03-26 10:49:00 +02:00
Gunnar Skjold
8e88d7e11b Show boot reason on status page 2023-03-26 10:40:02 +02:00
Gunnar Skjold
e36acef1d4 Fixed DST offset in day graph 2023-03-26 10:14:10 +02:00
Gunnar Skjold
07d5481a72 Actual fix for DST change 2023-03-26 10:06:37 +02:00
Gunnar Skjold
dbd6205cca Fixed issue with price graph in relation to entering or leaving DST 2023-03-25 19:14:21 +01:00
david-beinder
35d47902c6 Add HAN pullup config and mosquito board 2023-03-18 23:39:55 +01:00
Gunnar Skjold
938f9f69d1 Fixed memory leak in baud autodetect 2023-03-11 10:17:44 +01:00
Gunnar Skjold
cd27472e2d Fixed checkbox tick and changed backup/restore label 2023-03-11 09:19:09 +01:00
Gunnar Skjold
64f8414217 Fixed precision on domoticz energy counter 2023-03-11 08:53:51 +01:00
Gunnar Skjold
a2c1250724 Some updates for esp32c3 2023-02-27 09:00:07 +01:00
Gunnar Skjold
4b7160b502 Only report realtime data on MQTT if EnergyAccounting is initialized 2023-02-27 08:30:56 +01:00
Gunnar Skjold
cc50457404 Fixed spikes in realtime production on top of hour 2023-02-27 08:04:29 +01:00
Gunnar Skjold
cd48192a74 Removed classic UI 2023-02-22 18:09:16 +01:00
Gunnar Skjold
cd031c33aa Added forced 2400 baud for Pow-K 2023-02-22 07:12:54 +01:00
Gunnar Skjold
6c59b15681 Added esp32c3 release build 2023-02-22 07:07:51 +01:00
Gunnar Skjold
7c8593122b Removed forced baud rate 2023-02-22 07:07:31 +01:00
Gunnar Skjold
6d8fd4e083 Merge pull request #460 from dbeinder/c3-generic-boards
Add generic boards for ESP32-C3
2023-02-22 06:56:09 +01:00
david-beinder
79d674710f Add generic boards for ESP32-C3 2023-02-21 23:25:14 +01:00
Gunnar Skjold
062068eacd Increased range of tariff thresholds 2023-02-21 15:29:10 +01:00
Gunnar Skjold
98309ea532 Allow extended ascii in ssid 2023-02-21 13:55:38 +01:00
Gunnar Skjold
0f75fa4a58 Fixed redirect issue after initial setup with DHCP on ESP8266 2023-02-21 13:40:55 +01:00
Gunnar Skjold
d08f75d9c3 Fixed error changing from static ip to dhcp 2023-02-21 12:44:43 +01:00
Gunnar Skjold
dd4a43c831 Better feedback from entso-e 2023-02-21 07:54:11 +01:00
Gunnar Skjold
1f5a04e606 Increased range for instant values 2023-02-14 20:23:23 +01:00
Gunnar Skjold
4d6e63a171 Fixed GPIO HAN select for ESP8266 2023-02-12 20:48:38 +01:00
Gunnar Skjold
0093410e05 Removed kicad backup 2023-02-10 18:57:50 +01:00
Gunnar Skjold
ee63a606e8 Fixed voltage read issue 2023-02-10 18:55:55 +01:00
Gunnar Skjold
d0ccd2d007 Minor ui adjustment 2023-02-10 17:28:19 +01:00
Gunnar Skjold
bb2f74d1ca Various UI improvements 2023-02-06 18:20:48 +01:00
Gunnar Skjold
dfef18fa09 Updated Entsoe endpoint 2023-02-06 11:32:53 +01:00
Gunnar Skjold
485c21dc69 Removed unnecessary build flag 2023-02-06 10:59:40 +01:00
Gunnar Skjold
9ceb84bc9c C3 build 2023-02-06 07:46:34 +01:00
Gunnar Skjold
24e68428c4 Commited gui dist files 2023-02-05 19:26:48 +01:00
Gunnar Skjold
c2c5855e6a Fixed issue where authentication key got corrupted from configfile upload 2023-02-02 16:15:03 +01:00
Gunnar Skjold
de19de2129 Fixed incorrect change in previous commit 2023-02-01 19:00:50 +01:00
Gunnar Skjold
1719263de0 Fixed config migration issue from v2.1 2023-02-01 18:46:22 +01:00
Gunnar Skjold
e70b872c98 Removed v2.0 config loading 2023-02-01 18:46:06 +01:00
Gunnar Skjold
b7d28238ab Extended cache for js and css 2023-01-31 17:16:18 +01:00
Gunnar Skjold
6c9a8b0692 Label fix 2023-01-31 17:04:46 +01:00
Gunnar Skjold
9f3dba3aab Fixed reconnect after setup 2023-01-30 20:58:08 +01:00
Gunnar Skjold
e4e4ad4107 Fixed autodetect 2023-01-30 18:32:17 +01:00
Gunnar Skjold
e8fb9570bb Chasing a reboot bug... 2023-01-30 16:52:12 +01:00
Gunnar Skjold
bc42099962 Fixing some bug reports 2023-01-30 16:07:08 +01:00
Gunnar Skjold
be71cbe609 Some changes after bug reports 2023-01-30 12:02:23 +01:00
Gunnar Skjold
0d6df03c94 Fixed reboot loop, changed temperature sensor behaviour and changed some debugging 2023-01-29 22:45:35 +01:00
Gunnar Skjold
d777040c0a Fixed loading error from previous version energy accounting 2023-01-29 17:04:04 +01:00
125 changed files with 3698 additions and 8248 deletions

View File

@@ -166,3 +166,26 @@ jobs:
asset_path: esp32solo.zip
asset_name: ams2mqtt-esp32solo-${{ steps.release_tag.outputs.tag }}.zip
asset_content_type: application/zip
- name: Build esp32c3 firmware
run: pio run -e esp32c3
- name: Create esp32c3 zip file
run: /bin/sh scripts/esp32c3/mkzip.sh
- name: Upload esp32c3 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/esp32c3/firmware.bin
asset_name: ams2mqtt-esp32c3-${{ steps.release_tag.outputs.tag }}.bin
asset_content_type: application/octet-stream
- name: Upload esp32c3 zip 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: esp32c3.zip
asset_name: ams2mqtt-esp32c3-${{ steps.release_tag.outputs.tag }}.zip
asset_content_type: application/zip

View File

@@ -42,4 +42,4 @@ DB
09 06 01 00 47 07 00 FF 12 00 54 02 02 0F FE 16 21
09 06 01 00 0D 07 00 FF 10 03 CF 02 02 0F FD 16 FF // Power factor
09 0C 31 37 38 32 31 30 30 31 35 31 36 35 // Meter ID
01 67
01 67

33
frames/lng2.raw Normal file
View File

@@ -0,0 +1,33 @@
7E
A0 76 CE FF 03 13 3C 02 E6 E7 00
DB
08 4C 47 5A 67 72 A9 A1 11
5E 30 00 21 80 F7 FE B8 07 C6
72 B1 90 AE AC 15 D0 AD 95 7B AC 13 7E 67 D8 A2
F0 43 51 3C 63 B6 A1 89 10 AE 9A 7E 55 4A 12 49
B9 6D EB A5 7B 57 03 69 9A BF 16 5E AD 2A 54 41
65 5E 79 C6 95 71 92 46 A2 3F 5B 63 0D 53 96 7D
42 52 1F A3 80 1C 00 E8 E3
A4 B3 9B 86 CB E5 2D 2C CA B0 E2 B7
AE 4D
7E
0f00057e41 // UI Frame header
0c07e60c0c010c232dff800000 // Date & time
020e // Structure with 14 items
1200ec // U1 = 236 V
1200ec // U2 = 236 V
1200ec // U3 = 236 V
120000 // I1 = 0.00 A
12002e // I2 = 0.46 A
12001a // I3 = 0.26 A
060000007d // Active import = 125 W
0600000000 // Active export = 0 W
0601a96ebd // Accumulated import = 27881.149 kWh
0600001dc3 // Accumulated export = 7.619 kWh
120190 // 400 ?
120003 // 3 ?
120120 // 288 ?
09083330313337313831 // Meter ID = 30137181

View File

@@ -4,13 +4,14 @@
#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_CHECK_SUM 103 // 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_UPGRADE_INFO_START 216
#define CONFIG_UI_START 248
#define CONFIG_GPIO_START 266
#define CONFIG_ENTSOE_START 290
@@ -21,11 +22,8 @@
#define CONFIG_DOMOTICZ_START 856
#define CONFIG_NTP_START 872
#define CONFIG_MQTT_START 1004
#define CONFIG_HA_START 1680
#define CONFIG_MQTT_START_86 224
#define CONFIG_METER_START_87 784
#define CONFIG_ENTSOE_START_90 286
#define CONFIG_WIFI_START_91 16
#define CONFIG_METER_START_93 224
@@ -37,18 +35,6 @@ struct SystemConfig {
char country[3];
}; // 7
struct WiFiConfig91 {
char ssid[32];
char psk[64];
char ip[15];
char gateway[15];
char subnet[15];
char dns1[15];
char dns2[15];
char hostname[32];
bool mdns;
}; // 204
struct WiFiConfig {
char ssid[32];
char psk[64];
@@ -65,18 +51,6 @@ struct WiFiConfig {
bool autoreboot;
}; // 213
struct MqttConfig86 {
char host[128];
uint16_t port;
char clientId[32];
char publishTopic[64];
char subscribeTopic[64];
char username[64];
char password[64];
uint8_t payloadFormat;
bool ssl;
}; // 420
struct MqttConfig {
char host[128];
uint16_t port;
@@ -110,7 +84,7 @@ struct MeterConfig {
uint32_t accumulatedMultiplier;
uint8_t source;
uint8_t parser;
}; // 52
}; // 61
struct MeterConfig100 {
uint32_t baud;
@@ -127,7 +101,7 @@ struct MeterConfig100 {
uint32_t accumulatedMultiplier;
uint8_t source;
uint8_t parser;
}; // 50
}; // 59
struct MeterConfig95 {
uint32_t baud;
@@ -146,16 +120,6 @@ struct MeterConfig95 {
uint8_t parser;
}; // 50
struct MeterConfig87 {
uint8_t type;
uint8_t distributionSystem;
uint8_t mainFuse;
uint8_t productionCapacity;
uint8_t encryptionKey[16];
uint8_t authenticationKey[16];
bool substituteMissing;
}; // 37
struct DebugConfig {
bool telnet;
bool serial;
@@ -179,7 +143,8 @@ struct GpioConfig {
uint8_t vccBootLimit;
uint16_t vccResistorGnd;
uint16_t vccResistorVcc;
}; // 20
bool hanPinPullup;
}; // 21
struct DomoticzConfig {
uint16_t elidx;
@@ -189,6 +154,12 @@ struct DomoticzConfig {
uint16_t cl1idx;
}; // 10
struct HomeAssistantConfig {
char discoveryPrefix[64];
char discoveryHostname[64];
char discoveryNameTag[16];
}; // 145
struct NtpConfig {
bool enable;
bool dhcp;
@@ -210,9 +181,15 @@ struct EntsoeConfig {
char currency[4];
uint32_t multiplier;
bool enabled;
}; // 62
uint16_t fixedPrice;
}; // 64
struct EnergyAccountingConfig {
uint16_t thresholds[10];
uint8_t hours;
}; // 21
struct EnergyAccountingConfig101 {
uint8_t thresholds[10];
uint8_t hours;
}; // 11
@@ -237,6 +214,13 @@ struct TempSensorConfig {
bool common;
};
struct UpgradeInformation {
char fromVersion[8];
char toVersion[8];
int16_t exitCode;
int16_t errorCode;
}; // 20
class AmsConfiguration {
public:
bool hasConfig();
@@ -268,6 +252,7 @@ public:
bool getMeterConfig(MeterConfig&);
bool setMeterConfig(MeterConfig&);
void clearMeter(MeterConfig&);
void setMeterChanged();
bool isMeterChanged();
void ackMeterChanged();
@@ -286,8 +271,10 @@ public:
bool getDomoticzConfig(DomoticzConfig&);
bool setDomoticzConfig(DomoticzConfig&);
void clearDomo(DomoticzConfig&);
bool isDomoChanged();
void ackDomoChange();
bool getHomeAssistantConfig(HomeAssistantConfig&);
bool setHomeAssistantConfig(HomeAssistantConfig&);
void clearHomeAssistantConfig(HomeAssistantConfig&);
bool getNtpConfig(NtpConfig&);
bool setNtpConfig(NtpConfig&);
@@ -319,6 +306,10 @@ public:
bool isSensorAddressEqual(uint8_t a[8], uint8_t b[8]);
bool getUpgradeInformation(UpgradeInformation&);
bool setUpgradeInformation(int16_t exitCode, int16_t errorCode, const char* currentVersion, const char* nextVersion);
void clearUpgradeInformation(UpgradeInformation&);
void clear();
protected:
@@ -326,22 +317,22 @@ protected:
private:
uint8_t configVersion = 0;
bool wifiChanged, mqttChanged, meterChanged = true, domoChanged, ntpChanged = true, entsoeChanged = false, energyAccountingChanged = true;
bool wifiChanged, mqttChanged, meterChanged = true, ntpChanged = true, entsoeChanged = false, energyAccountingChanged = true;
uint8_t tempSensorCount = 0;
TempSensorConfig** tempSensors = NULL;
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 relocateConfig101(); // 2.2.0 through 2.2.8
bool relocateConfig102(); // 2.2.9 through 2.2.11
void saveToFs();
bool loadFromFs(uint8_t version);
void deleteFromFs(uint8_t version);
};
#endif

View File

@@ -7,6 +7,6 @@
String toHex(uint8_t* in);
String toHex(uint8_t* in, uint16_t size);
void fromHex(uint8_t *out, String in, uint16_t size);
void stripNonAscii(uint8_t* in, uint16_t size);
bool stripNonAscii(uint8_t* in, uint16_t size, bool extended = false);
#endif

View File

@@ -62,7 +62,7 @@ bool AmsConfiguration::setWiFiConfig(WiFiConfig& config) {
wifiChanged = true;
}
stripNonAscii((uint8_t*) config.ssid, 32);
stripNonAscii((uint8_t*) config.ssid, 32, true);
stripNonAscii((uint8_t*) config.psk, 64);
stripNonAscii((uint8_t*) config.ip, 16);
stripNonAscii((uint8_t*) config.gateway, 16);
@@ -266,6 +266,10 @@ void AmsConfiguration::ackMeterChanged() {
meterChanged = false;
}
void AmsConfiguration::setMeterChanged() {
meterChanged = true;
}
bool AmsConfiguration::getDebugConfig(DebugConfig& config) {
if(hasConfig()) {
EEPROM.begin(EEPROM_SIZE);
@@ -309,15 +313,14 @@ bool AmsConfiguration::getDomoticzConfig(DomoticzConfig& config) {
bool AmsConfiguration::setDomoticzConfig(DomoticzConfig& config) {
DomoticzConfig existing;
if(getDomoticzConfig(existing)) {
domoChanged |= config.elidx != existing.elidx;
domoChanged |= config.vl1idx != existing.vl1idx;
domoChanged |= config.vl2idx != existing.vl2idx;
domoChanged |= config.vl3idx != existing.vl3idx;
domoChanged |= config.cl1idx != existing.cl1idx;
mqttChanged |= config.elidx != existing.elidx;
mqttChanged |= config.vl1idx != existing.vl1idx;
mqttChanged |= config.vl2idx != existing.vl2idx;
mqttChanged |= config.vl3idx != existing.vl3idx;
mqttChanged |= config.cl1idx != existing.cl1idx;
} else {
domoChanged = true;
mqttChanged = true;
}
mqttChanged = domoChanged;
EEPROM.begin(EEPROM_SIZE);
EEPROM.put(CONFIG_DOMOTICZ_START, config);
bool ret = EEPROM.commit();
@@ -333,12 +336,46 @@ void AmsConfiguration::clearDomo(DomoticzConfig& config) {
config.cl1idx = 0;
}
bool AmsConfiguration::isDomoChanged() {
return domoChanged;
bool AmsConfiguration::getHomeAssistantConfig(HomeAssistantConfig& config) {
if(hasConfig()) {
EEPROM.begin(EEPROM_SIZE);
EEPROM.get(CONFIG_HA_START, config);
EEPROM.end();
if(stripNonAscii((uint8_t*) config.discoveryPrefix, 64) || stripNonAscii((uint8_t*) config.discoveryHostname, 64) || stripNonAscii((uint8_t*) config.discoveryNameTag, 16)) {
clearHomeAssistantConfig(config);
}
return true;
} else {
clearHomeAssistantConfig(config);
return false;
}
}
void AmsConfiguration::ackDomoChange() {
domoChanged = false;
bool AmsConfiguration::setHomeAssistantConfig(HomeAssistantConfig& config) {
HomeAssistantConfig existing;
if(getHomeAssistantConfig(existing)) {
mqttChanged |= strcmp(config.discoveryPrefix, existing.discoveryPrefix) != 0;
mqttChanged |= strcmp(config.discoveryHostname, existing.discoveryHostname) != 0;
mqttChanged |= strcmp(config.discoveryNameTag, existing.discoveryNameTag) != 0;
} else {
mqttChanged = true;
}
stripNonAscii((uint8_t*) config.discoveryPrefix, 64);
stripNonAscii((uint8_t*) config.discoveryHostname, 64);
stripNonAscii((uint8_t*) config.discoveryNameTag, 16);
EEPROM.begin(EEPROM_SIZE);
EEPROM.put(CONFIG_HA_START, config);
bool ret = EEPROM.commit();
EEPROM.end();
return ret;
}
void AmsConfiguration::clearHomeAssistantConfig(HomeAssistantConfig& config) {
strcpy(config.discoveryPrefix, "");
strcpy(config.discoveryHostname, "");
strcpy(config.discoveryNameTag, "");
}
bool AmsConfiguration::pinUsed(uint8_t pin, GpioConfig& config) {
@@ -358,8 +395,9 @@ bool AmsConfiguration::pinUsed(uint8_t pin, GpioConfig& config) {
}
bool AmsConfiguration::getGpioConfig(GpioConfig& config) {
if(hasConfig()) {
EEPROM.begin(EEPROM_SIZE);
EEPROM.begin(EEPROM_SIZE);
uint8_t configVersion = EEPROM.read(EEPROM_CONFIG_ADDRESS);
if(configVersion == EEPROM_CHECK_SUM || configVersion == EEPROM_CLEARED_INDICATOR) {
EEPROM.get(CONFIG_GPIO_START, config);
EEPROM.end();
return true;
@@ -373,42 +411,43 @@ bool AmsConfiguration::setGpioConfig(GpioConfig& config) {
GpioConfig existing;
if(getGpioConfig(existing)) {
meterChanged |= config.hanPin != existing.hanPin;
meterChanged |= config.hanPinPullup != existing.hanPinPullup;
}
/* This currently does not work, as it checks its own pin
if(pinUsed(config.hanPin, config)) {
Serial.println("HAN pin already used");
debugger->println(F("HAN pin already used"));
return false;
}
if(pinUsed(config.apPin, config)) {
Serial.println("AP pin already used");
debugger->println(F("AP pin already used"));
return false;
}
if(pinUsed(config.ledPin, config)) {
Serial.println("LED pin already used");
debugger->println(F("LED pin already used"));
return false;
}
if(pinUsed(config.ledPinRed, config)) {
Serial.println("LED RED pin already used");
debugger->println(F("LED RED pin already used"));
return false;
}
if(pinUsed(config.ledPinGreen, config)) {
Serial.println("LED GREEN pin already used");
debugger->println(F("LED GREEN pin already used"));
return false;
}
if(pinUsed(config.ledPinBlue, config)) {
Serial.println("LED BLUE pin already used");
debugger->println(F("LED BLUE pin already used"));
return false;
}
if(pinUsed(config.tempSensorPin, config)) {
Serial.println("Temp sensor pin already used");
debugger->println(F("Temp sensor pin already used"));
return false;
}
if(pinUsed(config.tempAnalogSensorPin, config)) {
Serial.println("Analog temp sensor pin already used");
debugger->println(F("Analog temp sensor pin already used"));
return false;
}
if(pinUsed(config.vccPin, config)) {
Serial.println("Vcc pin already used");
debugger->println(F("Vcc pin already used"));
return false;
}
*/
@@ -424,6 +463,7 @@ bool AmsConfiguration::setGpioConfig(GpioConfig& config) {
void AmsConfiguration::clearGpio(GpioConfig& config) {
config.hanPin = 3;
config.hanPinPullup = true;
config.apPin = 0xFF;
config.ledPin = 0xFF;
config.ledInverted = true;
@@ -505,6 +545,7 @@ bool AmsConfiguration::getEntsoeConfig(EntsoeConfig& config) {
}
return true;
} else {
clearEntsoe(config);
return false;
}
}
@@ -517,6 +558,7 @@ bool AmsConfiguration::setEntsoeConfig(EntsoeConfig& config) {
entsoeChanged |= strcmp(config.currency, existing.currency) != 0;
entsoeChanged |= config.multiplier != existing.multiplier;
entsoeChanged |= config.enabled != existing.enabled;
entsoeChanged |= config.fixedPrice != existing.fixedPrice;
} else {
entsoeChanged = true;
}
@@ -554,12 +596,13 @@ bool AmsConfiguration::getEnergyAccountingConfig(EnergyAccountingConfig& config)
EEPROM.begin(EEPROM_SIZE);
EEPROM.get(CONFIG_ENERGYACCOUNTING_START, config);
EEPROM.end();
if(config.thresholds[9] != 255) {
if(config.thresholds[9] != 0xFFFF) {
clearEnergyAccountingConfig(config);
}
if(config.hours > 5) config.hours = 5;
return true;
} else {
clearEnergyAccountingConfig(config);
return false;
}
}
@@ -573,7 +616,7 @@ bool AmsConfiguration::setEnergyAccountingConfig(EnergyAccountingConfig& config)
energyAccountingChanged = true;
}
}
config.thresholds[9] = 255;
config.thresholds[9] = 0xFFFF;
energyAccountingChanged |= config.hours != existing.hours;
} else {
energyAccountingChanged = true;
@@ -595,7 +638,7 @@ void AmsConfiguration::clearEnergyAccountingConfig(EnergyAccountingConfig& confi
config.thresholds[6] = 75;
config.thresholds[7] = 100;
config.thresholds[8] = 150;
config.thresholds[9] = 255;
config.thresholds[9] = 0xFFFF;
config.hours = 3;
}
@@ -643,6 +686,45 @@ void AmsConfiguration::clearUiConfig(UiConfig& config) {
config.showTemperaturePlot = 2;
}
bool AmsConfiguration::setUpgradeInformation(int16_t exitCode, int16_t errorCode, const char* currentVersion, const char* nextVersion) {
UpgradeInformation upinfo;
upinfo.exitCode = exitCode;
upinfo.errorCode = errorCode;
strcpy(upinfo.fromVersion, currentVersion);
strcpy(upinfo.toVersion, nextVersion);
stripNonAscii((uint8_t*) upinfo.fromVersion, 8);
stripNonAscii((uint8_t*) upinfo.toVersion, 8);
EEPROM.begin(EEPROM_SIZE);
EEPROM.put(CONFIG_UPGRADE_INFO_START, upinfo);
bool ret = EEPROM.commit();
EEPROM.end();
return ret;
}
bool AmsConfiguration::getUpgradeInformation(UpgradeInformation& upinfo) {
if(hasConfig()) {
EEPROM.begin(EEPROM_SIZE);
EEPROM.get(CONFIG_UPGRADE_INFO_START, upinfo);
EEPROM.end();
if(stripNonAscii((uint8_t*) upinfo.fromVersion, 8) || stripNonAscii((uint8_t*) upinfo.toVersion, 8)) {
clearUpgradeInformation(upinfo);
}
return true;
} else {
clearUpgradeInformation(upinfo);
return false;
}
}
void AmsConfiguration::clearUpgradeInformation(UpgradeInformation& upinfo) {
upinfo.exitCode = -1;
upinfo.errorCode = 0;
memset(upinfo.fromVersion, 0, 8);
memset(upinfo.toVersion, 0, 8);
}
void AmsConfiguration::clear() {
EEPROM.begin(EEPROM_SIZE);
@@ -674,6 +756,10 @@ void AmsConfiguration::clear() {
clearDomo(domo);
EEPROM.put(CONFIG_DOMOTICZ_START, domo);
HomeAssistantConfig haconf;
clearHomeAssistantConfig(haconf);
EEPROM.put(CONFIG_HA_START, haconf);
NtpConfig ntp;
clearNtp(ntp);
EEPROM.put(CONFIG_NTP_START, ntp);
@@ -694,6 +780,10 @@ void AmsConfiguration::clear() {
clearUiConfig(ui);
EEPROM.put(CONFIG_UI_START, ui);
UpgradeInformation upinfo;
clearUpgradeInformation(upinfo);
EEPROM.put(CONFIG_UPGRADE_INFO_START, upinfo);
EEPROM.put(EEPROM_CONFIG_ADDRESS, EEPROM_CLEARED_INDICATOR);
EEPROM.commit();
EEPROM.end();
@@ -713,30 +803,6 @@ bool AmsConfiguration::hasConfig() {
}
} else {
switch(configVersion) {
case 90:
configVersion = -1; // Prevent loop
if(relocateConfig90()) {
configVersion = 91;
} else {
configVersion = 0;
return false;
}
case 91:
configVersion = -1; // Prevent loop
if(relocateConfig91()) {
configVersion = 92;
} else {
configVersion = 0;
return false;
}
case 92:
configVersion = -1; // Prevent loop
if(relocateConfig92()) {
configVersion = 93;
} else {
configVersion = 0;
return false;
}
case 93:
configVersion = -1; // Prevent loop
if(relocateConfig93()) {
@@ -777,6 +843,22 @@ bool AmsConfiguration::hasConfig() {
configVersion = 0;
return false;
}
case 101:
configVersion = -1; // Prevent loop
if(relocateConfig101()) {
configVersion = 102;
} else {
configVersion = 0;
return false;
}
case 102:
configVersion = -1; // Prevent loop
if(relocateConfig102()) {
configVersion = 103;
} else {
configVersion = 0;
return false;
}
case EEPROM_CHECK_SUM:
return true;
default:
@@ -797,8 +879,6 @@ void AmsConfiguration::loadTempSensors() {
int address = EEPROM_TEMP_CONFIG_ADDRESS;
int c = 0;
int storedCount = EEPROM.read(address++);
Serial.print("Sensors: ");
Serial.println(storedCount);
if(storedCount > 0 && storedCount <= 32) {
for(int i = 0; i < storedCount; i++) {
TempSensorConfig* tsc = new TempSensorConfig();
@@ -829,61 +909,8 @@ void AmsConfiguration::saveTempSensors() {
}
}
bool AmsConfiguration::relocateConfig90() {
EntsoeConfig entsoe;
EEPROM.begin(EEPROM_SIZE);
EEPROM.get(CONFIG_ENTSOE_START_90, entsoe);
EEPROM.put(CONFIG_ENTSOE_START, entsoe);
EEPROM.put(EEPROM_CONFIG_ADDRESS, 91);
bool ret = EEPROM.commit();
EEPROM.end();
return ret;
}
bool AmsConfiguration::relocateConfig91() {
WiFiConfig91 wifi91;
WiFiConfig wifi;
EEPROM.begin(EEPROM_SIZE);
EEPROM.get(CONFIG_WIFI_START_91, wifi91);
strcpy(wifi.ssid, wifi91.ssid);
strcpy(wifi.psk, wifi91.psk);
strcpy(wifi.ip, wifi91.ip);
strcpy(wifi.gateway, wifi91.gateway);
strcpy(wifi.subnet, wifi91.subnet);
strcpy(wifi.dns1, wifi91.dns1);
strcpy(wifi.dns2, wifi91.dns2);
strcpy(wifi.hostname, wifi91.hostname);
wifi.mdns = wifi91.mdns;
EEPROM.put(CONFIG_WIFI_START, wifi);
EEPROM.put(EEPROM_CONFIG_ADDRESS, 92);
bool ret = EEPROM.commit();
EEPROM.end();
return ret;
}
bool AmsConfiguration::relocateConfig92() {
WiFiConfig wifi;
EEPROM.begin(EEPROM_SIZE);
EEPROM.get(CONFIG_WIFI_START, wifi);
#if defined(ESP32)
wifi.power = 195;
#elif defined(ESP8266)
wifi.power = 205;
#endif
EEPROM.put(CONFIG_WIFI_START, wifi);
EnergyAccountingConfig eac;
clearEnergyAccountingConfig(eac);
EEPROM.put(CONFIG_ENERGYACCOUNTING_START, eac);
EEPROM.put(EEPROM_CONFIG_ADDRESS, 93);
bool ret = EEPROM.commit();
EEPROM.end();
return ret;
}
bool AmsConfiguration::relocateConfig93() {
MeterConfig meter;
MeterConfig95 meter;
EEPROM.begin(EEPROM_SIZE);
EEPROM.get(CONFIG_METER_START_93, meter);
meter.wattageMultiplier = 0;
@@ -910,7 +937,7 @@ bool AmsConfiguration::relocateConfig94() {
}
bool AmsConfiguration::relocateConfig95() {
MeterConfig meter;
MeterConfig95 meter;
MeterConfig95 meter95;
EEPROM.begin(EEPROM_SIZE);
EEPROM.get(CONFIG_METER_START, meter);
@@ -931,7 +958,7 @@ bool AmsConfiguration::relocateConfig96() {
SystemConfig sys;
EEPROM.get(CONFIG_SYSTEM_START, sys);
MeterConfig meter;
MeterConfig100 meter;
EEPROM.get(CONFIG_METER_START, meter);
meter.source = 1; // Serial
meter.parser = 0; // Auto
@@ -1032,6 +1059,48 @@ bool AmsConfiguration::relocateConfig100() {
return ret;
}
bool AmsConfiguration::relocateConfig101() {
EEPROM.begin(EEPROM_SIZE);
EnergyAccountingConfig config;
EnergyAccountingConfig101 config101;
EEPROM.get(CONFIG_ENERGYACCOUNTING_START, config101);
for(uint8_t i = 0; i < 9; i++) {
config.thresholds[i] = config101.thresholds[i];
}
config.thresholds[9] = 0xFFFF;
config.hours = config101.hours;
EEPROM.put(CONFIG_ENERGYACCOUNTING_START, config);
EEPROM.put(EEPROM_CONFIG_ADDRESS, 102);
bool ret = EEPROM.commit();
EEPROM.end();
return ret;
}
bool AmsConfiguration::relocateConfig102() {
EEPROM.begin(EEPROM_SIZE);
GpioConfig gpioConfig;
EEPROM.get(CONFIG_GPIO_START, gpioConfig);
gpioConfig.hanPinPullup = true;
EEPROM.put(CONFIG_GPIO_START, gpioConfig);
HomeAssistantConfig haconf;
clearHomeAssistantConfig(haconf);
EEPROM.put(CONFIG_HA_START, haconf);
EntsoeConfig entsoe;
EEPROM.get(CONFIG_ENTSOE_START, entsoe);
entsoe.fixedPrice = 0;
EEPROM.put(CONFIG_ENTSOE_START, entsoe);
EEPROM.put(EEPROM_CONFIG_ADDRESS, 103);
bool ret = EEPROM.commit();
EEPROM.end();
return ret;
}
bool AmsConfiguration::save() {
EEPROM.begin(EEPROM_SIZE);
EEPROM.put(EEPROM_CONFIG_ADDRESS, EEPROM_CHECK_SUM);
@@ -1111,154 +1180,155 @@ void AmsConfiguration::deleteFromFs(uint8_t version) {
void AmsConfiguration::print(Print* debugger)
{
debugger->println("-----------------------------------------------");
debugger->println(F("-----------------------------------------------"));
WiFiConfig wifi;
if(getWiFiConfig(wifi)) {
debugger->println("--WiFi configuration--");
debugger->printf("SSID: '%s'\r\n", wifi.ssid);
debugger->printf("Psk: '%s'\r\n", wifi.psk);
debugger->println(F("--WiFi configuration--"));
debugger->printf_P(PSTR("SSID: '%s'\r\n"), wifi.ssid);
debugger->printf_P(PSTR("Psk: '%s'\r\n"), wifi.psk);
if(strlen(wifi.ip) > 0) {
debugger->printf("IP: '%s'\r\n", wifi.ip);
debugger->printf("Gateway: '%s'\r\n", wifi.gateway);
debugger->printf("Subnet: '%s'\r\n", wifi.subnet);
debugger->printf("DNS1: '%s'\r\n", wifi.dns1);
debugger->printf("DNS2: '%s'\r\n", wifi.dns2);
debugger->printf_P(PSTR("IP: '%s'\r\n"), wifi.ip);
debugger->printf_P(PSTR("Gateway: '%s'\r\n"), wifi.gateway);
debugger->printf_P(PSTR("Subnet: '%s'\r\n"), wifi.subnet);
debugger->printf_P(PSTR("DNS1: '%s'\r\n"), wifi.dns1);
debugger->printf_P(PSTR("DNS2: '%s'\r\n"), wifi.dns2);
}
debugger->printf("Hostname: '%s'\r\n", wifi.hostname);
debugger->printf("mDNS: '%s'\r\n", wifi.mdns ? "Yes" : "No");
debugger->println("");
debugger->printf_P(PSTR("Hostname: '%s'\r\n"), wifi.hostname);
debugger->printf_P(PSTR("mDNS: '%s'\r\n"), wifi.mdns ? "Yes" : "No");
debugger->println(F(""));
delay(10);
Serial.flush();
debugger->flush();
}
MqttConfig mqtt;
if(getMqttConfig(mqtt)) {
debugger->println("--MQTT configuration--");
debugger->println(F("--MQTT configuration--"));
if(strlen(mqtt.host) > 0) {
debugger->printf("Enabled: Yes\r\n");
debugger->printf("Host: '%s'\r\n", mqtt.host);
debugger->printf("Port: %i\r\n", mqtt.port);
debugger->printf("Client ID: '%s'\r\n", mqtt.clientId);
debugger->printf("Publish topic: '%s'\r\n", mqtt.publishTopic);
debugger->printf("Subscribe topic: '%s'\r\n", mqtt.subscribeTopic);
debugger->printf_P(PSTR("Enabled: Yes\r\n"));
debugger->printf_P(PSTR("Host: '%s'\r\n"), mqtt.host);
debugger->printf_P(PSTR("Port: %i\r\n"), mqtt.port);
debugger->printf_P(PSTR("Client ID: '%s'\r\n"), mqtt.clientId);
debugger->printf_P(PSTR("Publish topic: '%s'\r\n"), mqtt.publishTopic);
debugger->printf_P(PSTR("Subscribe topic: '%s'\r\n"), mqtt.subscribeTopic);
if (strlen(mqtt.username) > 0) {
debugger->printf("Username: '%s'\r\n", mqtt.username);
debugger->printf("Password: '%s'\r\n", mqtt.password);
debugger->printf_P(PSTR("Username: '%s'\r\n"), mqtt.username);
debugger->printf_P(PSTR("Password: '%s'\r\n"), mqtt.password);
}
debugger->printf("Payload format: %i\r\n", mqtt.payloadFormat);
debugger->printf("SSL: %s\r\n", mqtt.ssl ? "Yes" : "No");
debugger->printf_P(PSTR("Payload format: %i\r\n"), mqtt.payloadFormat);
debugger->printf_P(PSTR("SSL: %s\r\n"), mqtt.ssl ? "Yes" : "No");
} else {
debugger->printf("Enabled: No\r\n");
debugger->printf_P(PSTR("Enabled: No\r\n"));
}
debugger->println("");
debugger->println(F(""));
delay(10);
Serial.flush();
debugger->flush();
}
WebConfig web;
if(getWebConfig(web)) {
debugger->println("--Web configuration--");
debugger->printf("Security: %i\r\n", web.security);
debugger->println(F("--Web configuration--"));
debugger->printf_P(PSTR("Security: %i\r\n"), web.security);
if (web.security > 0) {
debugger->printf("Username: '%s'\r\n", web.username);
debugger->printf("Password: '%s'\r\n", web.password);
debugger->printf_P(PSTR("Username: '%s'\r\n"), web.username);
debugger->printf_P(PSTR("Password: '%s'\r\n"), web.password);
}
debugger->println("");
debugger->println(F(""));
delay(10);
Serial.flush();
debugger->flush();
}
MeterConfig meter;
if(getMeterConfig(meter)) {
debugger->println("--Meter configuration--");
debugger->printf("Baud: %i\r\n", meter.baud);
debugger->printf("Parity: %i\r\n", meter.parity);
debugger->printf("Invert serial: %s\r\n", meter.invert ? "Yes" : "No");
debugger->printf("Distribution system: %i\r\n", meter.distributionSystem);
debugger->printf("Main fuse: %i\r\n", meter.mainFuse);
debugger->printf("Production Capacity: %i\r\n", meter.productionCapacity);
debugger->println("");
debugger->println(F("--Meter configuration--"));
debugger->printf_P(PSTR("Baud: %i\r\n"), meter.baud);
debugger->printf_P(PSTR("Parity: %i\r\n"), meter.parity);
debugger->printf_P(PSTR("Invert serial: %s\r\n"), meter.invert ? "Yes" : "No");
debugger->printf_P(PSTR("Distribution system: %i\r\n"), meter.distributionSystem);
debugger->printf_P(PSTR("Main fuse: %i\r\n"), meter.mainFuse);
debugger->printf_P(PSTR("Production Capacity: %i\r\n"), meter.productionCapacity);
debugger->println(F(""));
delay(10);
Serial.flush();
debugger->flush();
}
GpioConfig gpio;
if(getGpioConfig(gpio)) {
debugger->println("--GPIO configuration--");
debugger->printf("HAN pin: %i\r\n", gpio.hanPin);
debugger->printf("LED pin: %i\r\n", gpio.ledPin);
debugger->printf("LED inverted: %s\r\n", gpio.ledInverted ? "Yes" : "No");
debugger->printf("LED red pin: %i\r\n", gpio.ledPinRed);
debugger->printf("LED green pin: %i\r\n", gpio.ledPinGreen);
debugger->printf("LED blue pin: %i\r\n", gpio.ledPinBlue);
debugger->printf("LED inverted: %s\r\n", gpio.ledRgbInverted ? "Yes" : "No");
debugger->printf("AP pin: %i\r\n", gpio.apPin);
debugger->printf("Temperature pin: %i\r\n", gpio.tempSensorPin);
debugger->printf("Temp analog pin: %i\r\n", gpio.tempAnalogSensorPin);
debugger->printf("Vcc pin: %i\r\n", gpio.vccPin);
debugger->println(F("--GPIO configuration--"));
debugger->printf_P(PSTR("HAN pin: %i\r\n"), gpio.hanPin);
debugger->printf_P(PSTR("HAN pin pullup %s\r\n"), gpio.hanPinPullup ? "Yes" : "No");
debugger->printf_P(PSTR("LED pin: %i\r\n"), gpio.ledPin);
debugger->printf_P(PSTR("LED inverted: %s\r\n"), gpio.ledInverted ? "Yes" : "No");
debugger->printf_P(PSTR("LED red pin: %i\r\n"), gpio.ledPinRed);
debugger->printf_P(PSTR("LED green pin: %i\r\n"), gpio.ledPinGreen);
debugger->printf_P(PSTR("LED blue pin: %i\r\n"), gpio.ledPinBlue);
debugger->printf_P(PSTR("LED inverted: %s\r\n"), gpio.ledRgbInverted ? "Yes" : "No");
debugger->printf_P(PSTR("AP pin: %i\r\n"), gpio.apPin);
debugger->printf_P(PSTR("Temperature pin: %i\r\n"), gpio.tempSensorPin);
debugger->printf_P(PSTR("Temp analog pin: %i\r\n"), gpio.tempAnalogSensorPin);
debugger->printf_P(PSTR("Vcc pin: %i\r\n"), gpio.vccPin);
if(gpio.vccMultiplier > 0) {
debugger->printf("Vcc multiplier: %f\r\n", gpio.vccMultiplier / 1000.0);
debugger->printf_P(PSTR("Vcc multiplier: %f\r\n"), gpio.vccMultiplier / 1000.0);
}
if(gpio.vccOffset > 0) {
debugger->printf("Vcc offset: %f\r\n", gpio.vccOffset / 100.0);
debugger->printf_P(PSTR("Vcc offset: %f\r\n"), gpio.vccOffset / 100.0);
}
if(gpio.vccBootLimit > 0) {
debugger->printf("Vcc boot limit: %f\r\n", gpio.vccBootLimit / 10.0);
debugger->printf_P(PSTR("Vcc boot limit: %f\r\n"), gpio.vccBootLimit / 10.0);
}
debugger->printf("GND resistor: %i\r\n", gpio.vccResistorGnd);
debugger->printf("Vcc resistor: %i\r\n", gpio.vccResistorVcc);
debugger->println("");
debugger->printf_P(PSTR("GND resistor: %i\r\n"), gpio.vccResistorGnd);
debugger->printf_P(PSTR("Vcc resistor: %i\r\n"), gpio.vccResistorVcc);
debugger->println(F(""));
delay(10);
Serial.flush();
debugger->flush();
}
DomoticzConfig domo;
if(getDomoticzConfig(domo)) {
debugger->println("--Domoticz configuration--");
debugger->println(F("--Domoticz configuration--"));
if(mqtt.payloadFormat == 3 && domo.elidx > 0) {
debugger->printf("Enabled: Yes\r\n");
debugger->printf("Domoticz ELIDX: %i\r\n", domo.elidx);
debugger->printf("Domoticz VL1IDX: %i\r\n", domo.vl1idx);
debugger->printf("Domoticz VL2IDX: %i\r\n", domo.vl2idx);
debugger->printf("Domoticz VL3IDX: %i\r\n", domo.vl3idx);
debugger->printf("Domoticz CL1IDX: %i\r\n", domo.cl1idx);
debugger->printf_P(PSTR("Enabled: Yes\r\n"));
debugger->printf_P(PSTR("Domoticz ELIDX: %i\r\n"), domo.elidx);
debugger->printf_P(PSTR("Domoticz VL1IDX: %i\r\n"), domo.vl1idx);
debugger->printf_P(PSTR("Domoticz VL2IDX: %i\r\n"), domo.vl2idx);
debugger->printf_P(PSTR("Domoticz VL3IDX: %i\r\n"), domo.vl3idx);
debugger->printf_P(PSTR("Domoticz CL1IDX: %i\r\n"), domo.cl1idx);
} else {
debugger->printf("Enabled: No\r\n");
debugger->printf_P(PSTR("Enabled: No\r\n"));
}
debugger->println("");
debugger->println(F(""));
delay(10);
Serial.flush();
debugger->flush();
}
NtpConfig ntp;
if(getNtpConfig(ntp)) {
debugger->println("--NTP configuration--");
debugger->printf("Enabled: %s\r\n", ntp.enable ? "Yes" : "No");
debugger->println(F("--NTP configuration--"));
debugger->printf_P(PSTR("Enabled: %s\r\n"), ntp.enable ? "Yes" : "No");
if(ntp.enable) {
debugger->printf("Timezone: %s\r\n", ntp.timezone);
debugger->printf("Server: %s\r\n", ntp.server);
debugger->printf("DHCP: %s\r\n", ntp.dhcp ? "Yes" : "No");
debugger->printf_P(PSTR("Timezone: %s\r\n"), ntp.timezone);
debugger->printf_P(PSTR("Server: %s\r\n"), ntp.server);
debugger->printf_P(PSTR("DHCP: %s\r\n"), ntp.dhcp ? "Yes" : "No");
}
debugger->println("");
debugger->println(F(""));
delay(10);
Serial.flush();
debugger->flush();
}
EntsoeConfig entsoe;
if(getEntsoeConfig(entsoe)) {
if(strlen(entsoe.area) > 0) {
debugger->println("--ENTSO-E configuration--");
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(F("--ENTSO-E configuration--"));
debugger->printf_P(PSTR("Area: %s\r\n"), entsoe.area);
debugger->printf_P(PSTR("Currency: %s\r\n"), entsoe.currency);
debugger->printf_P(PSTR("Multiplier: %f\r\n"), entsoe.multiplier / 1000.0);
debugger->printf_P(PSTR("Token: %s\r\n"), entsoe.token);
}
debugger->println("");
debugger->println(F(""));
delay(10);
Serial.flush();
debugger->flush();
}
debugger->printf("Temp sensor count: %i\r\n", this->getTempSensorCount());
debugger->printf_P(PSTR("Temp sensor count: %i\r\n"), this->getTempSensorCount());
debugger->println("-----------------------------------------------");
debugger->println(F("-----------------------------------------------"));
}

View File

@@ -22,15 +22,21 @@ void fromHex(uint8_t *out, String in, uint16_t size) {
}
}
void stripNonAscii(uint8_t* in, uint16_t size) {
bool stripNonAscii(uint8_t* in, uint16_t size, bool extended) {
bool ret = false;
for(uint16_t i = 0; i < size; i++) {
if(in[i] == 0) { // Clear the rest with null-terminator
memset(in+i, 0, size-i);
break;
}
if(in[i] < 32 || in[i] > 126) {
if(extended && (in[i] < 32 || in[i] == 127 || in[i] == 129 || in[i] == 141 || in[i] == 143 || in[i] == 144 || in[i] == 157)) {
memset(in+i, ' ', 1);
ret = true;
} else if(!extended && (in[i] < 32 || in[i] > 126)) {
memset(in+i, ' ', 1);
ret = true;
}
}
memset(in+size-1, 0, 1); // Make sure the last character is null-terminator
return ret;
}

View File

@@ -35,10 +35,10 @@ public:
time_t getMeterTimestamp();
uint16_t getActiveImportPower();
uint16_t getReactiveImportPower();
uint16_t getActiveExportPower();
uint16_t getReactiveExportPower();
uint32_t getActiveImportPower();
uint32_t getReactiveImportPower();
uint32_t getActiveExportPower();
uint32_t getReactiveExportPower();
float getL1Voltage();
float getL2Voltage();
@@ -77,9 +77,9 @@ protected:
unsigned long lastList2 = 0;
uint8_t listType = 0, meterType = AmsTypeUnknown;
time_t packageTimestamp = 0;
String listId, meterId, meterModel;
String listId = "", meterId = "", meterModel = "";
time_t meterTimestamp = 0;
uint16_t activeImportPower = 0, reactiveImportPower = 0, activeExportPower = 0, reactiveExportPower = 0;
uint32_t activeImportPower = 0, reactiveImportPower = 0, activeExportPower = 0, reactiveExportPower = 0;
float l1voltage = 0, l2voltage = 0, l3voltage = 0, l1current = 0, l2current = 0, l3current = 0;
float l1activeImportPower = 0, l2activeImportPower = 0, l3activeImportPower = 0;
float l1activeExportPower = 0, l2activeExportPower = 0, l3activeExportPower = 0;

View File

@@ -9,6 +9,10 @@
#include "HwTools.h"
#include "EntsoeApi.h"
#if defined(ESP32)
#include <esp_task_wdt.h>
#endif
class AmsMqttHandler {
public:
AmsMqttHandler(MQTTClient* mqtt, char* buf) {
@@ -17,7 +21,7 @@ public:
};
virtual ~AmsMqttHandler() {};
virtual bool publish(AmsData* data, AmsData* previousState, EnergyAccounting* ea);
virtual bool publish(AmsData* data, AmsData* previousState, EnergyAccounting* ea, EntsoeApi* eapi);
virtual bool publishTemperatures(AmsConfiguration*, HwTools*);
virtual bool publishPrices(EntsoeApi* eapi);
virtual bool publishSystem(HwTools*, EntsoeApi*, EnergyAccounting*);
@@ -25,7 +29,9 @@ public:
protected:
MQTTClient* mqtt;
char* json;
uint16_t BufferSize = 1024;
uint16_t BufferSize = 2048;
bool loop();
};
#endif

View File

@@ -114,19 +114,19 @@ time_t AmsData::getMeterTimestamp() {
return this->meterTimestamp;
}
uint16_t AmsData::getActiveImportPower() {
uint32_t AmsData::getActiveImportPower() {
return this->activeImportPower;
}
uint16_t AmsData::getReactiveImportPower() {
uint32_t AmsData::getReactiveImportPower() {
return this->reactiveImportPower;
}
uint16_t AmsData::getActiveExportPower() {
uint32_t AmsData::getActiveExportPower() {
return this->activeExportPower;
}
uint16_t AmsData::getReactiveExportPower() {
uint32_t AmsData::getReactiveExportPower() {
return this->reactiveExportPower;
}
@@ -219,7 +219,7 @@ bool AmsData::isTwoPhase() {
}
int8_t AmsData::getLastError() {
return lastErrorCount > 3 ? lastError : 0;
return lastErrorCount > 2 ? lastError : 0;
}
void AmsData::setLastError(int8_t lastError) {

View File

@@ -18,32 +18,32 @@ void AmsDataStorage::setTimezone(Timezone* tz) {
bool AmsDataStorage::update(AmsData* data) {
if(isHappy()) {
if(debugger->isActive(RemoteDebug::VERBOSE)) debugger->printf("(AmsDataStorage) Data is up to date\n");
if(debugger->isActive(RemoteDebug::VERBOSE)) debugger->printf_P(PSTR("(AmsDataStorage) Data is up to date\n"));
return false;
}
time_t now = time(nullptr);
if(debugger->isActive(RemoteDebug::VERBOSE)) debugger->printf("(AmsDataStorage) Time is: %lu\n", (int32_t) now);
if(debugger->isActive(RemoteDebug::VERBOSE)) debugger->printf_P(PSTR("(AmsDataStorage) Time is: %lu\n"), (int32_t) now);
if(tz == NULL) {
if(debugger->isActive(RemoteDebug::VERBOSE)) debugger->printf("(AmsDataStorage) Timezone is missing\n");
if(debugger->isActive(RemoteDebug::VERBOSE)) debugger->printf_P(PSTR("(AmsDataStorage) Timezone is missing\n"));
return false;
}
if(now < BUILD_EPOCH) {
if(data->getMeterTimestamp() > BUILD_EPOCH) {
now = data->getMeterTimestamp();
if(debugger->isActive(RemoteDebug::DEBUG)) {
debugger->printf("(AmsDataStorage) Using meter timestamp, which is: %lu\n", (int32_t) now);
debugger->printf_P(PSTR("(AmsDataStorage) Using meter timestamp, which is: %lu\n"), (int32_t) now);
}
} else if(data->getPackageTimestamp() > BUILD_EPOCH) {
now = data->getPackageTimestamp();
if(debugger->isActive(RemoteDebug::DEBUG)) {
debugger->printf("(AmsDataStorage) Using package timestamp, which is: %lu\n", (int32_t) now);
debugger->printf_P(PSTR("(AmsDataStorage) Using package timestamp, which is: %lu\n"), (int32_t) now);
}
}
}
if(now < BUILD_EPOCH) {
if(debugger->isActive(RemoteDebug::VERBOSE)) {
debugger->printf("(AmsDataStorage) Invalid time: %lu\n", (int32_t) now);
debugger->printf_P(PSTR("(AmsDataStorage) Invalid time: %lu\n"), (int32_t) now);
}
return false;
}
@@ -54,24 +54,27 @@ bool AmsDataStorage::update(AmsData* data) {
breakTime(now-3600, utcYesterday);
breakTime(tz->toLocal(now-3600), ltzYesterDay);
uint32_t importCounter = data->getActiveImportCounter() * 1000;
uint32_t exportCounter = data->getActiveExportCounter() * 1000;
// Clear hours between last update and now
if(day.lastMeterReadTime > now) {
if(debugger->isActive(RemoteDebug::WARNING)) {
debugger->printf("(AmsDataStorage) Invalid future timestamp for day plot, resetting\n");
debugger->printf_P(PSTR("(AmsDataStorage) Invalid future timestamp for day plot, resetting\n"));
}
day.activeImport = data->getActiveImportCounter() * 1000;
day.activeExport = data->getActiveExportCounter() * 1000;
day.activeImport = importCounter;
day.activeExport = exportCounter;
day.lastMeterReadTime = now;
return true;
} else {
if(debugger->isActive(RemoteDebug::DEBUG)) {
debugger->printf("(AmsDataStorage) Last day update: %lu\n", (int32_t) day.lastMeterReadTime);
debugger->printf_P(PSTR("(AmsDataStorage) Last day update: %lu\n"), (int32_t) day.lastMeterReadTime);
}
tmElements_t last;
breakTime(day.lastMeterReadTime, last);
for(int i = last.Hour; i < utc.Hour; i++) {
if(debugger->isActive(RemoteDebug::VERBOSE)) {
debugger->printf("(AmsDataStorage) Clearing hour: %d\n", i);
debugger->printf_P(PSTR("(AmsDataStorage) Clearing hour: %d\n"), i);
}
setHourImport(i, 0);
setHourExport(i, 0);
@@ -81,20 +84,20 @@ bool AmsDataStorage::update(AmsData* data) {
// Clear days between last update and now
if(month.lastMeterReadTime > now) {
if(debugger->isActive(RemoteDebug::WARNING)) {
debugger->printf("(AmsDataStorage) Invalid future timestamp for month plot, resetting\n");
debugger->printf_P(PSTR("(AmsDataStorage) Invalid future timestamp for month plot, resetting\n"));
}
month.activeImport = data->getActiveImportCounter() * 1000;
month.activeExport = data->getActiveExportCounter() * 1000;
month.activeImport = importCounter;
month.activeExport = exportCounter;
month.lastMeterReadTime = now;
} else {
if(debugger->isActive(RemoteDebug::DEBUG)) {
debugger->printf("(AmsDataStorage) Last month update: %lu\n", (int32_t) month.lastMeterReadTime);
debugger->printf_P(PSTR("(AmsDataStorage) Last month update: %lu\n"), (int32_t) month.lastMeterReadTime);
}
tmElements_t last;
breakTime(tz->toLocal(month.lastMeterReadTime), last);
for(int i = last.Day; i < ltz.Day; i++) {
if(debugger->isActive(RemoteDebug::VERBOSE)) {
debugger->printf("(AmsDataStorage) Clearing day: %d\n", i);
debugger->printf_P(PSTR("(AmsDataStorage) Clearing day: %d\n"), i);
}
setDayImport(i, 0);
setDayExport(i, 0);
@@ -102,7 +105,7 @@ bool AmsDataStorage::update(AmsData* data) {
}
if(data->getListType() < 3) {
debugger->printf("(AmsDataStorage) Not enough data in list type: %d\n", data->getListType());
debugger->printf_P(PSTR("(AmsDataStorage) Not enough data in list type: %d\n"), data->getListType());
return false;
}
@@ -110,36 +113,42 @@ bool AmsDataStorage::update(AmsData* data) {
// Update day plot
if(!isDayHappy()) {
if(day.activeImport == 0 || now - day.lastMeterReadTime > 86400) {
day.activeImport = data->getActiveImportCounter() * 1000;
day.activeExport = data->getActiveExportCounter() * 1000;
if(day.activeImport > importCounter || day.activeExport > exportCounter) {
day.activeImport = importCounter;
day.activeExport = exportCounter;
day.lastMeterReadTime = now;
setHourImport(utcYesterday.Hour, 0);
setHourExport(utcYesterday.Hour, 0);
} else if(day.activeImport == 0 || now - day.lastMeterReadTime > 86400) {
day.activeImport = importCounter;
day.activeExport = exportCounter;
day.lastMeterReadTime = now;
if(debugger->isActive(RemoteDebug::WARNING)) {
debugger->printf("(AmsDataStorage) Too long since last day update, clearing data\n");
debugger->printf_P(PSTR("(AmsDataStorage) Too long since last day update, clearing data\n"));
}
for(int i = 0; i<24; i++) {
setHourImport(i, 0);
setHourExport(i, 0);
}
} else if(now - day.lastMeterReadTime < 4000) {
uint32_t imp = (data->getActiveImportCounter() * 1000) - day.activeImport;
uint32_t exp = (data->getActiveExportCounter() * 1000) - day.activeExport;
uint32_t imp = importCounter - day.activeImport;
uint32_t exp = exportCounter - day.activeExport;
setHourImport(utcYesterday.Hour, imp);
setHourExport(utcYesterday.Hour, exp);
if(debugger->isActive(RemoteDebug::INFO)) debugger->printf("(AmsDataStorage) Usage for hour %d: %d - %d\n", ltzYesterDay.Hour, imp, exp);
day.activeImport = data->getActiveImportCounter() * 1000;
day.activeExport = data->getActiveExportCounter() * 1000;
if(debugger->isActive(RemoteDebug::INFO)) debugger->printf_P(PSTR("(AmsDataStorage) Usage for hour %d: %d - %d\n"), ltzYesterDay.Hour, imp, exp);
day.activeImport = importCounter;
day.activeExport = exportCounter;
day.lastMeterReadTime = now;
} else {
float mins = (now - day.lastMeterReadTime) / 60.0;
uint32_t im = (data->getActiveImportCounter() * 1000) - day.activeImport;
uint32_t ex = (data->getActiveExportCounter() * 1000) - day.activeExport;
uint32_t im = importCounter - day.activeImport;
uint32_t ex = exportCounter - day.activeExport;
float ipm = im / mins;
float epm = ex / mins;
if(debugger->isActive(RemoteDebug::DEBUG)) {
debugger->printf("(AmsDataStorage) Since last day update, minutes: %.1f, import: %d (%.2f/min), export: %d (%.2f/min)\n", mins, im, ipm, ex, epm);
debugger->printf_P(PSTR("(AmsDataStorage) Since last day update, minutes: %.1f, import: %d (%.2f/min), export: %d (%.2f/min)\n"), mins, im, ipm, ex, epm);
}
tmElements_t last;
@@ -158,7 +167,7 @@ bool AmsDataStorage::update(AmsData* data) {
setHourExport(last.Hour, exp);
if(debugger->isActive(RemoteDebug::INFO)) {
debugger->printf("(AmsDataStorage) Estimated usage for hour %u: %.1f - %.1f (%lu)\n", last.Hour, imp, exp, (int32_t) cur);
debugger->printf_P(PSTR("(AmsDataStorage) Estimated usage for hour %u: %.1f - %.1f (%lu)\n"), last.Hour, imp, exp, (int32_t) cur);
}
day.activeImport += imp;
@@ -171,29 +180,35 @@ bool AmsDataStorage::update(AmsData* data) {
// Update month plot
if(ltz.Hour == 0 && !isMonthHappy()) {
if(month.activeImport == 0 || now - month.lastMeterReadTime > 2678400) {
month.activeImport = data->getActiveImportCounter() * 1000;
month.activeExport = data->getActiveExportCounter() * 1000;
if(month.activeImport > importCounter || month.activeExport > exportCounter) {
month.activeImport = importCounter;
month.activeExport = exportCounter;
month.lastMeterReadTime = now;
setDayImport(ltzYesterDay.Day, 0);
setDayExport(ltzYesterDay.Day, 0);
} else if(month.activeImport == 0 || now - month.lastMeterReadTime > 2678400) {
month.activeImport = importCounter;
month.activeExport = exportCounter;
month.lastMeterReadTime = now;
if(debugger->isActive(RemoteDebug::WARNING)) {
debugger->printf("(AmsDataStorage) Too long since last month update, clearing data\n");
debugger->printf_P(PSTR("(AmsDataStorage) Too long since last month update, clearing data\n"));
}
for(int i = 1; i<=31; i++) {
setDayImport(i, 0);
setDayExport(i, 0);
}
} else if(now - month.lastMeterReadTime < 86500 && now - month.lastMeterReadTime > 86300) {
int32_t imp = (data->getActiveImportCounter() * 1000) - month.activeImport;
int32_t exp = (data->getActiveExportCounter() * 1000) - month.activeExport;
} else if(now - month.lastMeterReadTime < 90100 && now - month.lastMeterReadTime > 82700) { // DST days are 23h (82800s) and 25h (90000)
int32_t imp = importCounter - month.activeImport;
int32_t exp = exportCounter - month.activeExport;
if(debugger->isActive(RemoteDebug::INFO)) {
debugger->printf("(AmsDataStorage) Usage for day %d: %d - %d\n", ltzYesterDay.Day, imp, exp);
debugger->printf_P(PSTR("(AmsDataStorage) Usage for day %d: %d - %d\n"), ltzYesterDay.Day, imp, exp);
}
setDayImport(ltzYesterDay.Day, imp);
setDayExport(ltzYesterDay.Day, exp);
month.activeImport = data->getActiveImportCounter() * 1000;
month.activeExport = data->getActiveExportCounter() * 1000;
month.activeImport = importCounter;
month.activeExport = exportCounter;
month.lastMeterReadTime = now;
} else {
// Make sure last month read is at midnight
@@ -201,17 +216,17 @@ bool AmsDataStorage::update(AmsData* data) {
breakTime(tz->toLocal(month.lastMeterReadTime), last);
month.lastMeterReadTime = month.lastMeterReadTime - (last.Hour * 3600) - (last.Minute * 60) - last.Second;
if(debugger->isActive(RemoteDebug::DEBUG)) {
debugger->printf("(AmsDataStorage) Last month read after resetting to midnight: %lu\n", (int32_t) month.lastMeterReadTime);
debugger->printf_P(PSTR("(AmsDataStorage) Last month read after resetting to midnight: %lu\n"), (int32_t) month.lastMeterReadTime);
}
float hrs = (now - month.lastMeterReadTime) / 3600.0;
uint32_t im = (data->getActiveImportCounter() * 1000) - month.activeImport;
uint32_t ex = (data->getActiveExportCounter() * 1000) - month.activeExport;
uint32_t im = importCounter - month.activeImport;
uint32_t ex = exportCounter - month.activeExport;
float iph = im / hrs;
float eph = ex / hrs;
if(debugger->isActive(RemoteDebug::DEBUG)) {
debugger->printf("(AmsDataStorage) Since last month update, hours: %.1f, import: %d (%.2f/hr), export: %d (%.2f/hr)\n", hrs, im, iph, ex, eph);
debugger->printf_P(PSTR("(AmsDataStorage) Since last month update, hours: %.1f, import: %d (%.2f/hr), export: %d (%.2f/hr)\n"), hrs, im, iph, ex, eph);
}
time_t stopAt = now - (ltz.Hour * 3600) - (ltz.Minute * 60) - ltz.Second;
@@ -226,7 +241,7 @@ bool AmsDataStorage::update(AmsData* data) {
setDayExport(last.Day, exp);
if(debugger->isActive(RemoteDebug::INFO)) {
debugger->printf("(AmsDataStorage) Estimated usage for day %u: %.1f - %.1f (%lu)\n", last.Day, imp, exp, (int32_t) cur);
debugger->printf_P(PSTR("(AmsDataStorage) Estimated usage for day %u: %.1f - %.1f (%lu)\n"), last.Day, imp, exp, (int32_t) cur);
}
month.activeImport += imp;
@@ -398,7 +413,7 @@ uint32_t AmsDataStorage::getDayExport(uint8_t day) {
bool AmsDataStorage::load() {
if(!LittleFS.begin()) {
if(debugger->isActive(RemoteDebug::ERROR)) {
debugger->printf("(AmsDataStorage) Unable to load LittleFS\n");
debugger->printf_P(PSTR("(AmsDataStorage) Unable to load LittleFS\n"));
}
return false;
}
@@ -422,15 +437,13 @@ bool AmsDataStorage::load() {
ret = ret && setMonthData(*month);
}
LittleFS.end();
return ret;
}
bool AmsDataStorage::save() {
if(!LittleFS.begin()) {
if(debugger->isActive(RemoteDebug::ERROR)) {
debugger->printf("(AmsDataStorage) Unable to load LittleFS\n");
debugger->printf_P(PSTR("(AmsDataStorage) Unable to load LittleFS\n"));
}
return false;
}
@@ -452,8 +465,6 @@ bool AmsDataStorage::save() {
}
file.close();
}
LittleFS.end();
return true;
}
@@ -544,17 +555,17 @@ bool AmsDataStorage::isDayHappy() {
tmElements_t tm, last;
if(now < day.lastMeterReadTime) {
if(debugger->isActive(RemoteDebug::VERBOSE)) debugger->printf("(AmsDataStorage) Day %lu < %lu\n", (int32_t) now, (int32_t) day.lastMeterReadTime);
return false;
}
if(now-day.lastMeterReadTime > 3600) {
if(debugger->isActive(RemoteDebug::VERBOSE)) debugger->printf("(AmsDataStorage) Day %lu - %lu > 3600\n", (int32_t) now, (int32_t) day.lastMeterReadTime);
if(debugger->isActive(RemoteDebug::VERBOSE)) debugger->printf_P(PSTR("(AmsDataStorage) Day data timestamp %lu < %lu\n"), (int32_t) now, (int32_t) day.lastMeterReadTime);
return false;
}
breakTime(tz->toLocal(now), tm);
breakTime(tz->toLocal(day.lastMeterReadTime), last);
if(now-day.lastMeterReadTime > 3600) {
if(debugger->isActive(RemoteDebug::VERBOSE)) debugger->printf_P(PSTR("(AmsDataStorage) Day data timestamp age %lu - %lu > 3600\n"), (int32_t) now, (int32_t) day.lastMeterReadTime);
return false;
}
if(tm.Hour > last.Hour) {
if(debugger->isActive(RemoteDebug::VERBOSE)) debugger->printf("(AmsDataStorage) Day %d > %d\n", tm.Hour, last.Hour);
if(debugger->isActive(RemoteDebug::VERBOSE)) debugger->printf_P(PSTR("(AmsDataStorage) Day data hour of last timestamp %d > %d\n"), tm.Hour, last.Hour);
return false;
}
@@ -563,7 +574,7 @@ bool AmsDataStorage::isDayHappy() {
bool AmsDataStorage::isMonthHappy() {
if(tz == NULL) {
if(debugger->isActive(RemoteDebug::VERBOSE)) debugger->printf("(AmsDataStorage) Timezone is missing\n");
if(debugger->isActive(RemoteDebug::VERBOSE)) debugger->printf_P(PSTR("(AmsDataStorage) Timezone is missing\n"));
return false;
}
@@ -572,17 +583,14 @@ bool AmsDataStorage::isMonthHappy() {
tmElements_t tm, last;
if(now < month.lastMeterReadTime) {
if(debugger->isActive(RemoteDebug::VERBOSE)) debugger->printf("(AmsDataStorage) Month %lu < %lu\n", (int32_t) now, (int32_t) month.lastMeterReadTime);
return false;
}
if(now-month.lastMeterReadTime > 86400) {
if(debugger->isActive(RemoteDebug::VERBOSE)) debugger->printf("(AmsDataStorage) Month %lu - %lu > 3600\n", (int32_t) now, (int32_t) month.lastMeterReadTime);
if(debugger->isActive(RemoteDebug::VERBOSE)) debugger->printf_P(PSTR("(AmsDataStorage) Month data timestamp %lu < %lu\n"), (int32_t) now, (int32_t) month.lastMeterReadTime);
return false;
}
breakTime(tz->toLocal(now), tm);
breakTime(tz->toLocal(month.lastMeterReadTime), last);
if(tm.Day > last.Day) {
if(debugger->isActive(RemoteDebug::VERBOSE)) debugger->printf("(AmsDataStorage) Month %d > %d\n", tm.Day, last.Day);
if(debugger->isActive(RemoteDebug::VERBOSE)) debugger->printf_P(PSTR("(AmsDataStorage) Month data day of last timestamp %d > %d\n"), tm.Day, last.Day);
return false;
}

View File

@@ -44,6 +44,10 @@ int8_t GCMParser::parse(uint8_t *d, DataParserContext &ctx) {
ptr += 3;
headersize += 3;
} else {
len = *ptr;
ptr++;
headersize++;
}
if(len + headersize > ctx.length)
return DATA_PARSE_INCOMPLETE;

View File

@@ -1,935 +0,0 @@
var nextVersion;
var im, em;
var ds = 0;
var currency = "";
var swatt = false;
// Price plot
var pp;
var pa;
var po = {
title: 'Future energy price',
titleTextStyle: {
fontSize: 14
},
backgroundColor: { fill:'transparent' },
bar: { groupWidth: '90%' },
legend: { position: 'none' },
vAxis: {
viewWindowMode: 'maximized'
},
tooltip: { trigger: 'none'},
enableInteractivity: false,
};
var pl = null; // Last price
var tl = null; // Last temperature
// Day plot
var ep;
var ea;
var eo = {
title: 'Energy use last 24 hours',
titleTextStyle: {
fontSize: 14
},
colors: ['#6f42c1', '#6f42c1'],
backgroundColor: { fill:'transparent' },
bar: { groupWidth: '90%' },
legend: { position: 'none' },
vAxis: {
title: 'kWh',
viewWindowMode: 'maximized'
},
tooltip: { trigger: 'none'},
enableInteractivity: false,
isStacked: true
};
// Month plot
var mp;
var ma;
var mo = {
title: 'Energy use last month',
titleTextStyle: {
fontSize: 14
},
colors: ['#6f42c1', '#6f42c1'],
backgroundColor: { fill:'transparent' },
bar: { groupWidth: '90%' },
legend: { position: 'none' },
vAxis: {
title: 'kWh',
viewWindowMode: 'maximized'
},
tooltip: { trigger: 'none'},
enableInteractivity: false,
isStacked: true
};
// Voltage plot
var vp;
var va;
var vo = {
title: 'Phase voltage',
titleTextStyle: {
fontSize: 14
},
backgroundColor: { fill:'transparent' },
bar: { groupWidth: '90%' },
vAxis: {
minValue: 200,
maxValue: 260,
ticks: [
{ v: 207, f: '-10%'},
{ v: 230, f: '230V'},
{ v: 253, f: '+10%'}
]
},
legend: { position: 'none' },
tooltip: { trigger: 'none'},
enableInteractivity: false,
};
// Amperage plot
var ap;
var aa;
var ao = {
title: 'Phase current',
titleTextStyle: {
fontSize: 14
},
backgroundColor: { fill:'transparent' },
bar: { groupWidth: '90%' },
vAxis: {
minValue: 0,
maxValue: 100,
ticks: [
{ v: 25, f: '25%'},
{ v: 50, f: '50%'},
{ v: 75, f: '75%'},
{ v: 100, f: '100%'}
]
},
legend: { position: 'none' },
tooltip: { trigger: 'none'},
enableInteractivity: false,
};
// Import plot
var ip;
var ia;
var io = {
legend: 'none',
backgroundColor: {
fill:'transparent',
stroke: 'transparent'
},
pieHole: 0.6,
pieSliceText: 'none',
pieStartAngle: 216,
pieSliceBorderColor: 'transparent',
slices: {
0: { color: 'green' },
1: { color: '#eee' },
2: { color: 'transparent' }
},
legend: { position: 'none' },
tooltip: { trigger: 'none'},
enableInteractivity: false,
chartArea: {
left: 0,
top: 0,
width: '100%',
height: '100%',
backgroundColor: 'transparent'
}
};
// Export plot
var xp;
var xa;
var xo = {
legend: 'none',
backgroundColor: {
fill:'transparent',
stroke: 'transparent'
},
pieHole: 0.6,
pieSliceText: 'none',
pieStartAngle: 216,
pieSliceBorderColor: 'transparent',
slices: {
0: { color: 'green' },
1: { color: '#eee' },
2: { color: 'transparent' }
},
legend: { position: 'none' },
tooltip: { trigger: 'none'},
enableInteractivity: false,
chartArea: {
left: 0,
top: 0,
width: '100%',
height: '100%',
backgroundColor: 'transparent'
}
};
// Temperature plot
var td = false; // Disable temperature
var tp;
var ta;
var to = {
title: 'Temperature sensors',
titleTextStyle: {
fontSize: 14
},
backgroundColor: { fill:'transparent' },
bar: { groupWidth: '90%' },
legend: { position: 'none' },
vAxis: {
title: '°C',
viewWindowMode: 'maximized'
},
tooltip: { trigger: 'none'},
enableInteractivity: false,
};
$(function() {
var meters = $('.plot1');
if(meters.length > 0) {
// Chart
google.charts.load('current', {'packages':['corechart']});
google.charts.setOnLoadCallback(setupChart);
}
// For mqtt
$('#m').on('change', function() {
var inputs = $('.mc');
inputs.prop('disabled', !$(this).is(':checked'));
});
$('#f').on('change', function() {
var val = parseInt($(this).val());
if(val == 3) {
$('.f3-s').show();
} else {
$('.f3-s').hide();
}
});
$('#s').on('change', function() {
var port = $('#p');
if($(this).is(':checked')) {
if(port.val() == 1883) {
port.val(8883);
}
} else {
if(port.val() == 8883) {
port.val(1883);
}
}
});
$('#m').trigger('change');
$('#f').trigger('change');
// For wifi
$('#st').on('change', function() {
if($(this).is(':checked')) {
$('.sip').prop('disabled', false);
} else {
$('.sip').prop('disabled', true);
}
});
$('#st').trigger('change');
// For web
$('#as').on('change', function() {
var inputs = $('.ac');
inputs.prop('disabled', $(this).val() == 0);
});
$('#as').trigger('change');
// For file upload
$('#fileUploadField').on('change',function(){
var fileName = $(this).val();
$(this).next('.custom-file-label').html(fileName);
})
$('.upload-form').on('submit', function(i, form) {
$('#loading-indicator').show();
});
// For NTP
$('#n').on('change', function() {
var inputs = $('.nc');
inputs.prop('disabled', !$(this).is(':checked'));
});
$('#n').trigger('change');
$('.ipo,.epo').on('click', function() {
swatt = !swatt;
$('.ipo,.epo').html('wait');
});
// Navbar
switch(window.location.pathname) {
case '/temperature':
$('#temp-link').addClass('active');
break;
case '/price':
$('#price-link').addClass('active');
break;
case '/meter':
case '/wifi':
case '/mqtt':
case '/mqtt-ca':
case '/mqtt-cert':
case '/mqtt-key':
case '/domoticz':
case '/web':
case '/ntp':
case '/entsoe':
$('#config-link').addClass('active');
break;
case '/gpio':
case '/debugging':
case '/configfile':
case '/firmware':
$('#firmware-warn').show();
case '/reset':
$('#system-link').addClass('active');
break;
}
// Check for software upgrade
var swv = $('#swVersion');
var fwl = $('#fwLink');
if((meters.length > 0 || fwl.length > 0) && swv.length == 1) {
var v = swv.text().substring(1).split('.');
var v_major = parseInt(v[0]);
var v_minor = parseInt(v[1]);
var v_patch = parseInt(v[2]);
$.ajax({
url: swv.data('url'),
dataType: 'json'
}).done(function(releases) {
var isnew = false;
if(/^v\d{1,2}\.\d{1,2}\.\d{1,2}$/.test(swv.text()) && fwl.length == 0) {
releases.reverse();
var next_patch;
var next_minor;
var next_major;
$.each(releases, function(i, release) {
var ver2 = release.tag_name;
var v2 = ver2.substring(1).split('.');
var v2_major = parseInt(v2[0]);
var v2_minor = parseInt(v2[1]);
var v2_patch = parseInt(v2[2]);
if(v2_major == v_major) {
if(v2_minor == v_minor) {
if(v2_patch > v_patch) {
next_patch = release;
}
} 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) {
nextVersion = next_minor;
isnew = true;
} else if(next_major) {
nextVersion = next_major;
isnew = true;
} else if(next_patch) {
nextVersion = next_patch;
isnew = true;
}
} else {
nextVersion = releases[0];
}
if(nextVersion) {
if(fwl.length > 0) {
var chipset = fwl.data('chipset').toLowerCase();
$.each(releases, function(i, release) {
if(release.tag_name == nextVersion.tag_name) {
$.each(release.assets, function(i, asset) {
if(asset.name.includes(chipset) && !asset.name.includes("partitions")) {
fwl.prop('href', asset.browser_download_url);
$('#fwDownload').show();
return false;
}
});
}
});
};
if(isnew) {
$('#newVersionTag').text(nextVersion.tag_name);
$('#newVersionUrl').prop('href', nextVersion.html_url);
$('#newVersion').removeClass('d-none');
}
}
});
}
// Temperature
var tt = $('#temp-template');
if(tt.length > 0) {
setTimeout(loadTempSensors, 500);
}
});
var resizeTO;
$( window ).resize(function() {
if(resizeTO) clearTimeout(resizeTO);
resizeTO = setTimeout(function() {
$(this).trigger('resizeEnd');
}, 250);
});
$(window).on('resizeEnd', function() {
redraw();
});
var zeropad = function(num) {
num = num.toString();
while (num.length < 2) num = "0" + num;
return num;
}
var setupChart = function() {
pp = new google.visualization.ColumnChart(document.getElementById('pp'));
ep = new google.visualization.ColumnChart(document.getElementById('ep'));
mp = new google.visualization.ColumnChart(document.getElementById('mp'));
vp = new google.visualization.ColumnChart(document.getElementById('vp'));
ap = new google.visualization.ColumnChart(document.getElementById('ap'));
ip = new google.visualization.PieChart(document.getElementById('ip'));
xp = new google.visualization.PieChart(document.getElementById('xp'));
tp = new google.visualization.ColumnChart(document.getElementById('tp'));
fetch();
drawDay();
drawMonth();
};
var redraw = function() {
if(pl != null) {
pp.draw(pa, po);
}
ep.draw(ea, eo);
mp.draw(ma, mo);
vp.draw(va, vo);
ap.draw(aa, ao);
ip.draw(ia, io);
xp.draw(xa, xo);
tp.draw(ta, to);
if(tl != null) {
tp.draw(ta, to);
}
};
var drawPrices = function() {
$('#ppc').show();
$.ajax({
url: '/energyprice.json',
timeout: 30000,
dataType: 'json',
}).done(function(json) {
currency = json.currency;
data = [['Hour',json.currency + '/kWh', { role: 'style' }, { role: 'annotation' }]];
var r = 1;
var hour = moment.utc().hours();
var offset = moment().utcOffset()/60;
var min = 0;
var h = 0;
var d = json["20"] == null ? 2 : 1;
for(var i = hour; i<24; i++) {
var val = json[zeropad(h++)];
if(val == null) break;
data[r++] = [zeropad((i+offset)%24), val, "color: #6f42c1;opacity: 0.9;", val == null ? "" : val.toFixed(d)];
Math.min(0, val);
};
for(var i = 0; i < 24; i++) {
var val = json[zeropad(h++)];
if(val == null) break;
data[r++] = [zeropad((i+offset)%24), val, "color: #6f42c1;opacity: 0.9;", val == null ? "" : val.toFixed(d)];
Math.min(0, val);
};
pa = google.visualization.arrayToDataTable(data);
po.vAxis.title = json.currency;
if(min == 0)
po.vAxis.minValue = 0;
pp.draw(pa, po);
});
}
var drawDay = function() {
$.ajax({
url: '/dayplot.json',
timeout: 30000,
dataType: 'json',
}).done(function(json) {
data = [['Hour', 'Import', { role: 'style' }, { role: 'annotation' }, 'Export', { role: 'style' }, { role: 'annotation' }]];
var r = 1;
var hour = moment.utc().hours();
var offset = moment().utcOffset()/60;
var min = 0;
for(var i = hour; i<24; i++) {
var imp = json["i"+zeropad(i)];
var exp = json["e"+zeropad(i)];
data[r++] = [zeropad((i+offset)%24), imp, "opacity: 0.9;", imp == 0 ? "" : imp.toFixed(1), exp == 0 ? 0 : -exp, "opacity: 0.9;", exp == 0 ? "" : -exp.toFixed(1)];
min = Math.min(0, -exp);
};
for(var i = 0; i < hour; i++) {
var imp = json["i"+zeropad(i)];
var exp = json["e"+zeropad(i)];
data[r++] = [zeropad((i+offset)%24), imp, "opacity: 0.9;", imp == 0 ? "" : imp.toFixed(1), exp == 0 ? 0 : -exp, "opacity: 0.9;", exp == 0 ? "" : -exp.toFixed(1)];
min = Math.min(0, -exp);
};
ea = google.visualization.arrayToDataTable(data);
if(min == 0)
eo.vAxis.minValue = 0;
ep.draw(ea, eo);
setTimeout(drawDay, (61-moment().minute())*60000);
});
};
var drawMonth = function() {
$.ajax({
url: '/monthplot.json',
timeout: 30000,
dataType: 'json',
}).done(function(json) {
data = [['Hour', 'Import', { role: 'style' }, { role: 'annotation' }, 'Export', { role: 'style' }, { role: 'annotation' }]];
var r = 1;
var day = moment().date();
var eom = moment().subtract(1, 'months').endOf('month').date();
var min = 0;
for(var i = day; i<=eom; i++) {
var imp = json["i"+zeropad(i)];
var exp = json["e"+zeropad(i)];
data[r++] = [zeropad(i), imp, "opacity: 0.9;", imp == 0 ? "" : imp.toFixed(0), exp == 0 ? 0 : -exp, "opacity: 0.9;", exp == 0 ? "" : -exp.toFixed(0)];
min = Math.min(0, -exp);
}
for(var i = 1; i < day; i++) {
var imp = json["i"+zeropad(i)];
var exp = json["e"+zeropad(i)];
data[r++] = [zeropad(i), imp, "opacity: 0.9;", imp == 0 ? "" : imp.toFixed(0), exp == 0 ? 0 : -exp, "opacity: 0.9;", exp == 0 ? "" : -exp.toFixed(0)];
min = Math.min(0, -exp);
}
ma = google.visualization.arrayToDataTable(data);
if(min == 0)
mo.vAxis.minValue = 0;
mp.draw(ma, mo);
setTimeout(drawMonth, (24-moment().hour())*60000);
});
};
var drawTemperature = function() {
if(td) return;
$.ajax({
url: '/temperature.json',
timeout: 30000,
dataType: 'json',
}).done(function(json) {
if(json.c > 1) {
$('#tpc').show();
var r = 1;
var min = 0;
data = [['Sensor','°C', { role: 'style' }, { role: 'annotation' }]];
$.each(json.s, function(i, o) {
var name = o.n ? o.n : o.a;
data[r++] = [name, o.v, "color: #6f42c1;opacity: 0.9;", o.v.toFixed(1)];
Math.min(0, o.v);
});
if(min == 0)
to.vAxis.minValue = 0;
ta = google.visualization.arrayToDataTable(data);
ta.sort("Sensor");
tp.draw(ta, to);
td = false;
} else {
td = true;
}
});
};
var setStatus = function(id, sid) {
var item = $('#'+id);
item.removeClass('d-none');
item.removeClass (function (index, className) {
return (className.match (/(^|\s)badge-\S+/g) || []).join(' ');
});
var status;
if(sid == 0) {
status = "secondary";
} else if(sid == 1) {
status = "success";
} else if(sid == 2) {
status = "warning";
} else {
status = "danger";
}
item.addClass('badge badge-' + status);
};
var voltcol = function(pct) {
if(pct > 85) return '#d90000';
else if(pct > 75) return'#e32100';
else if(pct > 70) return '#ffb800';
else if(pct > 65) return '#dcd800';
else if(pct > 35) return '#32d900';
else if(pct > 25) return '#dcd800';
else if(pct > 20) return '#ffb800';
else if(pct > 15) return'#e32100';
else return '#d90000';
};
var ampcol = function(pct) {
if(pct > 90) return '#d90000';
else if(pct > 85) return'#e32100';
else if(pct > 80) return '#ffb800';
else if(pct > 75) return '#dcd800';
else return '#32d900';
};
var retrycount = 0;
var interval = 5000;
var fetch = function() {
$.ajax({
url: '/data.json',
timeout: 10000,
dataType: 'json',
}).done(function(json) {
retrycount = 0;
for(var id in json) {
var str = json[id];
if(typeof str === "object") {
continue;
}
if(isNaN(str)) {
$('.j'+id).html(str);
} else {
var num = parseFloat(str);
$('.j'+id).html(num.toFixed(num < 0 ? 0 : num < 10 ? 2 : 1));
}
$('.r'+id).show();
}
if(window.moment) {
$('.ju').html(moment.duration(parseInt(json.u), 'seconds').humanize());
}
ds = parseInt(json.ds);
var kib = parseInt(json.m)/1000;
$('.jm').html(kib.toFixed(1));
if(kib > 32) {
$('.ssl-capable').removeClass('d-none');
}
setStatus("esp", json.em);
setStatus("han", json.hm);
setStatus("wifi", json.wm);
setStatus("mqtt", json.mm);
if(ip) {
var v = parseInt(json.i);
var pct = Math.min((v*100)/parseInt(json.im), 100);
var append = "W";
if(v > 1000 && !swatt) {
v = (v/1000).toFixed(1);
append = "kW";
}
$('.ipo').html(v);
$('.ipoa').html(append);
var arr = [
['Slice', 'Value'],
['', (pct*2.88)],
['', ((100-pct)*2.88)],
['', 72],
];
io.slices[0].color = ampcol(pct);
ia = google.visualization.arrayToDataTable(arr);
ip.draw(ia, io);
}
var om = parseInt(json.om);
if(om > 0) {
$('.rex').show();
$('.rim').hide();
if(xp) {
var v = parseInt(json.e);
var pct = Math.min((v*100)/(om*1000), 100);
var append = "W";
if(v > 1000 && !swatt) {
v = (v/1000).toFixed(1);
append = "kW";
}
$('.epo').html(v);
$('.epoa').html(append);
var arr = [
['Slice', 'Value'],
['', (pct*2.88)],
['', ((100-pct)*2.88)],
['', 72],
];
xo.slices[0].color = ampcol(pct);
xa = google.visualization.arrayToDataTable(arr);
xp.draw(xa, xo);
}
} else {
$('.rex').hide();
$('.rim').show();
}
if(vp) {
switch(ds) {
case 1:
vo.title = 'Voltage between';
break;
case 2:
vo.title = 'Phase voltage';
break;
}
var c = 0;
var t = 0;
var r = 1;
var arr = [['Phase', 'Voltage', { role: 'style' }, { role: 'annotation' }]];
if(json.u1) {
var u1 = parseFloat(json.u1);
t += u1;
c++;
var pct = Math.min(Math.max(parseFloat(json.u1)-195.5, 1)*100/69, 100);
arr[r++] = [ds == 1 ? 'L1-L2' : 'L1', u1, "color: " + voltcol(pct) + ";opacity: 0.9;", u1 + "V"];
}
if(json.u2) {
var u2 = parseFloat(json.u2);
t += u2;
c++;
var pct = Math.min(Math.max(parseFloat(json.u2)-195.5, 1)*100/69, 100);
arr[r++] = [ds == 1 ? 'L1-L3' : 'L2', u2, "color: " + voltcol(pct) + ";opacity: 0.9;", u2 + "V"];
}
if(json.u3) {
var u3 = parseFloat(json.u3);
t += u3;
c++;
var pct = Math.min(Math.max(parseFloat(json.u3)-195.5, 1)*100/69, 100);
arr[r++] = [ds == 1 ? 'L2-L3' : 'L3', u3, "color: " + voltcol(pct) + ";opacity: 0.9;", u3 + "V"];
}
v = t/c;
if(v > 0) {
va = google.visualization.arrayToDataTable(arr);
vp.draw(va, vo);
}
}
if(ap && json.mf) {
switch(ds) {
case 1:
ao.title = 'Line current';
break;
case 2:
ao.title = 'Phase current';
break;
}
var dA = false;
var r = 1;
var arr = [['Phase', 'Amperage', { role: 'style' }, { role: 'annotation' }]];
if(json.i1 || json.u1) {
var i1 = parseFloat(json.i1);
dA = true;
var pct = Math.min((parseFloat(json.i1)/parseInt(json.mf))*100, 100);
arr[r++] = ['L1', pct, "color: " + ampcol(pct) + ";opacity: 0.9;", i1 + "A"];
}
if(json.i2 || json.u2) {
var i2 = parseFloat(json.i2);
dA = true;
var pct = Math.min((parseFloat(json.i2)/parseInt(json.mf))*100, 100);
arr[r++] = ['L2', pct, "color: " + ampcol(pct) + ";opacity: 0.9;", i2 + "A"];
}
if(json.i3 || json.u3) {
var i3 = parseFloat(json.i3);
dA = true;
var pct = Math.min((parseFloat(json.i3)/parseInt(json.mf))*100, 100);
arr[r++] = ['L3', pct, "color: " + ampcol(pct) + ";opacity: 0.9;", i3 + "A"];
}
if(dA) {
aa = google.visualization.arrayToDataTable(arr);
ap.draw(aa, ao);
}
}
if(json.ea) {
$('#each').html(json.ea.h.u.toFixed(2));
$('#eachc').html(json.ea.h.c.toFixed(2));
$('#eacd').html(json.ea.d.u.toFixed(1));
$('#eacdc').html(json.ea.d.c.toFixed(1));
$('#eacm').html(json.ea.m.u.toFixed(0));
$('#eacmc').html(json.ea.m.c.toFixed(0));
$('#eax').html(json.ea.x.toFixed(1));
$('#eat').html(json.ea.t.toFixed(0));
$('.cr').html(currency);
if(currency) {
$('.sp').show();
}
if(om > 0) {
$('.se').removeClass('d-none');
$('#eache').html(json.ea.h.p.toFixed(2));
$('#eacde').html(json.ea.d.p.toFixed(1));
$('#eacme').html(json.ea.m.p.toFixed(0));
}
}
if(json.me) {
$('.me').addClass('d-none');
$('.me'+json.me).removeClass('d-none');
$('#ml').html(json.me);
}
var temp = parseFloat(json.t);
if(temp == -127.0) {
$('.jt').html("N/A");
$('.rt').hide();
} else {
$('.rt').show();
if(tl != temp) {
drawTemperature();
}
tl = temp;
}
var vcc = parseFloat(json.v);
if(vcc > 0.0) {
$('.rv').show();
} else {
$('.rv').hide();
}
var unixtime = moment().unix();
var ts = parseInt(json.c);
if(Math.abs(unixtime-ts) < 300) {
$('.jc').html(moment(ts * 1000).format('DD. MMM HH:mm'));
$('.jc').removeClass('text-danger');
} else {
$('.jc').html(moment(ts * 1000).format('DD.MM.YYYY HH:mm'));
$('.jc').addClass('text-danger');
}
var mt = parseInt(json.mt);
switch(mt) {
case 1:
$('.jmt').html("Aidon");
break;
case 2:
$('.jmt').html("Kaifa");
break;
case 3:
$('.jmt').html("Kamstrup");
break;
case 8:
$('.jmt').html("Iskra");
break;
case 9:
$('.jmt').html("Landis+Gyr");
break;
case 10:
$('.jmt').html("Sagemcom");
break;
default:
$('.jmt').html("");
}
setTimeout(fetch, interval);
var price = parseFloat(json.p);
if(price && price != pl) {
pl = price;
drawPrices();
}
}).fail(function(x, text, error) {
if(retrycount > 2) {
console.log("Failed request");
console.log(text);
console.log(error);
setTimeout(fetch, interval*4);
setStatus("mqtt", 0);
setStatus("wifi", 0);
setStatus("han", 0);
setStatus("esp", 3);
} else {
setTimeout(fetch, interval);
}
retrycount++;
});
}
var upgrade = function() {
if(nextVersion) {
if(confirm("WARNING: If you have a M-BUS powered device (Pow-U), please keep USB power connected while upgrading.\n\nAre you sure you want to perform upgrade to " + nextVersion.tag_name + "?")) {
$('#loading-indicator').show();
window.location.href="/upgrade?version=" + nextVersion.tag_name;
}
}
}
var loadTempSensors = function() {
$.ajax({
url: '/temperature.json',
timeout: 10000,
dataType: 'json',
}).done(function(json) {
if($('#loading').length > 0) {
$('#loading').hide();
var list = $('#sensors');
if(json.c > 0) {
list.empty();
var temp = $.trim($('#temp-template').html());
$.each(json.s, function(i, o) {
var item = temp.replace(/{{index}}/ig, o.i);
var item = item.replace(/{{address}}/ig, o.a);
var item = item.replace(/{{name}}/ig, o.n);
var item = item.replace(/{{value}}/ig, o.v > -50 && o.v < 127 ? o.v : "N/A");
var item = item.replace(/{{common}}/ig, o.c ? "checked" : "");
list.append(item);
});
} else {
$('#notemp').show();
}
} else {
$.each(json.s, function(i, o) {
$('#temp-'+o.i).html(o.v > -50 && o.v < 127 ? o.v : "N/A");
});
}
setTimeout(loadTempSensors, 10000);
}).fail(function() {
setTimeout(loadTempSensors, 60000);
$('#loading').hide();
$('#error').show();
});
}

View File

@@ -1,432 +0,0 @@
/* Ripped necessary style from bootstrap 4.4.1 to make the page look good without internet access. Meant to be overridden by CSS from CDN */
:root {
--blue: #007bff;
--indigo: #6610f2;
--purple: #6f42c1;
--pink: #e83e8c;
--red: #dc3545;
--orange: #fd7e14;
--yellow: #ffc107;
--green: #28a745;
--teal: #20c997;
--cyan: #17a2b8;
--white: #fff;
--gray: #6c757d;
--gray-dark: #343a40;
--primary: #007bff;
--secondary: #6c757d;
--success: #28a745;
--info: #17a2b8;
--warning: #ffc107;
--danger: #dc3545;
--light: #f8f9fa;
--dark: #343a40;
--breakpoint-xs: 0;
--breakpoint-sm: 576px;
--breakpoint-md: 768px;
--breakpoint-lg: 992px;
--breakpoint-xl: 1200px;
--font-family-sans-serif: -apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,"Noto Sans",sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";
--font-family-monospace: SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace;
}
html {
font-family: sans-serif;
line-height: 1.15;
-webkit-text-size-adjust: 100%;
-webkit-tap-highlight-color: transparent;
color: -internal-root-color;
}
body {
display: block;
margin: 8px;
font-family: -apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,"Noto Sans",sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";
font-size: 1rem;
font-weight: 400;
line-height: 1.5;
color: #212529;
text-align: left;
background-color: #fff;
}
.bg-white {
background-color: #fff!important;
}
.bg-light {
background-color: #f8f9fa!important;
}
.bg-purple {
background-color: var(--purple);
}
.mt-2, .my-2 {
margin-top: .5rem!important;
}
.p-3 {
padding: 1rem!important;
}
.mb-3, .my-3 {
margin-bottom: 1rem!important;
}
.mt-3, .my-3 {
margin-top: 1rem!important;
}
.mb-4, .my-4 {
margin-bottom: 1.5rem!important;
}
.shadow {
box-shadow: 0 .5rem 1rem rgba(0,0,0,.15)!important;
}
.rounded {
border-radius: .25rem!important;
}
.container {
width: 100%;
padding-right: 15px;
padding-left: 15px;
margin-right: auto;
margin-left: auto;
}
article, aside, figcaption, figure, footer, header, hgroup, main, nav, section {
display: block;
}
main {
display: block;
}
.text-right {
text-align: right!important;
}
.h1, .h2, .h3, .h4, .h5, .h6, h1, h2, h3, h4, h5, h6 {
margin-bottom: .5rem;
font-weight: 500;
line-height: 1.2;
}
h1, h2, h3, h4, h5, h6 {
margin-top: 0;
margin-bottom: .5rem;
}
.h5, h5 {
font-size: 1.25rem;
}
.h6, h6 {
font-size: 1rem;
}
.row {
display: -ms-flexbox;
display: flex;
-ms-flex-wrap: wrap;
flex-wrap: wrap;
margin-right: -15px;
margin-left: -15px;
}
.col, .col-1, .col-10, .col-11, .col-12, .col-2, .col-3, .col-4, .col-5, .col-6, .col-7, .col-8, .col-9, .col-auto, .col-lg, .col-lg-1, .col-lg-10, .col-lg-11, .col-lg-12, .col-lg-2, .col-lg-3, .col-lg-4, .col-lg-5, .col-lg-6, .col-lg-7, .col-lg-8, .col-lg-9, .col-lg-auto, .col-md, .col-md-1, .col-md-10, .col-md-11, .col-md-12, .col-md-2, .col-md-3, .col-md-4, .col-md-5, .col-md-6, .col-md-7, .col-md-8, .col-md-9, .col-md-auto, .col-sm, .col-sm-1, .col-sm-10, .col-sm-11, .col-sm-12, .col-sm-2, .col-sm-3, .col-sm-4, .col-sm-5, .col-sm-6, .col-sm-7, .col-sm-8, .col-sm-9, .col-sm-auto, .col-xl, .col-xl-1, .col-xl-10, .col-xl-11, .col-xl-12, .col-xl-2, .col-xl-3, .col-xl-4, .col-xl-5, .col-xl-6, .col-xl-7, .col-xl-8, .col-xl-9, .col-xl-auto {
position: relative;
width: 100%;
padding-right: 15px;
padding-left: 15px;
}
.col-6 {
-ms-flex: 0 0 50%;
flex: 0 0 50%;
max-width: 50%;
}
.d-none {
display: none!important;
}
.flex-row {
-ms-flex-direction: row!important;
flex-direction: row!important;
}
.flex-column {
-ms-flex-direction: column!important;
flex-direction: column!important;
}
a {
color: #007bff;
text-decoration: none;
background-color: transparent;
}
.btn {
display: inline-block;
font-weight: 400;
color: #212529;
text-align: center;
vertical-align: middle;
cursor: pointer;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
background-color: transparent;
border: 1px solid transparent;
padding: .375rem .75rem;
font-size: 1rem;
line-height: 1.5;
border-radius: .25rem;
transition: color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;
}
.btn-group-sm>.btn, .btn-sm {
padding: .25rem .5rem;
font-size: .875rem;
line-height: 1.5;
border-radius: .2rem;
}
.btn-primary {
color: #fff;
background-color: #007bff;
border-color: #007bff;
}
.navbar {
position: relative;
display: -ms-flexbox;
display: flex;
-ms-flex-wrap: wrap;
flex-wrap: wrap;
-ms-flex-align: center;
align-items: center;
-ms-flex-pack: justify;
justify-content: space-between;
padding: .5rem 1rem;
}
.navbar-dark .navbar-brand {
color: #fff;
}
.navbar-expand {
-ms-flex-flow: row nowrap;
flex-flow: row nowrap;
-ms-flex-pack: start;
justify-content: flex-start;
}
.navbar-nav {
display: -ms-flexbox;
display: flex;
-ms-flex-direction: column;
flex-direction: column;
padding-left: 0;
margin-bottom: 0;
list-style: none;
}
.navbar-expand .navbar-nav {
-ms-flex-direction: row;
flex-direction: row;
}
.navbar-brand {
display: inline-block;
padding-top: .3125rem;
padding-bottom: .3125rem;
margin-right: 1rem;
font-size: 1.25rem;
line-height: inherit;
white-space: nowrap;
}
.navbar-dark .navbar-nav .nav-link {
color: rgba(255,255,255,.5);
}
.navbar-expand .navbar-nav .nav-link {
padding-right: .5rem;
padding-left: .5rem;
}
.navbar-nav .nav-link {
padding-right: 0;
padding-left: 0;
}
.nav-link {
display: block;
padding: .5rem 1rem;
}
.navbar-dark .navbar-nav .active>.nav-link, .navbar-dark .navbar-nav .nav-link.active, .navbar-dark .navbar-nav .nav-link.show, .navbar-dark .navbar-nav .show>.nav-link {
color: #fff;
}
.form-group {
margin-bottom: 1rem;
}
.form-control {
display: block;
width: 100%;
height: calc(1.5em + .75rem + 2px);
padding: .375rem .75rem;
font-size: 1rem;
font-weight: 400;
line-height: 1.5;
color: #495057;
background-color: #fff;
background-clip: padding-box;
border: 1px solid #ced4da;
border-radius: .25rem;
transition: border-color .15s ease-in-out,box-shadow .15s ease-in-out;
}
.form-control:disabled, .form-control[readonly] {
background-color: #e9ecef;
opacity: 1;
}
input:not([type="image" i]) {
box-sizing: border-box;
}
button, input {
overflow: visible;
}
button, input, optgroup, select, textarea {
margin: 0;
font-family: inherit;
font-size: inherit;
line-height: inherit;
}
input[type="radio"], input[type="checkbox"] {
box-sizing: border-box;
padding: 0;
}
.input-group {
position: relative;
display: -ms-flexbox;
display: flex;
-ms-flex-wrap: wrap;
flex-wrap: wrap;
-ms-flex-align: stretch;
align-items: stretch;
width: 100%;
}
.input-group-append {
margin-left: -1px;
}
.input-group-prepend {
margin-right: -1px;
}
.input-group-append, .input-group-prepend {
display: -ms-flexbox;
display: flex;
}
.input-group>.input-group-append>.btn, .input-group>.input-group-append>.input-group-text, .input-group>.input-group-prepend:first-child>.btn:not(:first-child), .input-group>.input-group-prepend:first-child>.input-group-text:not(:first-child), .input-group>.input-group-prepend:not(:first-child)>.btn, .input-group>.input-group-prepend:not(:first-child)>.input-group-text {
border-top-left-radius: 0;
border-bottom-left-radius: 0;
}
.input-group>.input-group-append:last-child>.btn:not(:last-child):not(.dropdown-toggle), .input-group>.input-group-append:last-child>.input-group-text:not(:last-child), .input-group>.input-group-append:not(:last-child)>.btn, .input-group>.input-group-append:not(:last-child)>.input-group-text, .input-group>.input-group-prepend>.btn, .input-group>.input-group-prepend>.input-group-text {
border-top-right-radius: 0;
border-bottom-right-radius: 0;
}
.input-group>.custom-select:not(:first-child), .input-group>.form-control:not(:first-child) {
border-top-left-radius: 0;
border-bottom-left-radius: 0;
}
.input-group-text {
display: -ms-flexbox;
display: flex;
-ms-flex-align: center;
align-items: center;
padding: .375rem .75rem;
margin-bottom: 0;
font-size: 1rem;
font-weight: 400;
line-height: 1.5;
color: #495057;
text-align: center;
white-space: nowrap;
background-color: #e9ecef;
border: 1px solid #ced4da;
border-radius: .25rem;
}
.input-group>.custom-select:not(:last-child), .input-group>.form-control:not(:last-child) {
border-top-right-radius: 0;
border-bottom-right-radius: 0;
}
.input-group>.custom-file, .input-group>.custom-select, .input-group>.form-control, .input-group>.form-control-plaintext {
position: relative;
-ms-flex: 1 1 0%;
flex: 1 1 0%;
min-width: 0;
margin-bottom: 0;
}
hr {
margin-top: 1rem;
margin-bottom: 1rem;
border: 0;
border-top: 1px solid rgba(0,0,0,.1);
box-sizing: content-box;
height: 0;
overflow: visible;
}
ul {
display: block;
list-style-type: disc;
margin-block-start: 1em;
margin-block-end: 1em;
margin-inline-start: 0px;
margin-inline-end: 0px;
padding-inline-start: 40px;
}
li {
display: list-item;
text-align: -webkit-match-parent;
}
dl, ol, ul {
margin-top: 0;
margin-bottom: 1rem;
}
@media (min-width: 576px) {
.container, .container-sm {
max-width: 540px;
}
.col-sm-6 {
-ms-flex: 0 0 50%;
flex: 0 0 50%;
max-width: 50%;
}
}
@media (min-width: 768px) {
.container, .container-md, .container-sm {
max-width: 720px;
}
.col-md-3 {
-ms-flex: 0 0 25%;
flex: 0 0 25%;
max-width: 25%;
}
.col-md-6 {
-ms-flex: 0 0 50%;
flex: 0 0 50%;
max-width: 50%;
}
.d-md-flex {
display: -ms-flexbox!important;
display: flex!important;
}
.flex-md-row {
-ms-flex-direction: row!important;
flex-direction: row!important;
}
.ml-md-auto, .mx-md-auto {
margin-left: auto!important;
}
}
@media (min-width: 992px) {
.container, .container-lg, .container-md, .container-sm {
max-width: 960px;
}
.col-lg-4 {
-ms-flex: 0 0 33.333333%;
flex: 0 0 33.333333%;
max-width: 33.333333%;
}
}
@media (min-width: 1200px) {
.container, .container-lg, .container-md, .container-sm, .container-xl {
max-width: 1140px;
}
.col-xl-2 {
-ms-flex: 0 0 16.666667%;
flex: 0 0 16.666667%;
max-width: 16.666667%;
}
.col-xl-3 {
-ms-flex: 0 0 25%;
flex: 0 0 25%;
max-width: 25%;
}
.col-xl-4 {
-ms-flex: 0 0 33.333333%;
flex: 0 0 33.333333%;
max-width: 33.333333%;
}
}
*, ::after, ::before {
box-sizing: border-box;
}

View File

@@ -1,71 +0,0 @@
<form method="get" action="/configfile.cfg">
<div class="my-3 p-3 bg-white rounded shadow">
<h6>Download configuration</h6>
<div class="row">
<div class="col-xl-2 col-md-3 col-sm-6">
<label><input type="checkbox" name="iw" value="true" checked/> WiFi</label>
</div>
<div class="col-xl-2 col-md-3 col-sm-6">
<label><input type="checkbox" name="im" value="true" checked/> MQTT</label>
</div>
<div class="col-xl-2 col-md-3 col-sm-6">
<label><input type="checkbox" name="ie" value="true" checked/> Web</label>
</div>
<div class="col-xl-2 col-md-3 col-sm-6">
<label><input type="checkbox" name="it" value="true" checked/> Meter</label>
</div>
<div class="col-xl-2 col-md-3 col-sm-6">
<label><input type="checkbox" name="ih" value="true" checked/> Thresholds</label>
</div>
<div class="col-xl-2 col-md-3 col-sm-6">
<label><input type="checkbox" name="ig" value="true" checked/> GPIO</label>
</div>
<div class="col-xl-2 col-md-3 col-sm-6">
<label><input type="checkbox" name="id" value="true" checked/> Domoticz</label>
</div>
<div class="col-xl-2 col-md-3 col-sm-6">
<label><input type="checkbox" name="in" value="true" checked/> NTP</label>
</div>
<div class="col-xl-2 col-md-3 col-sm-6">
<label><input type="checkbox" name="is" value="true" checked/> ENTSO-E</label>
</div>
<div class="col-xl-6 col-md-8">
<label><input type="checkbox" name="ic" value="true"/> Include Secrets (SSID, PSK, passwords and tokens)</label>
</div>
</div>
<div class="row">
<div class="col-12 text-right">
<button class="btn btn-primary">Download</button>
</div>
</div>
</div>
</form>
<form method="post" enctype="multipart/form-data" class="upload-form">
<div class="my-3 p-3 bg-white rounded shadow">
<h6>Upload configuration</h6>
<div class="row">
<div class="col-lg-6">
<div class="input-group mb-3">
<div class="input-group-prepend">
<span class="input-group-text">Upload</span>
</div>
<div class="custom-file">
<input name="file" type="file" class="custom-file-input" id="fileUploadField">
<label class="custom-file-label" for="fileUploadField">Choose file</label>
</div>
</div>
</div>
</div>
<div class="row">
<div class="col-12 text-right">
<button class="btn btn-primary">Upload</button>
</div>
</div>
</div>
</form>
<hr/>
<div class="row form-group">
<div class="col-6">
<a href="/" class="btn btn-outline-secondary">Back</a>
</div>
</div>

View File

@@ -1,60 +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" : %u,
"m" : %u,
"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
}
},
"c" : %u
}

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,43 +0,0 @@
<div class="alert alert-danger">
!!WARNING!!<br/>
Telnet debugging is not considered safe and should be switched off when not in use.<br/>
<br/>
!!WARNING!!<br/>
Enabling debugging can cause sudden reboots. Do not leave this on unless you are debugging!
</div>
<form method="post" action="/save">
<input type="hidden" name="debugConfig" value="true"/>
<div class="my-3 p-3 bg-white rounded shadow">
<h6>Debugging</h6>
<div class="row">
<div class="col-xl-2 col-md-3">
<label><input type="checkbox" name="debugTelnet" value="true" %s/> Telnet debugger</label>
</div>
<div class="col-xl-2 col-md-3">
<label><input type="checkbox" name="debugSerial" value="true" %s/> Serial debugger</label>
</div>
<div class="col-xl-3 col-md-4">
<div class="row form-group">
<label class="col-6">Debug level</label>
<div class="col-6">
<select class="form-control form-control-sm" name="debugLevel">
<option value="1" %s>Verbose</option>
<option value="2" %s>Debug</option>
<option value="3" %s>Info</option>
<option value="4" %s>Warning</option>
</select>
</div>
</div>
</div>
</div>
</div>
<hr/>
<div class="row form-group">
<div class="col-6">
<a href="/" class="btn btn-outline-secondary">Back</a>
</div>
<div class="col-6 text-right">
<button class="btn btn-primary">Save</button>
</div>
</div>
</form>

View File

@@ -1,14 +0,0 @@
<form method="post">
<div class="my-3 p-3 bg-white rounded shadow">
<div class="alert alert-warning">Are you sure you want to delete this file?</div>
</div>
<hr/>
<div class="row form-group">
<div class="col-6">
<a href="javascript:history.back();" class="btn btn-outline-secondary">Back</a>
</div>
<div class="col-6 text-right">
<button class="btn btn-danger">Delete</button>
</div>
</div>
</form>

View File

@@ -1,48 +0,0 @@
<form method="post" action="/save">
<input type="hidden" name="dc" value="true"/>
<div class="my-3 p-3 bg-white rounded shadow">
<div class="d-flex flex-row flex-wrap">
<div class="m-2 input-group input-group-sm" style="width: 200px;">
<div class="input-group-prepend">
<span class="input-group-text">Electricity IDX</span>
</div>
<input type="number" class="form-control" name="elidx" value="%d" min="0" max="65535"/>
</div>
<div class="m-2 input-group input-group-sm" style="width: 240px;">
<div class="input-group-prepend">
<span class="input-group-text">Current (3 Phase) IDX</span>
</div>
<input type="number" class="form-control" name="cl1idx" value="%d" min="0" max="65535"/>
</div>
</div>
<div class="d-flex flex-row flex-wrap">
<div class="m-2 input-group input-group-sm" style="width: 200px;">
<div class="input-group-prepend">
<span class="input-group-text">Voltage L1 IDX</span>
</div>
<input type="number" class="form-control" name="vl1idx" value="%d" min="0" max="65535"/>
</div>
<div class="m-2 input-group input-group-sm" style="width: 200px;">
<div class="input-group-prepend">
<span class="input-group-text">Voltage L2 IDX</span>
</div>
<input type="number" class="form-control" name="vl2idx" value="%d" min="0" max="65535"/>
</div>
<div class="m-2 input-group input-group-sm" style="width: 200px;">
<div class="input-group-prepend">
<span class="input-group-text">Voltage L3 IDX</span>
</div>
<input type="number" class="form-control" name="vl3idx" value="%d" min="0" max="65535"/>
</div>
</div>
</div>
<hr/>
<div class="row form-group">
<div class="col-6">
<a href="/mqtt" class="btn btn-outline-secondary">Back</a>
</div>
<div class="col-6 text-right">
<button class="btn btn-primary">Save</button>
</div>
</div>
</form>

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

View File

@@ -1,19 +0,0 @@
<div id="newVersion" class="alert alert-info d-none">New version <span id="newVersionTag"></span>!
<a id="newVersionUrl" href="#" target="_blank">view</a> or <a href="javascript:upgrade();">click here to upgrade</a>
</div>
</main>
<div id="loading-indicator" style="position: absolute; top: 0; left: 0; width: 100%; height: 100%; background-color: #dddddd99; z-index: 999999; padding-top: 20%; display: none;" class="text-center">
<div class="spinner-border text-primary" role="status" style="width: 5rem; height: 5rem;">
<span class="sr-only">Loading...</span>
</div>
</div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.29.1/moment.min.js" integrity="sha512-qTXRIMyZIFb8iQcfjXWCO8+M5Tbc38Qi5WzdPOYZHIlZpzBHG3L3by84BBBOiRGiEb7KKtAOAs5qYdUiZiQNNQ==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
<script src="https://code.jquery.com/jquery-3.6.0.min.js" integrity="sha256-/xUj+3OJU5yExlq6GSYGSHk7tPXikynS7ogEvDej/m4=" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.min.js" integrity="sha384-QJHtvGhmr9XOIpI6YVutG+2QOK9T+ZnN4kzFN1RtK3zEFEIsxhlmWl5/YESvpZ13" crossorigin="anonymous"></script>
<script src="https://www.gstatic.com/charts/loader.js"></script>
<script src="application-${version}.js"></script>
</body>
</html>

View File

@@ -1,110 +0,0 @@
<div class="alert alert-warning">!!WARNING!!<br/>Do not change anything here unless you know exactly what you are doing! Changing things here could cause the device to stop responding</div>
<form method="post" action="/save">
<input type="hidden" name="gc" value="true"/>
<div class="my-3 p-3 bg-white rounded shadow">
<h6>GPIO settings</h6>
<div class="d-flex flex-row flex-wrap">
<div class="m-2 input-group input-group-sm" style="width: 150px;">
<div class="input-group-prepend">
<span class="input-group-text">HAN</span>
</div>
<select name="h" class="form-control">
${h}
</select>
</div>
<div class="m-2 input-group input-group-sm" style="width: 150px;">
<div class="input-group-prepend">
<span class="input-group-text">LED</span>
</div>
<input name="l" type="number" min="2" max="${g}" class="form-control" value="${l}"/>
<div class="input-group-append" title="Inverted">
<label class="input-group-text">
<input type="checkbox" name="i" value="true" ${i}/> inv
</label>
</div>
</div>
<div class="m-2 input-group input-group-sm" style="width: 250px;">
<div class="input-group-prepend">
<span class="input-group-text">RGB</span>
</div>
<input name="r" type="number" min="2" max="${g}" class="form-control" value="${r}"/>
<input name="e" type="number" min="2" max="${g}" class="form-control" value="${e}"/>
<input name="b" type="number" min="2" max="${g}" class="form-control" value="${b}"/>
<div class="input-group-append" title="Inverted">
<label class="input-group-text">
<input type="checkbox" name="n" value="true" ${n}/> inv
</label>
</div>
</div>
<div class="m-2 input-group input-group-sm" style="width: 130px;">
<div class="input-group-prepend">
<span class="input-group-text">AP button</span>
</div>
<input name="a" type="number" min="0" max="${g}" class="form-control" value="${a}"/>
</div>
<div class="m-2 input-group input-group-sm" style="width: 150px;">
<div class="input-group-prepend">
<span class="input-group-text">Temperature</span>
</div>
<input name="t" type="number" min="0" max="${g}" class="form-control" value="${t}"/>
</div>
<div class="m-2 input-group input-group-sm" style="width: 150px;">
<div class="input-group-prepend">
<span class="input-group-text">Analog temp</span>
</div>
<input name="m" type="number" min="0" max="${g}" class="form-control" value="${m}"/>
</div>
<div class="m-2 input-group input-group-sm" style="width: 100px;">
<div class="input-group-prepend">
<span class="input-group-text">Vcc</span>
</div>
<input name="v" type="number" min="0" max="${g}" class="form-control" value="${v}"/>
</div>
<div class="m-2 input-group input-group-sm" style="width: 200px;">
<div class="input-group-prepend">
<span class="input-group-text">GND resistor</span>
</div>
<input type="number" min="1" max="1000" step="1" class="form-control" name="d" value="${d}" />
<div class="input-group-append" title="Inverted">
<label class="input-group-text">k&ohm;</label>
</div>
</div>
<div class="m-2 input-group input-group-sm" style="width: 190px;">
<div class="input-group-prepend">
<span class="input-group-text">Vcc resistor</span>
</div>
<input type="number" min="1" max="1000" step="1" class="form-control" name="s" value="${s}" />
<div class="input-group-append" title="Inverted">
<label class="input-group-text">k&ohm;</label>
</div>
</div>
<div class="m-2 input-group input-group-sm" style="width: 140px;">
<div class="input-group-prepend">
<span class="input-group-text">Multiplier</span>
</div>
<input type="number" min="0.1" max="10" step="0.01" class="form-control" name="u" value="${u}" />
</div>
<div class="m-2 input-group input-group-sm" style="width: 120px;">
<div class="input-group-prepend">
<span class="input-group-text">Offset</span>
</div>
<input type="number" min="0.0" max="3.5" step="0.01" class="form-control" name="o" value="${o}" />
</div>
<div class="m-2 input-group input-group-sm" style="width: 130px;">
<div class="input-group-prepend">
<span class="input-group-text">Boot limit</span>
</div>
<input type="number" min="2.5" max="3.5" step="0.1" class="form-control" name="c" value="${c}" />
</div>
</div>
</div>
<hr/>
<div class="row form-group">
<div class="col-6">
<a href="/" class="btn btn-outline-secondary">Back</a>
</div>
<div class="col-6 text-right">
<button class="btn btn-primary">Save</button>
</div>
</div>
</form>

View File

@@ -1,104 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>AMS reader</title>
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@4.6.1/dist/css/bootstrap.min.css" integrity="sha384-zCbKRCUGaJDkqS1kPbPd7TveP5iyJE0EjAuZQTgFLD2ylzuqKfdKlfG/eSrtxUkn" crossorigin="anonymous" referrerpolicy="no-referrer">
<style>
.navbar-expand .navbar-nav .nav-link,.navbar-brand {
padding-top: 0px;
padding-bottom: 0px;
margin-top: 0px;
margin-bottom: 0px;
}
.plot1 {
width: 100%;
height: 200px;
}
.plot2 {
width: 100%;
height: 224px;
}
.overlay-plot {
position: relative;
}
.plot-overlay {
position: absolute;
top: 70px;
left: 25%;
width: 50%;
text-align: center;
}
.ipo,.epo {
font-size: 1.7rem;
cursor: pointer;
}
.ipoa,.epoa {
font-size: 1.0rem;
color: grey;
}
.pol {
font-size: 1.0rem;
}
</style>
</head>
<body class="bg-light">
<main role="main" class="container">
<header class="navbar navbar-expand navbar-dark flex-column flex-lg-row rounded shadow mt-2 mb-3" style="background-color: var(--purple);">
<a href="/" class="">
<h6 class="navbar-brand">AMS reader <small id="swVersion" data-url="https://api.github.com/repos/UtilitechAS/amsreader-firmware/releases">${version}</small></h6>
</a>
<div class="navbar-nav-scroll">
<ul class="navbar-nav bd-navbar-nav flex-row">
<li class="nav-item">
<div class="dropdown">
<a class="dropdown-toggle nav-link" href="#" role="button" id="config-link" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
Config<span class="d-none d-md-inline d-lg-none d-xl-inline">uration</span>
</a>
<div class="dropdown-menu" aria-labelledby="config-link">
<a class="dropdown-item" href="/meter">Meter</a>
<a class="dropdown-item" href="/wifi">WiFi</a>
<a class="dropdown-item" href="/mqtt">MQTT</a>
<a class="dropdown-item" href="/web">Web</a>
<a class="dropdown-item" href="/ntp">NTP</a>
<a class="dropdown-item" href="/priceapi">Price API</a>
<div class="dropdown-divider"></div>
<a class="dropdown-item" href="https://github.com/UtilitechAS/amsreader-firmware/wiki" target="_blank">Documentation</a>
</div>
</div>
</li>
<li class="nav-item">
<div class="dropdown">
<a class="dropdown-toggle nav-link" href="#" role="button" id="system-link" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
Sys<span class="d-none d-sm-inline">tem</span>
</a>
<div class="dropdown-menu" aria-labelledby="system-link">
<a class="dropdown-item" href="/gpio">GPIO</a>
<a class="dropdown-item" href="/debugging">Debugging</a>
<a class="dropdown-item" href="/configfile">Configuration file</a>
<a class="dropdown-item" href="/firmware">Firmware</a>
<div class="dropdown-divider"></div>
<a class="dropdown-item" href="/restart">Restart</a>
<a class="dropdown-item text-danger" href="/reset">Factory reset</a>
</div>
</div>
</li>
</ul>
</div>
<div class="flex-row">
<div id="esp" class="d-none m-2">ESP</div>
<div id="han" class="d-none m-2">HAN</div>
<div id="wifi" class="d-none m-2">WiFi</div>
<div id="mqtt" class="d-none m-2">MQTT</div>
</div>
<ul class="navbar-nav flex-row ml-md-auto d-none d-lg-flex">
<li class="nav-item">
<a class="nav-link p-2" href="https://github.com/UtilitechAS/amsreader-firmware" target="_blank" rel="noopener" aria-label="GitHub">
<img class="d-inline-block align-text-top" style="width: 2rem; height: 2rem;" src="github.svg"/>
</a>
</li>
</ul>
</header>

View File

@@ -1,210 +0,0 @@
<div class="bg-white rounded shadow p-1 mb-3">
<div class="d-flex flex-wrap">
<div class="flex-fill">
<div class="text-center">Up <span class="ju">-</span></div>
</div>
<div class="flex-fill rt">
<div class="text-center">Temperature: <span class="jt">-</span>&deg;C</div>
</div>
<div class="flex-fill rv">
<div class="text-center">ESP volt: <span class="jv">-</span>V</div>
</div>
<div class="flex-fill">
<div class="text-center">WiFi RSSI: <span class="jr">-</span>dBm</div>
</div>
<div class="flex-fill">
<div class="text-center">Free mem: <span class="jm">-</span>kb</div>
</div>
<div class="flex-fill rc">
<div class="text-center"><span class="jc"></span></div>
</div>
</div>
</div>
<div class="row">
<div class="col-lg-3 col-sm-6 mb-3">
<div class="bg-white rounded shadow p-3">
<div class="text-center overlay-plot">
<div id="ip" class="plot1"></div>
<span class="plot-overlay">
<span class="ipo">-</span>
<span class="ipoa">W</span>
<br/>
<span class="pol">Import</span>
</span>
</div>
<div class="row ric" style="display: none;">
<div class="col-5"><span class="jmt"></span></div>
<div class="col-7 text-right"><span class="jic">-</span> kWh</div>
</div>
</div>
</div>
<div class="col-lg-3 col-sm-6 mb-3 rex" style="display: none;">
<div class="bg-white rounded shadow p-3">
<div class="text-center overlay-plot">
<div id="xp" class="plot1"></div>
<span class="plot-overlay">
<span class="epo">-</span>
<span class="epoa">W</span>
<br/>
<span class="pol">Export</span>
</span>
</div>
<div class="row rec" style="display: none;">
<div class="col-12 text-right"><span class="jec">-</span> kWh</div>
</div>
</div>
</div>
<div class="col-lg-3 col-sm-6 mb-3">
<div class="bg-white rounded shadow p-3">
<div class="text-center">
<div id="vp" class="plot2"></div>
</div>
</div>
</div>
<div class="col-lg-3 col-sm-6 mb-3">
<div class="bg-white rounded shadow p-3">
<div class="text-center">
<div id="ap" class="plot2"></div>
</div>
</div>
</div>
<div class="col-lg-3 col-sm-6 mb-3 rim" style="display: none;">
<div class="bg-white rounded shadow p-3">
<h5 class="text-center">Reactive</h5>
<div class="row rri">
<div class="col-12 font-weight-bold">Instant</div>
<div class="col-4">In</div>
<div class="col-8 text-right"><span class="jri">-</span> VAr</div>
<div class="col-4">Out</div>
<div class="col-8 text-right"><span class="jre">-</span> VAr</div>
</div>
<div class="row rric">
<div class="col-12 font-weight-bold">Total</div>
<div class="col-4">In</div>
<div class="col-8 text-right"><span class="jric">-</span> kVArh</div>
<div class="col-4">Out</div>
<div class="col-8 text-right"><span class="jrec">-</span> kVArh</div>
</div>
</div>
</div>
<div class="col-lg-6 mb-3 rex" style="display: none;">
<div class="bg-white rounded shadow p-3">
<div class="row rrec">
<div class="col-sm-4 font-weight-bold">Instant reactive</div>
<div class="col-4 col-sm-1">In</div>
<div class="col-8 col-sm-3 text-right"><span class="jri">-</span> VAr</div>
<div class="col-4 col-sm-1">Out</div>
<div class="col-8 col-sm-3 text-right"><span class="jre">-</span> VAr</div>
</div>
</div>
</div>
<div class="col-lg-6 mb-3 rex" style="display: none;">
<div class="bg-white rounded shadow p-3">
<div class="row rrec">
<div class="col-sm-4 font-weight-bold">Total reactive</div>
<div class="col-4 col-sm-1">In</div>
<div class="col-8 col-sm-3 text-right"><span class="jric">-</span> kVArh</div>
<div class="col-4 col-sm-1">Out</div>
<div class="col-8 col-sm-3 text-right"><span class="jrec">-</span> kVArh</div>
</div>
</div>
</div>
<div class="col-xl-12 mb-3">
<div class="bg-white rounded shadow pt-3 pb-3" style="font-size: 14px;">
<strong class="mr-3 ml-3">Real time consumption</strong><br/>
<div class="row">
<div class="col-lg-3 col-sm-6">
<div class="mr-3 ml-3 d-flex">
<div>Hour</div>
<div class="flex-fill text-right">
<span id="each"></span> kWh
<span class="sp text-nowrap" style="display: none;">(<span id="eachc"></span> <span class="cr"></span>)</span>
</div>
</div>
</div>
<div class="col-lg-3 col-sm-6">
<div class="mr-3 ml-3 d-flex">
<div>Day</div>
<div class="flex-fill text-right">
<span id="eacd"></span> kWh
<span class="sp text-nowrap" style="display: none;">(<span id="eacdc"></span> <span class="cr"></span>)</span>
</div>
</div>
</div>
<div class="col-lg-3 col-sm-6">
<div class="mr-3 ml-3 d-flex">
<div>Month</div>
<div class="flex-fill text-right">
<span id="eacm"></span> kWh
<span class="sp text-nowrap" style="display: none;">(<span id="eacmc"></span> <span class="cr"></span>)</span>
</div>
</div>
</div>
<div class="col-lg-3 col-sm-6">
<div class="mr-3 ml-3 d-flex">
<div>Max</div>
<div class="flex-fill text-right">
<span id="eax"></span> / <span id="eat"></span> kWh
</div>
</div>
</div>
</div>
<strong class="mr-3 ml-3 se d-none">Real time production</strong><br/>
<div class="row se d-none">
<div class="col-lg-3 col-sm-6">
<div class="mr-3 ml-3 d-flex">
<div>Hour</div>
<div class="flex-fill text-right">
<span id="eache"></span> kWh
</div>
</div>
</div>
<div class="col-lg-3 col-sm-6">
<div class="mr-3 ml-3 d-flex">
<div>Day</div>
<div class="flex-fill text-right">
<span id="eacde"></span> kWh
</div>
</div>
</div>
<div class="col-lg-3 col-sm-6">
<div class="mr-3 ml-3 d-flex">
<div>Month</div>
<div class="flex-fill text-right">
<span id="eacme"></span> kWh
</div>
</div>
</div>
<div class="col-lg-3 col-sm-6"></div>
</div>
</div>
</div>
<div id="ppc" class="col-xl-12 mb-3" style="display: none;">
<div class="bg-white rounded shadow" id="pp" style="width: 100%; height: 224px;"></div>
</div>
<div class="col-xl-12 mb-3">
<div class="bg-white rounded shadow" id="ep" style="width: 100%; height: 224px;"></div>
</div>
<div class="col-xl-12 mb-3">
<div class="bg-white rounded shadow" id="mp" style="width: 100%; height: 224px;"></div>
</div>
<div id="tpc" class="col-xl-12 mb-3" style="display: none;">
<div class="bg-white rounded shadow pb-3">
<div id="tp" style="width: 100%; height: 224px;"></div>
<a class="m-4" href="/temperature">Configuration</a>
</div>
</div>
<div class="col-lg-3 col-sm-6 mb-3 d-none me me-1 me-2 me-3 me-4 me-5 me-6 me-7 me-8 me-9 me-10 me-11 me-12 me-13">
<div class="d-none badge badge-danger me me-1 me-2 me-5 me-6 me-7 me-8 me-9 me-12">MQTT communication error (<span id="ml">-</span>)</div>
<div class="d-none badge badge-danger me me-3">MQTT failed to connect</div>
<div class="d-none badge badge-danger me me-4">MQTT network timeout</div>
<div class="d-none badge badge-danger me me-10">MQTT connection denied</div>
<div class="d-none badge badge-danger me me-11">MQTT failed to subscribe</div>
<div class="d-none badge badge-danger me me-13">MQTT lost connection</div>
</div>
</div>

View File

@@ -1,143 +0,0 @@
<form method="post" action="/save">
<input type="hidden" name="mc" value="true"/>
<div class="my-3 p-3 bg-white rounded shadow">
<h6>Meter</h6>
<div class="row">
<div class="col-lg-3 col-md-4 col-sm-6">
<div class="m-2 input-group input-group-sm">
<div class="input-group-prepend">
<span class="input-group-text">Manufacturer</span>
</div>
<input class="form-control" value="{maf}" disabled/>
</div>
</div>
<div class="col-lg-3 col-md-4 col-sm-6">
<div class="m-2 input-group input-group-sm">
<div class="input-group-prepend">
<span class="input-group-text">Model</span>
</div>
<input class="form-control" value="{mod}" disabled/>
</div>
</div>
<div class="col-lg-3 col-md-4 col-sm-6">
<div class="m-2 input-group input-group-sm">
<div class="input-group-prepend">
<span class="input-group-text">ID</span>
</div>
<input class="form-control" value="{mid}" disabled/>
</div>
</div>
</div>
<div class="row">
<div class="col-lg-3 col-md-4 col-sm-5">
<div class="m-2 input-group input-group-sm">
<div class="input-group-prepend">
<span class="input-group-text">Baud rate</span>
</div>
<select class="form-control sd" name="b">
<option value="300" {b300}>300</option>
<option value="2400" {b2400}>2400</option>
<option value="4800" {b4800}>4800</option>
<option value="9600" {b9600}>9600</option>
<option value="19200" {b19200}>19200</option>
<option value="38400" {b38400}>38400</option>
<option value="57600" {b57600}>57600</option>
<option value="115200" {b115200}>115200</option>
</select>
</div>
</div>
<div class="col-lg-2 col-md-3 col-sm-4 col-6">
<div class="m-2 input-group input-group-sm">
<div class="input-group-prepend">
<span class="input-group-text">Parity</span>
</div>
<select class="form-control sd" name="c">
<option value="2" {c2}>7N1</option>
<option value="3" {c3}>8N1</option>
<option value="10" {c10}>7E1</option>
<option value="11" {c11}>8E1</option>
</select>
</div>
</div>
<div class="col-lg-2 col-md-4 col-sm-3 col-6">
<div class="m-2">
<label class="small"><input type="checkbox" name="i" value="true" {i}/> Invert <span class="d-none d-md-inline">signal</span></label>
</div>
</div>
<div class="col-lg-4 col-sm-8">
<a href="https://github.com/UtilitechAS/amsreader-firmware/wiki/Known-hardware-configurations" target="_blank">Known hardware configurations</a>
</div>
</div>
<div class="row">
<div class="col-xl-3 col-lg-4 col-md-5 col-sm-7">
<div class="m-2 input-group input-group-sm">
<div class="input-group-prepend">
<span class="input-group-text">Distribution system</span>
</div>
<select class="form-control sd" name="d">
<option value="0" {d0}></option>
<option value="1" {d1}>IT or TT (230V)</option>
<option value="2" {d2}>TN (400V)</option>
</select>
</div>
</div>
<div class="col-lg-3 col-md-4 col-sm-5">
<div class="m-2 input-group input-group-sm">
<div class="input-group-prepend">
<span class="input-group-text">Main fuse</span>
</div>
<input class="form-control text-right" name="f" type="number" min="5" max="255" step="1" value="{f}"/>
<div class="input-group-append">
<span class="input-group-text">A</span>
</div>
</div>
</div>
<div class="col-xl-3 col-lg-4 col-md-5 col-sm-7">
<div class="m-2 input-group input-group-sm">
<div class="input-group-prepend">
<span class="input-group-text">Production capacity</span>
</div>
<input class="form-control text-right" name="p" type="number" min="0" max="255" step="1" value="{p}"/>
<div class="input-group-append">
<span class="input-group-text">kWp</span>
</div>
</div>
</div>
</div>
<div class="row">
<div class="col-lg-6">
<div class="m-2 input-group input-group-sm">
<div class="input-group-prepend">
<span class="input-group-text">Encryption key</span>
</div>
<input class="form-control" name="e" type="text" value="{e}" placeholder="If applicable"/>
</div>
</div>
<div class="col-lg-6">
<div class="m-2 input-group input-group-sm">
<div class="input-group-prepend">
<span class="input-group-text">Authentication key</span>
</div>
<input class="form-control" name="a" type="text" value="{a}" placeholder="If applicable"/>
</div>
</div>
</div>
<div class="row mt-3">
<div class="col-sm-6">
<a href="/thresholds">Configure tariff thresholds</a>
</div>
<div class="col-sm-6 text-right">
<a href="/meteradvanced">Multipliers</a>
</div>
</div>
</div>
<hr/>
<div class="row form-group">
<div class="col-6">
<a href="/" class="btn btn-outline-secondary">Back</a>
</div>
<div class="col-6 text-right">
<button class="btn btn-primary">Save</button>
</div>
</div>
</form>

View File

@@ -1,49 +0,0 @@
<form method="post" action="/save">
<input type="hidden" name="ma" value="true"/>
<div class="my-3 p-3 bg-white rounded shadow">
<h6>Multipliers</h6>
<div class="row">
<div class="col-lg-3 col-md-4 col-sm-6">
<div class="m-2 input-group input-group-sm">
<div class="input-group-prepend">
<span class="input-group-text">Instant</span>
</div>
<input type="number" class="form-control text-right" name="wm" value="%.2f" min="0.00" max="655.35" step="0.01"/>
</div>
</div>
<div class="col-lg-3 col-md-4 col-sm-6">
<div class="m-2 input-group input-group-sm">
<div class="input-group-prepend">
<span class="input-group-text">Voltage</span>
</div>
<input type="number" class="form-control text-right" name="vm" value="%.2f" min="0.00" max="655.35" step="0.01"/>
</div>
</div>
<div class="col-lg-3 col-md-4 col-sm-6">
<div class="m-2 input-group input-group-sm">
<div class="input-group-prepend">
<span class="input-group-text">Amperage</span>
</div>
<input type="number" class="form-control text-right" name="am" value="%.2f" min="0.00" max="655.35" step="0.01"/>
</div>
</div>
<div class="col-lg-3 col-md-4 col-sm-6">
<div class="m-2 input-group input-group-sm">
<div class="input-group-prepend">
<span class="input-group-text">Accumulated</span>
</div>
<input type="number" class="form-control text-right" name="cm" value="%.2f" min="0.00" max="655.35" step="0.01"/>
</div>
</div>
</div>
</div>
<hr/>
<div class="row form-group">
<div class="col-6">
<a href="/meter" class="btn btn-outline-secondary">Back</a>
</div>
<div class="col-6 text-right">
<button class="btn btn-primary">Save</button>
</div>
</div>
</form>

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,138 +0,0 @@
<form method="post" action="/save">
<input type="hidden" name="mqc" value="true"/>
<div class="my-3 p-3 bg-white rounded shadow">
<h6>MQTT</h6>
<label class="m-2"><input id="m" type="checkbox" name="m" value="true" {m}/> Enable</label>
<div class="row">
<div class="col-xl-4 col-lg-5 col-md-6">
<div class="m-2 input-group input-group-sm">
<div class="input-group-prepend">
<span class="input-group-text">Host</span>
</div>
<input type="text" class="form-control mc" name="h" value="{h}" maxlength="127"/>
</div>
</div>
<div class="col-xl-2 col-lg-3 col-md-3 col-sm-4">
<div class="m-2 input-group input-group-sm">
<div class="input-group-prepend">
<span class="input-group-text">Port</span>
</div>
<input id="p" type="number" class="form-control mc" name="p" value="{p}" min="1024" max="65535" placeholder="1883"/>
</div>
</div>
<div class="col-xl-3 col-lg-4 col-md-6 col-sm-8">
<div class="m-2 input-group input-group-sm">
<div class="input-group-prepend">
<span class="input-group-text">Client ID</span>
</div>
<input type="text" class="form-control mc" name="i" value="{i}" maxlength="31"/>
</div>
</div>
<div class="col-xl-3 col-lg-4 col-md-6">
<div class="m-2 input-group input-group-sm">
<div class="input-group-prepend">
<span class="input-group-text">Publish topic</span>
</div>
<input type="text" class="form-control mc" name="t" value="{t}" maxlength="63"/>
</div>
</div>
<div class="col-xl-3 col-lg-4 col-md-6 col-sm-6">
<div class="m-2 input-group input-group-sm">
<div class="input-group-prepend">
<span class="input-group-text">Username</span>
</div>
<input type="text" class="form-control mc" name="u" value="{u}" maxlength="127"/>
</div>
</div>
<div class="col-xl-3 col-lg-4 col-md-6 col-sm-6">
<div class="m-2 input-group input-group-sm">
<div class="input-group-prepend">
<span class="input-group-text">Password</span>
</div>
<input type="password" class="form-control mc" name="pw" value="{pw}" maxlength="255"/>
</div>
</div>
<div class="col-lg-3 col-md-4 col-sm-6">
<div class="m-2 input-group input-group-sm">
<div class="input-group-prepend">
<span class="input-group-text">Payload</span>
</div>
<select id="f" class="form-control mc" name="f">
<option value="0" {f0}>JSON</option>
<option value="1" {f1}>Raw values (minimal)</option>
<option value="2" {f2}>Raw values (full)</option>
<option value="3" {f3}>Domoticz</option>
<option value="4" {f4}>Home-Assistant</option>
<option value="255" {f255}>Raw data (bytes)</option>
</select>
</div>
</div>
<div class="col-md-3 col-sm-6 f3-s">
<div class="m-2">
<a href="/domoticz" class="btn btn-sm btn-outline-secondary">Configuration</a>
</div>
</div>
</div>
</div>
<div class="my-3 p-3 bg-white rounded shadow">
<div class="row">
<div class="col-md-2">
<label class="m-2"><input id="s" type="checkbox" name="s" value="true" {s}/> SSL</label>
</div>
<div class="col-lg-2 col-md-3">
<div class="m-2 input-group input-group-sm">
<div class="input-group-prepend">
<span class="input-group-text">CA</span>
</div>
<div class="input-group-append">
<span style="display: {dcu};">
<a href="/mqtt-ca" class="btn btn-sm btn-outline-secondary">Upload</a>
</span>
<span style="display: {dcf};">
<a href="/mqtt-ca" class="btn btn-sm btn-danger">Delete</a>
</span>
</div>
</div>
</div>
<div class="col-xl-2 col-md-3">
<div class="m-2 input-group input-group-sm">
<div class="input-group-prepend">
<span class="input-group-text">Certificate</span>
</div>
<div class="input-group-append">
<span style="display: {deu};">
<a href="/mqtt-cert" class="btn btn-sm btn-outline-secondary">Upload</a>
</span>
<span style="display: {def};">
<a href="/mqtt-cert" class="btn btn-sm btn-danger">Delete</a>
</span>
</div>
</div>
</div>
<div class="col-xl-2 col-md-4">
<div class="m-2 input-group input-group-sm">
<div class="input-group-prepend">
<span class="input-group-text">Private key</span>
</div>
<div class="input-group-append">
<span style="display: {dku};">
<a href="/mqtt-key" class="btn btn-sm btn-outline-secondary">Upload</a>
</span>
<span style="display: {dkf};">
<a href="/mqtt-key" class="btn btn-sm btn-danger">Delete</a>
</span>
</div>
</div>
</div>
</div>
</div>
<hr/>
<div class="row form-group">
<div class="col-6">
<a href="/" class="btn btn-outline-secondary">Back</a>
</div>
<div class="col-6 text-right">
<button class="btn btn-primary">Save</button>
</div>
</div>
</form>

View File

@@ -1 +0,0 @@
<div class="alert alert-danger">Page not found</div>

View File

@@ -1,54 +0,0 @@
<form method="post" action="/save">
<input type="hidden" name="nc" value="true"/>
<div class="my-3 p-3 bg-white rounded shadow">
<h6>NTP</h6>
<label class="m-2"><input id="n" type="checkbox" name="n" value="true" %s/> Enable</label>
<div class="row">
<div class="col-lg-3 col-md-4 col-sm-5">
<div class="m-2 input-group input-group-sm">
<div class="input-group-prepend">
<span class="input-group-text">Timezone</span>
</div>
<select id="o" class="form-control nc" name="o">
<option value="0" %s>UTC</option>
<option value="3600" %s>UTC+1</option>
<option value="7200" %s>UTC+2</option>
</select>
</div>
</div>
<div class="col-xl-3 col-lg-4 col-md-5 col-sm-7">
<div class="m-2 input-group input-group-sm">
<div class="input-group-prepend">
<span class="input-group-text">Summertime offset</span>
</div>
<select id="so" class="form-control nc" name="so">
<option value="0" %s>Disabled</option>
<option value="3600" %s>+1hr</option>
</select>
</div>
</div>
<div class="col-xl-4 col-lg-5 col-md-6">
<div class="m-2 input-group input-group-sm">
<div class="input-group-prepend">
<span class="input-group-text">Server</span>
</div>
<input type="text" class="form-control nc" name="ns" value="%s" maxlength="64"/>
</div>
</div>
<div class="col-lg-4 col-md-5 col-sm-6">
<div class="m-2">
<label class="small"><input type="checkbox" name="nd" value="true" %s class="nc"/> Obtain NTP server from DHCP</label>
</div>
</div>
</div>
</div>
<hr/>
<div class="row form-group">
<div class="col-6">
<a href="/" class="btn btn-outline-secondary">Back</a>
</div>
<div class="col-6 text-right">
<button class="btn btn-primary">Save</button>
</div>
</div>
</form>

View File

@@ -1,86 +0,0 @@
<form method="post" action="/save">
<input type="hidden" name="ec" value="true"/>
<div class="my-3 p-3 bg-white rounded shadow">
<h6>Price API</h6>
<div class="row">
<div class="col-xl-2 col-lg-3 col-md-4 col-sm-6">
<div class="m-2 input-group input-group-sm">
<div class="input-group-prepend">
<span class="input-group-text">Region</span>
</div>
<select name="ea" class="form-control">
<option value="">None</option>
<optgroup label="Norway">
<option value="10YNO-1--------2" {no1}>NO1</option>
<option value="10YNO-2--------T" {no2}>NO2</option>
<option value="10YNO-3--------J" {no3}>NO3</option>
<option value="10YNO-4--------9" {no4}>NO4</option>
<option value="10Y1001A1001A48H" {no5}>NO5</option>
</optgroup>
<optgroup label="Sweden">
<option value="10Y1001A1001A44P" {se1}>SE1</option>
<option value="10Y1001A1001A45N" {se2}>SE2</option>
<option value="10Y1001A1001A46L" {se3}>SE3</option>
<option value="10Y1001A1001A47J" {se4}>SE4</option>
</optgroup>
<optgroup label="Denmark">
<option value="10YDK-1--------W" {dk1}>DK1</option>
<option value="10YDK-2--------M" {dk2}>DK2</option>
</optgroup>
<option value="10YAT-APG------L" {at}>Austria</option>
<option value="10YBE----------2" {be}>Belgium</option>
<option value="10YCZ-CEPS-----N" {cz}>Czech Republic</option>
<option value="10Y1001A1001A39I" {ee}>Estonia</option>
<option value="10YFI-1--------U" {fi}>Finland</option>
<option value="10YFR-RTE------C" {fr}>France</option>
<option value="10Y1001A1001A83F" {de}>Germany</option>
<option value="10YGB----------A" {gb}>Great Britain</option>
<option value="10YLV-1001A00074" {lv}>Latvia</option>
<option value="10YLT-1001A0008Q" {lt}>Lithuania</option>
<option value="10YNL----------L" {nl}>Netherland</option>
<option value="10YPL-AREA-----S" {pl}>Poland</option>
<option value="10YCH-SWISSGRIDZ" {ch}>Switzerland</option>
</select>
</div>
</div>
<div class="col-xl-2 col-lg-3 col-md-4 col-sm-6">
<div class="m-2 input-group input-group-sm">
<div class="input-group-prepend">
<span class="input-group-text">Currency</span>
</div>
<select name="ecu" class="form-control">
<option value="NOK" {nok}>NOK</option>
<option value="SEK" {sek}>SEK</option>
<option value="DKK" {dkk}>DKK</option>
<option value="EUR" {eur}>EUR</option>
</select>
</div>
</div>
<div class="col-xl-2 col-lg-3 col-md-4 col-sm-6">
<div class="m-2 input-group input-group-sm">
<div class="input-group-prepend">
<span class="input-group-text">Multiplier</span>
</div>
<input name="em" type="number" min="0.001" max="1000" step="0.001" class="form-control" value="{em}"/>
</div>
</div>
<div class="col-xl-4 col-lg-6 col-md-8 {dt}">
<div class="m-2 input-group input-group-sm">
<div class="input-group-prepend">
<span class="input-group-text">ENTSO-E token</span>
</div>
<input type="text" name="et" class="form-control" value="{et}" placeholder="Optional"/>
</div>
</div>
</div>
</div>
<hr/>
<div class="row form-group">
<div class="col-6">
<a href="/" class="btn btn-outline-secondary">Back</a>
</div>
<div class="col-6 text-right">
<button class="btn btn-primary">Save</button>
</div>
</div>
</form>

View File

@@ -1,14 +0,0 @@
<form method="post">
<div class="my-3 p-3 bg-white rounded shadow">
<div class="alert alert-danger">Are you sure you want reset this device to factory settings? ALL configuration will be erased!</div>
</div>
<hr/>
<div class="row form-group">
<div class="col-6">
<a href="javascript:history.back();" class="btn btn-outline-secondary">Back</a>
</div>
<div class="col-6 text-right">
<button class="btn btn-danger" name="perform" value="true">Perform factory reset</button>
</div>
</div>
</form>

View File

@@ -1,14 +0,0 @@
<form method="post">
<div class="my-3 p-3 bg-white rounded shadow">
<div class="alert alert-warning">Are you sure you want restart?</div>
</div>
<hr/>
<div class="row form-group">
<div class="col-6">
<a href="javascript:history.back();" class="btn btn-outline-secondary">Back</a>
</div>
<div class="col-6 text-right">
<button class="btn btn-success" name="perform" value="true">Restart</button>
</div>
</div>
</form>

View File

@@ -1,63 +0,0 @@
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>AMS reader - Restarting, please wait</title>
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<link rel="stylesheet" type="text/css" href="boot.css"/>
</head>
<body class="bg-light">
<main role="main" class="container">
<header class="navbar navbar-expand navbar-dark flex-column flex-md-row bg-purple rounded mt-2 mb-4" style="background-color: var(--purple);">
<a href="/" class=""><h6 class="navbar-brand">AMS reader <small>${version}</small></h6></a>
<ul class="navbar-nav flex-row ml-md-auto d-none d-md-flex">
<li class="nav-item">
<a class="nav-link p-2" href="https://github.com/UtilitechAS/amsreader-firmware" target="_blank" rel="noopener" aria-label="GitHub">
<img class="d-inline-block align-text-top" style="width: 2rem; height: 2rem;" src="github.svg"/>
</a>
</li>
</ul>
</header>
<div class="my-3 p-3 bg-white rounded shadow {rs}">
Device is rebooting. You will be redirected to the main page when it is available again.
</div>
<div class="alert alert-warning shadow {us}">Firmware upgrade in progress, DO NOT disconnect power from the device. You will be redirected to the main page when firmware upgrade is complete.</div>
</main>
<script>
var tries = 0;
var ip = "${ip}";
var hostname = "${hostname}";
var url;
var fetch = function() {
tries++;
if(ip && tries%3 == 0) {
url = "http://" + ip;
} else if(hostname && tries%3 == 1) {
url = "http://" + hostname;
} else if(hostname && tries%3 == 2) {
url = "http://" + hostname + ".local";
} else {
url = "";
}
if(console) console.log("Trying url " + url);
var retry = function() {
setTimeout(fetch, 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();
};
setTimeout(fetch, 10000);
</script>
</body>
</html>

View File

@@ -1,138 +0,0 @@
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>AMS reader - Setup</title>
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<link rel="stylesheet" type="text/css" href="boot.css"/>
</head>
<body class="bg-light">
<main role="main" class="container">
<header class="navbar navbar-expand navbar-dark flex-column flex-md-row bg-purple rounded mt-2 mb-4">
<a href="/" class=""><h6 class="navbar-brand">AMS reader <small>${version}</small></h6></a>
<ul class="navbar-nav flex-row ml-md-auto d-none d-md-flex">
<li class="nav-item">
<a class="nav-link p-2" href="https://github.com/UtilitechAS/amsreader-firmware" target="_blank" rel="noopener" aria-label="GitHub">
<img style="width: 2rem; height: 2rem;" src="github.svg"/>
</a>
</li>
</ul>
</header>
<form method="post">
<div class="my-3 p-3 bg-white rounded shadow">
<div class="row">
<div class="col-xl-4 col-md-6">
<h5>Hardware</h5>
<select name="board" class="form-control" required>
<option value=""></option>
<optgroup label="Custom hardware">
<option value="7" ${config.boardType7}>Pow-U+ (ESP32) from amsleser.no</option>
<option value="6" ${config.boardType6}>Pow-P1 from amsleser.no</option>
<option value="5" ${config.boardType5}>Pow-K+ (ESP32) from amsleser.no</option>
<option value="4" ${config.boardType4}>Pow-U or Pow-K from amsleser.no (GPIO12)</option>
<option value="3" ${config.boardType3}>Pow-U or Pow-K from amsleser.no (UART0)</option>
<option value="2" ${config.boardType2}>HAN Reader 2.0 by Max Spencer</option>
<option value="1" ${config.boardType1}>Kamstrup module by Egil Opsahl</option>
<option value="0" ${config.boardType0}>Custom hardware by Roar Fredriksen</option>
</optgroup>
<optgroup label="ESP8266">
<option value="101" ${config.boardType101}>Wemos D1</option>
<option value="100" ${config.boardType100}>Generic ESP8266</option>
</optgroup>
<optgroup label="ESP32">
<option value="201" ${config.boardType201}>Wemos LOLIN D32</option>
<option value="202" ${config.boardType202}>Adafruit HUZZAH32</option>
<option value="203" ${config.boardType203}>DevKitC</option>
<option value="200" ${config.boardType200}>Generic ESP32</option>
</optgroup>
<optgroup label="ESP32-S2">
<option value="51" ${config.boardType51}>Wemos S2 mini</option>
<option value="50" ${config.boardType50}>Generic ESP32-S2</option>
</optgroup>
</select>
</div>
</div>
</div>
<div class="my-3 p-3 bg-white rounded shadow">
<h5>WiFi</h5>
<div class="row">
<div class="col-xl-3 col-md-6 form-group">
<div class="input-group">
<div class="input-group-prepend">
<span class="input-group-text">SSID</span>
</div>
<input type="text" name="wifiSsid" class="form-control" maxlength="32" placeholder="Name of your WiFi" required value="${config.wifiSsid}"/>
</div>
</div>
<div class="col-xl-3 col-md-6 form-group">
<div class="input-group">
<div class="input-group-prepend">
<span class="input-group-text">PSK</span>
</div>
<input type="password" name="wifiPassword" class="form-control" maxlength="63" placeholder="Password for WiFi" required value="${config.wifiPassword}"/>
</div>
</div>
<div class="col-xl-4 col-md-6 form-group">
<div class="input-group">
<div class="input-group-prepend">
<span class="input-group-text">Hostname</span>
</div>
<input type="text" name="wifiHostname" class="form-control" maxlength="32" pattern="[a-z0-9_-]+" placeholder="Optional, ex.: ams-reader" value="${config.wifiHostname}"/>
</div>
</div>
<div class="col-xl-2 col-md-6 form-group">
<label><input type="checkbox" name="wifiIpType" value="1" onchange="staticChecked(this);" ${config.wifiStaticIp}/> Static IP</label>
</div>
</div>
<div class="row" id="staticIp">
<div class="col-xl-3 col-lg-4 col-sm-6 form-group">
<div class="input-group">
<div class="input-group-prepend">
<span class="input-group-text">IP</span>
</div>
<input type="text" name="wifiIp" class="form-control" pattern="\d?\d?\d.\d?\d?\d.\d?\d?\d.\d?\d?\d" placeholder="Ex: 192.168.1.200" value="${config.wifiIp}"/>
</div>
</div>
<div class="col-xl-3 col-lg-4 col-sm-6 form-group">
<div class="input-group">
<div class="input-group-prepend">
<span class="input-group-text">Subnet</span>
</div>
<input type="text" name="wifiSubnet" class="form-control" pattern="\d?\d?\d.\d?\d?\d.\d?\d?\d.\d?\d?\d" placeholder="Ex.: 255.255.255.0" value="${config.wifiSubnet}"/>
</div>
</div>
<div class="col-xl-3 col-lg-4 col-sm-6 form-group">
<div class="input-group">
<div class="input-group-prepend">
<span class="input-group-text">Gateway</span>
</div>
<input type="text" name="wifiGw" class="form-control" pattern="\d?\d?\d.\d?\d?\d.\d?\d?\d.\d?\d?\d" placeholder="Ex.: 192.168.1.1" value="${config.wifiGw}"/>
</div>
</div>
<div class="col-xl-3 col-lg-4 col-sm-6 form-group">
<div class="input-group">
<div class="input-group-prepend">
<span class="input-group-text">DNS</span>
</div>
<input type="text" name="wifiDns1" class="form-control" pattern="\d?\d?\d.\d?\d?\d.\d?\d?\d.\d?\d?\d" placeholder="Ex.: 192.168.1.1" value="${config.wifiDns1}"/>
</div>
</div>
</div>
</div>
<hr/>
<div class="row">
<div class="col-6"></div>
<div class="col-6 text-right">
<button type="submit" class="btn btn-primary">Save & reboot</button>
</div>
</div>
</form>
</main>
<script>
document.getElementById('staticIp').style.display = "none";
var staticChecked = function(el) {
document.getElementById('staticIp').style.display = el.checked ? "" : "none";
}
</script>
</body>
</html>

View File

@@ -1,48 +0,0 @@
<script id="temp-template" type="template">
<div class="row mb-3">
<input type="hidden" name="sensor{{index}}" value="{{address}}"/>
<div class="col-xl-3 col-lg-4 col-sm-6">
<div class="input-group input-group-sm">
<div class="input-group-prepend">
<span class="input-group-text">Address {{index}}</span>
</div>
<input name="sensor{{index}}address" type="text" class="form-control" value="{{address}}" maxlength="16" disabled/>
</div>
</div>
<div class="col-xl-4 col-lg-3 col-sm-6">
<div class="input-group input-group-sm">
<div class="input-group-prepend">
<span class="input-group-text">Name</span>
</div>
<input name="sensor{{index}}name" type="text" class="form-control" value="{{name}}" maxlength="16"/>
</div>
</div>
<div class="col-xl-3 col-lg-3 col-sm-6">
<div class="form-check">
<input name="sensor{{index}}common" class="form-check-input" type="checkbox" value="true" id="sensor{{index}}common" {{common}}>
<label class="form-check-label" for="sensor{{index}}common">Include in average</label>
</div>
</div>
<div class="col-xl-2 col-lg-2 col-sm-6">
<span id="temp-{{index}}">{{value}}</span> &deg;C
</div>
</div>
</script>
<form method="post">
<input type="hidden" name="tempConfig" value="true"/>
<div id="sensors" class="my-3 p-3 bg-white rounded shadow">
<div id="loading" class="alert alert-info">Loading temperature sensors</div>
<div id="notemp" class="alert alert-info" style="display: none;">No temperature sensors are configured or found</div>
<div id="error" class="alert alert-danger" style="display: none;">Error loading data</div>
</div>
<hr/>
<div class="row form-group">
<div class="col-6">
<a href="/" class="btn btn-outline-secondary">Back</a>
</div>
<div class="col-6 text-right">
<button class="btn btn-primary">Save</button>
</div>
</div>
</form>

View File

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

View File

@@ -1,129 +0,0 @@
<form method="post" action="/save">
<input type="hidden" name="cc" value="true"/>
<div class="my-3 p-3 bg-white rounded shadow">
<h6>Tariff thresholds</h6>
<div class="row">
<div class="col-lg-2 col-md-3 col-sm-4 col-6">
<div class="m-2 input-group input-group-sm">
<div class="input-group-prepend">
<span class="input-group-text">1</span>
</div>
<input class="form-control text-right" name="t0" type="number" min="1" max="255" step="1" value="{t0}"/>
<div class="input-group-append">
<span class="input-group-text">kWh</span>
</div>
</div>
</div>
<div class="col-lg-2 col-md-3 col-sm-4 col-6">
<div class="m-2 input-group input-group-sm">
<div class="input-group-prepend">
<span class="input-group-text">2</span>
</div>
<input class="form-control text-right" name="t1" type="number" min="5" max="255" step="1" value="{t1}"/>
<div class="input-group-append">
<span class="input-group-text">kWh</span>
</div>
</div>
</div>
<div class="col-lg-2 col-md-3 col-sm-4 col-6">
<div class="m-2 input-group input-group-sm">
<div class="input-group-prepend">
<span class="input-group-text">3</span>
</div>
<input class="form-control text-right" name="t2" type="number" min="5" max="255" step="1" value="{t2}"/>
<div class="input-group-append">
<span class="input-group-text">kWh</span>
</div>
</div>
</div>
<div class="col-lg-2 col-md-3 col-sm-4 col-6">
<div class="m-2 input-group input-group-sm">
<div class="input-group-prepend">
<span class="input-group-text">4</span>
</div>
<input class="form-control text-right" name="t3" type="number" min="5" max="255" step="1" value="{t3}"/>
<div class="input-group-append">
<span class="input-group-text">kWh</span>
</div>
</div>
</div>
<div class="col-lg-2 col-md-3 col-sm-4 col-6">
<div class="m-2 input-group input-group-sm">
<div class="input-group-prepend">
<span class="input-group-text">5</span>
</div>
<input class="form-control text-right" name="t4" type="number" min="5" max="255" step="1" value="{t4}"/>
<div class="input-group-append">
<span class="input-group-text">kWh</span>
</div>
</div>
</div>
<div class="col-lg-2 col-md-3 col-sm-4 col-6">
<div class="m-2 input-group input-group-sm">
<div class="input-group-prepend">
<span class="input-group-text">6</span>
</div>
<input class="form-control text-right" name="t5" type="number" min="5" max="255" step="1" value="{t5}"/>
<div class="input-group-append">
<span class="input-group-text">kWh</span>
</div>
</div>
</div>
<div class="col-lg-2 col-md-3 col-sm-4 col-6">
<div class="m-2 input-group input-group-sm">
<div class="input-group-prepend">
<span class="input-group-text">7</span>
</div>
<input class="form-control text-right" name="t6" type="number" min="5" max="255" step="1" value="{t6}"/>
<div class="input-group-append">
<span class="input-group-text">kWh</span>
</div>
</div>
</div>
<div class="col-lg-2 col-md-3 col-sm-4 col-6">
<div class="m-2 input-group input-group-sm">
<div class="input-group-prepend">
<span class="input-group-text">8</span>
</div>
<input class="form-control text-right" name="t7" type="number" min="5" max="255" step="1" value="{t7}"/>
<div class="input-group-append">
<span class="input-group-text">kWh</span>
</div>
</div>
</div>
<div class="col-lg-2 col-md-3 col-sm-4 col-6">
<div class="m-2 input-group input-group-sm">
<div class="input-group-prepend">
<span class="input-group-text">9</span>
</div>
<input class="form-control text-right" name="t8" type="number" min="5" max="255" step="1" value="{t8}"/>
<div class="input-group-append">
<span class="input-group-text">kWh</span>
</div>
</div>
</div>
</div>
<div class="row">
<div class="col-lg-3 col-md-5 col-sm-6">
<div class="m-2 input-group input-group-sm">
<div class="input-group-prepend">
<span class="input-group-text">Average of top</span>
</div>
<input class="form-control text-right" name="h" type="number" min="1" max="5" step="1" value="{h}"/>
<div class="input-group-append">
<span class="input-group-text">hours</span>
</div>
</div>
</div>
</div>
</div>
<hr/>
<div class="row form-group">
<div class="col-6">
<a href="/meter" class="btn btn-outline-secondary">Back</a>
</div>
<div class="col-6 text-right">
<button class="btn btn-primary">Save</button>
</div>
</div>
</form>

View File

@@ -1,26 +0,0 @@
<form method="post" enctype="multipart/form-data" class="upload-form">
<div class="my-3 p-3 bg-white rounded shadow">
<div class="row">
<div class="col-lg-6">
<div class="input-group mb-3">
<div class="input-group-prepend">
<span class="input-group-text">Upload</span>
</div>
<div class="custom-file">
<input name="file" type="file" class="custom-file-input" id="fileUploadField">
<label class="custom-file-label" for="fileUploadField">Choose file</label>
</div>
</div>
</div>
</div>
</div>
<hr/>
<div class="row form-group">
<div class="col-6">
<a href="javascript:history.back();" class="btn btn-outline-secondary">Back</a>
</div>
<div class="col-6 text-right">
<button class="btn btn-primary">Upload</button>
</div>
</div>
</form>

View File

@@ -1,45 +0,0 @@
<form method="post" action="/save">
<input type="hidden" name="ac" value="true"/>
<div class="my-3 p-3 bg-white rounded shadow">
<h6>Web</h6>
<div class="row">
<div class="col-xl-3 col-lg-4 col-md-7">
<div class="m-2 input-group input-group-sm">
<div class="input-group-prepend">
<span class="input-group-text">Security</span>
</div>
<select id="as" class="form-control" name="as">
<option value="0" %s>None</option>
<option value="1" %s>Only configuration</option>
<option value="2" %s>Everything</option>
</select>
</div>
</div>
<div class="col-xl-3 col-lg-4 col-md-6">
<div class="m-2 input-group input-group-sm">
<div class="input-group-prepend">
<span class="input-group-text">Username</span>
</div>
<input type="text" class="form-control ac" name="au" value="%s" maxlength="64"/>
</div>
</div>
<div class="col-xl-3 col-lg-4 col-md-6">
<div class="m-2 input-group input-group-sm">
<div class="input-group-prepend">
<span class="input-group-text">Password</span>
</div>
<input type="password" class="form-control ac" name="ap" value="%s" maxlength="64"/>
</div>
</div>
</div>
</div>
<hr/>
<div class="row form-group">
<div class="col-6">
<a href="/" class="btn btn-outline-secondary">Back</a>
</div>
<div class="col-6 text-right">
<button class="btn btn-primary">Save</button>
</div>
</div>
</form>

View File

@@ -1,117 +0,0 @@
<form method="post" action="/save">
<input type="hidden" name="wc" value="true"/>
<div class="my-3 p-3 bg-white rounded shadow">
<h6>WiFi</h6>
<div class="row">
<div class="col-xl-3 col-md-6 form-group">
<div class="input-group input-group-sm">
<div class="input-group-prepend">
<span class="input-group-text">SSID</span>
</div>
<input type="text" name="s" class="form-control" maxlength="32" value="{s}" required/>
</div>
</div>
<div class="col-xl-3 col-md-6 form-group">
<div class="input-group input-group-sm">
<div class="input-group-prepend">
<span class="input-group-text">PSK</span>
</div>
<input type="password" name="p" class="form-control" maxlength="63" value="{p}" required/>
</div>
</div>
<div class="col-xl-4 col-md-6 form-group">
<div class="input-group input-group-sm">
<div class="input-group-prepend">
<span class="input-group-text">Hostname</span>
</div>
<input type="text" name="h" class="form-control" maxlength="32" pattern="[a-z0-9_-]+" placeholder="Optional, ex.: ams-reader" value="{h}"/>
<div class="input-group-append">
<label class="input-group-text">
<input type="checkbox" name="m" value="true" {m}/> mDNS
</label>
</div>
</div>
</div>
<div class="col-xl-2 col-md-6 form-group">
<label><input id="st" type="checkbox" name="st" value="1" {st}/> Static IP</label>
</div>
</div>
<div class="row" id="i">
<div class="col-xl-3 col-lg-4 col-md-6 form-group">
<div class="input-group input-group-sm">
<div class="input-group-prepend">
<span class="input-group-text">IP</span>
</div>
<input type="text" name="i" class="form-control sip" pattern="\d?\d?\d.\d?\d?\d.\d?\d?\d.\d?\d?\d" value="{i}"/>
</div>
</div>
<div class="col-xl-3 col-lg-4 col-md-6 form-group">
<div class="input-group input-group-sm">
<div class="input-group-prepend">
<span class="input-group-text">Netmask</span>
</div>
<input type="text" name="sn" class="form-control sip" pattern="\d?\d?\d.\d?\d?\d.\d?\d?\d.\d?\d?\d" value="{sn}"/>
</div>
</div>
<div class="col-xl-3 col-lg-4 col-md-6 form-group">
<div class="input-group input-group-sm">
<div class="input-group-prepend">
<span class="input-group-text">Gateway</span>
</div>
<input type="text" name="g" class="form-control sip" pattern="\d?\d?\d.\d?\d?\d.\d?\d?\d.\d?\d?\d" value="{g}"/>
</div>
</div>
<div class="col-xl-4 col-lg-5 col-md-6 form-group">
<div class="input-group input-group-sm">
<div class="input-group-prepend">
<span class="input-group-text">DNS 1</span>
</div>
<input type="text" name="d1" class="form-control sip" pattern="\d?\d?\d.\d?\d?\d.\d?\d?\d.\d?\d?\d" value="{d1}"/>
</div>
</div>
<div class="col-xl-4 col-lg-5 col-md-6 form-group">
<div class="input-group input-group-sm">
<div class="input-group-prepend">
<span class="input-group-text">DNS 2</span>
</div>
<input type="text" name="d2" class="form-control sip" pattern="\d?\d?\d.\d?\d?\d.\d?\d?\d.\d?\d?\d" value="{d2}"/>
</div>
</div>
<div class="col-lg-3 col-md-4 col-sm-6 form-group">
<div class="input-group input-group-sm">
<div class="input-group-prepend">
<span class="input-group-text">Power</span>
</div>
<input type="number" name="w" class="form-control text-right" min="0" max="{wm}" step="0.5" value="{w}"/>
<div class="input-group-append">
<span class="input-group-text">dBm</span>
</div>
</div>
</div>
<div class="col-lg-3 col-md-4 col-sm-6 form-group">
<div class="input-group input-group-sm">
<div class="input-group-prepend">
<span class="input-group-text">Power saving</span>
</div>
<select name="z" class="form-control">
<option value="255">Default</option>
<option value="0" {z0}>Off</option>
<option value="1" {z1}>Minimum</option>
<option value="2" {z2}>Maximum</option>
</select>
</div>
</div>
</div>
</div>
<hr/>
<div class="row form-group">
<div class="col-6">
<a href="/" class="btn btn-outline-secondary">Back</a>
</div>
<div class="col-6 text-right">
<button class="btn btn-primary">Save</button>
</div>
</div>
</form>

View File

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

View File

@@ -1,15 +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";

View File

@@ -1,146 +0,0 @@
#ifndef _AMSWEBSERVER_h
#define _AMSWEBSERVER_h
#define BOOTSTRAP_URL "https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/4.4.1/css/bootstrap.min.css"
#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);
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;
#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) // ARDUINO_ARCH_ESP32
WebServer server;
#endif
bool checkSecurity(byte level);
void indexHtml();
void applicationJs();
void temperature();
void temperaturePost();
void temperatureJson();
void configMeterHtml();
void configMeterAdvancedHtml();
void configWifiHtml();
void configMqttHtml();
void configWebHtml();
void configDomoticzHtml();
void configPriceApiHtml();
void configNtpHtml();
void configGpioHtml();
void configDebugHtml();
void configThresholdsHtml();
void bootCss();
void githubSvg();
void dataJson();
void dayplotJson();
void monthplotJson();
void energyPriceJson();
void configFileHtml();
void configFileDownload();
void configFileUpload();
void handleSetup();
void handleSave();
String getSerialSelectOptions(int selected);
void firmwareHtml();
void firmwarePost();
void firmwareUpload();
void firmwareDownload();
void restartHtml();
void restartPost();
void restartWaitHtml();
void isAliveCheck();
void uploadHtml(const char* label, const char* action, const char* menu);
void deleteHtml(const char* label, const char* action, const char* menu);
HTTPUpload& uploadFile(const char* path);
void deleteFile(const char* path);
void uploadPost();
void mqttCa();
void mqttCaUpload();
void mqttCaDelete();
void mqttCert();
void mqttCertUpload();
void mqttCertDelete();
void mqttKey();
void mqttKeyUpload();
void mqttKeyDelete();
void factoryResetHtml();
void factoryResetPost();
void notFound();
void printD(String fmt, ...);
void printI(String fmt, ...);
void printW(String fmt, ...);
void printE(String fmt, ...);
};
#endif

View File

@@ -1,80 +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")
webroot = "lib/ClassicUi/html"
srcroot = "lib/ClassicUi/include/root"
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(".html"):
content = html_minify(content)
elif filename.endswith(".css"):
content = css_minify(content)
elif (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(";");

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,47 @@
#ifndef _CLOUDCONNECTOR_H
#define _CLOUDCONNECTOR_H
#include "RemoteDebug.h"
#include "mbedtls/ssl.h"
#include "mbedtls/platform.h"
#include "mbedtls/net.h"
#include "mbedtls/esp_debug.h"
#include "mbedtls/entropy.h"
#include "mbedtls/ctr_drbg.h"
#include "mbedtls/error.h"
#include "mbedtls/certs.h"
#include "mbedtls/rsa.h"
const unsigned char PUBLIC_KEY[] = \
"-----BEGIN PUBLIC KEY-----\n"\
"MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDoIo0CSuuX3tAdF7KPssdlzJNX\n"\
"QryhgVV1rQIFPhHv3SxzyKtRrRM9s0CVfymcibhnEBXxxg3pxlGmwI/R6k7HHXJN\n"\
"lBsXzzDtZ/GHDVnw+xRakTfRT0Zt+xdJSH5xJNWq4EwpvJfjA22L1Nz4dKSpgWMx\n"\
"VRndAaXf0s7Q1XBz2wIDAQAB\n"\
"-----END PUBLIC KEY-----\0";
//const unsigned char PUBLIC_KEY[] = { 0x30, 0x81, 0x9f, 0x30, 0x0d, 0x06, 0x09, 0x2a, 0x86, 0x48, 0x86, 0xf7, 0x0d, 0x01, 0x01, 0x01, 0x05, 0x00, 0x03, 0x81, 0x8d, 0x00, 0x30, 0x81, 0x89, 0x02, 0x81, 0x81, 0x00, 0xe8, 0x22, 0x8d, 0x02, 0x4a, 0xeb, 0x97, 0xde, 0xd0, 0x1d, 0x17, 0xb2, 0x8f, 0xb2, 0xc7, 0x65, 0xcc, 0x93, 0x57, 0x42, 0xbc, 0xa1, 0x81, 0x55, 0x75, 0xad, 0x02, 0x05, 0x3e, 0x11, 0xef, 0xdd, 0x2c, 0x73, 0xc8, 0xab, 0x51, 0xad, 0x13, 0x3d, 0xb3, 0x40, 0x95, 0x7f, 0x29, 0x9c, 0x89, 0xb8, 0x67, 0x10, 0x15, 0xf1, 0xc6, 0x0d, 0xe9, 0xc6, 0x51, 0xa6, 0xc0, 0x8f, 0xd1, 0xea, 0x4e, 0xc7, 0x1d, 0x72, 0x4d, 0x94, 0x1b, 0x17, 0xcf, 0x30, 0xed, 0x67, 0xf1, 0x87, 0x0d, 0x59, 0xf0, 0xfb, 0x14, 0x5a, 0x91, 0x37, 0xd1, 0x4f, 0x46, 0x6d, 0xfb, 0x17, 0x49, 0x48, 0x7e, 0x71, 0x24, 0xd5, 0xaa, 0xe0, 0x4c, 0x29, 0xbc, 0x97, 0xe3, 0x03, 0x6d, 0x8b, 0xd4, 0xdc, 0xf8, 0x74, 0xa4, 0xa9, 0x81, 0x63, 0x31, 0x55, 0x19, 0xdd, 0x01, 0xa5, 0xdf, 0xd2, 0xce, 0xd0, 0xd5, 0x70, 0x73, 0xdb, 0x02, 0x03, 0x01, 0x00, 0x01};
struct CloudData {
uint8_t type;
int16_t data;
} __attribute__((packed));
class CloudConnector {
public:
CloudConnector(RemoteDebug*);
void setup(const unsigned char * key);
void send();
private:
RemoteDebug* debugger;
unsigned char buf[4096];
mbedtls_rsa_context* rsa = nullptr;
void debugPrint(byte *buffer, int start, int length);
};
#endif

View File

@@ -0,0 +1,56 @@
#include "CloudConnector.h"
CloudConnector::CloudConnector(RemoteDebug* debugger) {
this->debugger = debugger;
mbedtls_pk_context pk;
mbedtls_pk_init(&pk);
int error_code = 0;
if((error_code = mbedtls_pk_parse_public_key(&pk, PUBLIC_KEY, sizeof(PUBLIC_KEY))) == 0){
debugger->printf("RSA public key OK\n");
rsa = mbedtls_pk_rsa(pk);
} else {
debugger->printf("RSA public key read error: ");
mbedtls_strerror(error_code, (char*) buf, 4096);
debugger->printf("%s\n", buf);
}
debugger->flush();
//send();
}
void CloudConnector::send() {
if(rsa != nullptr && mbedtls_rsa_check_pubkey(rsa) == 0) {
memset(buf, 0, 4096);
CloudData data = {65, 127};
unsigned char toEncrypt[4096] = {0};
debugger->println("RSA clear data: ");
debugPrint(toEncrypt, 0, 256);
mbedtls_rsa_rsaes_pkcs1_v15_encrypt(rsa, NULL, NULL, MBEDTLS_RSA_PUBLIC, 256, toEncrypt, buf);
//byte hashResult[32];
//mbedtls_sha256(toEncrypt, strlen((char*) toEncrypt), hashResult, 0);
//int success = mbedtls_rsa_rsassa_pkcs1_v15_sign(rsa, NULL, NULL, MBEDTLS_RSA_PUBLIC, MBEDTLS_MD_SHA256, strlen((char*) hashResult), hashResult, buf);
debugger->println("RSA encrypted data: ");
debugPrint(buf, 0, 256);
} else {
debugger->println("RSA key is invalid");
}
}
void CloudConnector::debugPrint(byte *buffer, int start, int length) {
for (int i = start; i < start + length; i++) {
if (buffer[i] < 0x10)
debugger->print(F("0"));
debugger->print(buffer[i], HEX);
debugger->print(F(" "));
if ((i - start + 1) % 16 == 0)
debugger->println(F(""));
else if ((i - start + 1) % 4 == 0)
debugger->print(F(" "));
yield(); // Let other get some resources too
}
debugger->println(F(""));
}

View File

@@ -9,14 +9,14 @@ public:
DomoticzMqttHandler(MQTTClient* mqtt, char* buf, DomoticzConfig config) : AmsMqttHandler(mqtt, buf) {
this->config = config;
};
bool publish(AmsData* data, AmsData* previousState, EnergyAccounting* ea);
bool publish(AmsData* data, AmsData* previousState, EnergyAccounting* ea, EntsoeApi* eapi);
bool publishTemperatures(AmsConfiguration*, HwTools*);
bool publishPrices(EntsoeApi*);
bool publishSystem(HwTools* hw, EntsoeApi* eapi, EnergyAccounting* ea);
private:
DomoticzConfig config;
int energy = 0.0;
double energy = 0.0;
};
#endif

View File

@@ -1,7 +1,7 @@
#include "DomoticzMqttHandler.h"
#include "json/domoticz_json.h"
bool DomoticzMqttHandler::publish(AmsData* data, AmsData* previousState, EnergyAccounting* ea) {
bool DomoticzMqttHandler::publish(AmsData* data, AmsData* previousState, EnergyAccounting* ea, EntsoeApi* eapi) {
bool ret = false;
if (config.elidx > 0) {
if(data->getActiveImportCounter() > 1.0) {
@@ -9,12 +9,12 @@ bool DomoticzMqttHandler::publish(AmsData* data, AmsData* previousState, EnergyA
}
if(energy > 0.0) {
char val[16];
snprintf(val, 16, "%.1f;%.1f", (data->getActiveImportPower()/1.0), energy*1000.0);
snprintf_P(val, 16, PSTR("%.1f;%.1f"), (data->getActiveImportPower()/1.0), energy*1000.0);
snprintf_P(json, BufferSize, DOMOTICZ_JSON,
config.elidx,
val
);
ret = mqtt->publish("domoticz/in", json);
ret = mqtt->publish(F("domoticz/in"), json);
}
}
@@ -23,22 +23,22 @@ bool DomoticzMqttHandler::publish(AmsData* data, AmsData* previousState, EnergyA
if (config.vl1idx > 0){
char val[16];
snprintf(val, 16, "%.2f", data->getL1Voltage());
snprintf_P(val, 16, PSTR("%.2f"), data->getL1Voltage());
snprintf_P(json, BufferSize, DOMOTICZ_JSON,
config.vl1idx,
val
);
ret |= mqtt->publish("domoticz/in", json);
ret |= mqtt->publish(F("domoticz/in"), json);
}
if (config.vl2idx > 0){
char val[16];
snprintf(val, 16, "%.2f", data->getL2Voltage());
snprintf_P(val, 16, PSTR("%.2f"), data->getL2Voltage());
snprintf_P(json, BufferSize, DOMOTICZ_JSON,
config.vl2idx,
val
);
ret |= mqtt->publish("domoticz/in", json);
ret |= mqtt->publish(F("domoticz/in"), json);
}
if (config.vl3idx > 0){
@@ -48,7 +48,7 @@ bool DomoticzMqttHandler::publish(AmsData* data, AmsData* previousState, EnergyA
config.vl3idx,
val
);
ret |= mqtt->publish("domoticz/in", json);
ret |= mqtt->publish(F("domoticz/in"), json);
}
if (config.cl1idx > 0){
@@ -58,7 +58,7 @@ bool DomoticzMqttHandler::publish(AmsData* data, AmsData* previousState, EnergyA
config.cl1idx,
val
);
ret |= mqtt->publish("domoticz/in", json);
ret |= mqtt->publish(F("domoticz/in"), json);
}
return ret;
}

View File

@@ -52,25 +52,26 @@ public:
bool update(AmsData* amsData);
bool load();
bool save();
bool isInitialized();
double getUseThisHour();
double getUseToday();
double getUseThisMonth();
float getUseThisHour();
float getUseToday();
float getUseThisMonth();
double getProducedThisHour();
double getProducedToday();
double getProducedThisMonth();
float getProducedThisHour();
float getProducedToday();
float getProducedThisMonth();
double getCostThisHour();
double getCostToday();
double getCostYesterday();
double getCostThisMonth();
float getCostThisHour();
float getCostToday();
float getCostYesterday();
float getCostThisMonth();
uint16_t getCostLastMonth();
double getIncomeThisHour();
double getIncomeToday();
double getIncomeYesterday();
double getIncomeThisMonth();
float getIncomeThisHour();
float getIncomeToday();
float getIncomeYesterday();
float getIncomeThisMonth();
uint16_t getIncomeLastMonth();
float getMonthMax();
@@ -80,6 +81,9 @@ public:
EnergyAccountingData getData();
void setData(EnergyAccountingData&);
void setFixedPrice(float price);
float getPriceForHour(uint8_t h);
private:
RemoteDebug* debugger = NULL;
unsigned long lastUpdateMillis = 0;
@@ -89,9 +93,10 @@ private:
EnergyAccountingConfig *config = NULL;
Timezone *tz = NULL;
uint8_t currentHour = 0, currentDay = 0, currentThresholdIdx = 0;
double use, costHour, costDay;
double produce, incomeHour, incomeDay;
float use = 0, costHour = 0, costDay = 0;
float produce = 0, incomeHour = 0, incomeDay = 0;
EnergyAccountingData data = { 0, 0, 0, 0, 0, 0 };
float fixedPrice = 0;
void calcDayCost();
bool updateMax(uint16_t val, uint8_t day);

View File

@@ -26,12 +26,16 @@ void EnergyAccounting::setTimezone(Timezone* tz) {
this->tz = tz;
}
bool EnergyAccounting::isInitialized() {
return this->init;
}
bool EnergyAccounting::update(AmsData* amsData) {
if(config == NULL) return false;
time_t now = time(nullptr);
if(now < BUILD_EPOCH) return false;
if(tz == NULL) {
if(debugger->isActive(RemoteDebug::VERBOSE)) debugger->printf("(EnergyAccounting) Timezone is missing\n");
if(debugger->isActive(RemoteDebug::VERBOSE)) debugger->printf_P(PSTR("(EnergyAccounting) Timezone is missing\n"));
return false;
}
@@ -42,9 +46,9 @@ bool EnergyAccounting::update(AmsData* amsData) {
if(!init) {
currentHour = local.Hour;
currentDay = local.Day;
if(debugger->isActive(RemoteDebug::DEBUG)) debugger->printf("(EnergyAccounting) Initializing data at %lu\n", (int32_t) now);
if(debugger->isActive(RemoteDebug::DEBUG)) debugger->printf_P(PSTR("(EnergyAccounting) Initializing data at %lu\n"), (int32_t) now);
if(!load()) {
if(debugger->isActive(RemoteDebug::INFO)) debugger->printf("(EnergyAccounting) Unable to load existing data\n");
if(debugger->isActive(RemoteDebug::INFO)) debugger->printf_P(PSTR("(EnergyAccounting) Unable to load existing data\n"));
data = { 5, local.Month,
0, 0, 0, // Cost
0, 0, 0, // Income
@@ -56,21 +60,22 @@ bool EnergyAccounting::update(AmsData* amsData) {
};
} else if(debugger->isActive(RemoteDebug::DEBUG)) {
for(uint8_t i = 0; i < 5; i++) {
debugger->printf("(EnergyAccounting) Peak hour from day %d: %d\n", data.peaks[i].day, data.peaks[i].value*10);
debugger->printf_P(PSTR("(EnergyAccounting) Peak hour from day %d: %d\n"), data.peaks[i].day, data.peaks[i].value*10);
}
debugger->printf("(EnergyAccounting) Loaded cost yesterday: %.2f, this month: %d, last month: %d\n", data.costYesterday / 10.0, data.costThisMonth, data.costLastMonth);
debugger->printf("(EnergyAccounting) Loaded income yesterday: %.2f, this month: %d, last month: %d\n", data.incomeYesterday / 10.0, data.incomeThisMonth, data.incomeLastMonth);
debugger->printf_P(PSTR("(EnergyAccounting) Loaded cost yesterday: %.2f, this month: %d, last month: %d\n"), data.costYesterday / 10.0, data.costThisMonth, data.costLastMonth);
debugger->printf_P(PSTR("(EnergyAccounting) Loaded income yesterday: %.2f, this month: %d, last month: %d\n"), data.incomeYesterday / 10.0, data.incomeThisMonth, data.incomeLastMonth);
}
init = true;
}
if(!initPrice && eapi != NULL && eapi->getValueForHour(0) != ENTSOE_NO_VALUE) {
if(debugger->isActive(RemoteDebug::DEBUG)) debugger->printf("(EnergyAccounting) Initializing prices at %lu\n", (int32_t) now);
float price = getPriceForHour(0);
if(!initPrice && price != ENTSOE_NO_VALUE) {
if(debugger->isActive(RemoteDebug::DEBUG)) debugger->printf_P(PSTR("(EnergyAccounting) Initializing prices at %lu\n"), (int32_t) now);
calcDayCost();
}
if(local.Hour != currentHour && (amsData->getListType() >= 3 || local.Minute == 1)) {
if(debugger->isActive(RemoteDebug::INFO)) debugger->printf("(EnergyAccounting) New local hour %d\n", local.Hour);
if(debugger->isActive(RemoteDebug::INFO)) debugger->printf_P(PSTR("(EnergyAccounting) New local hour %d\n"), local.Hour);
tmElements_t oneHrAgo, oneHrAgoLocal;
breakTime(now-3600, oneHrAgo);
@@ -90,7 +95,7 @@ bool EnergyAccounting::update(AmsData* amsData) {
incomeHour = 0;
if(local.Day != currentDay) {
if(debugger->isActive(RemoteDebug::INFO)) debugger->printf("(EnergyAccounting) New day %d\n", local.Day);
if(debugger->isActive(RemoteDebug::INFO)) debugger->printf_P(PSTR("(EnergyAccounting) New day %d\n"), local.Day);
data.costYesterday = costDay * 10;
data.costThisMonth += costDay;
costDay = 0;
@@ -104,7 +109,7 @@ bool EnergyAccounting::update(AmsData* amsData) {
}
if(local.Month != data.month) {
if(debugger->isActive(RemoteDebug::INFO)) debugger->printf("(EnergyAccounting) New month %d\n", local.Month);
if(debugger->isActive(RemoteDebug::INFO)) debugger->printf_P(PSTR("(EnergyAccounting) New month %d\n"), local.Month);
data.costLastMonth = data.costThisMonth;
data.costThisMonth = 0;
data.incomeLastMonth = data.incomeThisMonth;
@@ -123,32 +128,30 @@ bool EnergyAccounting::update(AmsData* amsData) {
float kwhe = (amsData->getActiveExportPower() * (((float) ms) / 3600000.0)) / 1000.0;
lastUpdateMillis = amsData->getLastUpdateMillis();
if(kwhi > 0) {
if(debugger->isActive(RemoteDebug::VERBOSE)) debugger->printf("(EnergyAccounting) Adding %.4f kWh import\n", kwhi);
if(debugger->isActive(RemoteDebug::VERBOSE)) debugger->printf_P(PSTR("(EnergyAccounting) Adding %.4f kWh import\n"), kwhi);
use += kwhi;
if(eapi != NULL && eapi->getValueForHour(0) != ENTSOE_NO_VALUE) {
float price = eapi->getValueForHour(0);
if(price != ENTSOE_NO_VALUE) {
float cost = price * kwhi;
if(debugger->isActive(RemoteDebug::VERBOSE)) debugger->printf("(EnergyAccounting) and %.4f %s\n", cost / 100.0, eapi->getCurrency());
if(debugger->isActive(RemoteDebug::VERBOSE)) debugger->printf_P(PSTR("(EnergyAccounting) and %.4f %s\n"), cost / 100.0, eapi->getCurrency());
costHour += cost;
costDay += cost;
}
}
if(kwhe > 0) {
if(debugger->isActive(RemoteDebug::VERBOSE)) debugger->printf("(EnergyAccounting) Adding %.4f kWh export\n", kwhe);
if(debugger->isActive(RemoteDebug::VERBOSE)) debugger->printf_P(PSTR("(EnergyAccounting) Adding %.4f kWh export\n"), kwhe);
produce += kwhe;
if(eapi != NULL && eapi->getValueForHour(0) != ENTSOE_NO_VALUE) {
float price = eapi->getValueForHour(0);
if(price != ENTSOE_NO_VALUE) {
float income = price * kwhe;
if(debugger->isActive(RemoteDebug::VERBOSE)) debugger->printf("(EnergyAccounting) and %.4f %s\n", income / 100.0, eapi->getCurrency());
if(debugger->isActive(RemoteDebug::VERBOSE)) debugger->printf_P(PSTR("(EnergyAccounting) and %.4f %s\n"), income / 100.0, eapi->getCurrency());
incomeHour += income;
incomeDay += income;
}
}
if(config != NULL) {
if(debugger->isActive(RemoteDebug::VERBOSE)) debugger->printf("(EnergyAccounting) calculating threshold, currently at %d\n", currentThresholdIdx);
if(debugger->isActive(RemoteDebug::VERBOSE)) debugger->printf_P(PSTR("(EnergyAccounting) calculating threshold, currently at %d\n"), currentThresholdIdx);
while(getMonthMax() > config->thresholds[currentThresholdIdx] && currentThresholdIdx < 10) currentThresholdIdx++;
if(debugger->isActive(RemoteDebug::VERBOSE)) debugger->printf("(EnergyAccounting) new threshold %d\n", currentThresholdIdx);
if(debugger->isActive(RemoteDebug::VERBOSE)) debugger->printf_P(PSTR("(EnergyAccounting) new threshold %d\n"), currentThresholdIdx);
}
return ret;
@@ -159,13 +162,13 @@ void EnergyAccounting::calcDayCost() {
tmElements_t local, utc;
breakTime(tz->toLocal(now), local);
if(eapi != NULL && eapi->getValueForHour(0) != ENTSOE_NO_VALUE) {
if(getPriceForHour(0) != ENTSOE_NO_VALUE) {
if(initPrice) {
costDay = 0;
incomeDay = 0;
}
for(int i = 0; i < currentHour; i++) {
float price = eapi->getValueForHour(i - local.Hour);
for(uint8_t i = 0; i < currentHour; i++) {
float price = getPriceForHour(i - local.Hour);
if(price == ENTSOE_NO_VALUE) break;
breakTime(now - ((local.Hour - i) * 3600), utc);
int16_t wh = ds->getHourImport(utc.Hour);
@@ -178,73 +181,75 @@ void EnergyAccounting::calcDayCost() {
}
}
double EnergyAccounting::getUseThisHour() {
float EnergyAccounting::getUseThisHour() {
return use;
}
double EnergyAccounting::getUseToday() {
float EnergyAccounting::getUseToday() {
float ret = 0.0;
time_t now = time(nullptr);
if(now < BUILD_EPOCH) return 0;
if(now < BUILD_EPOCH) return 0.0;
if(tz == NULL) return 0.0;
tmElements_t utc, local;
breakTime(tz->toLocal(now), local);
for(int i = 0; i < currentHour; i++) {
for(uint8_t i = 0; i < currentHour; i++) {
breakTime(now - ((local.Hour - i) * 3600), utc);
ret += ds->getHourImport(utc.Hour) / 1000.0;
}
return ret + getUseThisHour();
}
double EnergyAccounting::getUseThisMonth() {
float EnergyAccounting::getUseThisMonth() {
time_t now = time(nullptr);
if(now < BUILD_EPOCH) return 0;
if(now < BUILD_EPOCH) return 0.0;
float ret = 0;
for(int i = 0; i < currentDay; i++) {
for(uint8_t i = 0; i < currentDay; i++) {
ret += ds->getDayImport(i) / 1000.0;
}
return ret + getUseToday();
}
double EnergyAccounting::getProducedThisHour() {
float EnergyAccounting::getProducedThisHour() {
return produce;
}
double EnergyAccounting::getProducedToday() {
float EnergyAccounting::getProducedToday() {
float ret = 0.0;
time_t now = time(nullptr);
if(now < BUILD_EPOCH) return 0;
tmElements_t utc;
for(int i = 0; i < currentHour; i++) {
breakTime(now - ((currentHour - i) * 3600), utc);
if(now < BUILD_EPOCH) return 0.0;
tmElements_t utc, local;
breakTime(tz->toLocal(now), local);
for(uint8_t i = 0; i < currentHour; i++) {
breakTime(now - ((local.Hour - i) * 3600), utc);
ret += ds->getHourExport(utc.Hour) / 1000.0;
}
return ret + getProducedThisHour();
}
double EnergyAccounting::getProducedThisMonth() {
float EnergyAccounting::getProducedThisMonth() {
time_t now = time(nullptr);
if(now < BUILD_EPOCH) return 0;
if(now < BUILD_EPOCH) return 0.0;
float ret = 0;
for(int i = 0; i < currentDay; i++) {
for(uint8_t i = 0; i < currentDay; i++) {
ret += ds->getDayExport(i) / 1000.0;
}
return ret + getProducedToday();
}
double EnergyAccounting::getCostThisHour() {
float EnergyAccounting::getCostThisHour() {
return costHour;
}
double EnergyAccounting::getCostToday() {
float EnergyAccounting::getCostToday() {
return costDay;
}
double EnergyAccounting::getCostYesterday() {
float EnergyAccounting::getCostYesterday() {
return data.costYesterday / 10.0;
}
double EnergyAccounting::getCostThisMonth() {
float EnergyAccounting::getCostThisMonth() {
return data.costThisMonth + getCostToday();
}
@@ -252,19 +257,19 @@ uint16_t EnergyAccounting::getCostLastMonth() {
return data.costLastMonth;
}
double EnergyAccounting::getIncomeThisHour() {
float EnergyAccounting::getIncomeThisHour() {
return incomeHour;
}
double EnergyAccounting::getIncomeToday() {
float EnergyAccounting::getIncomeToday() {
return incomeDay;
}
double EnergyAccounting::getIncomeYesterday() {
float EnergyAccounting::getIncomeYesterday() {
return data.incomeYesterday / 10.0;
}
double EnergyAccounting::getIncomeThisMonth() {
float EnergyAccounting::getIncomeThisMonth() {
return data.incomeThisMonth + getIncomeToday();
}
@@ -279,6 +284,8 @@ uint8_t EnergyAccounting::getCurrentThreshold() {
}
float EnergyAccounting::getMonthMax() {
if(config == NULL)
return 0.0;
uint8_t count = 0;
uint32_t maxHour = 0.0;
bool included[5] = { false, false, false, false, false };
@@ -308,6 +315,8 @@ float EnergyAccounting::getMonthMax() {
}
EnergyAccountingPeak EnergyAccounting::getPeak(uint8_t num) {
if(config == NULL)
return EnergyAccountingPeak({0,0});
if(num < 1 || num > 5) return EnergyAccountingPeak({0,0});
uint8_t count = 0;
@@ -343,7 +352,7 @@ EnergyAccountingPeak EnergyAccounting::getPeak(uint8_t num) {
bool EnergyAccounting::load() {
if(!LittleFS.begin()) {
if(debugger->isActive(RemoteDebug::ERROR)) {
debugger->printf("(EnergyAccounting) Unable to load LittleFS\n");
debugger->printf_P(PSTR("(EnergyAccounting) Unable to load LittleFS\n"));
}
return false;
}
@@ -354,7 +363,7 @@ bool EnergyAccounting::load() {
char buf[file.size()];
file.readBytes(buf, file.size());
if(debugger->isActive(RemoteDebug::DEBUG)) debugger->printf("(EnergyAccounting) Data version %d\n", buf[0]);
if(debugger->isActive(RemoteDebug::DEBUG)) debugger->printf_P(PSTR("(EnergyAccounting) Data version %d\n"), buf[0]);
if(buf[0] == 5) {
EnergyAccountingData* data = (EnergyAccountingData*) buf;
memcpy(&this->data, data, sizeof(this->data));
@@ -362,7 +371,9 @@ bool EnergyAccounting::load() {
} else if(buf[0] == 4) {
EnergyAccountingData4* data = (EnergyAccountingData4*) buf;
this->data = { 5, data->month,
(uint16_t) (data->costYesterday / 10), (uint16_t) (data->costThisMonth / 100), (uint16_t) (data->costLastMonth / 100),
data->costYesterday,
data->costThisMonth,
data->costLastMonth,
0,0,0, // Income from production
data->peaks[0].day, data->peaks[0].value,
data->peaks[1].day, data->peaks[1].value,
@@ -417,25 +428,23 @@ bool EnergyAccounting::load() {
this->data.peaks[0].value = data->maxHour;
ret = true;
} else {
if(debugger->isActive(RemoteDebug::WARNING)) debugger->printf("(EnergyAccounting) Unknown version\n");
if(debugger->isActive(RemoteDebug::WARNING)) debugger->printf_P(PSTR("(EnergyAccounting) Unknown version\n"));
ret = false;
}
}
file.close();
} else {
if(debugger->isActive(RemoteDebug::WARNING)) debugger->printf("(EnergyAccounting) File not found\n");
if(debugger->isActive(RemoteDebug::WARNING)) debugger->printf_P(PSTR("(EnergyAccounting) File not found\n"));
}
LittleFS.end();
return ret;
}
bool EnergyAccounting::save() {
if(!LittleFS.begin()) {
if(debugger->isActive(RemoteDebug::ERROR)) {
debugger->printf("(EnergyAccounting) Unable to load LittleFS\n");
debugger->printf_P(PSTR("(EnergyAccounting) Unable to load LittleFS\n"));
}
return false;
}
@@ -448,8 +457,6 @@ bool EnergyAccounting::save() {
}
file.close();
}
LittleFS.end();
return true;
}
@@ -465,7 +472,7 @@ bool EnergyAccounting::updateMax(uint16_t val, uint8_t day) {
for(uint8_t i = 0; i < 5; i++) {
if(data.peaks[i].day == day || data.peaks[i].day == 0) {
if(val > data.peaks[i].value) {
if(debugger->isActive(RemoteDebug::INFO)) debugger->printf("(EnergyAccounting) Adding new max %d for day %d which is larger than %d\n", val*10, day, data.peaks[i].value*10);
if(debugger->isActive(RemoteDebug::INFO)) debugger->printf_P(PSTR("(EnergyAccounting) Adding new max %d for day %d which is larger than %d\n"), val*10, day, data.peaks[i].value*10);
data.peaks[i].day = day;
data.peaks[i].value = val;
return true;
@@ -484,10 +491,20 @@ bool EnergyAccounting::updateMax(uint16_t val, uint8_t day) {
}
}
if(idx < 5) {
if(debugger->isActive(RemoteDebug::INFO)) debugger->printf("(EnergyAccounting) Adding new max %d for day %d\n", val*10, day);
if(debugger->isActive(RemoteDebug::INFO)) debugger->printf_P(PSTR("(EnergyAccounting) Adding new max %d for day %d\n"), val*10, day);
data.peaks[idx].value = val;
data.peaks[idx].day = day;
return true;
}
return false;
}
void EnergyAccounting::setFixedPrice(float price) {
this->fixedPrice = price;
}
float EnergyAccounting::getPriceForHour(uint8_t h) {
if(fixedPrice > 0.0) return fixedPrice;
if(eapi == NULL) return ENTSOE_NO_VALUE;
return eapi->getValueForHour(h);
}

View File

@@ -32,7 +32,7 @@ public:
private:
char currency[4];
char measurementUnit[4];
float points[24];
float points[25];
char buf[64];
uint8_t pos = 0;

View File

@@ -38,6 +38,7 @@ private:
uint8_t currentDay = 0, currentHour = 0;
uint8_t tomorrowFetchMinute = 15; // How many minutes over 13:00 should it fetch prices
uint8_t nextFetchDelayMinutes = 15;
uint64_t lastTodayFetch = 0;
uint64_t lastTomorrowFetch = 0;
uint64_t lastCurrencyFetch = 0;
@@ -61,8 +62,6 @@ private:
bool retrieve(const char* url, Stream* doc);
float getCurrencyMultiplier(const char* from, const char* to, time_t t);
void printD(String fmt, ...);
void printE(String fmt, ...);
void debugPrint(byte *buffer, int start, int length);
};
#endif

View File

@@ -3,6 +3,6 @@
struct PricesContainer {
char currency[4];
char measurementUnit[4];
int32_t points[24];
int32_t points[25];
};
#endif

View File

@@ -2,7 +2,7 @@
#include "HardwareSerial.h"
EntsoeA44Parser::EntsoeA44Parser() {
for(int i = 0; i < 24; i++) points[i] = ENTSOE_NO_VALUE;
for(int i = 0; i < 25; i++) points[i] = ENTSOE_NO_VALUE;
}
EntsoeA44Parser::~EntsoeA44Parser() {
@@ -18,7 +18,7 @@ char* EntsoeA44Parser::getMeasurementUnit() {
}
float EntsoeA44Parser::getPoint(uint8_t position) {
if(position >= 24) return ENTSOE_NO_VALUE;
if(position >= 25) return ENTSOE_NO_VALUE;
return points[position];
}
@@ -111,30 +111,7 @@ void EntsoeA44Parser::get(PricesContainer* container) {
strcpy(container->currency, currency);
strcpy(container->measurementUnit, measurementUnit);
container->points[0] = points[0] == ENTSOE_NO_VALUE ? ENTSOE_NO_VALUE : points[0] * 10000;
container->points[1] = points[1] == ENTSOE_NO_VALUE ? ENTSOE_NO_VALUE : points[1] * 10000;
container->points[2] = points[2] == ENTSOE_NO_VALUE ? ENTSOE_NO_VALUE : points[2] * 10000;
container->points[3] = points[3] == ENTSOE_NO_VALUE ? ENTSOE_NO_VALUE : points[3] * 10000;
container->points[4] = points[4] == ENTSOE_NO_VALUE ? ENTSOE_NO_VALUE : points[4] * 10000;
container->points[5] = points[5] == ENTSOE_NO_VALUE ? ENTSOE_NO_VALUE : points[5] * 10000;
container->points[6] = points[6] == ENTSOE_NO_VALUE ? ENTSOE_NO_VALUE : points[6] * 10000;
container->points[7] = points[7] == ENTSOE_NO_VALUE ? ENTSOE_NO_VALUE : points[7] * 10000;
container->points[8] = points[8] == ENTSOE_NO_VALUE ? ENTSOE_NO_VALUE : points[8] * 10000;
container->points[9] = points[9] == ENTSOE_NO_VALUE ? ENTSOE_NO_VALUE : points[9] * 10000;
container->points[10] = points[10] == ENTSOE_NO_VALUE ? ENTSOE_NO_VALUE : points[10] * 10000;
container->points[11] = points[11] == ENTSOE_NO_VALUE ? ENTSOE_NO_VALUE : points[11] * 10000;
container->points[12] = points[12] == ENTSOE_NO_VALUE ? ENTSOE_NO_VALUE : points[12] * 10000;
container->points[13] = points[13] == ENTSOE_NO_VALUE ? ENTSOE_NO_VALUE : points[13] * 10000;
container->points[14] = points[14] == ENTSOE_NO_VALUE ? ENTSOE_NO_VALUE : points[14] * 10000;
container->points[15] = points[15] == ENTSOE_NO_VALUE ? ENTSOE_NO_VALUE : points[15] * 10000;
container->points[16] = points[16] == ENTSOE_NO_VALUE ? ENTSOE_NO_VALUE : points[16] * 10000;
container->points[17] = points[17] == ENTSOE_NO_VALUE ? ENTSOE_NO_VALUE : points[17] * 10000;
container->points[18] = points[18] == ENTSOE_NO_VALUE ? ENTSOE_NO_VALUE : points[18] * 10000;
container->points[19] = points[19] == ENTSOE_NO_VALUE ? ENTSOE_NO_VALUE : points[19] * 10000;
container->points[20] = points[20] == ENTSOE_NO_VALUE ? ENTSOE_NO_VALUE : points[20] * 10000;
container->points[21] = points[21] == ENTSOE_NO_VALUE ? ENTSOE_NO_VALUE : points[21] * 10000;
container->points[22] = points[22] == ENTSOE_NO_VALUE ? ENTSOE_NO_VALUE : points[22] * 10000;
container->points[23] = points[23] == ENTSOE_NO_VALUE ? ENTSOE_NO_VALUE : points[23] * 10000;
for(uint8_t i = 0; i < 25; i++) {
container->points[i] = points[i] == ENTSOE_NO_VALUE ? ENTSOE_NO_VALUE : points[i] * 10000;
}
}

View File

@@ -72,23 +72,34 @@ float EntsoeApi::getValueForHour(int8_t hour) {
return getValueForHour(cur, hour);
}
float EntsoeApi::getValueForHour(time_t cur, int8_t hour) {
float EntsoeApi::getValueForHour(time_t ts, int8_t hour) {
tmElements_t tm;
if(tz != NULL)
cur = tz->toLocal(cur);
breakTime(cur, tm);
int pos = tm.Hour + hour;
int8_t pos = hour;
breakTime(tz->toLocal(ts), tm);
while(tm.Hour > 0) {
ts -= 3600;
breakTime(tz->toLocal(ts), tm);
pos++;
}
uint8_t hoursToday = 0;
uint8_t todayDate = tm.Day;
while(tm.Day == todayDate) {
ts += 3600;
breakTime(tz->toLocal(ts), tm);
hoursToday++;
}
if(pos >= 48)
return ENTSOE_NO_VALUE;
double value = ENTSOE_NO_VALUE;
double multiplier = config->multiplier / 1000.0;
if(pos > 23) {
float value = ENTSOE_NO_VALUE;
float multiplier = config->multiplier / 1000.0;
if(pos >= hoursToday) {
if(tomorrow == NULL)
return ENTSOE_NO_VALUE;
if(tomorrow->points[pos-24] == ENTSOE_NO_VALUE)
if(tomorrow->points[pos-hoursToday] == ENTSOE_NO_VALUE)
return ENTSOE_NO_VALUE;
value = tomorrow->points[pos-24] / 10000.0;
value = tomorrow->points[pos-hoursToday] / 10000.0;
if(strcmp(tomorrow->measurementUnit, "KWH") == 0) {
// Multiplier is 1
} else if(strcmp(tomorrow->measurementUnit, "MWH") == 0) {
@@ -96,7 +107,7 @@ float EntsoeApi::getValueForHour(time_t cur, int8_t hour) {
} else {
return ENTSOE_NO_VALUE;
}
float mult = getCurrencyMultiplier(tomorrow->currency, config->currency, cur);
float mult = getCurrencyMultiplier(tomorrow->currency, config->currency, time(nullptr));
if(mult == 0) return ENTSOE_NO_VALUE;
multiplier *= mult;
} else if(pos >= 0) {
@@ -112,7 +123,7 @@ float EntsoeApi::getValueForHour(time_t cur, int8_t hour) {
} else {
return ENTSOE_NO_VALUE;
}
float mult = getCurrencyMultiplier(today->currency, config->currency, cur);
float mult = getCurrencyMultiplier(today->currency, config->currency, time(nullptr));
if(mult == 0) return ENTSOE_NO_VALUE;
multiplier *= mult;
}
@@ -147,7 +158,7 @@ bool EntsoeApi::loop() {
}
if(currentDay != tm.Day) {
if(debugger->isActive(RemoteDebug::INFO)) debugger->printf("(EntsoeApi) Rotating price objects at %lu\n", t);
if(debugger->isActive(RemoteDebug::INFO)) debugger->printf_P(PSTR("(EntsoeApi) Rotating price objects at %lu\n"), t);
if(today != NULL) delete today;
if(tomorrow != NULL) {
today = tomorrow;
@@ -161,25 +172,33 @@ bool EntsoeApi::loop() {
return today != NULL; // Only trigger MQTT publish if we have todays prices.
}
if(today == NULL && (lastTodayFetch == 0 || now - lastTodayFetch > 60000)) {
bool readyToFetchForTomorrow = tomorrow == NULL && (tm.Hour > 13 || (tm.Hour == 13 && tm.Minute >= tomorrowFetchMinute)) && (lastTomorrowFetch == 0 || now - lastTomorrowFetch > (nextFetchDelayMinutes*60000));
if(today == NULL && (lastTodayFetch == 0 || now - lastTodayFetch > (nextFetchDelayMinutes*60000))) {
try {
lastTodayFetch = now;
today = fetchPrices(t);
} catch(const std::exception& e) {
if(lastError == 0) lastError = 900;
if(lastError == 0) {
lastError = 900;
nextFetchDelayMinutes = 60;
}
today = NULL;
}
return today != NULL; // Only trigger MQTT publish if we have todays prices.
return today != NULL && !readyToFetchForTomorrow; // Only trigger MQTT publish if we have todays prices and we are not immediately ready to fetch price for tomorrow.
}
// 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)) {
if(readyToFetchForTomorrow) {
try {
lastTomorrowFetch = now;
tomorrow = fetchPrices(t+SECS_PER_DAY);
} catch(const std::exception& e) {
if(lastError == 0) lastError = 900;
if(lastError == 0) {
lastError = 900;
nextFetchDelayMinutes = 60;
}
tomorrow = NULL;
}
return tomorrow != NULL;
@@ -191,7 +210,7 @@ bool EntsoeApi::loop() {
bool EntsoeApi::retrieve(const char* url, Stream* doc) {
#if defined(ESP32)
if(http.begin(url)) {
printD("Connection established");
if(debugger->isActive(RemoteDebug::DEBUG)) debugger->printf_P(PSTR("Connection established\n"));
#if defined(ESP32)
esp_task_wdt_reset();
@@ -208,16 +227,24 @@ bool EntsoeApi::retrieve(const char* url, Stream* doc) {
#endif
if(status == HTTP_CODE_OK) {
printD("Receiving data");
if(debugger->isActive(RemoteDebug::DEBUG)) debugger->printf_P(PSTR("Receiving data\n"));
http.writeToStream(doc);
http.end();
lastError = 0;
nextFetchDelayMinutes = 1;
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());
if(status == 429) {
nextFetchDelayMinutes = 60;
} else if(status == 404) {
nextFetchDelayMinutes = 180;
} else {
nextFetchDelayMinutes = 30;
}
if(debugger->isActive(RemoteDebug::ERROR)) debugger->printf_P(PSTR("(EntsoeApi) Communication error, returned status: %d\n"), status);
if(debugger->isActive(RemoteDebug::ERROR)) debugger->printf(http.errorToString(status).c_str());
if(debugger->isActive(RemoteDebug::DEBUG)) debugger->printf(http.getString().c_str());
http.end();
return false;
@@ -245,18 +272,18 @@ float EntsoeApi::getCurrencyMultiplier(const char* from, const char* to, time_t
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);
snprintf_P(buf, BufferSize, PSTR("https://data.norges-bank.no/api/data/EXR/M.%s.NOK.SP?lastNObservations=1"), from);
if(debugger->isActive(RemoteDebug::INFO)) debugger->printf_P(PSTR("(EntsoeApi) Retrieving %s to NOK conversion\n"), from);
if(debugger->isActive(RemoteDebug::DEBUG)) debugger->printf_P(PSTR("(EntsoeApi) url: %s\n"), buf);
if(retrieve(buf, &p)) {
if(debugger->isActive(RemoteDebug::DEBUG)) debugger->printf("(EntsoeApi) got exchange rate %.4f\n", p.getValue());
if(debugger->isActive(RemoteDebug::DEBUG)) debugger->printf_P(PSTR("(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);
snprintf_P(buf, BufferSize, PSTR("https://data.norges-bank.no/api/data/EXR/M.%s.NOK.SP?lastNObservations=1"), to);
if(debugger->isActive(RemoteDebug::INFO)) debugger->printf_P(PSTR("(EntsoeApi) Retrieving %s to NOK conversion\n"), to);
if(debugger->isActive(RemoteDebug::DEBUG)) debugger->printf_P(PSTR("(EntsoeApi) url: %s\n"), buf);
if(retrieve(buf, &p)) {
if(debugger->isActive(RemoteDebug::DEBUG)) debugger->printf("(EntsoeApi) got exchange rate %.4f\n", p.getValue());
if(debugger->isActive(RemoteDebug::DEBUG)) debugger->printf_P(PSTR("(EntsoeApi) got exchange rate %.4f\n"), p.getValue());
currencyMultiplier /= p.getValue();
} else {
return 0;
@@ -265,7 +292,7 @@ float EntsoeApi::getCurrencyMultiplier(const char* from, const char* to, time_t
} else {
return 0;
}
if(debugger->isActive(RemoteDebug::DEBUG)) debugger->printf("(EntsoeApi) Resulting currency multiplier: %.4f\n", currencyMultiplier);
if(debugger->isActive(RemoteDebug::DEBUG)) debugger->printf_P(PSTR("(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);
@@ -283,8 +310,8 @@ PricesContainer* EntsoeApi::fetchPrices(time_t t) {
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(),
snprintf_P(buf, BufferSize, PSTR("https://web-api.tp.entsoe.eu/api?securityToken=%s&documentType=A44&periodStart=%04d%02d%02d%02d%02d&periodEnd=%04d%02d%02d%02d%02d&in_Domain=%s&out_Domain=%s"),
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);
@@ -295,8 +322,8 @@ PricesContainer* EntsoeApi::fetchPrices(time_t t) {
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);
if(debugger->isActive(RemoteDebug::INFO)) debugger->printf_P(PSTR("(EntsoeApi) Fetching prices for %d.%d.%d\n"), tm.Day, tm.Month, tm.Year+1970);
if(debugger->isActive(RemoteDebug::DEBUG)) debugger->printf_P(PSTR("(EntsoeApi) url: %s\n"), buf);
EntsoeA44Parser a44;
if(retrieve(buf, &a44) && a44.getPoint(0) != ENTSOE_NO_VALUE) {
PricesContainer* ret = new PricesContainer();
@@ -307,8 +334,7 @@ PricesContainer* EntsoeApi::fetchPrices(time_t t) {
}
} else if(hub) {
String data;
snprintf(buf, BufferSize, "%s/%s/%d/%d/%d?currency=%s",
"http://hub.amsleser.no/hub/price",
snprintf_P(buf, BufferSize, PSTR("http://hub.amsleser.no/hub/price/%s/%d/%d/%d?currency=%s"),
config->area,
tm.Year+1970,
tm.Month,
@@ -331,78 +357,76 @@ PricesContainer* EntsoeApi::fetchPrices(time_t t) {
#endif
if(status == HTTP_CODE_OK) {
printD("Receiving data");
if(debugger->isActive(RemoteDebug::DEBUG)) debugger->printf_P(PSTR("Receiving data\n"));
data = http.getString();
http.end();
lastError = 0;
uint8_t* content = (uint8_t*) (data.c_str());
if(debugger->isActive(RemoteDebug::DEBUG)) {
if(debugger->isActive(RemoteDebug::DEBUG)) debugger->printf_P(PSTR("Received content for prices:\n"));
debugPrint(content, 0, data.length());
}
DataParserContext ctx = {0,0,0,0};
ctx.length = data.length();
GCMParser gcm(key, auth);
int8_t gcmRet = gcm.parse(content, ctx);
if(debugger->isActive(RemoteDebug::DEBUG)) {
if(debugger->isActive(RemoteDebug::DEBUG)) debugger->printf_P(PSTR("Decrypted content for prices:\n"));
debugPrint(content, 0, data.length());
}
if(gcmRet > 0) {
if(debugger->isActive(RemoteDebug::DEBUG)) debugger->printf_P(PSTR("(EntsoeApi) Price data starting at: %d\n"), gcmRet);
PricesContainer* ret = new PricesContainer();
for(uint8_t i = 0; i < 25; i++) {
ret->points[i] = ENTSOE_NO_VALUE;
}
memcpy(ret, content+gcmRet, sizeof(*ret));
for(uint8_t i = 0; i < 25; i++) {
ret->points[i] = ntohl(ret->points[i]);
}
lastError = 0;
nextFetchDelayMinutes = 1;
return ret;
} else {
lastError = gcmRet;
nextFetchDelayMinutes = 60;
if(debugger->isActive(RemoteDebug::ERROR)) debugger->printf_P(PSTR("(EntsoeApi) Error code while decrypting prices: %d\n"), gcmRet);
}
} 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());
if(status == 429) {
nextFetchDelayMinutes = 60;
} else if(status == 404) {
nextFetchDelayMinutes = 180;
} else {
nextFetchDelayMinutes = 30;
}
if(debugger->isActive(RemoteDebug::ERROR)) debugger->printf_P(PSTR("(EntsoeApi) Communication error, returned status: %d\n"), status);
if(debugger->isActive(RemoteDebug::ERROR)) debugger->printf(http.errorToString(status).c_str());
if(debugger->isActive(RemoteDebug::DEBUG)) debugger->printf(http.getString().c_str());
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(F("0"));
debugger->print(buffer[i], HEX);
debugger->print(" ");
debugger->print(F(" "));
if ((i - start + 1) % 16 == 0)
debugger->println("");
debugger->println(F(""));
else if ((i - start + 1) % 4 == 0)
debugger->print(" ");
debugger->print(F(" "));
yield(); // Let other get some resources too
}
debugger->println("");
debugger->println(F(""));
}
int16_t EntsoeApi::getLastError() {

View File

@@ -2,34 +2,171 @@
#define _HOMEASSISTANTMQTTHANDLER_H
#include "AmsMqttHandler.h"
#include "HomeAssistantStatic.h"
#include "AmsConfiguration.h"
class HomeAssistantMqttHandler : public AmsMqttHandler {
public:
HomeAssistantMqttHandler(MQTTClient* mqtt, char* buf, const char* clientId, const char* topic, HwTools* hw) : AmsMqttHandler(mqtt, buf) {
HomeAssistantMqttHandler(MQTTClient* mqtt, char* buf, const char* clientId, const char* topic, uint8_t boardType, HomeAssistantConfig config, HwTools* hw) : AmsMqttHandler(mqtt, buf) {
this->clientId = clientId;
this->topic = String(topic);
this->hw = hw;
l1Init = l2Init = l2eInit = l3Init = l3eInit = l4Init = l4eInit = rtInit = rteInit = pInit = sInit = false;
if(strlen(config.discoveryNameTag) > 0) {
snprintf_P(buf, 128, PSTR("AMS reader (%s)"), config.discoveryNameTag);
deviceName = String(buf);
snprintf_P(buf, 128, PSTR("[%s] "), config.discoveryNameTag);
sensorNamePrefix = String(buf);
} else {
deviceName = F("AMS reader");
sensorNamePrefix = "";
}
deviceModel = boardTypeToString(boardType);
manufacturer = boardManufacturerToString(boardType);
#if defined(ESP8266)
String hostname = WiFi.hostname();
#elif defined(ESP32)
String hostname = WiFi.getHostname();
#endif
deviceUid = hostname; // Maybe configurable in the future?
if(strlen(config.discoveryHostname) > 0) {
if(strncmp_P(config.discoveryHostname, PSTR("http"), 4) == 0) {
deviceUrl = String(config.discoveryHostname);
} else {
snprintf_P(buf, 128, PSTR("http://%s/"), config.discoveryHostname);
deviceUrl = String(buf);
}
} else {
snprintf_P(buf, 128, PSTR("http://%s.local/"), hostname);
deviceUrl = String(buf);
}
if(strlen(config.discoveryPrefix) > 0) {
snprintf_P(buf, 128, PSTR("%s/sensor/"), config.discoveryPrefix);
discoveryTopic = String(buf);
} else {
discoveryTopic = F("homeassistant/sensor/");
}
};
bool publish(AmsData* data, AmsData* previousState, EnergyAccounting* ea);
bool publish(AmsData* data, AmsData* previousState, EnergyAccounting* ea, EntsoeApi* eapi);
bool publishTemperatures(AmsConfiguration*, HwTools*);
bool publishPrices(EntsoeApi*);
bool publishSystem(HwTools* hw, EntsoeApi* eapi, EnergyAccounting* ea);
protected:
bool loop();
private:
String haTopic = "homeassistant/sensor/";
String deviceName;
String deviceModel;
String deviceUid;
String manufacturer;
String deviceUrl;
String haName = "AMS reader";
#if defined(ESP32)
String haModel = "ESP32";
#elif defined(ESP8266)
String haModel = "ESP8266";
#endif
String haManuf = "AmsToMqttBridge";
String discoveryTopic;
String sensorNamePrefix;
bool autodiscoverInit = false;
bool l1Init, l2Init, l2eInit, l3Init, l3eInit, l4Init, l4eInit, rtInit, rteInit, pInit, sInit;
bool tInit[32] = {false};
bool prInit[38] = {false};
String clientId;
String topic;
HwTools* hw;
bool publishList1(AmsData* data, EnergyAccounting* ea);
bool publishList2(AmsData* data, EnergyAccounting* ea);
bool publishList3(AmsData* data, EnergyAccounting* ea);
bool publishList4(AmsData* data, EnergyAccounting* ea);
String getMeterModel(AmsData* data);
bool publishRealtime(AmsData* data, EnergyAccounting* ea, EntsoeApi* eapi);
void publishSensor(const HomeAssistantSensor& sensor);
void publishList1Sensors();
void publishList1ExportSensors();
void publishList2Sensors();
void publishList2ExportSensors();
void publishList3Sensors();
void publishList3ExportSensors();
void publishList4Sensors();
void publishList4ExportSensors();
void publishRealtimeSensors(EnergyAccounting* ea, EntsoeApi* eapi);
void publishRealtimeExportSensors(EnergyAccounting* ea, EntsoeApi* eapi);
void publishTemperatureSensor(uint8_t index, String id);
void publishPriceSensors(EntsoeApi* eapi);
void publishSystemSensors();
String boardTypeToString(uint8_t b) {
switch(b) {
case 5:
#if defined(ESP8266)
return F("Pow-K");
#elif defined(ESP32)
return F("Pow-K+");
#endif
case 7:
#if defined(ESP8266)
return F("Pow-U");
#elif defined(ESP32)
return F("Pow-U+");
#endif
case 6:
return F("Pow-P1");
case 51:
return F("S2 mini");
case 50:
return F("ESP32-S2");
case 201:
return F("LOLIN D32");
case 202:
return F("HUZZAH32");
case 203:
return F("DevKitC");
case 200:
return F("ESP32");
case 2:
return F("HAN Reader 2.0 by Max Spencer");
case 0:
return F("Custom hardware by Roar Fredriksen");
case 1:
return F("Kamstrup module by Egil Opsahl");
case 3:
return F("Pow-K");
case 4:
return F("Pow-U");
case 101:
return F("D1 mini");
case 100:
return F("ESP8266");
case 70:
return F("ESP32-C3");
case 71:
return F("ESP32-C3-DevKitM-1");
}
#if defined(ESP8266)
return F("ESP8266");
#elif defined(ESP32)
return F("ESP32");
#endif
};
String boardManufacturerToString(uint8_t b) {
if(b >= 3 && b <= 7)
return F("amsleser.no");
if(b < 50)
return F("Custom");
switch(b) {
case 51:
case 101:
case 201:
return F("Wemos");
case 202:
return F("Adafruit");
}
return F("Espressif");
};
};
#endif

View File

@@ -3,7 +3,7 @@
#include "Arduino.h"
struct HomeAssistantSensor {
typedef struct HomeAssistantSensor {
const char* name;
const char* topic;
const char* path;
@@ -13,69 +13,100 @@ struct HomeAssistantSensor {
};
const uint8_t HA_SENSOR_COUNT PROGMEM = 60;
HomeAssistantSensor HA_SENSORS[HA_SENSOR_COUNT] PROGMEM = {
{"Status", "/state", "rssi", "dBm", "signal_strength", "\"measurement\""},
{"Supply volt", "/state", "vcc", "V", "voltage", "\"measurement\""},
{"Temperature", "/state", "temp", "°C", "temperature", "\"measurement\""},
{"Active import", "/power", "P", "W", "power", "\"measurement\""},
{"L1 active import", "/power", "P1", "W", "power", "\"measurement\""},
{"L2 active import", "/power", "P2", "W", "power", "\"measurement\""},
{"L3 active import", "/power", "P3", "W", "power", "\"measurement\""},
const uint8_t List1SensorCount PROGMEM = 1;
const HomeAssistantSensor List1Sensors[List1SensorCount] PROGMEM = {
{"Active import", "/power", "P", "W", "power", "\"measurement\""}
};
const uint8_t List2SensorCount PROGMEM = 8;
const HomeAssistantSensor List2Sensors[List2SensorCount] PROGMEM = {
{"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\""},
{"L1 current", "/power", "I1", "A", "current", "\"measurement\""},
{"L2 current", "/power", "I2", "A", "current", "\"measurement\""},
{"L3 current", "/power", "I3", "A", "current", "\"measurement\""},
{"L1 voltage", "/power", "U1", "V", "voltage", "\"measurement\""},
{"L2 voltage", "/power", "U2", "V", "voltage", "\"measurement\""},
{"L3 voltage", "/power", "U3", "V", "voltage", "\"measurement\""},
{"L3 voltage", "/power", "U3", "V", "voltage", "\"measurement\""}
};
const uint8_t List2ExportSensorCount PROGMEM = 1;
const HomeAssistantSensor List2ExportSensors[List2ExportSensorCount] PROGMEM = {
{"Active export", "/power", "PO", "W", "power", "\"measurement\""}
};
const uint8_t List3SensorCount PROGMEM = 3;
const HomeAssistantSensor List3Sensors[List3SensorCount] PROGMEM = {
{"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\""},
{"Accumulated reactive import","/energy", "tQI", "", "energy", "\"total_increasing\""},
{"Accumulated reactive export","/energy", "tQO", "", "energy", "\"total_increasing\""}
};
const uint8_t List3ExportSensorCount PROGMEM = 1;
const HomeAssistantSensor List3ExportSensors[List3ExportSensorCount] PROGMEM = {
{"Accumulated active export", "/energy", "tPO", "kWh", "energy", "\"total_increasing\""}
};
const uint8_t List4SensorCount PROGMEM = 7;
const HomeAssistantSensor List4Sensors[List4SensorCount] PROGMEM = {
{"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", ""},
{"Price in three hour", "/prices", "prices['3']", "", "monetary", ""},
{"Price in four hour", "/prices", "prices['4']", "", "monetary", ""},
{"Price in five hour", "/prices", "prices['5']", "", "monetary", ""},
{"Price in six hour", "/prices", "prices['6']", "", "monetary", ""},
{"Price in seven hour", "/prices", "prices['7']", "", "monetary", ""},
{"Price in eight hour", "/prices", "prices['8']", "", "monetary", ""},
{"Price in nine hour", "/prices", "prices['9']", "", "monetary", ""},
{"Price in ten hour", "/prices", "prices['10']", "", "monetary", ""},
{"Price in eleven hour", "/prices", "prices['11']", "", "monetary", ""},
{"L1 active import", "/power", "P1", "W", "power", "\"measurement\""},
{"L2 active import", "/power", "P2", "W", "power", "\"measurement\""},
{"L3 active import", "/power", "P3", "W", "power", "\"measurement\""}
};
const uint8_t List4ExportSensorCount PROGMEM = 3;
const HomeAssistantSensor List4ExportSensors[List4ExportSensorCount] PROGMEM = {
{"L1 active export", "/power", "PO1", "W", "power", "\"measurement\""},
{"L2 active export", "/power", "PO2", "W", "power", "\"measurement\""},
{"L3 active export", "/power", "PO3", "W", "power", "\"measurement\""}
};
const uint8_t RealtimeSensorCount PROGMEM = 8;
const HomeAssistantSensor RealtimeSensors[RealtimeSensorCount] PROGMEM = {
{"Month max", "/realtime","max", "kWh", "energy", "\"total_increasing\""},
{"Tariff threshold", "/realtime","threshold", "kWh", "energy", "\"total_increasing\""},
{"Current hour used", "/realtime","hour.use", "kWh", "energy", "\"total_increasing\""},
{"Current hour cost", "/realtime","hour.cost", "", "monetary", ""},
{"Current day used", "/realtime","day.use", "kWh", "energy", "\"total_increasing\""},
{"Current day cost", "/realtime","day.cost", "", "monetary", ""},
{"Current month used", "/realtime","month.use", "kWh", "energy", "\"total_increasing\""},
{"Current month cost", "/realtime","month.cost", "", "monetary", ""}
};
const uint8_t RealtimeExportSensorCount PROGMEM = 6;
const HomeAssistantSensor RealtimeExportSensors[RealtimeExportSensorCount] PROGMEM = {
{"Current hour produced", "/realtime","hour.produced", "kWh", "energy", "\"total_increasing\""},
{"Current hour income", "/realtime","hour.income", "", "monetary", ""},
{"Current day produced", "/realtime","day.produced", "kWh", "energy", "\"total_increasing\""},
{"Current day income", "/realtime","day.income", "", "monetary", ""},
{"Current month produced", "/realtime","month.produced", "kWh", "energy", "\"total_increasing\""},
{"Current month income", "/realtime","month.income", "", "monetary", ""}
};
const HomeAssistantSensor RealtimePeakSensor PROGMEM = {"Current month peak %d", "/realtime", "peaks[%d]", "kWh", "energy", ""};
const uint8_t PriceSensorCount PROGMEM = 5;
const HomeAssistantSensor PriceSensors[PriceSensorCount] PROGMEM = {
{"Minimum price ahead", "/prices", "prices.min", "", "monetary", ""},
{"Maximum price ahead", "/prices", "prices.max", "", "monetary", ""},
{"Cheapest 1hr period ahead", "/prices", "prices.cheapest1hr","", "timestamp", ""},
{"Cheapest 3hr period ahead", "/prices", "prices.cheapest3hr","", "timestamp", ""},
{"Cheapest 6hr period ahead", "/prices", "prices.cheapest6hr","", "timestamp", ""},
{"Month max", "/realtime","max", "kWh", "energy", "\"total_increasing\""},
{"Tariff threshold", "/realtime","threshold", "kWh", "energy", "\"total_increasing\""},
{"Current hour used", "/realtime","hour.use", "kWh", "energy", "\"total_increasing\""},
{"Current hour cost", "/realtime","hour.cost", "", "monetary", "\"total_increasing\""},
{"Current hour produced", "/realtime","hour.produced", "kWh", "energy", "\"total_increasing\""},
{"Current day used", "/realtime","day.use", "kWh", "energy", "\"total_increasing\""},
{"Current day cost", "/realtime","day.cost", "", "monetary", "\"total_increasing\""},
{"Current day produced", "/realtime","day.produced", "kWh", "energy", "\"total_increasing\""},
{"Current month used", "/realtime","month.use", "kWh", "energy", "\"total_increasing\""},
{"Current month cost", "/realtime","month.cost", "", "monetary", "\"total_increasing\""},
{"Current month produced", "/realtime","month.produced", "kWh", "energy", "\"total_increasing\""},
{"Current month peak 1", "/realtime","peaks[0]", "kWh", "energy", ""},
{"Current month peak 2", "/realtime","peaks[1]", "kWh", "energy", ""},
{"Current month peak 3", "/realtime","peaks[2]", "kWh", "energy", ""},
{"Current month peak 4", "/realtime","peaks[3]", "kWh", "energy", ""},
{"Current month peak 5", "/realtime","peaks[4]", "kWh", "energy", ""},
{"Cheapest 6hr period ahead", "/prices", "prices.cheapest6hr","", "timestamp", ""}
};
const HomeAssistantSensor PriceSensor PROGMEM = {"Price in %02d %s", "/prices", "prices['%d']", "", "monetary", ""};
const uint8_t SystemSensorCount PROGMEM = 2;
const HomeAssistantSensor SystemSensors[SystemSensorCount] PROGMEM = {
{"Status", "/state", "rssi", "dBm", "signal_strength", "\"measurement\""},
{"Supply volt", "/state", "vcc", "V", "voltage", "\"measurement\""}
};
const HomeAssistantSensor TemperatureSensor PROGMEM = {"Temperature sensor %s", "/temperatures", "temperatures['%s']", "°C", "temperature", "\"measurement\""};
#endif

View File

@@ -1,5 +1,5 @@
{
"name" : "%s",
"name" : "%s%s",
"stat_t" : "%s%s",
"uniq_id" : "%s_%s",
"obj_id" : "%s_%s",

View File

@@ -5,16 +5,19 @@
"hour" : {
"use" : %.2f,
"cost" : %.2f,
"produced" : %.2f
"produced" : %.2f,
"income" : %.2f
},
"day" : {
"use" : %.2f,
"cost" : %.2f,
"produced" : %.2f
"produced" : %.2f,
"income" : %.2f
},
"month" : {
"use" : %.2f,
"cost" : %.2f,
"produced" : %.2f
"produced" : %.2f,
"income" : %.2f
}
}

View File

@@ -1,5 +1,4 @@
#include "HomeAssistantMqttHandler.h"
#include "HomeAssistantStatic.h"
#include "hexutils.h"
#include "Uptime.h"
#include "version.h"
@@ -12,124 +11,172 @@
#include "json/hadiscover_json.h"
#include "json/realtime_json.h"
bool HomeAssistantMqttHandler::publish(AmsData* data, AmsData* previousState, EnergyAccounting* ea) {
#if defined(ESP32)
#include <esp_task_wdt.h>
#endif
bool HomeAssistantMqttHandler::publish(AmsData* data, AmsData* previousState, EnergyAccounting* ea, EntsoeApi* eapi) {
if(topic.isEmpty() || !mqtt->connected())
return false;
if(data->getListType() >= 3) { // publish energy counts
snprintf_P(json, BufferSize, HA2_JSON,
data->getActiveImportCounter(),
data->getActiveExportCounter(),
data->getReactiveImportCounter(),
data->getReactiveExportCounter(),
data->getMeterTimestamp()
);
mqtt->publish(topic + "/energy", json);
mqtt->loop();
publishList3(data, ea);
loop();
}
String meterModel = data->getMeterModel();
meterModel.replace("\\", "\\\\");
if(data->getListType() == 1) { // publish power counts
snprintf_P(json, BufferSize, HA1_JSON,
data->getActiveImportPower()
);
mqtt->publish(topic + "/power", json);
publishList1(data, ea);
} else if(data->getListType() <= 3) { // publish power counts and volts/amps
snprintf_P(json, BufferSize, HA3_JSON,
data->getListId().c_str(),
data->getMeterId().c_str(),
meterModel.c_str(),
data->getActiveImportPower(),
data->getReactiveImportPower(),
data->getActiveExportPower(),
data->getReactiveExportPower(),
data->getL1Current(),
data->getL2Current(),
data->getL3Current(),
data->getL1Voltage(),
data->getL2Voltage(),
data->getL3Voltage()
);
mqtt->publish(topic + "/power", json);
publishList2(data, ea);
} else if(data->getListType() == 4) { // publish power counts and volts/amps/phase power and PF
snprintf_P(json, BufferSize, HA4_JSON,
data->getListId().c_str(),
data->getMeterId().c_str(),
meterModel.c_str(),
data->getActiveImportPower(),
data->getL1ActiveImportPower(),
data->getL2ActiveImportPower(),
data->getL3ActiveImportPower(),
data->getReactiveImportPower(),
data->getActiveExportPower(),
data->getL1ActiveExportPower(),
data->getL2ActiveExportPower(),
data->getL3ActiveExportPower(),
data->getReactiveExportPower(),
data->getL1Current(),
data->getL2Current(),
data->getL3Current(),
data->getL1Voltage(),
data->getL2Voltage(),
data->getL3Voltage(),
data->getPowerFactor() == 0 ? 1 : data->getPowerFactor(),
data->getPowerFactor() == 0 ? 1 : data->getL1PowerFactor(),
data->getPowerFactor() == 0 ? 1 : data->getL2PowerFactor(),
data->getPowerFactor() == 0 ? 1 : data->getL3PowerFactor()
);
mqtt->publish(topic + "/power", json);
publishList4(data, ea);
}
loop();
String peaks = "";
uint8_t peakCount = ea->getConfig()->hours;
if(peakCount > 5) peakCount = 5;
for(uint8_t i = 1; i <= peakCount; i++) {
if(!peaks.isEmpty()) peaks += ",";
peaks += String(ea->getPeak(i).value / 100.0, 2);
}
snprintf_P(json, BufferSize, REALTIME_JSON,
ea->getMonthMax(),
peaks.c_str(),
ea->getCurrentThreshold(),
ea->getUseThisHour(),
ea->getCostThisHour(),
ea->getProducedThisHour(),
ea->getUseToday(),
ea->getCostToday(),
ea->getProducedToday(),
ea->getUseThisMonth(),
ea->getCostThisMonth(),
ea->getProducedThisMonth()
);
mqtt->publish(topic + "/realtime", json);
if(ea->isInitialized()) {
publishRealtime(data, ea, eapi);
loop();
}
return true;
}
bool HomeAssistantMqttHandler::publishList1(AmsData* data, EnergyAccounting* ea) {
publishList1Sensors();
snprintf_P(json, BufferSize, HA1_JSON,
data->getActiveImportPower()
);
return mqtt->publish(topic + "/power", json);
}
bool HomeAssistantMqttHandler::publishList2(AmsData* data, EnergyAccounting* ea) {
publishList2Sensors();
if(data->getActiveExportPower() > 0) publishList2ExportSensors();
snprintf_P(json, BufferSize, HA3_JSON,
data->getListId().c_str(),
data->getMeterId().c_str(),
getMeterModel(data).c_str(),
data->getActiveImportPower(),
data->getReactiveImportPower(),
data->getActiveExportPower(),
data->getReactiveExportPower(),
data->getL1Current(),
data->getL2Current(),
data->getL3Current(),
data->getL1Voltage(),
data->getL2Voltage(),
data->getL3Voltage()
);
return mqtt->publish(topic + "/power", json);
}
bool HomeAssistantMqttHandler::publishList3(AmsData* data, EnergyAccounting* ea) {
publishList3Sensors();
if(data->getActiveExportCounter() > 0.0) publishList3ExportSensors();
snprintf_P(json, BufferSize, HA2_JSON,
data->getActiveImportCounter(),
data->getActiveExportCounter(),
data->getReactiveImportCounter(),
data->getReactiveExportCounter(),
data->getMeterTimestamp()
);
return mqtt->publish(topic + "/energy", json);
}
bool HomeAssistantMqttHandler::publishList4(AmsData* data, EnergyAccounting* ea) {
publishList4Sensors();
if(data->getL1ActiveExportPower() > 0 || data->getL2ActiveExportPower() > 0 || data->getL3ActiveExportPower() > 0) publishList4ExportSensors();
snprintf_P(json, BufferSize, HA4_JSON,
data->getListId().c_str(),
data->getMeterId().c_str(),
getMeterModel(data).c_str(),
data->getActiveImportPower(),
data->getL1ActiveImportPower(),
data->getL2ActiveImportPower(),
data->getL3ActiveImportPower(),
data->getReactiveImportPower(),
data->getActiveExportPower(),
data->getL1ActiveExportPower(),
data->getL2ActiveExportPower(),
data->getL3ActiveExportPower(),
data->getReactiveExportPower(),
data->getL1Current(),
data->getL2Current(),
data->getL3Current(),
data->getL1Voltage(),
data->getL2Voltage(),
data->getL3Voltage(),
data->getPowerFactor() == 0 ? 1 : data->getPowerFactor(),
data->getPowerFactor() == 0 ? 1 : data->getL1PowerFactor(),
data->getPowerFactor() == 0 ? 1 : data->getL2PowerFactor(),
data->getPowerFactor() == 0 ? 1 : data->getL3PowerFactor()
);
return mqtt->publish(topic + "/power", json);
}
String HomeAssistantMqttHandler::getMeterModel(AmsData* data) {
String meterModel = data->getMeterModel();
meterModel.replace("\\", "\\\\");
return meterModel;
}
bool HomeAssistantMqttHandler::publishRealtime(AmsData* data, EnergyAccounting* ea, EntsoeApi* eapi) {
publishRealtimeSensors(ea, eapi);
if(ea->getProducedThisHour() > 0.0 || ea->getProducedToday() > 0.0 || ea->getProducedThisMonth() > 0.0) publishRealtimeExportSensors(ea, eapi);
String peaks = "";
uint8_t peakCount = ea->getConfig()->hours;
if(peakCount > 5) peakCount = 5;
for(uint8_t i = 1; i <= peakCount; i++) {
if(!peaks.isEmpty()) peaks += ",";
peaks += String(ea->getPeak(i).value / 100.0, 2);
}
snprintf_P(json, BufferSize, REALTIME_JSON,
ea->getMonthMax(),
peaks.c_str(),
ea->getCurrentThreshold(),
ea->getUseThisHour(),
ea->getCostThisHour(),
ea->getProducedThisHour(),
ea->getIncomeThisHour(),
ea->getUseToday(),
ea->getCostToday(),
ea->getProducedToday(),
ea->getIncomeToday(),
ea->getUseThisMonth(),
ea->getCostThisMonth(),
ea->getProducedThisMonth(),
ea->getIncomeThisMonth()
);
return mqtt->publish(topic + "/realtime", json);
}
bool HomeAssistantMqttHandler::publishTemperatures(AmsConfiguration* config, HwTools* hw) {
int count = hw->getTempSensorCount();
if(count == 0) return false;
if(count < 2) return false;
int size = 32 + (count * 26);
char buf[size];
snprintf(buf, 24, "{\"temperatures\":{");
snprintf_P(buf, 24, PSTR("{\"temperatures\":{"));
for(int i = 0; i < count; i++) {
TempSensorData* data = hw->getTempSensorData(i);
if(data != NULL) {
char* pos = buf+strlen(buf);
snprintf(pos, 26, "\"%s\":%.2f,",
toHex(data->address, 8).c_str(),
String id = toHex(data->address, 8);
snprintf_P(pos, 26, PSTR("\"%s\":%.2f,"),
id.c_str(),
data->lastRead
);
data->changed = false;
delay(1);
publishTemperatureSensor(i+1, id);
}
}
char* pos = buf+strlen(buf);
snprintf(count == 0 ? pos : pos-1, 8, "}}");
return mqtt->publish(topic + "/temperatures", buf);
snprintf_P(count == 0 ? pos : pos-1, 8, PSTR("}}"));
bool ret = mqtt->publish(topic + "/temperatures", buf);
loop();
return ret;
}
bool HomeAssistantMqttHandler::publishPrices(EntsoeApi* eapi) {
@@ -138,14 +185,16 @@ bool HomeAssistantMqttHandler::publishPrices(EntsoeApi* eapi) {
if(eapi->getValueForHour(0) == ENTSOE_NO_VALUE)
return false;
publishPriceSensors(eapi);
time_t now = time(nullptr);
float min1hr = 0.0, min3hr = 0.0, min6hr = 0.0;
int8_t min1hrIdx = -1, min3hrIdx = -1, min6hrIdx = -1;
float min = INT16_MAX, max = INT16_MIN;
float values[24];
for(int i = 0;i < 24; i++) values[i] = ENTSOE_NO_VALUE;
for(uint8_t i = 0; i < 24; i++) {
float values[38];
for(int i = 0;i < 38; i++) values[i] = ENTSOE_NO_VALUE;
for(uint8_t i = 0; i < 38; i++) {
float val = eapi->getValueForHour(now, i);
values[i] = val;
@@ -194,28 +243,25 @@ bool HomeAssistantMqttHandler::publishPrices(EntsoeApi* eapi) {
memset(ts1hr, 0, 24);
if(min1hrIdx > -1) {
time_t ts = now + (SECS_PER_HOUR * min1hrIdx);
//Serial.printf("1hr: %d %lu\n", min1hrIdx, ts);
tmElements_t tm;
breakTime(ts, tm);
sprintf(ts1hr, "%04d-%02d-%02dT%02d:00:00Z", tm.Year+1970, tm.Month, tm.Day, tm.Hour);
sprintf_P(ts1hr, PSTR("%04d-%02d-%02dT%02d:00:00Z"), tm.Year+1970, tm.Month, tm.Day, tm.Hour);
}
char ts3hr[24];
memset(ts3hr, 0, 24);
if(min3hrIdx > -1) {
time_t ts = now + (SECS_PER_HOUR * min3hrIdx);
//Serial.printf("3hr: %d %lu\n", min3hrIdx, ts);
tmElements_t tm;
breakTime(ts, tm);
sprintf(ts3hr, "%04d-%02d-%02dT%02d:00:00Z", tm.Year+1970, tm.Month, tm.Day, tm.Hour);
sprintf_P(ts3hr, PSTR("%04d-%02d-%02dT%02d:00:00Z"), tm.Year+1970, tm.Month, tm.Day, tm.Hour);
}
char ts6hr[24];
memset(ts6hr, 0, 24);
if(min6hrIdx > -1) {
time_t ts = now + (SECS_PER_HOUR * min6hrIdx);
//Serial.printf("6hr: %d %lu\n", min6hrIdx, ts);
tmElements_t tm;
breakTime(ts, tm);
sprintf(ts6hr, "%04d-%02d-%02dT%02d:00:00Z", tm.Year+1970, tm.Month, tm.Day, tm.Hour);
sprintf_P(ts6hr, PSTR("%04d-%02d-%02dT%02d:00:00Z"), tm.Year+1970, tm.Month, tm.Day, tm.Hour);
}
snprintf_P(json, BufferSize, JSONPRICES_JSON,
@@ -232,19 +278,50 @@ bool HomeAssistantMqttHandler::publishPrices(EntsoeApi* eapi) {
values[9],
values[10],
values[11],
values[12],
values[13],
values[14],
values[15],
values[16],
values[17],
values[18],
values[19],
values[20],
values[21],
values[22],
values[23],
values[24],
values[25],
values[26],
values[27],
values[28],
values[29],
values[30],
values[31],
values[32],
values[33],
values[34],
values[35],
values[36],
values[37],
min == INT16_MAX ? 0.0 : min,
max == INT16_MIN ? 0.0 : max,
ts1hr,
ts3hr,
ts6hr
);
return mqtt->publish(topic + "/prices", json, true, 0);
bool ret = mqtt->publish(topic + "/prices", json, true, 0);
loop();
return ret;
}
bool HomeAssistantMqttHandler::publishSystem(HwTools* hw, EntsoeApi* eapi, EnergyAccounting* ea) {
if(topic.isEmpty() || !mqtt->connected())
return false;
publishSystemSensors();
if(hw->getTemperature() > -50) publishTemperatureSensor(0, "");
snprintf_P(json, BufferSize, JSONSYS_JSON,
WiFi.macAddress().c_str(),
clientId.c_str(),
@@ -254,64 +331,220 @@ bool HomeAssistantMqttHandler::publishSystem(HwTools* hw, EntsoeApi* eapi, Energ
hw->getTemperature(),
VERSION
);
mqtt->publish(topic + "/state", json);
if(!autodiscoverInit) {
#if defined(ESP8266)
String haUID = WiFi.hostname();
#elif defined(ESP32)
String haUID = WiFi.getHostname();
#endif
String haUrl = "http://" + haUID + ".local/";
// Could this be necessary? haUID.replace("-", "_");
uint8_t peakCount = ea->getConfig()->hours;
if(peakCount > 5) peakCount = 5;
uint8_t peaks = 0;
for(int i=0;i<HA_SENSOR_COUNT;i++) {
HomeAssistantSensor sensor = HA_SENSORS[i];
String uid = String(sensor.path);
uid.replace(".", "");
uid.replace("[", "");
uid.replace("]", "");
uid.replace("'", "");
String uom = String(sensor.uom);
if(strncmp(sensor.devcl, "monetary", 8) == 0) {
if(eapi == NULL) continue;
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,
haUID.c_str(), uid.c_str(),
haUID.c_str(), uid.c_str(),
uom.c_str(),
sensor.path,
sensor.devcl,
haUID.c_str(),
haName.c_str(),
haModel.c_str(),
VERSION,
haManuf.c_str(),
haUrl.c_str(),
strlen_P(sensor.stacl) > 0 ? ", \"stat_cla\" :" : "",
strlen_P(sensor.stacl) > 0 ? (char *) FPSTR(sensor.stacl) : ""
);
mqtt->publish(haTopic + haUID + "_" + uid.c_str() + "/config", json, true, 0);
}
autodiscoverInit = true;
}
return true;
bool ret = mqtt->publish(topic + "/state", json);
loop();
return ret;
}
void HomeAssistantMqttHandler::publishSensor(const HomeAssistantSensor& sensor) {
String uid = String(sensor.path);
uid.replace(".", "");
uid.replace("[", "");
uid.replace("]", "");
uid.replace("'", "");
snprintf_P(json, BufferSize, HADISCOVER_JSON,
sensorNamePrefix.c_str(),
sensor.name,
topic.c_str(), sensor.topic,
deviceUid.c_str(), uid.c_str(),
deviceUid.c_str(), uid.c_str(),
sensor.uom,
sensor.path,
sensor.devcl,
deviceUid.c_str(),
deviceName.c_str(),
deviceModel.c_str(),
VERSION,
manufacturer.c_str(),
deviceUrl.c_str(),
strlen_P(sensor.stacl) > 0 ? ", \"stat_cla\" :" : "",
strlen_P(sensor.stacl) > 0 ? (char *) FPSTR(sensor.stacl) : ""
);
mqtt->publish(discoveryTopic + deviceUid + "_" + uid.c_str() + "/config", json, true, 0);
loop();
}
void HomeAssistantMqttHandler::publishList1Sensors() {
if(l1Init) return;
for(uint8_t i = 0; i < List1SensorCount; i++) {
publishSensor(List1Sensors[i]);
}
l1Init = true;
}
void HomeAssistantMqttHandler::publishList2Sensors() {
publishList1Sensors();
if(l2Init) return;
for(uint8_t i = 0; i < List2SensorCount; i++) {
publishSensor(List2Sensors[i]);
}
l2Init = true;
}
void HomeAssistantMqttHandler::publishList2ExportSensors() {
if(l2eInit) return;
for(uint8_t i = 0; i < List2ExportSensorCount; i++) {
publishSensor(List2ExportSensors[i]);
}
l2eInit = true;
}
void HomeAssistantMqttHandler::publishList3Sensors() {
publishList2Sensors();
if(l3Init) return;
for(uint8_t i = 0; i < List3SensorCount; i++) {
publishSensor(List3Sensors[i]);
}
l3Init = true;
}
void HomeAssistantMqttHandler::publishList3ExportSensors() {
publishList2ExportSensors();
if(l3eInit) return;
for(uint8_t i = 0; i < List3ExportSensorCount; i++) {
publishSensor(List3ExportSensors[i]);
}
l3eInit = true;
}
void HomeAssistantMqttHandler::publishList4Sensors() {
publishList3Sensors();
if(l4Init) return;
for(uint8_t i = 0; i < List4SensorCount; i++) {
publishSensor(List4Sensors[i]);
}
l4Init = true;
}
void HomeAssistantMqttHandler::publishList4ExportSensors() {
publishList3ExportSensors();
if(l4eInit) return;
for(uint8_t i = 0; i < List4ExportSensorCount; i++) {
publishSensor(List4ExportSensors[i]);
}
l4eInit = true;
}
void HomeAssistantMqttHandler::publishRealtimeSensors(EnergyAccounting* ea, EntsoeApi* eapi) {
if(rtInit) return;
for(uint8_t i = 0; i < RealtimeSensorCount; i++) {
HomeAssistantSensor sensor = RealtimeSensors[i];
if(strncmp_P(sensor.devcl, PSTR("monetary"), 8) == 0) {
if(eapi == NULL) continue;
sensor.uom = eapi->getCurrency();
}
publishSensor(sensor);
}
uint8_t peakCount = ea->getConfig()->hours;
if(peakCount > 5) peakCount = 5;
for(uint8_t i = 0; i < peakCount; i++) {
char name[strlen(RealtimePeakSensor.name)];
snprintf(name, strlen(RealtimePeakSensor.name), RealtimePeakSensor.name, i+1);
char path[strlen(RealtimePeakSensor.path)];
snprintf(path, strlen(RealtimePeakSensor.path), RealtimePeakSensor.path, i+1);
HomeAssistantSensor sensor = {
name,
RealtimePeakSensor.topic,
path,
RealtimePeakSensor.uom,
RealtimePeakSensor.devcl,
RealtimePeakSensor.stacl
};
publishSensor(sensor);
}
rtInit = true;
}
void HomeAssistantMqttHandler::publishRealtimeExportSensors(EnergyAccounting* ea, EntsoeApi* eapi) {
if(rteInit) return;
for(uint8_t i = 0; i < RealtimeExportSensorCount; i++) {
HomeAssistantSensor sensor = RealtimeExportSensors[i];
if(strncmp_P(sensor.devcl, PSTR("monetary"), 8) == 0) {
if(eapi == NULL) continue;
sensor.uom = eapi->getCurrency();
}
publishSensor(sensor);
}
rteInit = true;
}
void HomeAssistantMqttHandler::publishTemperatureSensor(uint8_t index, String id) {
if(index > 32) return;
if(tInit[index]) return;
char name[strlen(TemperatureSensor.name)+id.length()];
snprintf(name, strlen(TemperatureSensor.name)+id.length(), TemperatureSensor.name, id.c_str());
char path[strlen(TemperatureSensor.path)+id.length()];
if(index == 0) {
memcpy_P(path, PSTR("temp\0"), 5);
} else {
snprintf(path, strlen(TemperatureSensor.path)+id.length(), TemperatureSensor.path, id.c_str());
}
HomeAssistantSensor sensor = {
name,
index == 0 ? SystemSensors[0].topic : TemperatureSensor.topic,
path,
TemperatureSensor.uom,
TemperatureSensor.devcl,
TemperatureSensor.stacl
};
publishSensor(sensor);
tInit[index] = true;
}
void HomeAssistantMqttHandler::publishPriceSensors(EntsoeApi* eapi) {
if(eapi == NULL) return;
String uom = String(eapi->getCurrency()) + "/kWh";
if(!pInit) {
for(uint8_t i = 0; i < PriceSensorCount; i++) {
HomeAssistantSensor sensor = PriceSensors[i];
if(strncmp_P(sensor.devcl, PSTR("monetary"), 8) == 0) {
sensor.uom = uom.c_str();
}
publishSensor(sensor);
}
pInit = true;
}
for(uint8_t i = 0; i < 38; i++) {
if(prInit[i]) continue;
float val = eapi->getValueForHour(i);
if(val == ENTSOE_NO_VALUE) continue;
char name[strlen(PriceSensor.name)+2];
snprintf(name, strlen(PriceSensor.name)+2, PriceSensor.name, i, i == 1 ? "hour" : "hours");
char path[strlen(PriceSensor.path)+1];
snprintf(path, strlen(PriceSensor.path)+1, PriceSensor.path, i);
HomeAssistantSensor sensor = {
i == 0 ? "Price current hour" : name,
PriceSensor.topic,
path,
uom.c_str(),
PriceSensor.devcl,
PriceSensor.stacl
};
publishSensor(sensor);
prInit[i] = true;
}
}
void HomeAssistantMqttHandler::publishSystemSensors() {
if(sInit) return;
for(uint8_t i = 0; i < SystemSensorCount; i++) {
publishSensor(SystemSensors[i]);
}
sInit = true;
}
bool HomeAssistantMqttHandler::loop() {
bool ret = mqtt->loop();
delay(10);
yield();
#if defined(ESP32)
esp_task_wdt_reset();
#elif defined(ESP8266)
ESP.wdtFeed();
#endif
return ret;
}

View File

@@ -37,13 +37,13 @@ struct AdcConfig {
class HwTools {
public:
void setup(GpioConfig*, AmsConfiguration*);
double getVcc();
float getVcc();
uint8_t getTempSensorCount();
TempSensorData* getTempSensorData(uint8_t);
bool updateTemperatures();
double getTemperature();
double getTemperatureAnalog();
double getTemperature(uint8_t address[8]);
float getTemperature();
float getTemperatureAnalog();
float getTemperature(uint8_t address[8]);
int getWifiRssi();
bool ledOn(uint8_t color);
bool ledOff(uint8_t color);

View File

@@ -119,74 +119,102 @@ void HwTools::getAdcChannel(uint8_t pin, AdcConfig& config) {
config.unit = ADC_UNIT_1;
config.channel = ADC1_CHANNEL_4;
break;
#if defined(ADC1_CHANNEL_5_GPIO_NUM)
case ADC1_CHANNEL_5_GPIO_NUM:
config.unit = ADC_UNIT_1;
config.channel = ADC1_CHANNEL_5;
break;
#endif
#if defined(ADC1_CHANNEL_6_GPIO_NUM)
case ADC1_CHANNEL_6_GPIO_NUM:
config.unit = ADC_UNIT_1;
config.channel = ADC1_CHANNEL_6;
break;
#endif
#if defined(ADC1_CHANNEL_7_GPIO_NUM)
case ADC1_CHANNEL_7_GPIO_NUM:
config.unit = ADC_UNIT_1;
config.channel = ADC1_CHANNEL_7;
break;
#if defined(CONFIG_IDF_TARGET_ESP32S2)
#endif
#if defined(ADC1_CHANNEL_8_GPIO_NUM)
case ADC1_CHANNEL_8_GPIO_NUM:
config.unit = ADC_UNIT_1;
config.channel = ADC1_CHANNEL_8;
break;
#endif
#if defined(ADC1_CHANNEL_9_GPIO_NUM)
case ADC1_CHANNEL_9_GPIO_NUM:
config.unit = ADC_UNIT_1;
config.channel = ADC1_CHANNEL_9;
break;
#endif
#if defined(ADC2_CHANNEL_0_GPIO_NUM)
case ADC2_CHANNEL_0_GPIO_NUM:
config.unit = ADC_UNIT_2;
config.channel = ADC2_CHANNEL_0;
break;
#endif
#if defined(ADC2_CHANNEL_1_GPIO_NUM)
case ADC2_CHANNEL_1_GPIO_NUM:
config.unit = ADC_UNIT_2;
config.channel = ADC2_CHANNEL_1;
break;
#endif
#if defined(ADC2_CHANNEL_2_GPIO_NUM)
case ADC2_CHANNEL_2_GPIO_NUM:
config.unit = ADC_UNIT_2;
config.channel = ADC2_CHANNEL_2;
break;
#endif
#if defined(ADC2_CHANNEL_3_GPIO_NUM)
case ADC2_CHANNEL_3_GPIO_NUM:
config.unit = ADC_UNIT_2;
config.channel = ADC2_CHANNEL_3;
break;
#endif
#if defined(ADC2_CHANNEL_4_GPIO_NUM)
case ADC2_CHANNEL_4_GPIO_NUM:
config.unit = ADC_UNIT_2;
config.channel = ADC2_CHANNEL_4;
break;
#endif
#if defined(ADC2_CHANNEL_5_GPIO_NUM)
case ADC2_CHANNEL_5_GPIO_NUM:
config.unit = ADC_UNIT_2;
config.channel = ADC2_CHANNEL_5;
break;
#endif
#if defined(ADC2_CHANNEL_6_GPIO_NUM)
case ADC2_CHANNEL_6_GPIO_NUM:
config.unit = ADC_UNIT_2;
config.channel = ADC2_CHANNEL_6;
break;
#endif
#if defined(ADC2_CHANNEL_7_GPIO_NUM)
case ADC2_CHANNEL_7_GPIO_NUM:
config.unit = ADC_UNIT_2;
config.channel = ADC2_CHANNEL_7;
break;
#endif
#if defined(ADC2_CHANNEL_8_GPIO_NUM)
case ADC2_CHANNEL_8_GPIO_NUM:
config.unit = ADC_UNIT_2;
config.channel = ADC2_CHANNEL_8;
break;
#endif
#if defined(ADC2_CHANNEL_9_GPIO_NUM)
case ADC2_CHANNEL_9_GPIO_NUM:
config.unit = ADC_UNIT_2;
config.channel = ADC2_CHANNEL_9;
break;
#endif
}
#endif
}
double HwTools::getVcc() {
double volts = 0.0;
float HwTools::getVcc() {
float volts = 0.0;
if(config->vccPin != 0xFF) {
#if defined(ESP32)
if(voltAdc.unit != 0xFF) {
@@ -229,7 +257,7 @@ double HwTools::getVcc() {
if(volts == 0.0) return 0.0;
if(config->vccResistorGnd > 0 && config->vccResistorVcc > 0) {
volts *= ((double) (config->vccResistorGnd + config->vccResistorVcc) / config->vccResistorGnd);
volts *= ((float) (config->vccResistorGnd + config->vccResistorVcc) / config->vccResistorGnd);
}
@@ -290,7 +318,7 @@ bool HwTools::updateTemperatures() {
}
tempSensors[sensorCount++] = data;
}
delay(10);
yield();
}
} else {
if(sensorCount > 0) {
@@ -320,10 +348,10 @@ bool HwTools::isSensorAddressEqual(uint8_t a[8], uint8_t b[8]) {
return true;
}
double HwTools::getTemperature() {
float HwTools::getTemperature() {
uint8_t c = 0;
double ret = 0;
double analogTemp = getTemperatureAnalog();
float ret = 0;
float analogTemp = getTemperatureAnalog();
if(analogTemp != DEVICE_DISCONNECTED_C) {
ret += analogTemp;
c++;
@@ -338,10 +366,10 @@ double HwTools::getTemperature() {
}
return c == 0 ? DEVICE_DISCONNECTED_C : ret/c;
}
double HwTools::getTemperatureAnalog() {
float HwTools::getTemperatureAnalog() {
if(config->tempAnalogSensorPin != 0xFF) {
float adcCalibrationFactor = 1.06587;
int volts = ((double) analogRead(config->tempAnalogSensorPin) / analogRange) * 3.3;
int volts = ((float) analogRead(config->tempAnalogSensorPin) / analogRange) * 3.3;
return ((volts * adcCalibrationFactor) - 0.4) / 0.0195;
}
return DEVICE_DISCONNECTED_C;

View File

@@ -10,14 +10,23 @@ public:
this->topic = String(topic);
this->hw = hw;
};
bool publish(AmsData* data, AmsData* previousState, EnergyAccounting* ea);
bool publish(AmsData* data, AmsData* previousState, EnergyAccounting* ea, EntsoeApi* eapi);
bool publishTemperatures(AmsConfiguration*, HwTools*);
bool publishPrices(EntsoeApi*);
bool publishSystem(HwTools* hw, EntsoeApi* eapi, EnergyAccounting* ea);
protected:
bool loop();
private:
String clientId;
String topic;
HwTools* hw;
bool publishList1(AmsData* data, EnergyAccounting* ea);
bool publishList2(AmsData* data, EnergyAccounting* ea);
bool publishList3(AmsData* data, EnergyAccounting* ea);
bool publishList4(AmsData* data, EnergyAccounting* ea);
String getMeterModel(AmsData* data);
};
#endif

View File

@@ -13,6 +13,32 @@
"9" : %.4f,
"10" : %.4f,
"11" : %.4f,
"12" : %.4f,
"13" : %.4f,
"14" : %.4f,
"15" : %.4f,
"16" : %.4f,
"17" : %.4f,
"18" : %.4f,
"19" : %.4f,
"20" : %.4f,
"21" : %.4f,
"22" : %.4f,
"23" : %.4f,
"24" : %.4f,
"25" : %.4f,
"26" : %.4f,
"27" : %.4f,
"28" : %.4f,
"29" : %.4f,
"30" : %.4f,
"31" : %.4f,
"32" : %.4f,
"33" : %.4f,
"34" : %.4f,
"35" : %.4f,
"36" : %.4f,
"37" : %.4f,
"min" : %.4f,
"max" : %.4f,
"cheapest1hr" : "%s",

View File

@@ -9,142 +9,165 @@
#include "json/jsonsys_json.h"
#include "json/jsonprices_json.h"
bool JsonMqttHandler::publish(AmsData* data, AmsData* previousState, EnergyAccounting* ea) {
bool JsonMqttHandler::publish(AmsData* data, AmsData* previousState, EnergyAccounting* ea, EntsoeApi* eapi) {
if(topic.isEmpty() || !mqtt->connected())
return false;
bool ret = false;
if(data->getListType() == 1) {
ret = publishList1(data, ea);
} else if(data->getListType() == 2) {
ret = publishList2(data, ea);
} else if(data->getListType() == 3) {
ret = publishList3(data, ea);
} else if(data->getListType() == 4) {
ret = publishList4(data, ea);
}
loop();
return ret;
}
bool JsonMqttHandler::publishList1(AmsData* data, EnergyAccounting* ea) {
snprintf_P(json, BufferSize, JSON1_JSON,
WiFi.macAddress().c_str(),
clientId.c_str(),
(uint32_t) (millis64()/1000),
data->getPackageTimestamp(),
hw->getVcc(),
hw->getWifiRssi(),
hw->getTemperature(),
data->getActiveImportPower(),
ea->getUseThisHour(),
ea->getUseToday(),
ea->getCurrentThreshold(),
ea->getMonthMax(),
ea->getProducedThisHour(),
ea->getProducedToday()
);
return mqtt->publish(topic, json);
}
bool JsonMqttHandler::publishList2(AmsData* data, EnergyAccounting* ea) {
snprintf_P(json, BufferSize, JSON2_JSON,
WiFi.macAddress().c_str(),
clientId.c_str(),
(uint32_t) (millis64()/1000),
data->getPackageTimestamp(),
hw->getVcc(),
hw->getWifiRssi(),
hw->getTemperature(),
data->getListId().c_str(),
data->getMeterId().c_str(),
getMeterModel(data).c_str(),
data->getActiveImportPower(),
data->getReactiveImportPower(),
data->getActiveExportPower(),
data->getReactiveExportPower(),
data->getL1Current(),
data->getL2Current(),
data->getL3Current(),
data->getL1Voltage(),
data->getL2Voltage(),
data->getL3Voltage(),
ea->getUseThisHour(),
ea->getUseToday(),
ea->getCurrentThreshold(),
ea->getMonthMax(),
ea->getProducedThisHour(),
ea->getProducedToday()
);
return mqtt->publish(topic, json);
}
bool JsonMqttHandler::publishList3(AmsData* data, EnergyAccounting* ea) {
snprintf_P(json, BufferSize, JSON3_JSON,
WiFi.macAddress().c_str(),
clientId.c_str(),
(uint32_t) (millis64()/1000),
data->getPackageTimestamp(),
hw->getVcc(),
hw->getWifiRssi(),
hw->getTemperature(),
data->getListId().c_str(),
data->getMeterId().c_str(),
getMeterModel(data).c_str(),
data->getActiveImportPower(),
data->getReactiveImportPower(),
data->getActiveExportPower(),
data->getReactiveExportPower(),
data->getL1Current(),
data->getL2Current(),
data->getL3Current(),
data->getL1Voltage(),
data->getL2Voltage(),
data->getL3Voltage(),
data->getActiveImportCounter(),
data->getActiveExportCounter(),
data->getReactiveImportCounter(),
data->getReactiveExportCounter(),
data->getMeterTimestamp(),
ea->getUseThisHour(),
ea->getUseToday(),
ea->getCurrentThreshold(),
ea->getMonthMax(),
ea->getProducedThisHour(),
ea->getProducedToday()
);
return mqtt->publish(topic, json);
}
bool JsonMqttHandler::publishList4(AmsData* data, EnergyAccounting* ea) {
snprintf_P(json, BufferSize, JSON4_JSON,
WiFi.macAddress().c_str(),
clientId.c_str(),
(uint32_t) (millis64()/1000),
data->getPackageTimestamp(),
hw->getVcc(),
hw->getWifiRssi(),
hw->getTemperature(),
data->getListId().c_str(),
data->getMeterId().c_str(),
getMeterModel(data).c_str(),
data->getActiveImportPower(),
data->getL1ActiveImportPower(),
data->getL2ActiveImportPower(),
data->getL3ActiveImportPower(),
data->getReactiveImportPower(),
data->getActiveExportPower(),
data->getL1ActiveExportPower(),
data->getL2ActiveExportPower(),
data->getL3ActiveExportPower(),
data->getReactiveExportPower(),
data->getL1Current(),
data->getL2Current(),
data->getL3Current(),
data->getL1Voltage(),
data->getL2Voltage(),
data->getL3Voltage(),
data->getPowerFactor(),
data->getL1PowerFactor(),
data->getL2PowerFactor(),
data->getL3PowerFactor(),
data->getActiveImportCounter(),
data->getActiveExportCounter(),
data->getReactiveImportCounter(),
data->getReactiveExportCounter(),
data->getMeterTimestamp(),
ea->getUseThisHour(),
ea->getUseToday(),
ea->getCurrentThreshold(),
ea->getMonthMax(),
ea->getProducedThisHour(),
ea->getProducedToday()
);
return mqtt->publish(topic, json);
}
String JsonMqttHandler::getMeterModel(AmsData* data) {
String meterModel = data->getMeterModel();
meterModel.replace("\\", "\\\\");
if(data->getListType() == 1) {
snprintf_P(json, BufferSize, JSON1_JSON,
WiFi.macAddress().c_str(),
clientId.c_str(),
(uint32_t) (millis64()/1000),
data->getPackageTimestamp(),
hw->getVcc(),
hw->getWifiRssi(),
hw->getTemperature(),
data->getActiveImportPower(),
ea->getUseThisHour(),
ea->getUseToday(),
ea->getCurrentThreshold(),
ea->getMonthMax(),
ea->getProducedThisHour(),
ea->getProducedToday()
);
return mqtt->publish(topic, json);
} else if(data->getListType() == 2) {
snprintf_P(json, BufferSize, JSON2_JSON,
WiFi.macAddress().c_str(),
clientId.c_str(),
(uint32_t) (millis64()/1000),
data->getPackageTimestamp(),
hw->getVcc(),
hw->getWifiRssi(),
hw->getTemperature(),
data->getListId().c_str(),
data->getMeterId().c_str(),
meterModel.c_str(),
data->getActiveImportPower(),
data->getReactiveImportPower(),
data->getActiveExportPower(),
data->getReactiveExportPower(),
data->getL1Current(),
data->getL2Current(),
data->getL3Current(),
data->getL1Voltage(),
data->getL2Voltage(),
data->getL3Voltage(),
ea->getUseThisHour(),
ea->getUseToday(),
ea->getCurrentThreshold(),
ea->getMonthMax(),
ea->getProducedThisHour(),
ea->getProducedToday()
);
return mqtt->publish(topic, json);
} else if(data->getListType() == 3) {
snprintf_P(json, BufferSize, JSON3_JSON,
WiFi.macAddress().c_str(),
clientId.c_str(),
(uint32_t) (millis64()/1000),
data->getPackageTimestamp(),
hw->getVcc(),
hw->getWifiRssi(),
hw->getTemperature(),
data->getListId().c_str(),
data->getMeterId().c_str(),
meterModel.c_str(),
data->getActiveImportPower(),
data->getReactiveImportPower(),
data->getActiveExportPower(),
data->getReactiveExportPower(),
data->getL1Current(),
data->getL2Current(),
data->getL3Current(),
data->getL1Voltage(),
data->getL2Voltage(),
data->getL3Voltage(),
data->getActiveImportCounter(),
data->getActiveExportCounter(),
data->getReactiveImportCounter(),
data->getReactiveExportCounter(),
data->getMeterTimestamp(),
ea->getUseThisHour(),
ea->getUseToday(),
ea->getCurrentThreshold(),
ea->getMonthMax(),
ea->getProducedThisHour(),
ea->getProducedToday()
);
return mqtt->publish(topic, json);
} else if(data->getListType() == 4) {
snprintf_P(json, BufferSize, JSON4_JSON,
WiFi.macAddress().c_str(),
clientId.c_str(),
(uint32_t) (millis64()/1000),
data->getPackageTimestamp(),
hw->getVcc(),
hw->getWifiRssi(),
hw->getTemperature(),
data->getListId().c_str(),
data->getMeterId().c_str(),
meterModel.c_str(),
data->getActiveImportPower(),
data->getL1ActiveImportPower(),
data->getL2ActiveImportPower(),
data->getL3ActiveImportPower(),
data->getReactiveImportPower(),
data->getActiveExportPower(),
data->getL1ActiveExportPower(),
data->getL2ActiveExportPower(),
data->getL3ActiveExportPower(),
data->getReactiveExportPower(),
data->getL1Current(),
data->getL2Current(),
data->getL3Current(),
data->getL1Voltage(),
data->getL2Voltage(),
data->getL3Voltage(),
data->getPowerFactor(),
data->getL1PowerFactor(),
data->getL2PowerFactor(),
data->getL3PowerFactor(),
data->getActiveImportCounter(),
data->getActiveExportCounter(),
data->getReactiveImportCounter(),
data->getReactiveExportCounter(),
data->getMeterTimestamp(),
ea->getUseThisHour(),
ea->getUseToday(),
ea->getCurrentThreshold(),
ea->getMonthMax(),
ea->getProducedThisHour(),
ea->getProducedToday()
);
return mqtt->publish(topic, json);
}
return false;
return meterModel;
}
bool JsonMqttHandler::publishTemperatures(AmsConfiguration* config, HwTools* hw) {
@@ -153,23 +176,24 @@ bool JsonMqttHandler::publishTemperatures(AmsConfiguration* config, HwTools* hw)
return false;
}
snprintf(json, 24, "{\"temperatures\":{");
snprintf_P(json, 24, PSTR("{\"temperatures\":{"));
for(int i = 0; i < count; i++) {
TempSensorData* data = hw->getTempSensorData(i);
if(data != NULL) {
char* pos = json+strlen(json);
snprintf(pos, 26, "\"%s\":%.2f,",
snprintf_P(pos, 26, PSTR("\"%s\":%.2f,"),
toHex(data->address, 8).c_str(),
data->lastRead
);
data->changed = false;
delay(1);
}
}
char* pos = json+strlen(json);
snprintf(count == 0 ? pos : pos-1, 8, "}}");
return mqtt->publish(topic, json);
snprintf_P(count == 0 ? pos : pos-1, 8, PSTR("}}"));
bool ret = mqtt->publish(topic, json);
loop();
return ret;
}
bool JsonMqttHandler::publishPrices(EntsoeApi* eapi) {
@@ -183,9 +207,9 @@ bool JsonMqttHandler::publishPrices(EntsoeApi* eapi) {
float min1hr = 0.0, min3hr = 0.0, min6hr = 0.0;
int8_t min1hrIdx = -1, min3hrIdx = -1, min6hrIdx = -1;
float min = INT16_MAX, max = INT16_MIN;
float values[24];
for(int i = 0;i < 24; i++) values[i] = ENTSOE_NO_VALUE;
for(uint8_t i = 0; i < 24; i++) {
float values[38];
for(int i = 0;i < 38; i++) values[i] = ENTSOE_NO_VALUE;
for(uint8_t i = 0; i < 38; i++) {
float val = eapi->getValueForHour(now, i);
values[i] = val;
@@ -234,28 +258,25 @@ bool JsonMqttHandler::publishPrices(EntsoeApi* eapi) {
memset(ts1hr, 0, 24);
if(min1hrIdx > -1) {
time_t ts = now + (SECS_PER_HOUR * min1hrIdx);
//Serial.printf("1hr: %d %lu\n", min1hrIdx, ts);
tmElements_t tm;
breakTime(ts, tm);
sprintf(ts1hr, "%04d-%02d-%02dT%02d:00:00Z", tm.Year+1970, tm.Month, tm.Day, tm.Hour);
sprintf_P(ts1hr, PSTR("%04d-%02d-%02dT%02d:00:00Z"), tm.Year+1970, tm.Month, tm.Day, tm.Hour);
}
char ts3hr[24];
memset(ts3hr, 0, 24);
if(min3hrIdx > -1) {
time_t ts = now + (SECS_PER_HOUR * min3hrIdx);
//Serial.printf("3hr: %d %lu\n", min3hrIdx, ts);
tmElements_t tm;
breakTime(ts, tm);
sprintf(ts3hr, "%04d-%02d-%02dT%02d:00:00Z", tm.Year+1970, tm.Month, tm.Day, tm.Hour);
sprintf_P(ts3hr, PSTR("%04d-%02d-%02dT%02d:00:00Z"), tm.Year+1970, tm.Month, tm.Day, tm.Hour);
}
char ts6hr[24];
memset(ts6hr, 0, 24);
if(min6hrIdx > -1) {
time_t ts = now + (SECS_PER_HOUR * min6hrIdx);
//Serial.printf("6hr: %d %lu\n", min6hrIdx, ts);
tmElements_t tm;
breakTime(ts, tm);
sprintf(ts6hr, "%04d-%02d-%02dT%02d:00:00Z", tm.Year+1970, tm.Month, tm.Day, tm.Hour);
sprintf_P(ts6hr, PSTR("%04d-%02d-%02dT%02d:00:00Z"), tm.Year+1970, tm.Month, tm.Day, tm.Hour);
}
snprintf_P(json, BufferSize, JSONPRICES_JSON,
@@ -272,13 +293,41 @@ bool JsonMqttHandler::publishPrices(EntsoeApi* eapi) {
values[9],
values[10],
values[11],
values[12],
values[13],
values[14],
values[15],
values[16],
values[17],
values[18],
values[19],
values[20],
values[21],
values[22],
values[23],
values[24],
values[25],
values[26],
values[27],
values[28],
values[29],
values[30],
values[31],
values[32],
values[33],
values[34],
values[35],
values[36],
values[37],
min == INT16_MAX ? 0.0 : min,
max == INT16_MIN ? 0.0 : max,
ts1hr,
ts3hr,
ts6hr
);
return mqtt->publish(topic, json);
bool ret = mqtt->publish(topic, json);
loop();
return ret;
}
bool JsonMqttHandler::publishSystem(HwTools* hw, EntsoeApi* eapi, EnergyAccounting* ea) {
@@ -294,5 +343,19 @@ bool JsonMqttHandler::publishSystem(HwTools* hw, EntsoeApi* eapi, EnergyAccounti
hw->getTemperature(),
VERSION
);
return mqtt->publish(topic, json);
bool ret = mqtt->publish(topic, json);
loop();
return ret;
}
bool JsonMqttHandler::loop() {
bool ret = mqtt->loop();
delay(10);
yield();
#if defined(ESP32)
esp_task_wdt_reset();
#elif defined(ESP8266)
ESP.wdtFeed();
#endif
return ret;
}

View File

@@ -9,13 +9,22 @@ public:
this->topic = String(topic);
this->full = full;
};
bool publish(AmsData* data, AmsData* previousState, EnergyAccounting* ea);
bool publish(AmsData* data, AmsData* previousState, EnergyAccounting* ea, EntsoeApi* eapi);
bool publishTemperatures(AmsConfiguration*, HwTools*);
bool publishPrices(EntsoeApi*);
bool publishSystem(HwTools* hw, EntsoeApi* eapi, EnergyAccounting* ea);
protected:
bool loop();
private:
String topic;
bool full;
bool publishList1(AmsData* data, AmsData* meterState);
bool publishList2(AmsData* data, AmsData* meterState);
bool publishList3(AmsData* data, AmsData* meterState);
bool publishList4(AmsData* data, AmsData* meterState);
bool publishRealtime(EnergyAccounting* ea);
};
#endif

View File

@@ -2,7 +2,7 @@
#include "hexutils.h"
#include "Uptime.h"
bool RawMqttHandler::publish(AmsData* data, AmsData* meterState, EnergyAccounting* ea) {
bool RawMqttHandler::publish(AmsData* data, AmsData* meterState, EnergyAccounting* ea, EntsoeApi* eapi) {
if(topic.isEmpty() || !mqtt->connected())
return false;
@@ -11,85 +11,117 @@ bool RawMqttHandler::publish(AmsData* data, AmsData* meterState, EnergyAccountin
}
switch(data->getListType()) {
case 4:
if(full || meterState->getL1ActiveImportPower() != data->getL1ActiveImportPower()) {
mqtt->publish(topic + "/meter/import/l1", String(data->getL1ActiveImportPower(), 2));
}
if(full || meterState->getL2ActiveImportPower() != data->getL2ActiveImportPower()) {
mqtt->publish(topic + "/meter/import/l2", String(data->getL2ActiveImportPower(), 2));
}
if(full || meterState->getL3ActiveImportPower() != data->getL3ActiveImportPower()) {
mqtt->publish(topic + "/meter/import/l3", String(data->getL3ActiveImportPower(), 2));
}
if(full || meterState->getL1ActiveExportPower() != data->getL1ActiveExportPower()) {
mqtt->publish(topic + "/meter/export/l1", String(data->getL1ActiveExportPower(), 2));
}
if(full || meterState->getL2ActiveExportPower() != data->getL2ActiveExportPower()) {
mqtt->publish(topic + "/meter/export/l2", String(data->getL2ActiveExportPower(), 2));
}
if(full || meterState->getL3ActiveExportPower() != data->getL3ActiveExportPower()) {
mqtt->publish(topic + "/meter/export/l3", String(data->getL3ActiveExportPower(), 2));
}
if(full || meterState->getPowerFactor() != data->getPowerFactor()) {
mqtt->publish(topic + "/meter/powerfactor", String(data->getPowerFactor(), 2));
}
if(full || meterState->getL1PowerFactor() != data->getL1PowerFactor()) {
mqtt->publish(topic + "/meter/l1/powerfactor", String(data->getL1PowerFactor(), 2));
}
if(full || meterState->getL2PowerFactor() != data->getL2PowerFactor()) {
mqtt->publish(topic + "/meter/l2/powerfactor", String(data->getL2PowerFactor(), 2));
}
if(full || meterState->getL3PowerFactor() != data->getL3PowerFactor()) {
mqtt->publish(topic + "/meter/l3/powerfactor", String(data->getL3PowerFactor(), 2));
}
publishList4(data, meterState);
loop();
case 3:
// ID and type belongs to List 2, but I see no need to send that every 10s
mqtt->publish(topic + "/meter/id", data->getMeterId(), true, 0);
mqtt->publish(topic + "/meter/type", data->getMeterModel(), true, 0);
mqtt->publish(topic + "/meter/clock", String(data->getMeterTimestamp()));
mqtt->publish(topic + "/meter/import/reactive/accumulated", String(data->getReactiveImportCounter(), 3), true, 0);
mqtt->publish(topic + "/meter/import/active/accumulated", String(data->getActiveImportCounter(), 3), true, 0);
mqtt->publish(topic + "/meter/export/reactive/accumulated", String(data->getReactiveExportCounter(), 3), true, 0);
mqtt->publish(topic + "/meter/export/active/accumulated", String(data->getActiveExportCounter(), 3), true, 0);
publishList3(data, meterState);
loop();
case 2:
// Only send data if changed. ID and Type is sent on the 10s interval only if changed
if(full || meterState->getMeterId() != data->getMeterId()) {
mqtt->publish(topic + "/meter/id", data->getMeterId());
}
if(full || meterState->getMeterModel() != data->getMeterModel()) {
mqtt->publish(topic + "/meter/type", data->getMeterModel());
}
if(full || meterState->getL1Current() != data->getL1Current()) {
mqtt->publish(topic + "/meter/l1/current", String(data->getL1Current(), 2));
}
if(full || meterState->getL1Voltage() != data->getL1Voltage()) {
mqtt->publish(topic + "/meter/l1/voltage", String(data->getL1Voltage(), 2));
}
if(full || meterState->getL2Current() != data->getL2Current()) {
mqtt->publish(topic + "/meter/l2/current", String(data->getL2Current(), 2));
}
if(full || meterState->getL2Voltage() != data->getL2Voltage()) {
mqtt->publish(topic + "/meter/l2/voltage", String(data->getL2Voltage(), 2));
}
if(full || meterState->getL3Current() != data->getL3Current()) {
mqtt->publish(topic + "/meter/l3/current", String(data->getL3Current(), 2));
}
if(full || meterState->getL3Voltage() != data->getL3Voltage()) {
mqtt->publish(topic + "/meter/l3/voltage", String(data->getL3Voltage(), 2));
}
if(full || meterState->getReactiveExportPower() != data->getReactiveExportPower()) {
mqtt->publish(topic + "/meter/export/reactive", String(data->getReactiveExportPower()));
}
if(full || meterState->getActiveExportPower() != data->getActiveExportPower()) {
mqtt->publish(topic + "/meter/export/active", String(data->getActiveExportPower()));
}
if(full || meterState->getReactiveImportPower() != data->getReactiveImportPower()) {
mqtt->publish(topic + "/meter/import/reactive", String(data->getReactiveImportPower()));
}
publishList2(data, meterState);
loop();
case 1:
if(full || meterState->getActiveImportPower() != data->getActiveImportPower()) {
mqtt->publish(topic + "/meter/import/active", String(data->getActiveImportPower()));
}
publishList1(data, meterState);
loop();
}
if(ea->isInitialized()) {
publishRealtime(ea);
loop();
}
return true;
}
bool RawMqttHandler::publishList1(AmsData* data, AmsData* meterState) {
if(full || meterState->getActiveImportPower() != data->getActiveImportPower()) {
mqtt->publish(topic + "/meter/import/active", String(data->getActiveImportPower()));
}
return true;
}
bool RawMqttHandler::publishList2(AmsData* data, AmsData* meterState) {
// Only send data if changed. ID and Type is sent on the 10s interval only if changed
if(full || meterState->getMeterId() != data->getMeterId()) {
mqtt->publish(topic + "/meter/id", data->getMeterId());
}
if(full || meterState->getMeterModel() != data->getMeterModel()) {
mqtt->publish(topic + "/meter/type", data->getMeterModel());
}
if(full || meterState->getL1Current() != data->getL1Current()) {
mqtt->publish(topic + "/meter/l1/current", String(data->getL1Current(), 2));
}
if(full || meterState->getL1Voltage() != data->getL1Voltage()) {
mqtt->publish(topic + "/meter/l1/voltage", String(data->getL1Voltage(), 2));
}
if(full || meterState->getL2Current() != data->getL2Current()) {
mqtt->publish(topic + "/meter/l2/current", String(data->getL2Current(), 2));
}
if(full || meterState->getL2Voltage() != data->getL2Voltage()) {
mqtt->publish(topic + "/meter/l2/voltage", String(data->getL2Voltage(), 2));
}
if(full || meterState->getL3Current() != data->getL3Current()) {
mqtt->publish(topic + "/meter/l3/current", String(data->getL3Current(), 2));
}
if(full || meterState->getL3Voltage() != data->getL3Voltage()) {
mqtt->publish(topic + "/meter/l3/voltage", String(data->getL3Voltage(), 2));
}
if(full || meterState->getReactiveExportPower() != data->getReactiveExportPower()) {
mqtt->publish(topic + "/meter/export/reactive", String(data->getReactiveExportPower()));
}
if(full || meterState->getActiveExportPower() != data->getActiveExportPower()) {
mqtt->publish(topic + "/meter/export/active", String(data->getActiveExportPower()));
}
if(full || meterState->getReactiveImportPower() != data->getReactiveImportPower()) {
mqtt->publish(topic + "/meter/import/reactive", String(data->getReactiveImportPower()));
}
return true;
}
bool RawMqttHandler::publishList3(AmsData* data, AmsData* meterState) {
// ID and type belongs to List 2, but I see no need to send that every 10s
mqtt->publish(topic + "/meter/id", data->getMeterId(), true, 0);
mqtt->publish(topic + "/meter/type", data->getMeterModel(), true, 0);
mqtt->publish(topic + "/meter/clock", String(data->getMeterTimestamp()));
mqtt->publish(topic + "/meter/import/reactive/accumulated", String(data->getReactiveImportCounter(), 3), true, 0);
mqtt->publish(topic + "/meter/import/active/accumulated", String(data->getActiveImportCounter(), 3), true, 0);
mqtt->publish(topic + "/meter/export/reactive/accumulated", String(data->getReactiveExportCounter(), 3), true, 0);
mqtt->publish(topic + "/meter/export/active/accumulated", String(data->getActiveExportCounter(), 3), true, 0);
return true;
}
bool RawMqttHandler::publishList4(AmsData* data, AmsData* meterState) {
if(full || meterState->getL1ActiveImportPower() != data->getL1ActiveImportPower()) {
mqtt->publish(topic + "/meter/import/l1", String(data->getL1ActiveImportPower(), 2));
}
if(full || meterState->getL2ActiveImportPower() != data->getL2ActiveImportPower()) {
mqtt->publish(topic + "/meter/import/l2", String(data->getL2ActiveImportPower(), 2));
}
if(full || meterState->getL3ActiveImportPower() != data->getL3ActiveImportPower()) {
mqtt->publish(topic + "/meter/import/l3", String(data->getL3ActiveImportPower(), 2));
}
if(full || meterState->getL1ActiveExportPower() != data->getL1ActiveExportPower()) {
mqtt->publish(topic + "/meter/export/l1", String(data->getL1ActiveExportPower(), 2));
}
if(full || meterState->getL2ActiveExportPower() != data->getL2ActiveExportPower()) {
mqtt->publish(topic + "/meter/export/l2", String(data->getL2ActiveExportPower(), 2));
}
if(full || meterState->getL3ActiveExportPower() != data->getL3ActiveExportPower()) {
mqtt->publish(topic + "/meter/export/l3", String(data->getL3ActiveExportPower(), 2));
}
if(full || meterState->getPowerFactor() != data->getPowerFactor()) {
mqtt->publish(topic + "/meter/powerfactor", String(data->getPowerFactor(), 2));
}
if(full || meterState->getL1PowerFactor() != data->getL1PowerFactor()) {
mqtt->publish(topic + "/meter/l1/powerfactor", String(data->getL1PowerFactor(), 2));
}
if(full || meterState->getL2PowerFactor() != data->getL2PowerFactor()) {
mqtt->publish(topic + "/meter/l2/powerfactor", String(data->getL2PowerFactor(), 2));
}
if(full || meterState->getL3PowerFactor() != data->getL3PowerFactor()) {
mqtt->publish(topic + "/meter/l3/powerfactor", String(data->getL3PowerFactor(), 2));
}
return true;
}
bool RawMqttHandler::publishRealtime(EnergyAccounting* ea) {
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));
@@ -182,7 +214,6 @@ bool RawMqttHandler::publishPrices(EntsoeApi* eapi) {
char ts1hr[24];
if(min1hrIdx > -1) {
time_t ts = now + (SECS_PER_HOUR * min1hrIdx);
//Serial.printf("1hr: %d %lu\n", min1hrIdx, ts);
tmElements_t tm;
breakTime(ts, tm);
sprintf(ts1hr, "%04d-%02d-%02dT%02d:00:00Z", tm.Year+1970, tm.Month, tm.Day, tm.Hour);
@@ -190,7 +221,6 @@ bool RawMqttHandler::publishPrices(EntsoeApi* eapi) {
char ts3hr[24];
if(min3hrIdx > -1) {
time_t ts = now + (SECS_PER_HOUR * min3hrIdx);
//Serial.printf("3hr: %d %lu\n", min3hrIdx, ts);
tmElements_t tm;
breakTime(ts, tm);
sprintf(ts3hr, "%04d-%02d-%02dT%02d:00:00Z", tm.Year+1970, tm.Month, tm.Day, tm.Hour);
@@ -198,7 +228,6 @@ bool RawMqttHandler::publishPrices(EntsoeApi* eapi) {
char ts6hr[24];
if(min6hrIdx > -1) {
time_t ts = now + (SECS_PER_HOUR * min6hrIdx);
//Serial.printf("6hr: %d %lu\n", min6hrIdx, ts);
tmElements_t tm;
breakTime(ts, tm);
sprintf(ts6hr, "%04d-%02d-%02dT%02d:00:00Z", tm.Year+1970, tm.Month, tm.Day, tm.Hour);
@@ -249,4 +278,16 @@ bool RawMqttHandler::publishSystem(HwTools* hw, EntsoeApi* eapi, EnergyAccountin
mqtt->publish(topic + "/temperature", String(hw->getTemperature(), 2));
}
return true;
}
}
bool RawMqttHandler::loop() {
bool ret = mqtt->loop();
delay(10);
yield();
#if defined(ESP32)
esp_task_wdt_reset();
#elif defined(ESP8266)
ESP.wdtFeed();
#endif
return ret;
}

View File

@@ -8,8 +8,6 @@ pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
# Editor directories and files

19
lib/SvelteUi/app/dist/favicon.svg vendored Normal file
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

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

1
lib/SvelteUi/app/dist/index.css vendored Normal file

File diff suppressed because one or more lines are too long

16
lib/SvelteUi/app/dist/index.html vendored Normal file
View File

@@ -0,0 +1,16 @@
<!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>
<script type="module" crossorigin src="/index.js"></script>
<link rel="stylesheet" href="/index.css">
</head>
<body class="bg-gray-100">
<div id="app"></div>
</body>
</html>

14
lib/SvelteUi/app/dist/index.js vendored Normal file

File diff suppressed because one or more lines are too long

View File

@@ -64,6 +64,10 @@
{#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 sysinfo.trying}
<Mask active=true message="Device is booting, please wait. Trying to reach it on {sysinfo.trying}"/>
{:else}
<Mask active=true message="Device is booting, please wait"/>
{/if}
{/if}
</div>

View File

@@ -11,6 +11,10 @@
@apply bg-white m-2 p-2 rounded shadow-lg
}
.gwf {
@apply 2xl:col-span-6 xl:col-span-5 lg:col-span-4 md:col-span-3 sm:col-span-2 h-64
}
.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
}
@@ -38,25 +42,28 @@
@apply text-right
}
.bd-grn {
.bd-green {
@apply my-auto bg-green-500 text-green-100 text-xs font-semibold mr-2 px-2.5 py-0.5 rounded
}
.bd-ylo {
.bd-yellow {
@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 {
.bd-blue {
@apply my-auto bg-blue-500 text-blue-100 text-xs font-semibold mr-2 px-2.5 py-0.5 rounded
}
.bd-gry {
.bd-gray {
@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
}
.btn-pri-sm {
@apply text-xs py-1 px-2 rounded bg-blue-500 text-white mr-3
}
.pl-root {
position: relative;

View File

@@ -1,54 +1,70 @@
<script>
import { fmtnum } from "./Helpers";
export let data;
export let currency;
export let hasExport;
let hasExport = data && (data.om || data.e > 0);
let hasCost = false;
let cols = 3
$: {
hasCost = data && data.h && (data.h.c || data.d.c || data.m.c || data.h.i || data.d.i || data.m.i);
cols = hasCost ? 3 : 2;
}
</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">
<br/><br/>
{#if data}
{#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>
<strong>Import</strong>
<div class="grid grid-cols-{cols} mb-3">
<div>Hour</div>
<div class="text-right">{fmtnum(data.h.u,2)} kWh</div>
{#if hasCost}<div class="text-right">{fmtnum(data.h.c,2)} {currency}</div>{/if}
<div>Day</div>
<div class="text-right">{fmtnum(data.d.u,1)} kWh</div>
{#if hasCost}<div class="text-right">{fmtnum(data.d.c,1)} {currency}</div>{/if}
<div>Month</div>
<div class="text-right">{fmtnum(data.m.u)} kWh</div>
{#if hasCost}<div class="text-right">{fmtnum(data.m.c)} {currency}</div>{/if}
</div>
<strong>Export</strong>
<div class="grid grid-cols-{cols}">
<div>Hour</div>
<div class="text-right">{fmtnum(data.h.p,2)} kWh</div>
{#if hasCost}<div class="text-right">{fmtnum(data.h.i,2)} {currency}</div>{/if}
<div>Day</div>
<div class="text-right">{fmtnum(data.d.p,1)} kWh</div>
{#if hasCost}<div class="text-right">{fmtnum(data.d.i,1)} {currency}</div>{/if}
<div>Month</div>
<div class="text-right">{fmtnum(data.m.p)} kWh</div>
{#if hasCost}<div class="text-right">{fmtnum(data.m.i)} {currency}</div>{/if}
</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>
<strong>Consumption</strong>
<div class="grid grid-cols-2 mb-3">
<div>Hour</div>
<div class="text-right">{fmtnum(data.h.u,2)} kWh</div>
<div>Day</div>
<div class="text-right">{fmtnum(data.d.u,1)} kWh</div>
<div>Month</div>
<div class="text-right">{fmtnum(data.m.u)} kWh</div>
</div>
{#if hasCost}
<strong>Cost</strong>
<div class="grid grid-cols-2">
<div>Hour</div>
<div class="text-right">{fmtnum(data.h.c,2)} {currency}</div>
<div>Day</div>
<div class="text-right">{fmtnum(data.d.c,1)} {currency}</div>
<div>Month</div>
<div class="text-right">{fmtnum(data.m.c)} {currency}</div>
</div>
{/if}
{/if}
</div>
{/if}
</div>

View File

@@ -1,6 +1,6 @@
<script>
import BarChart from './BarChart.svelte';
import { ampcol } from './Helpers.js';
import { ampcol, fmtnum } from './Helpers.js';
export let u1;
export let u2;
@@ -12,32 +12,28 @@
let config = {};
function point(v) {
return {
label: fmtnum(v) +'A',
value: isNaN(v) ? 0 : v,
color: ampcol(v ? (v)/(max)*100 : 0)
};
};
$: {
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)
});
points.push(point(i1));
}
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)
});
points.push(point(i2));
}
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)
});
points.push(point(i3));
}
config = {
padding: { top: 20, right: 15, bottom: 20, left: 35 },

View File

@@ -3,14 +3,4 @@
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}
<span title={title} class="bd-{color}">{text}</span>

View File

@@ -52,7 +52,9 @@
<g class="axis x-axis">
{#each config.x.ticks as point, i}
<g class="tick" transform="translate({xScale(i)},{heightAvailable})">
{#if barWidth > 20 || i%2 == 0}
<text x="{barWidth/2}" y="-4">{point.label}</text>
{/if}
</g>
{/each}
</g>

View File

@@ -41,6 +41,15 @@
<option value={50}>{boardtype(chip, 50)}</option>
</optgroup>
{/if}
{#if chip == 'esp32c3'}
<optgroup label="Custom hardware">
<option value={8}>{boardtype(chip, 8)}</option>
</optgroup>
<optgroup label="Generic hardware">
<option value={71}>{boardtype(chip, 71)}</option>
<option value={70}>{boardtype(chip, 70)}</option>
</optgroup>
{/if}
{#if chip == 'esp32solo'}
<optgroup label="Generic hardware">
<option value={200}>{boardtype(chip, 200)}</option>

View File

@@ -2,10 +2,11 @@
import { zeropad, monthnames } from './Helpers.js';
export let timestamp;
export let fullTimeColor;
</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>
<span class="{fullTimeColor}">{`${zeropad(timestamp.getDate())}.${zeropad(timestamp.getMonth()+1)}.${timestamp.getFullYear()} ${zeropad(timestamp.getHours())}:${zeropad(timestamp.getMinutes())}`}</span>
{/if}

View File

@@ -1,16 +1,53 @@
<script>
import { getConfiguration, configurationStore } from './ConfigurationStore'
import { sysinfoStore } from './DataStores.js';
import { wiki } from './Helpers.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';
import SubnetOptions from './SubnetOptions.svelte';
export let sysinfo = {}
let uiElements = [{
name: 'Import gauge',
key: 'i'
},{
name: 'Export gauge',
key: 'e'
},{
name: 'Voltage',
key: 'v'
},{
name: 'Amperage',
key: 'a'
},{
name: 'Reactive',
key: 'r'
},{
name: 'Realtime',
key: 'c'
},{
name: 'Peaks',
key: 't'
},{
name: 'Price',
key: 'p'
},{
name: 'Day plot',
key: 'd'
},{
name: 'Month plot',
key: 'm'
},{
name: 'Temperature plot',
key: 's'
}];
let loading = true;
let saving = false;
@@ -42,7 +79,7 @@
t: [0,0,0,0,0,0,0,0,0,0], h: 1
},
p: {
e: false, t: '', r: '', c: '', m: 1.0
e: false, t: '', r: '', c: '', m: 1.0, f: null
},
d: {
s: false, t: false, l: 5
@@ -51,11 +88,15 @@
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,
h: { p: null, u: true },
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 }
},
h: {
t: '', h: '', n: ''
}
};
configurationStore.subscribe(update => {
@@ -67,8 +108,10 @@
getConfiguration();
let isFactoryReset = false;
let isFactoryResetComplete = false;
async function factoryReset() {
if(confirm("Are you sure you want to factory reset the device?")) {
isFactoryReset = true;
const data = new URLSearchParams();
data.append("perform", "true");
const response = await fetch('/reset', {
@@ -76,7 +119,8 @@
body: data
});
let res = (await response.json());
isFactoryReset = res.success;
isFactoryReset = false;
isFactoryResetComplete = res.success;
}
}
@@ -129,13 +173,18 @@
if(configuration.q.p == 8883) configuration.q.p = 1883;
}
}
let gpioMax = 44;
$: {
gpioMax = sysinfo.chip == 'esp8266' ? 16 : sysinfo.chip == 'esp32s2' ? 44 : 39;
}
</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>
<a href="https://github.com/UtilitechAS/amsreader-firmware/wiki/General-configuration" target="_blank" class="float-right"><HelpIcon/></a>
<a href="{wiki('General-configuration')}" target="_blank" class="float-right"><HelpIcon/></a>
<input type="hidden" name="g" value="true"/>
<div class="my-1">
<div class="flex">
@@ -145,7 +194,7 @@
</div>
<div>
Time zone<br/>
<select name="gt" bind:value={configuration.g.t} class="in-l">
<select name="gt" bind:value={configuration.g.t} class="in-l w-full">
<CountrySelectOptions/>
</select>
</div>
@@ -153,50 +202,57 @@
</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>
<div class="flex">
<div class="w-full">
Price region<br/>
<select name="pr" bind:value={configuration.p.r} class="in-f w-full">
<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="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>
<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>
Currency<br/>
<select name="pc" bind:value={configuration.p.c} class="in-l">
{#each ["NOK","SEK","DKK","EUR"] as c}
<option value={c}>{c}</option>
{/each}
</select>
</div>
</div>
</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>
Fixed price<br/>
<input name="pf" bind:value={configuration.p.f} type="number" min="0.001" max="65" step="0.001" class="in-f tr w-full"/>
</div>
<div class="w-1/2">
Multiplier<br/>
@@ -231,20 +287,16 @@
</div>
<div class="cnt">
<strong class="text-sm">Meter</strong>
<a href="https://github.com/UtilitechAS/amsreader-firmware/wiki/Meter-configuration" target="_blank" class="float-right"><HelpIcon/></a>
<a href="{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>
{#each [24,48,96,192,384,576,1152] as b}
<option value={b*100}>{b*100}</option>
{/each}
</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>
@@ -320,7 +372,7 @@
</div>
<div class="cnt">
<strong class="text-sm">WiFi</strong>
<a href="https://github.com/UtilitechAS/amsreader-firmware/wiki/WiFi-configuration" target="_blank" class="float-right"><HelpIcon/></a>
<a href="{wiki('WiFi-configuration')}" target="_blank" class="float-right"><HelpIcon/></a>
<input type="hidden" name="w" value="true"/>
<div class="my-1">
SSID<br/>
@@ -354,7 +406,7 @@
</div>
<div class="cnt">
<strong class="text-sm">Network</strong>
<a href="https://github.com/UtilitechAS/amsreader-firmware/wiki/Network-configuration" target="_blank" class="float-right"><HelpIcon/></a>
<a href="{wiki('Network-configuration')}" target="_blank" class="float-right"><HelpIcon/></a>
<div class="my-1">
IP<br/>
<div class="flex">
@@ -362,11 +414,9 @@
<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>
<input name="ni" bind:value={configuration.n.i} type="text" class="in-m w-full" disabled={configuration.n.m == 'dhcp'} required={configuration.n.m == 'static'}/>
<select name="ns" bind:value={configuration.n.s} class="in-l" disabled={configuration.n.m == 'dhcp'} required={configuration.n.m == 'static'}>
<SubnetOptions/>
</select>
</div>
</div>
@@ -396,7 +446,7 @@
</div>
<div class="cnt">
<strong class="text-sm">MQTT</strong>
<a href="https://github.com/UtilitechAS/amsreader-firmware/wiki/MQTT-configuration" target="_blank" class="float-right"><HelpIcon/></a>
<a href="{wiki('MQTT-configuration')}" target="_blank" class="float-right"><HelpIcon/></a>
<input type="hidden" name="q" value="true"/>
<div class="my-1">
Server
@@ -471,7 +521,7 @@
{#if configuration.q.m == 3}
<div class="cnt">
<strong class="text-sm">Domoticz</strong>
<a href="https://github.com/UtilitechAS/amsreader-firmware/wiki/MQTT-configuration#domoticz" target="_blank" class="float-right"><HelpIcon/></a>
<a href="{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">
@@ -493,57 +543,38 @@
</div>
</div>
{/if}
{#if configuration.q.m == 4}
<div class="cnt">
<strong class="text-sm">Home-Assistant</strong>
<a href="{wiki('MQTT-configuration#home-assistant')}" target="_blank" class="float-right"><HelpIcon/></a>
<input type="hidden" name="h" value="true"/>
<div class="my-1">
Discovery topic prefix<br/>
<input name="ht" bind:value={configuration.h.t} type="text" class="in-s" placeholder="homeassistant"/>
</div>
<div class="my-1">
Hostname for URL<br/>
<input name="hh" bind:value={configuration.h.h} type="text" class="in-s" placeholder="{configuration.g.h}.local"/>
</div>
<div class="my-1">
Name tag<br/>
<input name="hn" bind:value={configuration.h.n} type="text" class="in-s"/>
</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/UtilitechAS/amsreader-firmware/wiki/Threshold-configuration" target="_blank" class="float-right"><HelpIcon/></a>
<a href="{wiki('Threshold-configuration')}" target="_blank" class="float-right"><HelpIcon/></a>
<input type="hidden" name="t" value="true"/>
<div class="flex flex-wrap my-1">
{#each {length: 9} as _, i}
<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-pre">{i+1}</span>
<input name="t{i}" bind:value={configuration.t.t[i]} type="number" min="0" max="65535" class="in-txt w-full"/>
<span class="in-post">kWh</span>
</label>
{/each}
</div>
<label class="flex m-1">
<span class="in-pre">Average of</span>
@@ -554,142 +585,64 @@
{/if}
<div class="cnt">
<strong class="text-sm">User interface</strong>
<a href="https://github.com/UtilitechAS/amsreader-firmware/wiki/User-interface" target="_blank" class="float-right"><HelpIcon/></a>
<a href="{wiki('User-interface')}" target="_blank" class="float-right"><HelpIcon/></a>
<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>
{#each uiElements as el}
<div class="w-1/2">
{el.name}<br/>
<select name="u{el.key}" bind:value={configuration.u[el.key]} class="in-s">
<option value={0}>Hide</option>
<option value={1}>Show</option>
<option value={2}>Dynamic</option>
</select>
</div>
{/each}
</div>
</div>
{#if sysinfo.board > 20 || sysinfo.chip == 'esp8266'}
<div class="cnt">
<strong class="text-sm">Hardware</strong>
<a href="https://github.com/UtilitechAS/amsreader-firmware/wiki/GPIO-configuration" target="_blank" class="float-right"><HelpIcon/></a>
<a href="{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">
HAN<label class="ml-2"><input name="ihu" value="true" bind:checked={configuration.i.h.u} type="checkbox" class="rounded mb-1"/> pullup</label><br/>
<select name="ihp" bind:value={configuration.i.h.p} 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"/>
<input name="ia" bind:value={configuration.i.a} type="number" min="0" max={gpioMax} 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"/>
<input name="ilp" bind:value={configuration.i.l.p} type="number" min="0" max={gpioMax} 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"/>
<input name="irr" bind:value={configuration.i.r.r} type="number" min="0" max={gpioMax} class="in-f tr w-1/3"/>
<input name="irg" bind:value={configuration.i.r.g} type="number" min="0" max={gpioMax} class="in-m tr w-1/3"/>
<input name="irb" bind:value={configuration.i.r.b} type="number" min="0" max={gpioMax} 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"/>
<input name="itd" bind:value={configuration.i.t.d} type="number" min="0" max={gpioMax} 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"/>
<input name="ita" bind:value={configuration.i.t.a} type="number" min="0" max={gpioMax} 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"/>
<input name="ivp" bind:value={configuration.i.v.p} type="number" min="0" max={gpioMax} class="in-s tr w-full"/>
</div>
{/if}
{#if configuration.i.v.p > 0}
@@ -765,4 +718,5 @@
<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"/>
<Mask active={isFactoryReset} message="Performing factory reset"/>
<Mask active={isFactoryResetComplete} message="Device have been factory reset and switched to AP mode"/>

View File

@@ -2,6 +2,7 @@
import { sysinfoStore } from './DataStores.js';
import Mask from './Mask.svelte'
import { navigate } from 'svelte-navigator';
import { wiki } from './Helpers';
export let sysinfo = {}
@@ -41,7 +42,7 @@
<hr/>
<div class="my-3">
Enable one-click upgrade? (implies data collection)<br/>
<a href="https://github.com/UtilitechAS/amsreader-firmware/wiki/Data-collection-on-one-click-firmware-upgrade" target="_blank" class="text-blue-600 hover:text-blue-800">Read more</a><br/>
<a href="{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">

View File

@@ -1,6 +1,6 @@
<script>
import { pricesStore, dayPlotStore, monthPlotStore, temperaturesStore } from './DataStores.js';
import { metertype, uiVisibility } from './Helpers.js';
import { ampcol, exportcol, metertype, uiVisibility } from './Helpers.js';
import PowerGauge from './PowerGauge.svelte';
import VoltPlot from './VoltPlot.svelte';
import AmpPlot from './AmpPlot.svelte';
@@ -32,23 +32,23 @@
});
</script>
<div class="grid xl:grid-cols-6 lg:grid-cols-4 md:grid-cols-3 sm:grid-cols-2">
<div class="grid 2xl:grid-cols-6 xl:grid-cols-5 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 class="cnt">
<div class="grid grid-cols-2">
<div class="col-span-2">
<PowerGauge val={data.i ? data.i : 0} max={data.im ? data.im : 15000} unit="W" label="Import" sub={data.p} subunit={data.pc} colorFn={ampcol}/>
</div>
<div>{data.mt ? metertype(data.mt) : '-'}</div>
<div class="text-right">{data.ic ? data.ic.toFixed(1) : '-'} kWh</div>
</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"/>
<PowerGauge val={data.e ? data.e : 0} max={data.om ? data.om * 1000 : 10000} unit="W" label="Export" colorFn={exportcol}/>
</div>
<div></div>
<div class="text-right">{data.ec ? data.ec.toFixed(1) : '-'} kWh</div>
@@ -56,48 +56,48 @@
</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>
<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>
<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>
<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>
<div class="cnt">
<AccountingData data={data.ea} currency={data.pc} hasExport={data.om > 0 || data.e > 0}/>
</div>
{/if}
{#if data && data.pr && (data.pr.startsWith("10YNO") || data.pr == '10Y1001A1001A48H')}
<div class="cnt h-64">
<TariffPeakChart />
</div>
{#if uiVisibility(sysinfo.ui.t, 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">
{#if uiVisibility(sysinfo.ui.p, data.pe && !Number.isNaN(data.p))}
<div class="cnt gwf">
<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>
<div class="cnt gwf">
<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>
<div class="cnt gwf">
<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>
<div class="cnt gwf">
<TemperaturePlot json={temperatures} />
</div>
{/if}
</div>

View File

@@ -25,7 +25,15 @@ let sysinfo = {
booting: false,
upgrading: false,
ui: {},
security: 0
security: 0,
boot_reason: 0,
upgrade: {
x: -1,
e: 0,
f: null,
t: null
},
trying: null
};
export const sysinfoStore = writable(sysinfo);
export async function getSysinfo() {
@@ -49,7 +57,7 @@ export const dataStore = readable(data, (set) => {
lastTemp = data.t;
setTimeout(getTemperatures, 2000);
}
if(lastPrice != data.p) {
if(lastPrice != data.p && data.pe) {
lastPrice = data.p;
setTimeout(getPrices, 4000);
}

View File

@@ -1,5 +1,5 @@
<script>
import { zeropad } from './Helpers.js';
import { zeropad, addHours } from './Helpers.js';
import BarChart from './BarChart.svelte';
export let json;
@@ -13,16 +13,16 @@
let yTicks = [];
let xTicks = [];
let points = [];
let cur = new Date();
let offset = -cur.getTimezoneOffset()/60;
for(i = cur.getUTCHours(); i<24; i++) {
let cur = addHours(new Date(), -24);
let currentHour = new Date().getUTCHours();
for(i = currentHour; 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)
label: zeropad(cur.getHours())
});
points.push({
label: imp.toFixed(1),
@@ -33,15 +33,16 @@
});
min = Math.max(min, exp*10);
max = Math.max(max, imp*10);
addHours(cur, 1);
};
for(i = 0; i < cur.getUTCHours(); i++) {
for(i = 0; i < currentHour; 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)
label: zeropad(cur.getHours())
});
points.push({
label: imp.toFixed(1),
@@ -52,6 +53,7 @@
});
min = Math.max(min, exp*10);
max = Math.max(max, imp*10);
addHours(cur, 1);
};
let boundary = Math.ceil(Math.max(min, max));

View File

@@ -1,8 +1,8 @@
<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 { upgrade, getNextVersion, upgradeWarningText } from './UpgradeHelper';
import { boardtype, hanError, mqttError, priceError, isBusPowered, wiki, bcol } from './Helpers.js';
import AmsleserSvg from "./../assets/favicon.svg";
import GitHubLogo from './../assets/github.svg';
import Uptime from "./Uptime.svelte";
@@ -20,12 +20,12 @@
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.')) {
if(!isBusPowered(sysinfo.board) || confirm(upgradeWarningText(boardtype(sysinfo.chip, sysinfo.board)))) {
sysinfoStore.update(s => {
s.upgrading = true;
return s;
});
upgrade(nextVersion);
upgrade(nextVersion.tag_name);
}
}
}
@@ -54,10 +54,10 @@
<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'}/>
<Badge title="ESP" text={sysinfo.booting ? 'Booting' : data.v > 2.0 ? data.v.toFixed(2)+"V" : "ESP"} color={bcol(sysinfo.booting ? 2 : data.em)}/>
<Badge title="HAN" text="HAN" color={bcol(sysinfo.booting ? 9 : data.hm)}/>
<Badge title="WiFi" text={data.r ? data.r.toFixed(0)+"dBm" : "WiFi"} color={bcol(sysinfo.booting ? 9 : data.wm)}/>
<Badge title="MQTT" text="MQTT" color={bcol(sysinfo.booting ? 9 : data.mm)}/>
</div>
{#if data.he < 0 || data.he > 0}
<div class="bd-red">{ 'HAN: ' + hanError(data.he) }</div>
@@ -73,7 +73,7 @@
<a class="float-right" href='https://github.com/UtilitechAS/amsreader-firmware' 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) } />
<Clock timestamp={ data.c ? new Date(data.c * 1000) : new Date(0) } fullTimeColor="text-red-500" />
</div>
{#if sysinfo.vndcfg && sysinfo.usrcfg}
<div class="flex-none px-1 mt-1" title="Configuration">
@@ -84,7 +84,7 @@
</div>
{/if}
<div class="flex-none px-1 mt-1" title="Documentation">
<a href="https://github.com/UtilitechAS/amsreader-firmware/wiki" target='_blank' rel="noreferrer"><HelpIcon/></a>
<a href={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}">

View File

@@ -1,5 +1,9 @@
export let monthnames = ["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"];
export function bcol(num) {
return num === 1 ? 'green' : num === 2 ? 'yellow' : num === 3 ? 'red' : 'gray';
}
export function voltcol(volt) {
if(volt > 218 && volt < 242) return '#32d900';
if(volt > 212 && volt < 248) return '#b1d900';
@@ -15,6 +19,13 @@ export function ampcol(pct) {
else return '#32d900';
};
export function exportcol(pct) {
if(pct > 75) return '#32d900';
else if(pct > 50) return '#77d900';
else if(pct > 25) return '#94d900';
else return '#dcd800';
};
export function metertype(mt) {
switch(mt) {
case 1:
@@ -75,7 +86,9 @@ export function boardtype(c, b) {
case 0:
return "Custom hardware by Roar Fredriksen";
case 1:
return "Kamstrup module by Egil Opsahl"
return "Kamstrup module by Egil Opsahl";
case 8:
return "µHAN mosquito by dbeinder"
case 3:
return "Pow-K (UART0)";
case 4:
@@ -84,6 +97,10 @@ export function boardtype(c, b) {
return "Wemos D1 mini";
case 100:
return "Generic ESP8266";
case 70:
return "Generic ESP32-C3";
case 71:
return "ESP32-C3-DevKitM-1";
}
}
@@ -100,6 +117,12 @@ export function hanError(err) {
case -52: return "Decryption failed";
case -53: return "Encryption key invalid";
case 90: return "No HAN data received last 30s";
case 91: return "Serial break";
case 92: return "Serial buffer full";
case 93: return "Serial FIFO overflow";
case 94: return "Serial frame error";
case 95: return "Serial parity error";
case 96: return "RX error";
case 98: return "Exception in code, debugging necessary";
case 99: return "Autodetection failed";
}
@@ -127,6 +150,10 @@ export function priceError(err) {
return "Unauthorized, check API key";
case 404:
return "Price unavailable, not found";
case 425:
return "Server says its too early";
case 429:
return "Exceeded API rate limit";
case 500:
return "Internal server error";
case -2: return "Incomplete data received";
@@ -153,3 +180,53 @@ export function isBusPowered(boardType) {
export function uiVisibility(choice, state) {
return choice == 1 || (choice == 2 && state);
}
export function wiki(page) {
return "https://github.com/UtilitechAS/amsreader-firmware/wiki/" + page;
}
export function fmtnum(v,d) {
if(isNaN(v)) return '-';
if(isNaN(d))
d = v < 10 ? 1 : 0;
return v.toFixed(d);
}
export function addHours(date, hours) {
date.setTime(date.getTime() + hours * 3600000);
return date;
}
export function getResetReason(sysinfo) {
if(sysinfo.chip == 'esp8266') {
switch (sysinfo.boot_reason) {
case 0: return "Normal";
case 1: return "WDT reset";
case 2: return "Exception reset";
case 3: return "Soft WDT reset";
case 4: return "Software restart";
case 5: return "Deep sleep";
case 6: return "External reset";
default: return "Unknown (8266)";
}
} else {
switch (sysinfo.boot_reason) {
case 1 : return "Vbat power on reset";
case 3 : return "Software reset";
case 4 : return "WDT reset";
case 5 : return "Deep sleep";
case 6 : return "SLC reset";
case 7 : return "Timer Group0 WDT reset";
case 8 : return "Timer Group1 WDT reset";
case 9 : return "RTC WDT reset";
case 10: return "Instrusion test reset CPU";
case 11: return "Time Group reset CPU";
case 12: return "Software reset CPU";
case 13: return "RTC WTD reset CPU";
case 14: return "PRO CPU";
case 15: return "Brownout";
case 16: return "RTC reset";
default: return "Unknown";
}
}
}

View File

@@ -1,6 +1,5 @@
<script>
import PowerGaugeSvg from './PowerGaugeSvg.svelte';
import { ampcol } from './Helpers.js';
export let val;
export let max;
@@ -8,10 +7,16 @@
export let label;
export let sub = "";
export let subunit = "";
export let colorFn;
let pct = 0;
$: {
pct = (Math.min(val,max)/max) * 100
}
</script>
<div class="pl-root">
<PowerGaugeSvg pct={val/max * 100} color={ampcol(val/max * 100)}/>
<PowerGaugeSvg pct={pct} color={colorFn(pct)}/>
<span class="pl-ov">
<span class="pl-lab">{label}</span>
<br/>

View File

@@ -1,5 +1,5 @@
<script>
import { zeropad } from './Helpers.js';
import { zeropad, addHours } from './Helpers.js';
import BarChart from './BarChart.svelte';
export let json;
@@ -19,7 +19,6 @@
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({
@@ -34,9 +33,9 @@
});
min = Math.min(min, val*100);
max = Math.max(max, val*100);
addHours(cur, 1);
};
for(i = 0; i < 24; i++) {
cur.setUTCHours(i);
val = json[zeropad(h++)];
if(val == null) break;
xTicks.push({
@@ -51,6 +50,7 @@
});
min = Math.min(min, val*100);
max = Math.max(max, val*100);
addHours(cur, 1);
};
max = Math.ceil(max);

View File

@@ -5,8 +5,8 @@
export let exportTotal;
</script>
<div class="mx-2">
<strong class="text-sm">Reactive</strong>
<div class="mx-2 text-sm">
<strong>Reactive</strong>
<div class="grid grid-cols-2 mt-4">
<div>Instant in</div>

View File

@@ -1,6 +1,7 @@
<script>
import { sysinfoStore } from './DataStores.js';
import Mask from './Mask.svelte'
import SubnetOptions from './SubnetOptions.svelte';
export let sysinfo = {}
@@ -12,7 +13,15 @@
var url = "";
tries++;
var retry = function() {
setTimeout(scanForDevice, 1000);
};
if(sysinfo.net.ip && tries%3 == 0) {
if(!sysinfo.net.ip) {
retry();
return;
};
url = "http://" + sysinfo.net.ip;
} else if(sysinfo.hostname && tries%3 == 1) {
url = "http://" + sysinfo.hostname;
@@ -22,10 +31,10 @@
url = "";
}
if(console) console.log("Trying url " + url);
var retry = function() {
setTimeout(scanForDevice, 1000);
};
sysinfoStore.update(s => {
s.trying = url;
return s;
});
var xhr = new XMLHttpRequest();
xhr.timeout = 5000;
@@ -42,12 +51,10 @@
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', {
@@ -58,9 +65,15 @@
loadingOrSaving = false;
sysinfoStore.update(s => {
s.hostname = hostname;
s.hostname = formData.get('sh');
s.usrcfg = res.success;
s.booting = res.reboot;
if(staticIp) {
s.net.ip = formData.get('si');
s.net.mask = formData.get('su');
s.net.gw = formData.get('sg');
s.net.dns1 = formData.get('sd');
}
setTimeout(scanForDevice, 5000);
return s;
});
@@ -75,14 +88,14 @@
<strong class="text-sm">Setup</strong>
<div class="my-3">
SSID<br/>
<input name="ss" type="text" class="in-s"/>
<input name="ss" type="text" class="in-s" required/>
</div>
<div class="my-3">
PSK<br/>
<input name="sp" type="password" class="in-s" autocomplete="off"/>
</div>
<div>
Hostname:
Hostname
<input name="sh" bind:value={sysinfo.hostname} type="text" class="in-s" maxlength="32" pattern="[a-z0-9_-]+" placeholder="Optional, ex.: ams-reader" autocomplete="off"/>
</div>
<div class="my-3">
@@ -92,9 +105,7 @@
<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>
<SubnetOptions/>
</select>
</div>
{/if}

View File

@@ -1,13 +1,40 @@
<script>
import { metertype, boardtype, isBusPowered } from './Helpers.js';
import { metertype, boardtype, isBusPowered, getResetReason } from './Helpers.js';
import { getSysinfo, gitHubReleaseStore, sysinfoStore } from './DataStores.js';
import { upgrade, getNextVersion } from './UpgradeHelper';
import { upgrade, getNextVersion, upgradeWarningText } from './UpgradeHelper';
import DownloadIcon from './DownloadIcon.svelte';
import { Link } from 'svelte-navigator';
import Clock from './Clock.svelte';
import Mask from './Mask.svelte';
export let data;
export let sysinfo;
let cfgItems = [{
name: 'WiFi',
key: 'iw'
},{
name: 'MQTT',
key: 'im'
},{
name: 'Web',
key: 'ie'
},{
name: 'Meter',
key: 'it'
},{
name: 'Thresholds',
key: 'ih'
},{
name: 'GPIO',
key: 'ig'
},{
name: 'NTP',
key: 'in'
},{
name: 'Price API',
key: 'is'
}];
let nextVersion = {};
gitHubReleaseStore.subscribe(releases => {
@@ -19,12 +46,12 @@
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((sysinfo.board != 2 && sysinfo.board != 4 && sysinfo.board != 7) || confirm(upgradeWarningText(boardtype(sysinfo.chip, sysinfo.board)))) {
sysinfoStore.update(s => {
s.upgrading = true;
return s;
});
upgrade(nextVersion);
upgrade(nextVersion.tag_name);
}
}
}
@@ -64,7 +91,7 @@
Chip: {sysinfo.chip}
</div>
<div class="my-2">
Device: {boardtype(sysinfo.chip, sysinfo.board)}
Device: <Link to="/vendor">{boardtype(sysinfo.chip, sysinfo.board)}</Link>
</div>
<div class="my-2">
MAC: {sysinfo.mac}
@@ -73,10 +100,21 @@
<div class="my-2">
AP MAC: {sysinfo.apmac}
</div>
<div class="my-2">
Last boot:
{#if data.u > 0}
<Clock timestamp={new Date(new Date().getTime() - (data.u * 1000))} fullTimeColor="" />
{:else}
-
{/if}
</div>
<div class="my-2">
Reason: {getResetReason(sysinfo)} ({sysinfo.boot_reason}/{sysinfo.ex_cause})
</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>
<span class="btn-pri-sm">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>
@@ -108,7 +146,7 @@
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}
DNS: {sysinfo.net.dns1} {#if sysinfo.net.dns2}/ {sysinfo.net.dns2}{/if}
</div>
</div>
{/if}
@@ -117,6 +155,11 @@
<div class="my-2">
Installed version: {sysinfo.version}
</div>
{#if sysinfo.upgrade.t && sysinfo.upgrade.t != sysinfo.version}
<div class="my-2">
<div class="bd-yellow">Previous upgrade attempt ({sysinfo.upgrade.t}) does not match current version ({sysinfo.version}) [{sysinfo.upgrade.x}/{sysinfo.upgrade.e}]</div>
</div>
{/if}
{#if nextVersion}
<div class="my-2 flex">
Latest version:
@@ -129,13 +172,13 @@
</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 class="bd-yellow">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.
{upgradeWarningText(boardtype(sysinfo.chip, sysinfo.board))}
</div>
{/if}
{#if sysinfo.security == 0 || data.a}
@@ -143,10 +186,10 @@
<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>
<button type="button" on:click={()=>{firmwareFileInput.click();}} class="btn-pri-sm float-right">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>
<button type="submit" class="btn-pri-sm float-right">Upload</button>
{/if}
</form>
</div>
@@ -154,31 +197,25 @@
</div>
{#if sysinfo.security == 0 || data.a}
<div class="cnt">
<strong class="text-sm">Configuration</strong>
<strong class="text-sm">Backup & restore</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>
{#each cfgItems as el}
<label class="my-1 mx-3"><input type="checkbox" class="rounded" name="{el.key}" value="true" checked/> {el.name}</label>
{/each}
<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>
<button type="submit" class="btn-pri-sm float-right">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>
<button type="button" on:click={()=>{configFileInput.click();}} class="btn-pri-sm">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>
<button type="submit" class="btn-pri-sm">Upload</button>
{/if}
</form>
</div>

View File

@@ -0,0 +1,16 @@
<optgroup label="Most common is /24 (255.255.255.0)">
<option value="255.255.255.0">/24</option>
</optgroup>
<optgroup label="Smaller subnets">
<option value="255.255.255.128">/25</option>
<option value="255.255.255.192">/26</option>
<option value="255.255.255.224">/27</option>
<option value="255.255.255.240">/28</option>
<option value="255.255.255.248">/29</option>
</optgroup>
<optgroup label="Larger subnets">
<option value="255.255.254.0">/23</option>
<option value="255.255.252.0">/22</option>
<option value="255.255.0.0">/16</option>
</optgroup>

View File

@@ -1,8 +1,21 @@
<script>
export let chip;
let gpioMax = 39;
$: {
switch (chip) {
case 'esp8266': gpioMax = 16; break;
case 'esp32s2': gpioMax = 44; break;
case 'esp32c3': gpioMax = 19; break;
}
}
</script>
{#if chip == 'esp32c3'}
<option value={20}>UART0</option>
{:else}
<option value={3}>UART0</option>
{/if}
{#if chip == 'esp8266'}
<option value={113}>UART2</option>
{/if}
@@ -13,46 +26,13 @@
{#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}
{#each {length: gpioMax+1} as _, i}
{#if i > 3
&& !(chip == 'esp32' && (i == 9 || i == 16))
&& !(chip == 'esp32s2' && i == 18)
&& !(chip == 'esp8266' && (i == 3 || i == 113))
}
<option value={i}>GPIO{i}</option>
{/if}
{/each}

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