Initial changes for v2.3

This commit is contained in:
Gunnar Skjold 2023-11-16 18:21:56 +01:00
parent 8cada69aaf
commit ae82914795
121 changed files with 6175 additions and 3080 deletions

View File

@ -1,31 +1,54 @@
/**
* @copyright Utilitech AS 2023
* License: Fair Source
*
*/
#ifndef _AMSCONFIGURATION_h
#define _AMSCONFIGURATION_h
#include <EEPROM.h>
#include "Arduino.h"
#define EEPROM_SIZE 1024*3
#define EEPROM_CHECK_SUM 103 // Used to check if config is stored. Change if structure changes
#define EEPROM_CHECK_SUM 104 // 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
#define CONFIG_WIFI_START 360
#define CONFIG_ENERGYACCOUNTING_START 576
#define CONFIG_WEB_START 648
#define CONFIG_DEBUG_START 824
#define CONFIG_DOMOTICZ_START 856
#define CONFIG_NTP_START 872
#define CONFIG_MQTT_START 1004
#define CONFIG_HA_START 1680
#define CONFIG_UPGRADE_INFO_START 16
#define CONFIG_NETWORK_START 40
#define CONFIG_METER_START 296
#define CONFIG_GPIO_START 368
#define CONFIG_ENTSOE_START 400
#define CONFIG_ENERGYACCOUNTING_START 464
#define CONFIG_WEB_START 488
#define CONFIG_DEBUG_START 624
#define CONFIG_NTP_START 632
#define CONFIG_MQTT_START 760
#define CONFIG_DOMOTICZ_START 1528
#define CONFIG_HA_START 1544
#define CONFIG_UI_START 1712
#define CONFIG_CLOUD_START 1728
#define CONFIG_METER_START_93 224
#define CONFIG_METER_START_103 32
#define CONFIG_UPGRADE_INFO_START_103 216
#define CONFIG_UI_START_103 248
#define CONFIG_GPIO_START_103 266
#define CONFIG_ENTSOE_START_103 290
#define CONFIG_WIFI_START_103 360
#define CONFIG_ENERGYACCOUNTING_START_103 576
#define CONFIG_WEB_START_103 648
#define CONFIG_DEBUG_START_103 824
#define CONFIG_DOMOTICZ_START_103 856
#define CONFIG_NTP_START_103 872
#define CONFIG_MQTT_START_103 1004
#define CONFIG_HA_START_103 1680
#define LED_BEHAVIOUR_DEFAULT 0
#define LED_BEHAVIOUR_BOOT 1
#define LED_BEHAVIOUR_ERROR_ONLY 3
#define LED_BEHAVIOUR_OFF 9
struct SystemConfig {
uint8_t boardType;
@ -33,9 +56,10 @@ struct SystemConfig {
bool userConfigured;
uint8_t dataCollectionConsent; // 0 = unknown, 1 = accepted, 2 = declined
char country[3];
}; // 7
uint8_t energyspeedometer;
}; // 8
struct WiFiConfig {
struct NetworkConfig {
char ssid[32];
char psk[64];
char ip[16];
@ -49,7 +73,8 @@ struct WiFiConfig {
uint8_t sleep;
uint8_t use11b;
bool autoreboot;
}; // 213
uint8_t mode;
}; // 214
struct MqttConfig {
char host[128];
@ -85,41 +110,10 @@ struct MeterConfig {
uint8_t source;
uint8_t parser;
uint8_t bufferSize;
}; // 62
struct MeterConfig100 {
uint32_t baud;
uint8_t parity;
bool invert;
uint8_t distributionSystem;
uint8_t mainFuse;
uint8_t productionCapacity;
uint8_t encryptionKey[16];
uint8_t authenticationKey[16];
uint32_t wattageMultiplier;
uint32_t voltageMultiplier;
uint32_t amperageMultiplier;
uint32_t accumulatedMultiplier;
uint8_t source;
uint8_t parser;
}; // 59
struct MeterConfig95 {
uint32_t baud;
uint8_t parity;
bool invert;
uint8_t distributionSystem;
uint8_t mainFuse;
uint8_t productionCapacity;
uint8_t encryptionKey[16];
uint8_t authenticationKey[16];
uint16_t wattageMultiplier;
uint16_t voltageMultiplier;
uint16_t amperageMultiplier;
uint16_t accumulatedMultiplier;
uint8_t source;
uint8_t parser;
}; // 50
uint8_t rxPin;
bool rxPinPullup;
uint8_t txPin;
}; // 65
struct DebugConfig {
bool telnet;
@ -128,6 +122,26 @@ struct DebugConfig {
}; // 3
struct GpioConfig {
uint8_t apPin;
uint8_t ledPin;
bool ledInverted;
uint8_t ledPinRed;
uint8_t ledPinGreen;
uint8_t ledPinBlue;
bool ledRgbInverted;
uint8_t tempSensorPin;
uint8_t tempAnalogSensorPin;
uint8_t vccPin;
int16_t vccOffset;
uint16_t vccMultiplier;
uint8_t vccBootLimit;
uint16_t vccResistorGnd;
uint16_t vccResistorVcc;
uint8_t ledDisablePin;
uint8_t ledBehaviour;
}; // 21
struct GpioConfig103 {
uint8_t hanPin;
uint8_t apPin;
uint8_t ledPin;
@ -145,7 +159,9 @@ struct GpioConfig {
uint16_t vccResistorGnd;
uint16_t vccResistorVcc;
bool hanPinPullup;
}; // 21
uint8_t ledDisablePin;
uint8_t ledBehaviour;
}; // 23
struct DomoticzConfig {
uint16_t elidx;
@ -168,14 +184,6 @@ struct NtpConfig {
char timezone[32];
}; // 98
struct NtpConfig96 {
bool enable;
bool dhcp;
int16_t offset;
int16_t summerOffset;
char server[64];
}; // 70
struct EntsoeConfig {
char token[37];
char area[17];
@ -207,7 +215,9 @@ struct UiConfig {
uint8_t showDayPlot;
uint8_t showMonthPlot;
uint8_t showTemperaturePlot;
}; // 11
uint8_t showRealtimePlot;
uint8_t darkMode;
}; // 12
struct TempSensorConfig {
uint8_t address[8];
@ -222,6 +232,15 @@ struct UpgradeInformation {
int16_t errorCode;
}; // 20
struct CloudConfig {
bool enabled;
uint8_t interval;
char hostname[64];
uint16_t port;
char clientId[17];
char clientSecret[17];
};
class AmsConfiguration {
public:
bool hasConfig();
@ -231,13 +250,15 @@ public:
bool getSystemConfig(SystemConfig&);
bool setSystemConfig(SystemConfig&);
bool isSystemConfigChanged();
void ackSystemConfigChanged();
bool getWiFiConfig(WiFiConfig&);
bool setWiFiConfig(WiFiConfig&);
void clearWifi(WiFiConfig&);
void clearWifiIp(WiFiConfig&);
bool isWifiChanged();
void ackWifiChange();
bool getNetworkConfig(NetworkConfig&);
bool setNetworkConfig(NetworkConfig&);
void clearNetworkConfig(NetworkConfig&);
void clearNetworkConfigIp(NetworkConfig&);
bool isNetworkConfigChanged();
void ackNetworkConfigChange();
bool getMqttConfig(MqttConfig&);
bool setMqttConfig(MqttConfig&);
@ -299,18 +320,16 @@ public:
bool setUiConfig(UiConfig&);
void clearUiConfig(UiConfig&);
void loadTempSensors();
void saveTempSensors();
uint8_t getTempSensorCount();
TempSensorConfig* getTempSensorConfig(uint8_t address[8]);
void updateTempSensorConfig(uint8_t address[8], const char name[32], bool common);
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&);
bool getCloudConfig(CloudConfig&);
bool setCloudConfig(CloudConfig&);
void clearCloudConfig(CloudConfig&);
bool isCloudChanged();
void ackCloudConfig();
void clear();
protected:
@ -318,18 +337,11 @@ protected:
private:
uint8_t configVersion = 0;
bool wifiChanged, mqttChanged, meterChanged = true, ntpChanged = true, entsoeChanged = false, energyAccountingChanged = true;
bool sysChanged = false, networkChanged, mqttChanged, meterChanged = true, ntpChanged = true, entsoeChanged = false, energyAccountingChanged = true, cloudChanged = true;
uint8_t tempSensorCount = 0;
TempSensorConfig** tempSensors = NULL;
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
bool relocateConfig103(); // 2.2.12 onward
void saveToFs();
bool loadFromFs(uint8_t version);

View File

@ -1,3 +1,9 @@
/**
* @copyright Utilitech AS 2023
* License: Fair Source
*
*/
#ifndef _AMSSTORAGE_H
#define _AMSSTORAGE_H

View File

@ -1,3 +1,9 @@
/**
* @copyright Utilitech AS 2023
* License: Fair Source
*
*/
#include <Timezone.h>
#define JULY1970 15634800

View File

@ -1,3 +1,9 @@
/**
* @copyright Utilitech AS 2023
* License: Fair Source
*
*/
#ifndef _HEXUTILS_H
#define _HEXUTILS_H

View File

@ -1,3 +1,9 @@
/**
* @copyright Utilitech AS 2023
* License: Fair Source
*
*/
#include "AmsConfiguration.h"
#include "hexutils.h"
@ -13,12 +19,22 @@ bool AmsConfiguration::getSystemConfig(SystemConfig& config) {
config.vendorConfigured = false;
config.userConfigured = false;
config.dataCollectionConsent = 0;
config.energyspeedometer = 0;
strcpy(config.country, "");
return false;
}
}
bool AmsConfiguration::setSystemConfig(SystemConfig& config) {
SystemConfig existing;
if(getSystemConfig(existing)) {
sysChanged |= config.boardType != existing.boardType;
sysChanged |= config.vendorConfigured != existing.vendorConfigured;
sysChanged |= config.userConfigured != existing.userConfigured;
sysChanged |= config.dataCollectionConsent != existing.dataCollectionConsent;
sysChanged |= strcmp(config.country, existing.country) != 0;
sysChanged |= config.energyspeedometer != existing.energyspeedometer;
}
EEPROM.begin(EEPROM_SIZE);
stripNonAscii((uint8_t*) config.country, 2);
EEPROM.put(CONFIG_SYSTEM_START, config);
@ -27,39 +43,48 @@ bool AmsConfiguration::setSystemConfig(SystemConfig& config) {
return ret;
}
bool AmsConfiguration::getWiFiConfig(WiFiConfig& config) {
bool AmsConfiguration::isSystemConfigChanged() {
return sysChanged;
}
void AmsConfiguration::ackSystemConfigChanged() {
sysChanged = false;
}
bool AmsConfiguration::getNetworkConfig(NetworkConfig& config) {
if(hasConfig()) {
EEPROM.begin(EEPROM_SIZE);
EEPROM.get(CONFIG_WIFI_START, config);
EEPROM.get(CONFIG_NETWORK_START, config);
EEPROM.end();
if(config.sleep > 2) config.sleep = 1;
return true;
} else {
clearWifi(config);
clearNetworkConfig(config);
return false;
}
}
bool AmsConfiguration::setWiFiConfig(WiFiConfig& config) {
WiFiConfig existing;
bool AmsConfiguration::setNetworkConfig(NetworkConfig& config) {
NetworkConfig existing;
if(config.sleep > 2) config.sleep = 1;
if(getWiFiConfig(existing)) {
wifiChanged |= strcmp(config.ssid, existing.ssid) != 0;
wifiChanged |= strcmp(config.psk, existing.psk) != 0;
wifiChanged |= strcmp(config.ip, existing.ip) != 0;
if(getNetworkConfig(existing)) {
networkChanged |= strcmp(config.ssid, existing.ssid) != 0;
networkChanged |= strcmp(config.psk, existing.psk) != 0;
networkChanged |= strcmp(config.ip, existing.ip) != 0;
if(strlen(config.ip) > 0) {
wifiChanged |= strcmp(config.gateway, existing.gateway) != 0;
wifiChanged |= strcmp(config.subnet, existing.subnet) != 0;
wifiChanged |= strcmp(config.dns1, existing.dns1) != 0;
wifiChanged |= strcmp(config.dns2, existing.dns2) != 0;
networkChanged |= strcmp(config.gateway, existing.gateway) != 0;
networkChanged |= strcmp(config.subnet, existing.subnet) != 0;
networkChanged |= strcmp(config.dns1, existing.dns1) != 0;
networkChanged |= strcmp(config.dns2, existing.dns2) != 0;
}
wifiChanged |= strcmp(config.hostname, existing.hostname) != 0;
wifiChanged |= config.power != existing.power;
wifiChanged |= config.sleep != existing.sleep;
wifiChanged |= config.use11b != existing.use11b;
wifiChanged |= config.autoreboot != existing.autoreboot;
networkChanged |= strcmp(config.hostname, existing.hostname) != 0;
networkChanged |= config.power != existing.power;
networkChanged |= config.sleep != existing.sleep;
networkChanged |= config.use11b != existing.use11b;
networkChanged |= config.autoreboot != existing.autoreboot;
networkChanged |= config.mode != existing.mode;
} else {
wifiChanged = true;
networkChanged = true;
}
stripNonAscii((uint8_t*) config.ssid, 32, true);
@ -72,20 +97,20 @@ bool AmsConfiguration::setWiFiConfig(WiFiConfig& config) {
stripNonAscii((uint8_t*) config.hostname, 32);
EEPROM.begin(EEPROM_SIZE);
EEPROM.put(CONFIG_WIFI_START, config);
EEPROM.put(CONFIG_NETWORK_START, config);
bool ret = EEPROM.commit();
EEPROM.end();
return ret;
}
void AmsConfiguration::clearWifi(WiFiConfig& config) {
void AmsConfiguration::clearNetworkConfig(NetworkConfig& config) {
strcpy(config.ssid, "");
strcpy(config.psk, "");
clearWifiIp(config);
clearNetworkConfigIp(config);
uint16_t chipId;
#if defined(ESP32)
chipId = ESP.getEfuseMac();
chipId = ( ESP.getEfuseMac() >> 32 ) % 0xFFFFFFFF;
config.power = 195;
#else
chipId = ESP.getChipId();
@ -97,7 +122,7 @@ void AmsConfiguration::clearWifi(WiFiConfig& config) {
config.use11b = 1;
}
void AmsConfiguration::clearWifiIp(WiFiConfig& config) {
void AmsConfiguration::clearNetworkConfigIp(NetworkConfig& config) {
strcpy(config.ip, "");
strcpy(config.gateway, "");
strcpy(config.subnet, "");
@ -105,12 +130,12 @@ void AmsConfiguration::clearWifiIp(WiFiConfig& config) {
strcpy(config.dns2, "");
}
bool AmsConfiguration::isWifiChanged() {
return wifiChanged;
bool AmsConfiguration::isNetworkConfigChanged() {
return networkChanged;
}
void AmsConfiguration::ackWifiChange() {
wifiChanged = false;
void AmsConfiguration::ackNetworkConfigChange() {
networkChanged = false;
}
bool AmsConfiguration::getMqttConfig(MqttConfig& config) {
@ -234,6 +259,9 @@ bool AmsConfiguration::setMeterConfig(MeterConfig& config) {
meterChanged |= strcmp((char*) config.encryptionKey, (char*) existing.encryptionKey);
meterChanged |= strcmp((char*) config.authenticationKey, (char*) existing.authenticationKey);
meterChanged |= config.bufferSize != existing.bufferSize;
meterChanged |= config.rxPin != existing.rxPin;
meterChanged |= config.rxPinPullup != existing.rxPinPullup;
meterChanged |= config.txPin != existing.txPin;
} else {
meterChanged = true;
}
@ -245,6 +273,9 @@ bool AmsConfiguration::setMeterConfig(MeterConfig& config) {
}
void AmsConfiguration::clearMeter(MeterConfig& config) {
config.rxPin = 0xFF;
config.txPin = 0xFF;
config.rxPinPullup = true;
config.baud = 0;
config.parity = 0;
config.invert = false;
@ -386,7 +417,6 @@ bool AmsConfiguration::pinUsed(uint8_t pin, GpioConfig& config) {
if(pin == 0xFF)
return false;
return
pin == config.hanPin ||
pin == config.apPin ||
pin == config.ledPin ||
pin == config.ledPinRed ||
@ -394,7 +424,8 @@ bool AmsConfiguration::pinUsed(uint8_t pin, GpioConfig& config) {
pin == config.ledPinBlue ||
pin == config.tempSensorPin ||
pin == config.tempAnalogSensorPin ||
pin == config.vccPin
pin == config.vccPin ||
pin == config.ledDisablePin
;
}
@ -413,10 +444,6 @@ bool AmsConfiguration::getGpioConfig(GpioConfig& config) {
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)) {
debugger->println(F("HAN pin already used"));
@ -454,6 +481,10 @@ bool AmsConfiguration::setGpioConfig(GpioConfig& config) {
debugger->println(F("Vcc pin already used"));
return false;
}
if(pinUsed(config.ledDisablePin, config)) {
debugger->println(F("ledDisablePin already used"));
return false;
}
*/
if(config.apPin >= 0)
pinMode(config.apPin, INPUT_PULLUP);
@ -466,8 +497,6 @@ 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;
@ -483,6 +512,8 @@ void AmsConfiguration::clearGpio(GpioConfig& config) {
config.vccBootLimit = 0;
config.vccResistorGnd = 0;
config.vccResistorVcc = 0;
config.ledDisablePin = 0xFF;
config.ledBehaviour = LED_BEHAVIOUR_DEFAULT;
}
bool AmsConfiguration::getNtpConfig(NtpConfig& config) {
@ -502,7 +533,7 @@ bool AmsConfiguration::setNtpConfig(NtpConfig& config) {
if(getNtpConfig(existing)) {
if(config.enable != existing.enable) {
if(!existing.enable) {
wifiChanged = true;
networkChanged = true;
} else {
ntpChanged = true;
}
@ -660,6 +691,7 @@ bool AmsConfiguration::getUiConfig(UiConfig& config) {
EEPROM.begin(EEPROM_SIZE);
EEPROM.get(CONFIG_UI_START, config);
if(config.showImport > 2) clearUiConfig(config); // Must be wrong
if(config.showRealtimePlot > 2) config.showRealtimePlot = 1; // TODO: Move to new config version for v2.3
EEPROM.end();
return true;
} else {
@ -677,7 +709,7 @@ bool AmsConfiguration::setUiConfig(UiConfig& config) {
}
void AmsConfiguration::clearUiConfig(UiConfig& config) {
// 1 = Always, 2 = If value present, 0 = Hidden
// 1 = Enable, 2 = Auto, 0 = Disable
config.showImport = 1;
config.showExport = 2;
config.showVoltage = 2;
@ -689,6 +721,8 @@ void AmsConfiguration::clearUiConfig(UiConfig& config) {
config.showDayPlot = 1;
config.showMonthPlot = 1;
config.showTemperaturePlot = 2;
config.showRealtimePlot = 1;
config.darkMode = 2;
}
bool AmsConfiguration::setUpgradeInformation(int16_t exitCode, int16_t errorCode, const char* currentVersion, const char* nextVersion) {
@ -730,6 +764,58 @@ void AmsConfiguration::clearUpgradeInformation(UpgradeInformation& upinfo) {
memset(upinfo.toVersion, 0, 8);
}
bool AmsConfiguration::getCloudConfig(CloudConfig& config) {
if(hasConfig()) {
EEPROM.begin(EEPROM_SIZE);
EEPROM.get(CONFIG_CLOUD_START, config);
EEPROM.end();
return true;
} else {
clearCloudConfig(config);
return false;
}
}
bool AmsConfiguration::setCloudConfig(CloudConfig& config) {
CloudConfig existing;
if(getCloudConfig(existing)) {
cloudChanged |= config.enabled != existing.enabled;
cloudChanged |= config.interval!= existing.interval;
cloudChanged |= config.port!= existing.port;
cloudChanged |= strcmp(config.hostname, existing.hostname) != 0;
cloudChanged |= strcmp(config.clientId, existing.clientId) != 0;
cloudChanged |= strcmp(config.clientSecret, existing.clientSecret) != 0;
} else {
cloudChanged = true;
}
stripNonAscii((uint8_t*) config.hostname, 64);
stripNonAscii((uint8_t*) config.clientId, 17);
stripNonAscii((uint8_t*) config.clientSecret, 17);
EEPROM.begin(EEPROM_SIZE);
EEPROM.put(CONFIG_CLOUD_START, config);
bool ret = EEPROM.commit();
EEPROM.end();
return ret;
}
void AmsConfiguration::clearCloudConfig(CloudConfig& config) {
config.enabled = false;
strcpy(config.hostname, "cloud.amsleser.no");
config.port = 7443;
config.interval = 10;
strcpy(config.clientId, "");
strcpy(config.clientSecret, "");
}
bool AmsConfiguration::isCloudChanged() {
return cloudChanged;
}
void AmsConfiguration::ackCloudConfig() {
cloudChanged = false;
}
void AmsConfiguration::clear() {
EEPROM.begin(EEPROM_SIZE);
@ -738,6 +824,7 @@ void AmsConfiguration::clear() {
EEPROM.get(CONFIG_SYSTEM_START, sys);
sys.userConfigured = false;
sys.dataCollectionConsent = 0;
sys.energyspeedometer = 0;
strcpy(sys.country, "");
EEPROM.put(CONFIG_SYSTEM_START, sys);
@ -745,9 +832,9 @@ void AmsConfiguration::clear() {
clearMeter(meter);
EEPROM.put(CONFIG_METER_START, meter);
WiFiConfig wifi;
clearWifi(wifi);
EEPROM.put(CONFIG_WIFI_START, wifi);
NetworkConfig network;
clearNetworkConfig(network);
EEPROM.put(CONFIG_NETWORK_START, network);
MqttConfig mqtt;
clearMqtt(mqtt);
@ -789,6 +876,10 @@ void AmsConfiguration::clear() {
clearUpgradeInformation(upinfo);
EEPROM.put(CONFIG_UPGRADE_INFO_START, upinfo);
CloudConfig cloud;
clearCloudConfig(cloud);
EEPROM.put(CONFIG_CLOUD_START, cloud);
EEPROM.put(EEPROM_CONFIG_ADDRESS, EEPROM_CLEARED_INDICATOR);
EEPROM.commit();
EEPROM.end();
@ -808,46 +899,6 @@ bool AmsConfiguration::hasConfig() {
}
} else {
switch(configVersion) {
case 93:
configVersion = -1; // Prevent loop
if(relocateConfig93()) {
configVersion = 94;
} else {
configVersion = 0;
return false;
}
case 94:
configVersion = -1; // Prevent loop
if(relocateConfig94()) {
configVersion = 95;
} else {
configVersion = 0;
return false;
}
case 95:
configVersion = -1; // Prevent loop
if(relocateConfig95()) {
configVersion = 96;
} else {
configVersion = 0;
return false;
}
case 96:
configVersion = -1; // Prevent loop
if(relocateConfig96()) {
configVersion = 100;
} else {
configVersion = 0;
return false;
}
case 100:
configVersion = -1; // Prevent loop
if(relocateConfig100()) {
configVersion = 101;
} else {
configVersion = 0;
return false;
}
case 101:
configVersion = -1; // Prevent loop
if(relocateConfig101()) {
@ -864,6 +915,14 @@ bool AmsConfiguration::hasConfig() {
configVersion = 0;
return false;
}
case 103:
configVersion = -1; // Prevent loop
if(relocateConfig103()) {
configVersion = 104;
} else {
configVersion = 0;
return false;
}
case EEPROM_CHECK_SUM:
return true;
default:
@ -878,204 +937,18 @@ int AmsConfiguration::getConfigVersion() {
return configVersion;
}
void AmsConfiguration::loadTempSensors() {
EEPROM.begin(EEPROM_SIZE);
TempSensorConfig* tempSensors[32];
int address = EEPROM_TEMP_CONFIG_ADDRESS;
int c = 0;
int storedCount = EEPROM.read(address++);
if(storedCount > 0 && storedCount <= 32) {
for(int i = 0; i < storedCount; i++) {
TempSensorConfig* tsc = new TempSensorConfig();
EEPROM.get(address, *tsc);
if(tsc->address[0] != 0xFF) {
tempSensors[c++] = tsc;
}
address += sizeof(*tsc);
}
}
this->tempSensors = new TempSensorConfig*[c];
for(int i = 0; i < c; i++) {
this->tempSensors[i] = tempSensors[i];
}
tempSensorCount = c;
EEPROM.end();
}
void AmsConfiguration::saveTempSensors() {
int address = EEPROM_TEMP_CONFIG_ADDRESS;
EEPROM.put(address++, tempSensorCount);
for(int i = 0; i < tempSensorCount; i++) {
TempSensorConfig* tsc = tempSensors[i];
if(tsc->address[0] != 0xFF) {
EEPROM.put(address, *tsc);
address += sizeof(*tsc);
}
}
}
bool AmsConfiguration::relocateConfig93() {
MeterConfig95 meter;
EEPROM.begin(EEPROM_SIZE);
EEPROM.get(CONFIG_METER_START_93, meter);
meter.wattageMultiplier = 0;
meter.voltageMultiplier = 0;
meter.amperageMultiplier = 0;
meter.accumulatedMultiplier = 0;
EEPROM.put(CONFIG_METER_START, meter);
EEPROM.put(EEPROM_CONFIG_ADDRESS, 94);
bool ret = EEPROM.commit();
EEPROM.end();
return ret;
}
bool AmsConfiguration::relocateConfig94() {
EnergyAccountingConfig eac;
EEPROM.begin(EEPROM_SIZE);
EEPROM.get(CONFIG_ENERGYACCOUNTING_START, eac);
eac.hours = 1;
EEPROM.put(CONFIG_ENERGYACCOUNTING_START, eac);
EEPROM.put(EEPROM_CONFIG_ADDRESS, 95);
bool ret = EEPROM.commit();
EEPROM.end();
return ret;
}
bool AmsConfiguration::relocateConfig95() {
MeterConfig95 meter;
MeterConfig95 meter95;
EEPROM.begin(EEPROM_SIZE);
EEPROM.get(CONFIG_METER_START, meter);
EEPROM.get(CONFIG_METER_START, meter95);
meter.wattageMultiplier = meter95.wattageMultiplier;
meter.voltageMultiplier = meter95.voltageMultiplier;
meter.amperageMultiplier = meter95.amperageMultiplier;
meter.accumulatedMultiplier = meter95.accumulatedMultiplier;
EEPROM.put(CONFIG_METER_START, meter);
EEPROM.put(EEPROM_CONFIG_ADDRESS, 96);
bool ret = EEPROM.commit();
EEPROM.end();
return ret;
}
bool AmsConfiguration::relocateConfig96() {
EEPROM.begin(EEPROM_SIZE);
SystemConfig sys;
EEPROM.get(CONFIG_SYSTEM_START, sys);
MeterConfig100 meter;
EEPROM.get(CONFIG_METER_START, meter);
meter.source = 1; // Serial
meter.parser = 0; // Auto
EEPROM.put(CONFIG_METER_START, meter);
#if defined(ESP8266)
GpioConfig gpio;
EEPROM.get(CONFIG_GPIO_START, gpio);
switch(sys.boardType) {
case 3: // Pow UART0 -- Now Pow-K UART0
case 4: // Pow GPIO12 -- Now Pow-U UART0
case 5: // Pow-K+ -- Now also Pow-K GPIO12
case 7: // Pow-U+ -- Now also Pow-U GPIO12
if(meter.baud == 2400 && meter.parity == 3) { // 3 == 8N1, assuming Pow-K
if(gpio.hanPin == 3) { // UART0
sys.boardType = 3;
} else if(gpio.hanPin == 12) {
sys.boardType = 5;
}
} else { // Assuming Pow-U
if(gpio.hanPin == 3) { // UART0
sys.boardType = 4;
} else if(gpio.hanPin == 12) {
sys.boardType = 7;
}
}
break;
}
#endif
sys.vendorConfigured = true;
sys.userConfigured = true;
sys.dataCollectionConsent = 0;
strcpy(sys.country, "");
EEPROM.put(CONFIG_SYSTEM_START, sys);
WiFiConfig wifi;
EEPROM.get(CONFIG_WIFI_START, wifi);
wifi.use11b = 1;
wifi.autoreboot = true;
EEPROM.put(CONFIG_WIFI_START, wifi);
NtpConfig ntp;
NtpConfig96 ntp96;
EEPROM.get(CONFIG_NTP_START, ntp96);
ntp.enable = ntp96.enable;
ntp.dhcp = ntp96.dhcp;
if(ntp96.offset == 360 && ntp96.summerOffset == 360) {
strcpy(ntp.timezone, "Europe/Oslo");
} else {
strcpy(ntp.timezone, "GMT");
}
strcpy(ntp.server, ntp96.server);
EEPROM.put(CONFIG_NTP_START, ntp);
EntsoeConfig entsoe;
EEPROM.get(CONFIG_ENTSOE_START, entsoe);
entsoe.enabled = strlen(entsoe.token) > 0;
EEPROM.put(CONFIG_ENTSOE_START, entsoe);
EEPROM.put(EEPROM_CONFIG_ADDRESS, 100);
bool ret = EEPROM.commit();
EEPROM.end();
return ret;
}
bool AmsConfiguration::relocateConfig100() {
EEPROM.begin(EEPROM_SIZE);
MeterConfig100 meter100;
EEPROM.get(CONFIG_METER_START, meter100);
MeterConfig meter;
meter.baud = meter100.baud;
meter.parity = meter100.parity;
meter.invert = meter100.invert;
meter.distributionSystem = meter100.distributionSystem;
meter.mainFuse = meter100.mainFuse;
meter.productionCapacity = meter100.productionCapacity;
memcpy(meter.encryptionKey, meter100.encryptionKey, 16);
memcpy(meter.authenticationKey, meter100.authenticationKey, 16);
meter.wattageMultiplier = meter100.wattageMultiplier;
meter.voltageMultiplier = meter100.voltageMultiplier;
meter.amperageMultiplier = meter100.amperageMultiplier;
meter.accumulatedMultiplier = meter100.accumulatedMultiplier;
meter.source = meter100.source;
meter.parser = meter100.parser;
EEPROM.put(CONFIG_METER_START, meter);
UiConfig ui;
clearUiConfig(ui);
EEPROM.put(CONFIG_UI_START, ui);
EEPROM.put(EEPROM_CONFIG_ADDRESS, 101);
bool ret = EEPROM.commit();
EEPROM.end();
return ret;
}
bool AmsConfiguration::relocateConfig101() {
EEPROM.begin(EEPROM_SIZE);
EnergyAccountingConfig config;
EnergyAccountingConfig101 config101;
EEPROM.get(CONFIG_ENERGYACCOUNTING_START, config101);
EEPROM.get(CONFIG_ENERGYACCOUNTING_START_103, 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(CONFIG_ENERGYACCOUNTING_START_103, config);
EEPROM.put(EEPROM_CONFIG_ADDRESS, 102);
bool ret = EEPROM.commit();
@ -1086,19 +959,19 @@ bool AmsConfiguration::relocateConfig101() {
bool AmsConfiguration::relocateConfig102() {
EEPROM.begin(EEPROM_SIZE);
GpioConfig gpioConfig;
EEPROM.get(CONFIG_GPIO_START, gpioConfig);
GpioConfig103 gpioConfig;
EEPROM.get(CONFIG_GPIO_START_103, gpioConfig);
gpioConfig.hanPinPullup = true;
EEPROM.put(CONFIG_GPIO_START, gpioConfig);
EEPROM.put(CONFIG_GPIO_START_103, gpioConfig);
HomeAssistantConfig haconf;
clearHomeAssistantConfig(haconf);
EEPROM.put(CONFIG_HA_START, haconf);
EEPROM.put(CONFIG_HA_START_103, haconf);
EntsoeConfig entsoe;
EEPROM.get(CONFIG_ENTSOE_START, entsoe);
EEPROM.get(CONFIG_ENTSOE_START_103, entsoe);
entsoe.fixedPrice = 0;
EEPROM.put(CONFIG_ENTSOE_START, entsoe);
EEPROM.put(CONFIG_ENTSOE_START_103, entsoe);
EEPROM.put(EEPROM_CONFIG_ADDRESS, 103);
bool ret = EEPROM.commit();
@ -1106,10 +979,89 @@ bool AmsConfiguration::relocateConfig102() {
return ret;
}
bool AmsConfiguration::relocateConfig103() {
EEPROM.begin(EEPROM_SIZE);
MeterConfig meter;
UpgradeInformation upinfo;
UiConfig ui;
GpioConfig103 gpio103;
EntsoeConfig entsoe;
NetworkConfig wifi;
EnergyAccountingConfig eac;
WebConfig web;
DebugConfig debug;
DomoticzConfig domo;
NtpConfig ntp;
MqttConfig mqtt;
HomeAssistantConfig ha;
EEPROM.get(CONFIG_METER_START_103, meter);
EEPROM.get(CONFIG_UPGRADE_INFO_START_103, upinfo);
EEPROM.get(CONFIG_UI_START_103, ui);
EEPROM.get(CONFIG_GPIO_START_103, gpio103);
EEPROM.get(CONFIG_ENTSOE_START_103, entsoe);
EEPROM.get(CONFIG_WIFI_START_103, wifi);
EEPROM.get(CONFIG_ENERGYACCOUNTING_START_103, eac);
EEPROM.get(CONFIG_WEB_START_103, web);
EEPROM.get(CONFIG_DEBUG_START_103, debug);
EEPROM.get(CONFIG_DOMOTICZ_START_103, domo);
EEPROM.get(CONFIG_NTP_START_103, ntp);
EEPROM.get(CONFIG_MQTT_START_103, mqtt);
EEPROM.get(CONFIG_HA_START_103, ha);
meter.rxPin = gpio103.hanPin;
meter.txPin = 0xFF;
meter.rxPinPullup = gpio103.hanPinPullup;
wifi.mode = 1; // 1 == WiFi client
GpioConfig gpio = {
gpio103.apPin,
gpio103.ledPin,
gpio103.ledInverted,
gpio103.ledPinRed,
gpio103.ledPinGreen,
gpio103.ledPinBlue,
gpio103.ledRgbInverted,
gpio103.tempSensorPin,
gpio103.tempAnalogSensorPin,
gpio103.vccPin,
gpio103.vccOffset,
gpio103.vccMultiplier,
gpio103.vccBootLimit,
gpio103.vccResistorGnd,
gpio103.vccResistorVcc,
gpio103.ledDisablePin,
gpio103.ledBehaviour
};
EEPROM.put(CONFIG_UPGRADE_INFO_START, upinfo);
EEPROM.put(CONFIG_NETWORK_START, wifi);
EEPROM.put(CONFIG_METER_START, meter);
EEPROM.put(CONFIG_GPIO_START, gpio);
EEPROM.put(CONFIG_ENTSOE_START, entsoe);
EEPROM.put(CONFIG_ENERGYACCOUNTING_START, eac);
EEPROM.put(CONFIG_WEB_START, web);
EEPROM.put(CONFIG_DEBUG_START, debug);
EEPROM.put(CONFIG_NTP_START, ntp);
EEPROM.put(CONFIG_MQTT_START, mqtt);
EEPROM.put(CONFIG_DOMOTICZ_START, domo);
EEPROM.put(CONFIG_HA_START, ha);
EEPROM.put(CONFIG_UI_START, ui);
CloudConfig cloud;
clearCloudConfig(cloud);
EEPROM.put(CONFIG_CLOUD_START, cloud);
EEPROM.put(EEPROM_CONFIG_ADDRESS, 104);
bool ret = EEPROM.commit();
EEPROM.end();
return ret;
}
bool AmsConfiguration::save() {
EEPROM.begin(EEPROM_SIZE);
EEPROM.put(EEPROM_CONFIG_ADDRESS, EEPROM_CHECK_SUM);
saveTempSensors();
bool success = EEPROM.commit();
EEPROM.end();
@ -1117,60 +1069,6 @@ bool AmsConfiguration::save() {
return success;
}
uint8_t AmsConfiguration::getTempSensorCount() {
return tempSensorCount;
}
TempSensorConfig* AmsConfiguration::getTempSensorConfig(uint8_t address[8]) {
if(tempSensors == NULL)
return NULL;
for(int x = 0; x < tempSensorCount; x++) {
TempSensorConfig *conf = tempSensors[x];
if(isSensorAddressEqual(conf->address, address)) {
return conf;
}
}
return NULL;
}
void AmsConfiguration::updateTempSensorConfig(uint8_t address[8], const char name[32], bool common) {
bool found = false;
if(tempSensors != NULL) {
for(int x = 0; x < tempSensorCount; x++) {
TempSensorConfig *data = tempSensors[x];
if(isSensorAddressEqual(data->address, address)) {
found = true;
strcpy(data->name, name);
data->common = common;
}
}
}
if(!found) {
TempSensorConfig** tempSensors = new TempSensorConfig*[tempSensorCount+1];
if(this->tempSensors != NULL) {
for(int i = 0;i < tempSensorCount; i++) {
tempSensors[i] = this->tempSensors[i];
}
}
TempSensorConfig *data = new TempSensorConfig();
memcpy(data->address, address, 8);
strcpy(data->name, name);
data->common = common;
tempSensors[tempSensorCount++] = data;
if(this->tempSensors != NULL) {
delete this->tempSensors;
}
this->tempSensors = tempSensors;
}
}
bool AmsConfiguration::isSensorAddressEqual(uint8_t a[8], uint8_t b[8]) {
for(int i = 0; i < 8; i++) {
if(a[i] != b[i]) return false;
}
return true;
}
void AmsConfiguration::saveToFs() {
}
@ -1186,21 +1084,32 @@ void AmsConfiguration::deleteFromFs(uint8_t version) {
void AmsConfiguration::print(Print* debugger)
{
debugger->println(F("-----------------------------------------------"));
WiFiConfig wifi;
if(getWiFiConfig(wifi)) {
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_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);
NetworkConfig network;
if(getNetworkConfig(network)) {
debugger->println(F("--Network configuration--"));
switch(network.mode) {
case 1:
debugger->printf_P(PSTR("Mode: 'WiFi client'\r\n"));
break;
case 2:
debugger->printf_P(PSTR("Mode: 'WiFi AP'\r\n"));
break;
case 3:
debugger->printf_P(PSTR("Mode: 'Ethernet'\r\n"));
break;
}
debugger->printf_P(PSTR("Hostname: '%s'\r\n"), wifi.hostname);
debugger->printf_P(PSTR("mDNS: '%s'\r\n"), wifi.mdns ? "Yes" : "No");
debugger->printf_P(PSTR("802.11b: '%s'\r\n"), wifi.use11b ? "Yes" : "No");
debugger->printf_P(PSTR("SSID: '%s'\r\n"), network.ssid);
debugger->printf_P(PSTR("Psk: '%s'\r\n"), network.psk);
if(strlen(network.ip) > 0) {
debugger->printf_P(PSTR("IP: '%s'\r\n"), network.ip);
debugger->printf_P(PSTR("Gateway: '%s'\r\n"), network.gateway);
debugger->printf_P(PSTR("Subnet: '%s'\r\n"), network.subnet);
debugger->printf_P(PSTR("DNS1: '%s'\r\n"), network.dns1);
debugger->printf_P(PSTR("DNS2: '%s'\r\n"), network.dns2);
}
debugger->printf_P(PSTR("Hostname: '%s'\r\n"), network.hostname);
debugger->printf_P(PSTR("mDNS: '%s'\r\n"), network.mdns ? "Yes" : "No");
debugger->printf_P(PSTR("802.11b: '%s'\r\n"), network.use11b ? "Yes" : "No");
debugger->println(F(""));
delay(10);
debugger->flush();
@ -1246,6 +1155,8 @@ void AmsConfiguration::print(Print* debugger)
MeterConfig meter;
if(getMeterConfig(meter)) {
debugger->println(F("--Meter configuration--"));
debugger->printf_P(PSTR("HAN RX: %i\r\n"), meter.rxPin);
debugger->printf_P(PSTR("HAN RX pullup %s\r\n"), meter.rxPinPullup ? "Yes" : "No");
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");
@ -1261,8 +1172,6 @@ void AmsConfiguration::print(Print* debugger)
GpioConfig gpio;
if(getGpioConfig(gpio)) {
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);
@ -1273,6 +1182,8 @@ void AmsConfiguration::print(Print* debugger)
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);
debugger->printf_P(PSTR("LED disable pin: %i\r\n"), gpio.ledDisablePin);
debugger->printf_P(PSTR("LED behaviour: %i\r\n"), gpio.ledBehaviour);
if(gpio.vccMultiplier > 0) {
debugger->printf_P(PSTR("Vcc multiplier: %f\r\n"), gpio.vccMultiplier / 1000.0);
}
@ -1335,7 +1246,5 @@ void AmsConfiguration::print(Print* debugger)
debugger->flush();
}
debugger->printf_P(PSTR("Temp sensor count: %i\r\n"), this->getTempSensorCount());
debugger->println(F("-----------------------------------------------"));
}

View File

@ -1,3 +1,9 @@
/**
* @copyright Utilitech AS 2023
* License: Fair Source
*
*/
#include "hexutils.h"
String toHex(uint8_t* in) {

View File

@ -1,8 +1,15 @@
/**
* @copyright Utilitech AS 2023
* License: Fair Source
*
*/
#ifndef _AMSDATA_H
#define _AMSDATA_H
#include "Arduino.h"
#include <Timezone.h>
#include "OBIScodes.h"
enum AmsType {
AmsTypeAutodetect = 0x00,
@ -21,6 +28,7 @@ public:
AmsData();
void apply(AmsData& other);
void apply(const OBIS_code_t obis, double value);
uint64_t getLastUpdateMillis();
@ -68,6 +76,7 @@ public:
bool isThreePhase();
bool isTwoPhase();
bool isCounterEstimated();
int8_t getLastError();
void setLastError(int8_t);

View File

@ -1,37 +0,0 @@
#ifndef _AMSMQTTHANDLER_H
#define _AMSMQTTHANDLER_H
#include "Arduino.h"
#include <MQTT.h>
#include "AmsData.h"
#include "AmsConfiguration.h"
#include "EnergyAccounting.h"
#include "HwTools.h"
#include "EntsoeApi.h"
#if defined(ESP32)
#include <esp_task_wdt.h>
#endif
class AmsMqttHandler {
public:
AmsMqttHandler(MQTTClient* mqtt, char* buf) {
this->mqtt = mqtt;
this->json = buf;
};
virtual ~AmsMqttHandler() {};
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*);
protected:
MQTTClient* mqtt;
char* json;
uint16_t BufferSize = 2048;
bool loop();
};
#endif

View File

@ -0,0 +1,79 @@
/**
* @copyright Utilitech AS 2023
* License: Fair Source
*
*/
#ifndef _OBISCODES_H
#define _OBISCODES_H
#include "lwip/def.h"
#define OBIS_MEDIUM_ABSTRACT 0
#define OBIS_MEDIUM_ELECTRICITY 1
#define OBIS_CHAN_0 0
#define OBIS_CHAN_1 1
#define OBIS_RANGE_NA 0xFF
struct OBIS_head_t {
uint8_t medium;
uint8_t channel;
} __attribute__((packed));
struct OBIS_code_t {
uint8_t sensor;
uint8_t gr;
uint8_t tariff;
} __attribute__((packed));
struct OBIS_t {
OBIS_head_t head;
OBIS_code_t code;
uint8_t range;
} __attribute__((packed));
const OBIS_code_t OBIS_NULL PROGMEM = { 0, 0, 0 };
const OBIS_code_t OBIS_VERSION PROGMEM = { 0, 2, 129 };
const OBIS_code_t OBIS_METER_MODEL PROGMEM = { 96, 1, 1 };
const OBIS_code_t OBIS_METER_MODEL_2 PROGMEM = { 96, 1, 7 };
const OBIS_code_t OBIS_METER_ID PROGMEM = { 96, 1, 0 };
const OBIS_code_t OBIS_METER_ID_2 PROGMEM = { 0, 0, 5 };
const OBIS_code_t OBIS_METER_TIMESTAMP PROGMEM = { 1, 0, 0 };
const OBIS_code_t OBIS_ACTIVE_IMPORT PROGMEM = { 1, 7, 0 };
const OBIS_code_t OBIS_ACTIVE_IMPORT_COUNT PROGMEM = { 1, 8, 0 };
const OBIS_code_t OBIS_ACTIVE_EXPORT PROGMEM = { 2, 7, 0 };
const OBIS_code_t OBIS_ACTIVE_EXPORT_COUNT PROGMEM = { 2, 8, 0 };
const OBIS_code_t OBIS_REACTIVE_IMPORT PROGMEM = { 3, 7, 0 };
const OBIS_code_t OBIS_REACTIVE_IMPORT_COUNT PROGMEM = { 3, 8, 0 };
const OBIS_code_t OBIS_REACTIVE_EXPORT PROGMEM = { 4, 7, 0 };
const OBIS_code_t OBIS_REACTIVE_EXPORT_COUNT PROGMEM = { 4, 8, 0 };
const OBIS_code_t OBIS_POWER_FACTOR PROGMEM = { 13, 7, 0 };
const OBIS_code_t OBIS_ACTIVE_IMPORT_L1 PROGMEM = { 21, 7, 0 };
const OBIS_code_t OBIS_ACTIVE_EXPORT_L1 PROGMEM = { 22, 7, 0 };
const OBIS_code_t OBIS_CURRENT_L1 PROGMEM = { 31, 7, 0 };
const OBIS_code_t OBIS_VOLTAGE_L1 PROGMEM = { 32, 7, 0 };
const OBIS_code_t OBIS_POWER_FACTOR_L1 PROGMEM = { 33, 7, 0 };
const OBIS_code_t OBIS_ACTIVE_IMPORT_L2 PROGMEM = { 41, 7, 0 };
const OBIS_code_t OBIS_ACTIVE_EXPORT_L2 PROGMEM = { 42, 7, 0 };
const OBIS_code_t OBIS_CURRENT_L2 PROGMEM = { 51, 7, 0 };
const OBIS_code_t OBIS_VOLTAGE_L2 PROGMEM = { 52, 7, 0 };
const OBIS_code_t OBIS_POWER_FACTOR_L2 PROGMEM = { 53, 7, 0 };
const OBIS_code_t OBIS_ACTIVE_IMPORT_L3 PROGMEM = { 61, 7, 0 };
const OBIS_code_t OBIS_ACTIVE_EXPORT_L3 PROGMEM = { 62, 7, 0 };
const OBIS_code_t OBIS_CURRENT_L3 PROGMEM = { 71, 7, 0 };
const OBIS_code_t OBIS_VOLTAGE_L3 PROGMEM = { 72, 7, 0 };
const OBIS_code_t OBIS_POWER_FACTOR_L3 PROGMEM = { 73, 7, 0 };
#endif

View File

@ -1,3 +1,9 @@
/**
* @copyright Utilitech AS 2023
* License: Fair Source
*
*/
#include "AmsData.h"
AmsData::AmsData() {}
@ -82,6 +88,91 @@ void AmsData::apply(AmsData& other) {
this->activeExportPower = other.getActiveExportPower();
}
void AmsData::apply(OBIS_code_t obis, double value) {
switch(obis.gr) {
case 1:
switch(obis.sensor) {
case 7:
switch(obis.tariff) {
case 0:
activeImportPower = value;
listType = max(listType, (uint8_t) 1);
break;
}
break;
case 8:
switch(obis.tariff) {
case 0:
activeImportCounter = value;
listType = max(listType, (uint8_t) 3);
break;
}
break;
}
break;
case 2:
switch(obis.sensor) {
case 7:
switch(obis.tariff) {
case 0:
activeExportPower = value;
listType = max(listType, (uint8_t) 2);
break;
}
break;
case 8:
switch(obis.tariff) {
case 0:
activeExportCounter = value;
listType = max(listType, (uint8_t) 3);
break;
}
break;
}
break;
case 3:
switch(obis.sensor) {
case 7:
switch(obis.tariff) {
case 0:
reactiveImportPower = value;
listType = max(listType, (uint8_t) 2);
break;
}
break;
case 8:
switch(obis.tariff) {
case 0:
reactiveImportCounter = value;
listType = max(listType, (uint8_t) 3);
break;
}
break;
}
break;
case 4:
switch(obis.sensor) {
case 7:
switch(obis.tariff) {
case 0:
reactiveExportPower = value;
listType = max(listType, (uint8_t) 2);
break;
}
break;
case 8:
switch(obis.tariff) {
case 0:
reactiveExportCounter = value;
listType = max(listType, (uint8_t) 3);
break;
}
break;
}
break;
}
}
uint64_t AmsData::getLastUpdateMillis() {
return this->lastUpdateMillis;
}
@ -218,6 +309,10 @@ bool AmsData::isTwoPhase() {
return this->twoPhase;
}
bool AmsData::isCounterEstimated() {
return this->counterEstimated;
}
int8_t AmsData::getLastError() {
return lastErrorCount > 2 ? lastError : 0;
}

View File

@ -1,3 +1,9 @@
/**
* @copyright Utilitech AS 2023
* License: Fair Source
*
*/
#ifndef _AMSDATASTORAGE_H
#define _AMSDATASTORAGE_H
#include "Arduino.h"

View File

@ -1,3 +1,9 @@
/**
* @copyright Utilitech AS 2023
* License: Fair Source
*
*/
#include "AmsDataStorage.h"
#include <lwip/apps/sntp.h>
#include "LittleFS.h"
@ -578,7 +584,6 @@ bool AmsDataStorage::isHappy() {
bool AmsDataStorage::isDayHappy() {
if(tz == NULL) {
if(debugger->isActive(RemoteDebug::VERBOSE)) debugger->printf_P(PSTR("(AmsDataStorage) Timezone is missing\n"));
return false;
}
@ -586,11 +591,9 @@ bool AmsDataStorage::isDayHappy() {
if(now < FirmwareVersion::BuildEpoch) return false;
if(now < 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;
}
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;
}
@ -598,7 +601,6 @@ bool AmsDataStorage::isDayHappy() {
breakTime(tz->toLocal(now), tm);
breakTime(tz->toLocal(day.lastMeterReadTime), last);
if(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;
}
@ -607,7 +609,6 @@ bool AmsDataStorage::isDayHappy() {
bool AmsDataStorage::isMonthHappy() {
if(tz == NULL) {
if(debugger->isActive(RemoteDebug::VERBOSE)) debugger->printf_P(PSTR("(AmsDataStorage) Timezone is missing\n"));
return false;
}
@ -616,19 +617,16 @@ bool AmsDataStorage::isMonthHappy() {
tmElements_t tm, last;
if(now < 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_P(PSTR("(AmsDataStorage) Month data day of last timestamp %d > %d\n"), tm.Day, last.Day);
return false;
}
if(now-month.lastMeterReadTime > 90100) {
if(debugger->isActive(RemoteDebug::VERBOSE)) debugger->printf("(AmsDataStorage) Month %lu - %lu > 3600\n", (int32_t) now, (int32_t) month.lastMeterReadTime);
return false;
}

View File

@ -1,3 +1,9 @@
/**
* @copyright Utilitech AS 2023
* License: Fair Source
*
*/
#ifndef _COSEM_H
#define _COSEM_H

View File

@ -1,3 +1,9 @@
/**
* @copyright Utilitech AS 2023
* License: Fair Source
*
*/
#ifndef _DATAPASERSER_H
#define _DATAPASERSER_H
@ -10,6 +16,10 @@
#define DATA_TAG_MBUS 0x68
#define DATA_TAG_GBT 0xE0
#define DATA_TAG_GCM 0xDB
#define DATA_TAG_SNRM 0x81
#define DATA_TAG_AARQ 0x60
#define DATA_TAG_AARE 0x61
#define DATA_TAG_RES 0xC4 // Get Response
#define DATA_PARSE_OK 0
#define DATA_PARSE_FAIL -1

View File

@ -1,3 +1,9 @@
/**
* @copyright Utilitech AS 2023
* License: Fair Source
*
*/
#ifndef _DATAPASERSERS_H
#define _DATAPASERSERS_H

View File

@ -1,3 +1,9 @@
/**
* @copyright Utilitech AS 2023
* License: Fair Source
*
*/
#ifndef _DLMSPARSER_H
#define _DLMSPARSER_H

View File

@ -1,3 +1,9 @@
/**
* @copyright Utilitech AS 2023
* License: Fair Source
*
*/
#ifndef _DSMRPARSER_H
#define _DSMRPARSER_H

View File

@ -1,3 +1,9 @@
/**
* @copyright Utilitech AS 2023
* License: Fair Source
*
*/
#ifndef _GBTPARSER_H
#define _GBTPARSER_H

View File

@ -1,3 +1,9 @@
/**
* @copyright Utilitech AS 2023
* License: Fair Source
*
*/
#ifndef _GCMPARSER_H
#define _GCMPARSER_H

View File

@ -1,3 +1,9 @@
/**
* @copyright Utilitech AS 2023
* License: Fair Source
*
*/
#ifndef _HDLCPARSER_H
#define _HDLCPARSER_H

View File

@ -1,3 +1,9 @@
/**
* @copyright Utilitech AS 2023
* License: Fair Source
*
*/
#ifndef _LLCPARSER_H
#define _LLCPARSER_H

View File

@ -1,3 +1,9 @@
/**
* @copyright Utilitech AS 2023
* License: Fair Source
*
*/
#ifndef _MBUSPARSER_H
#define _MBUSPARSER_H

View File

@ -1,3 +1,9 @@
/**
* @copyright Utilitech AS 2023
* License: Fair Source
*
*/
#ifndef _CRC_H
#define _CRC_H

View File

@ -1,3 +1,9 @@
/**
* @copyright Utilitech AS 2023
* License: Fair Source
*
*/
#ifndef _NTOHLL_H
#define _NTOHLL_H

View File

@ -1,3 +1,9 @@
/**
* @copyright Utilitech AS 2023
* License: Fair Source
*
*/
#include "Cosem.h"
#include "lwip/def.h"
#include <TimeLib.h>

View File

@ -1,3 +1,9 @@
/**
* @copyright Utilitech AS 2023
* License: Fair Source
*
*/
#include "DlmsParser.h"
#include "Cosem.h"

View File

@ -1,3 +1,9 @@
/**
* @copyright Utilitech AS 2023
* License: Fair Source
*
*/
#include "DsmrParser.h"
#include "crc.h"
#include "hexutils.h"

View File

@ -1,3 +1,9 @@
/**
* @copyright Utilitech AS 2023
* License: Fair Source
*
*/
#include "GbtParser.h"
#include "lwip/def.h"

View File

@ -1,3 +1,9 @@
/**
* @copyright Utilitech AS 2023
* License: Fair Source
*
*/
#include "GcmParser.h"
#include "lwip/def.h"
#if defined(ESP8266)

View File

@ -1,3 +1,9 @@
/**
* @copyright Utilitech AS 2023
* License: Fair Source
*
*/
#include "HdlcParser.h"
#include "lwip/def.h"
#include "crc.h"
@ -49,7 +55,10 @@ int8_t HDLCParser::parse(uint8_t *d, DataParserContext &ctx) {
ptr += 3;
// Exclude all of header and 3 byte footer
ctx.length -= ptr-d+3;
ctx.length -= ptr-d;
if(ctx.length > 1) {
ctx.length -= 3;
}
return ptr-d;
}
return DATA_PARSE_UNKNOWN_DATA;

View File

@ -1,3 +1,9 @@
/**
* @copyright Utilitech AS 2023
* License: Fair Source
*
*/
#include "LlcParser.h"
int8_t LLCParser::parse(uint8_t *buf, DataParserContext &ctx) {

View File

@ -1,3 +1,9 @@
/**
* @copyright Utilitech AS 2023
* License: Fair Source
*
*/
#include "MbusParser.h"
int8_t MBUSParser::parse(uint8_t *d, DataParserContext &ctx) {

View File

@ -1,3 +1,9 @@
/**
* @copyright Utilitech AS 2023
* License: Fair Source
*
*/
#include "crc.h"
uint16_t crc16_x25(const uint8_t* p, int len)

View File

@ -1,3 +1,9 @@
/**
* @copyright Utilitech AS 2023
* License: Fair Source
*
*/
#include "ntohll.h"
uint64_t ntohll(uint64_t x) {

View File

@ -0,0 +1,66 @@
/**
* @copyright Utilitech AS 2023
* License: Fair Source
*
*/
#ifndef _AMSMQTTHANDLER_H
#define _AMSMQTTHANDLER_H
#include "Arduino.h"
#include <MQTT.h>
#include "AmsData.h"
#include "AmsConfiguration.h"
#include "EnergyAccounting.h"
#include "HwTools.h"
#include "EntsoeApi.h"
#if defined(ESP32)
#include <esp_task_wdt.h>
#endif
class AmsMqttHandler {
public:
AmsMqttHandler(MqttConfig& mqttConfig, RemoteDebug* debugger, char* buf) {
this->mqttConfig = mqttConfig;
this->debugger = debugger;
this->json = buf;
mqtt.dropOverflow(true);
};
void setCaVerification(bool);
bool connect();
void disconnect();
lwmqtt_err_t lastError();
bool connected();
bool loop();
virtual uint8_t getFormat() { return 0; };
virtual bool publish(AmsData* data, AmsData* previousState, EnergyAccounting* ea, EntsoeApi* eapi) { return false; };
virtual bool publishTemperatures(AmsConfiguration*, HwTools*) { return false; };
virtual bool publishPrices(EntsoeApi* eapi) { return false; };
virtual bool publishSystem(HwTools*, EntsoeApi*, EnergyAccounting*) { return false; };
virtual bool publishRaw(String data) { return false; };
virtual ~AmsMqttHandler() {
if(mqttClient != NULL) {
mqttClient->stop();
delete mqttClient;
}
};
protected:
RemoteDebug* debugger;
MqttConfig mqttConfig;
MQTTClient mqtt = MQTTClient(256);
unsigned long lastMqttRetry = -10000;
bool caVerification = true;
WiFiClient *mqttClient = NULL;
WiFiClientSecure *mqttSecureClient = NULL;
char* json;
uint16_t BufferSize = 2048;
};
#endif

View File

@ -0,0 +1,171 @@
/**
* @copyright Utilitech AS 2023
* License: Fair Source
*
*/
#include "AmsMqttHandler.h"
#include "FirmwareVersion.h"
#include "AmsStorage.h"
#include "LittleFS.h"
void AmsMqttHandler::setCaVerification(bool caVerification) {
this->caVerification = caVerification;
}
bool AmsMqttHandler::connect() {
if(millis() - lastMqttRetry < 10000) {
yield();
return false;
}
lastMqttRetry = millis();
time_t epoch = time(nullptr);
if(mqttConfig.ssl) {
if(epoch < FirmwareVersion::BuildEpoch) {
if(debugger->isActive(RemoteDebug::INFO)) debugger->printf_P(PSTR("NTP not ready for MQTT SSL\n"));
return false;
}
if(debugger->isActive(RemoteDebug::INFO)) debugger->printf_P(PSTR("MQTT SSL is configured (%dkb free heap)\n"), ESP.getFreeHeap());
if(mqttSecureClient == NULL) {
mqttSecureClient = new WiFiClientSecure();
#if defined(ESP8266)
mqttSecureClient->setBufferSizes(512, 512);
if(debugger->isActive(RemoteDebug::DEBUG)) debugger->printf_P(PSTR("ESP8266 firmware does not have enough memory...\n"));
return false;
#endif
if(caVerification && LittleFS.begin()) {
File file;
if(LittleFS.exists(FILE_MQTT_CA)) {
if(debugger->isActive(RemoteDebug::INFO)) debugger->printf_P(PSTR("Found MQTT CA file (%dkb free heap)\n"), ESP.getFreeHeap());
file = LittleFS.open(FILE_MQTT_CA, (char*) "r");
#if defined(ESP8266)
BearSSL::X509List *serverTrustedCA = new BearSSL::X509List(file);
mqttSecureClient->setTrustAnchors(serverTrustedCA);
#elif defined(ESP32)
if(mqttSecureClient->loadCACert(file, file.size())) {
if(debugger->isActive(RemoteDebug::INFO)) debugger->printf_P(PSTR("CA accepted\n"));
} else {
if(debugger->isActive(RemoteDebug::WARNING)) debugger->printf_P(PSTR("CA was rejected\n"));
delete mqttSecureClient;
mqttSecureClient = NULL;
return false;
}
#endif
file.close();
if(LittleFS.exists(FILE_MQTT_CERT) && LittleFS.exists(FILE_MQTT_KEY)) {
#if defined(ESP8266)
if(debugger->isActive(RemoteDebug::INFO)) debugger->printf_P(PSTR("Found MQTT certificate file (%dkb free heap)\n"), ESP.getFreeHeap());
file = LittleFS.open(FILE_MQTT_CERT, (char*) "r");
BearSSL::X509List *serverCertList = new BearSSL::X509List(file);
file.close();
if(debugger->isActive(RemoteDebug::INFO)) debugger->printf_P(PSTR("Found MQTT key file (%dkb free heap)\n"), ESP.getFreeHeap());
file = LittleFS.open(FILE_MQTT_KEY, (char*) "r");
BearSSL::PrivateKey *serverPrivKey = new BearSSL::PrivateKey(file);
file.close();
if(debugger->isActive(RemoteDebug::DEBUG)) debugger->printf_P(PSTR("Setting client certificates (%dkb free heap)"), ESP.getFreeHeap());
mqttSecureClient->setClientRSACert(serverCertList, serverPrivKey);
#elif defined(ESP32)
if(debugger->isActive(RemoteDebug::INFO)) debugger->printf_P(PSTR("Found MQTT certificate file (%dkb free heap)\n"), ESP.getFreeHeap());
file = LittleFS.open(FILE_MQTT_CERT, (char*) "r");
mqttSecureClient->loadCertificate(file, file.size());
file.close();
if(debugger->isActive(RemoteDebug::INFO)) debugger->printf_P(PSTR("Found MQTT key file (%dkb free heap)\n"), ESP.getFreeHeap());
file = LittleFS.open(FILE_MQTT_KEY, (char*) "r");
mqttSecureClient->loadPrivateKey(file, file.size());
file.close();
#endif
}
} else {
if(debugger->isActive(RemoteDebug::INFO)) debugger->printf_P(PSTR("No CA, disabling validation\n"));
mqttSecureClient->setInsecure();
}
LittleFS.end();
} else {
if(debugger->isActive(RemoteDebug::INFO)) debugger->printf_P(PSTR("CA verification disabled\n"));
mqttSecureClient->setInsecure();
}
mqttClient = mqttSecureClient;
if(debugger->isActive(RemoteDebug::DEBUG)) debugger->printf_P(PSTR("MQTT SSL setup complete (%dkb free heap)\n"), ESP.getFreeHeap());
}
}
if(mqttClient == NULL) {
if(debugger->isActive(RemoteDebug::INFO)) debugger->printf_P(PSTR("No SSL, using client without SSL support\n"));
mqttClient = new WiFiClient();
}
if(debugger->isActive(RemoteDebug::INFO)) debugger->printf_P(PSTR("Connecting to MQTT %s:%d\n"), mqttConfig.host, mqttConfig.port);
mqtt.begin(mqttConfig.host, mqttConfig.port, *mqttClient);
#if defined(ESP8266)
if(mqttSecureClient) {
time_t epoch = time(nullptr);
if(debugger->isActive(RemoteDebug::DEBUG)) debugger->printf_P(PSTR("Setting NTP time %lu for secure MQTT connection\n"), epoch);
mqttSecureClient->setX509Time(epoch);
}
#endif
// Connect to a unsecure or secure MQTT server
if ((strlen(mqttConfig.username) == 0 && mqtt.connect(mqttConfig.clientId)) ||
(strlen(mqttConfig.username) > 0 && mqtt.connect(mqttConfig.clientId, mqttConfig.username, mqttConfig.password))) {
if(debugger->isActive(RemoteDebug::INFO)) debugger->printf_P(PSTR("Successfully connected to MQTT!\n"));
return true;
} else {
if (debugger->isActive(RemoteDebug::ERROR)) {
debugger->printf_P(PSTR("Failed to connect to MQTT: %d\n"), mqtt.lastError());
#if defined(ESP8266)
if(mqttSecureClient) {
mqttSecureClient->getLastSSLError((char*) json, BufferSize);
debugger->println((char*) json);
}
#endif
}
return false;
}
}
void AmsMqttHandler::disconnect() {
mqtt.disconnect();
mqtt.loop();
delay(10);
yield();
if(mqttClient != NULL) {
mqttClient->stop();
delete mqttClient;
mqttClient = NULL;
if(mqttSecureClient != NULL) {
mqttSecureClient = NULL;
}
}
}
lwmqtt_err_t AmsMqttHandler::lastError() {
return mqtt.lastError();
}
bool AmsMqttHandler::connected() {
return mqtt.connected();
}
bool AmsMqttHandler::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

@ -1,3 +1,9 @@
/**
* @copyright Utilitech AS 2023
* License: Fair Source
*
*/
#ifndef _CLOUDCONNECTOR_H
#define _CLOUDCONNECTOR_H
@ -11,18 +17,28 @@
#include "mbedtls/error.h"
#include "mbedtls/certs.h"
#include "mbedtls/rsa.h"
#include "AmsConfiguration.h"
#include "AmsData.h"
#include "EnergyAccounting.h"
#include "HwTools.h"
#if defined(ESP8266)
#include <ESP8266HTTPClient.h>
#elif defined(ESP32) // ARDUINO_ARCH_ESP32
#include <HTTPClient.h>
#include <esp_wifi.h>
#include <esp_task_wdt.h>
#include <WiFiUdp.h>
#else
#warning "Unsupported board type"
#endif
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";
#define CC_BUF_SIZE 1024
//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};
static const char CC_JSON_POWER[] PROGMEM = ",\"%s\":{\"P\":%lu,\"Q\":%lu}";
static const char CC_JSON_POWER_LIST3[] PROGMEM = ",\"%s\":{\"P\":%lu,\"Q\":%lu,\"tP\":%.3f,\"tQ\":%.3f}";
static const char CC_JSON_PHASE[] PROGMEM = "%s\"%d\":{\"u\":%.2f,\"i\":%.2f}";
static const char CC_JSON_PHASE_LIST4[] PROGMEM = "%s\"%d\":{\"u\":%.2f,\"i\":%.2f,\"Pim\":%lu,\"Pex\":%lu,\"pf\":%.2f}";
struct CloudData {
uint8_t type;
@ -32,16 +48,41 @@ struct CloudData {
class CloudConnector {
public:
CloudConnector(RemoteDebug*);
void setup(const unsigned char * key);
void send();
void setup(CloudConfig& config, HwTools* hw);
void update(AmsData& data, EnergyAccounting& ea);
void forceUpdate();
private:
RemoteDebug* debugger;
HwTools* hw;
CloudConfig config;
HTTPClient http;
WiFiUDP udp;
bool initialized = false;
unsigned long lastUpdate = 0;
char mac[18];
unsigned char buf[4096];
char clearBuffer[CC_BUF_SIZE];
unsigned char encryptedBuffer[256];
mbedtls_rsa_context* rsa = nullptr;
mbedtls_ctr_drbg_context ctr_drbg;
mbedtls_entropy_context entropy;
char* pers = "amsreader";
bool init();
void debugPrint(byte *buffer, int start, int length);
String meterManufacturer(uint8_t type) {
switch(type) {
case AmsTypeAidon: return F("Aidon");
case AmsTypeKaifa: return F("Kaifa");
case AmsTypeKamstrup: return F("Kamstrup");
case AmsTypeIskra: return F("Iskra");
case AmsTypeLandisGyr: return F("Landis+Gyr");
case AmsTypeSagemcom: return F("Sagemcom");
}
return F("");
}
};
#endif

View File

@ -1,44 +1,223 @@
/**
* @copyright Utilitech AS 2023
* License: Fair Source
*
*/
#include "CloudConnector.h"
#include "FirmwareVersion.h"
#include "crc.h"
#include "Uptime.h"
#include "hexutils.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();
http.setFollowRedirects(HTTPC_STRICT_FOLLOW_REDIRECTS);
http.setReuse(false);
http.setTimeout(60000);
http.setUserAgent("ams2mqtt/" + String(FirmwareVersion::VersionString));
http.useHTTP10(true);
uint8_t mac[6];
#if defined(ESP8266)
wifi_get_macaddr(STATION_IF, mac);
#elif defined(ESP32)
esp_wifi_get_mac((wifi_interface_t)ESP_IF_WIFI_STA, mac);
#endif
sprintf(this->mac, "%02X:%02X:%02X:%02X:%02X:%02X", mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]);
}
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::setup(CloudConfig& config, HwTools* hw) {
this->config = config;
this->hw = hw;
}
bool CloudConnector::init() {
if(config.enabled && strlen(config.hostname) > 0 && config.port > 0) {
snprintf_P(clearBuffer, CC_BUF_SIZE, PSTR("http://%s/hub/cloud/public.key"), config.hostname);
if(debugger->isActive(RemoteDebug::INFO)) debugger->printf("(CloudConnector) Downloading public key from %s\n", clearBuffer);
#if defined(ESP8266)
WiFiClient client;
client.setTimeout(5000);
if(http.begin(client, clearBuffer)) {
#elif defined(ESP32)
if(http.begin(clearBuffer)) {
#endif
int status = http.GET();
#if defined(ESP32)
esp_task_wdt_reset();
#elif defined(ESP8266)
ESP.wdtFeed();
#endif
if(status == HTTP_CODE_OK) {
String pub = http.getString();
http.end();
memset(clearBuffer, 0, CC_BUF_SIZE);
snprintf(clearBuffer, CC_BUF_SIZE, pub.c_str());
if(debugger->isActive(RemoteDebug::DEBUG)) debugger->printf("Cloud public key:\n%s\n", clearBuffer);
mbedtls_pk_context pk;
mbedtls_pk_init(&pk);
int error_code = 0;
if((error_code = mbedtls_pk_parse_public_key(&pk, (unsigned char*) clearBuffer, strlen((const char*) clearBuffer)+1)) == 0){
if(debugger->isActive(RemoteDebug::DEBUG)) debugger->printf("RSA public key OK\n");
rsa = mbedtls_pk_rsa(pk);
mbedtls_ctr_drbg_init(&ctr_drbg);
mbedtls_entropy_init(&entropy);
int ret = mbedtls_ctr_drbg_seed(&ctr_drbg, mbedtls_entropy_func,
&entropy, (const unsigned char *) pers,
strlen(pers));
if(ret != 0) {
if(debugger->isActive(RemoteDebug::ERROR)) debugger->printf("mbedtls_ctr_drbg_seed return code: %d\n", ret);
}
return ret == 0;
} else {
if(debugger->isActive(RemoteDebug::ERROR)) debugger->printf("RSA public key read error: ");
mbedtls_strerror(error_code, clearBuffer, CC_BUF_SIZE);
if(debugger->isActive(RemoteDebug::ERROR)) debugger->printf("%s\n", clearBuffer);
}
} else {
if(debugger->isActive(RemoteDebug::ERROR)) debugger->printf_P(PSTR("(CloudConnector) 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;
}
void CloudConnector::update(AmsData& data, EnergyAccounting& ea) {
if(!config.enabled || strlen(config.hostname) == 0 || config.port == 0) return;
unsigned long now = millis();
if(now-lastUpdate < config.interval*1000) return;
lastUpdate = now;
if(strlen(config.clientId) == 0 || strlen(config.clientSecret) == 0) {
if(debugger->isActive(RemoteDebug::WARNING)) debugger->printf("(CloudConnector) Client ID and secret is missing\n");
return;
}
if(data.getListType() < 2) return;
memset(clearBuffer, 0, CC_BUF_SIZE);
int pos = 0;
if(initialized) {
pos += snprintf_P(clearBuffer+pos, CC_BUF_SIZE-pos, PSTR("{\"id\":\"%s\",\"secret\":\"%s\",\"data\":{\"clock\":%lu,\"up\":%lu,\"lastUpdate\":%lu,\"est\":%s"),
config.clientId,
config.clientSecret,
(uint32_t) time(nullptr),
(uint32_t) (millis64()/1000),
(uint32_t) (data.getLastUpdateMillis()/1000),
data.isCounterEstimated() ? "true" : "false"
);
if(data.getListType() > 2) {
pos += snprintf_P(clearBuffer+pos, CC_BUF_SIZE-pos, CC_JSON_POWER_LIST3, "import", data.getActiveImportPower(), data.getReactiveImportPower(), data.getActiveImportCounter(), data.getReactiveImportCounter());
} else {
pos += snprintf_P(clearBuffer+pos, CC_BUF_SIZE-pos, CC_JSON_POWER, "import", data.getActiveImportPower(), data.getReactiveImportPower());
}
if(data.getListType() > 2) {
pos += snprintf_P(clearBuffer+pos, CC_BUF_SIZE-pos, CC_JSON_POWER_LIST3, "export", data.getActiveExportPower(), data.getReactiveExportPower(), data.getActiveExportCounter(), data.getReactiveExportCounter());
} else {
pos += snprintf_P(clearBuffer+pos, CC_BUF_SIZE-pos, CC_JSON_POWER, "export", data.getActiveExportPower(), data.getReactiveExportPower());
}
if(data.getListType() > 1) {
pos += snprintf_P(clearBuffer+pos, CC_BUF_SIZE-pos, PSTR(",\"phases\":{"));
bool first = true;
if(data.getL1Voltage() > 0.0) {
if(data.getListType() > 3) {
pos += snprintf_P(clearBuffer+pos, CC_BUF_SIZE-pos, CC_JSON_PHASE_LIST4, first ? "" : ",", 1, data.getL1Voltage(), data.getL1Current(), data.getL1ActiveImportPower(), data.getL1ActiveExportPower(), data.getL1PowerFactor());
} else {
pos += snprintf_P(clearBuffer+pos, CC_BUF_SIZE-pos, CC_JSON_PHASE, first ? "" : ",", 1, data.getL1Voltage(), data.getL1Current());
}
first = false;
}
if(data.getL2Voltage() > 0.0) {
if(data.getListType() > 3) {
pos += snprintf_P(clearBuffer+pos, CC_BUF_SIZE-pos, CC_JSON_PHASE_LIST4, first ? "" : ",", 2, data.getL2Voltage(), data.getL2Current(), data.getL2ActiveImportPower(), data.getL2ActiveExportPower(), data.getL2PowerFactor());
} else {
pos += snprintf_P(clearBuffer+pos, CC_BUF_SIZE-pos, CC_JSON_PHASE, first ? "" : ",", 2, data.getL2Voltage(), data.getL2Current());
}
first = false;
}
if(data.getL3Voltage() > 0.0) {
if(data.getListType() > 3) {
pos += snprintf_P(clearBuffer+pos, CC_BUF_SIZE-pos, CC_JSON_PHASE_LIST4, first ? "" : ",", 3, data.getL3Voltage(), data.getL3Current(), data.getL3ActiveImportPower(), data.getL3ActiveExportPower(), data.getL3PowerFactor());
} else {
pos += snprintf_P(clearBuffer+pos, CC_BUF_SIZE-pos, CC_JSON_PHASE, first ? "" : ",", 3, data.getL3Voltage(), data.getL3Current());
}
first = false;
}
pos += snprintf_P(clearBuffer+pos, CC_BUF_SIZE-pos, PSTR("}"));
}
if(data.getListType() > 3) {
pos += snprintf_P(clearBuffer+pos, CC_BUF_SIZE-pos, PSTR(",\"pf\":%.2f"), data.getPowerFactor());
}
pos += snprintf_P(clearBuffer+pos, CC_BUF_SIZE-pos, PSTR(",\"realtime\":{\"import\":%.3f,\"export\":%.3f}"), ea.getUseThisHour(), ea.getProducedThisHour());
pos += snprintf_P(clearBuffer+pos, CC_BUF_SIZE-pos, PSTR(",\"vcc\":%.2f,\"temp\":%.2f,\"rssi\":%d,\"free\":%d"), hw->getVcc(), hw->getTemperature(), hw->getWifiRssi(), ESP.getFreeHeap());
pos += snprintf_P(clearBuffer+pos, CC_BUF_SIZE-pos, PSTR("}"));
} else {
if(!init()) {
if(debugger->isActive(RemoteDebug::WARNING)) debugger->printf("Unable to initialize cloud connector\n");
return;
}
pos += snprintf_P(clearBuffer+pos, CC_BUF_SIZE-pos, PSTR("{\"id\":\"%s\",\"secret\":\"%s\",\"init\":{\"mac\":\"%s\",\"version\":\"%s\"},\"meter\":{\"manufacturer\":\"%s\",\"model\":\"%s\",\"id\":\"%s\"}"),
config.clientId,
config.clientSecret,
mac,
FirmwareVersion::VersionString,
meterManufacturer(data.getMeterType()).c_str(),
data.getMeterModel().c_str(),
data.getMeterId().c_str()
);
initialized = true;
}
uint16_t crc = crc16((uint8_t*) clearBuffer, pos);
pos += snprintf_P(clearBuffer+pos, CC_BUF_SIZE-pos, PSTR(",\"crc\":\"%04X\"}"), crc);
if(rsa == nullptr) return;
int ret = mbedtls_rsa_check_pubkey(rsa);
if(ret != 0) {
if(debugger->isActive(RemoteDebug::ERROR)) debugger->printf("mbedtls_rsa_pkcs1_encrypt return code: %d\n", ret);
mbedtls_strerror(ret, clearBuffer, CC_BUF_SIZE);
if(debugger->isActive(RemoteDebug::ERROR)) debugger->printf("%s\n", clearBuffer);
return;
}
memset(encryptedBuffer, 0, rsa->len);
int maxlen = 100 * (rsa->len/128);
if(debugger->isActive(RemoteDebug::DEBUG)) debugger->printf("(CloudConnector) Sending %d bytes and maxlen %d\n", pos, maxlen);
udp.beginPacket(config.hostname,7443);
for(int i = 0; i < pos; i += maxlen) {
int size = min(maxlen, pos-i);
int ret = mbedtls_rsa_pkcs1_encrypt(rsa, mbedtls_ctr_drbg_random, &ctr_drbg, MBEDTLS_RSA_PUBLIC, size, (unsigned char*) (clearBuffer+i), encryptedBuffer);
if(ret == 0) {
udp.write(encryptedBuffer, rsa->len);
delay(1);
} else {
if(debugger->isActive(RemoteDebug::ERROR)) debugger->printf("mbedtls_rsa_pkcs1_encrypt return code: %d\n", ret);
mbedtls_strerror(ret, clearBuffer, CC_BUF_SIZE);
if(debugger->isActive(RemoteDebug::ERROR)) debugger->printf("%s\n", clearBuffer);
}
}
udp.endPacket();
}
void CloudConnector::forceUpdate() {
lastUpdate = 0;
}
void CloudConnector::debugPrint(byte *buffer, int start, int length) {
for (int i = start; i < start + length; i++) {
if (buffer[i] < 0x10)

View File

@ -1,3 +1,9 @@
/**
* @copyright Utilitech AS 2023
* License: Fair Source
*
*/
#ifndef _DOMOTICZMQTTHANDLER_H
#define _DOMOTICZMQTTHANDLER_H
@ -6,13 +12,16 @@
class DomoticzMqttHandler : public AmsMqttHandler {
public:
DomoticzMqttHandler(MQTTClient* mqtt, char* buf, DomoticzConfig config) : AmsMqttHandler(mqtt, buf) {
DomoticzMqttHandler(MqttConfig& mqttConfig, RemoteDebug* debugger, char* buf, DomoticzConfig config) : AmsMqttHandler(mqttConfig, debugger, buf) {
this->config = config;
};
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);
bool publishRaw(String data);
uint8_t getFormat();
private:
DomoticzConfig config;

View File

@ -1,3 +1,9 @@
/**
* @copyright Utilitech AS 2023
* License: Fair Source
*
*/
#include "DomoticzMqttHandler.h"
#include "json/domoticz_json.h"
@ -14,7 +20,7 @@ bool DomoticzMqttHandler::publish(AmsData* data, AmsData* previousState, EnergyA
config.elidx,
val
);
ret = mqtt->publish(F("domoticz/in"), json);
ret = mqtt.publish(F("domoticz/in"), json);
}
}
@ -28,7 +34,7 @@ bool DomoticzMqttHandler::publish(AmsData* data, AmsData* previousState, EnergyA
config.vl1idx,
val
);
ret |= mqtt->publish(F("domoticz/in"), json);
ret |= mqtt.publish(F("domoticz/in"), json);
}
if (config.vl2idx > 0){
@ -38,7 +44,7 @@ bool DomoticzMqttHandler::publish(AmsData* data, AmsData* previousState, EnergyA
config.vl2idx,
val
);
ret |= mqtt->publish(F("domoticz/in"), json);
ret |= mqtt.publish(F("domoticz/in"), json);
}
if (config.vl3idx > 0){
@ -48,7 +54,7 @@ bool DomoticzMqttHandler::publish(AmsData* data, AmsData* previousState, EnergyA
config.vl3idx,
val
);
ret |= mqtt->publish(F("domoticz/in"), json);
ret |= mqtt.publish(F("domoticz/in"), json);
}
if (config.cl1idx > 0){
@ -58,7 +64,7 @@ bool DomoticzMqttHandler::publish(AmsData* data, AmsData* previousState, EnergyA
config.cl1idx,
val
);
ret |= mqtt->publish(F("domoticz/in"), json);
ret |= mqtt.publish(F("domoticz/in"), json);
}
return ret;
}
@ -74,3 +80,11 @@ bool DomoticzMqttHandler::publishPrices(EntsoeApi* eapi) {
bool DomoticzMqttHandler::publishSystem(HwTools* hw, EntsoeApi* eapi, EnergyAccounting* ea) {
return false;
}
uint8_t DomoticzMqttHandler::getFormat() {
return 3;
}
bool DomoticzMqttHandler::publishRaw(String data) {
return false;
}

View File

@ -1,3 +1,9 @@
/**
* @copyright Utilitech AS 2023
* License: Fair Source
*
*/
#ifndef _ENERGYACCOUNTING_H
#define _ENERGYACCOUNTING_H

View File

@ -1,3 +1,9 @@
/**
* @copyright Utilitech AS 2023
* License: Fair Source
*
*/
#include "EnergyAccounting.h"
#include "LittleFS.h"
#include "AmsStorage.h"

View File

@ -1,3 +1,9 @@
/**
* @copyright Utilitech AS 2023
* License: Fair Source
*
*/
#ifndef _DNBCURRPARSER_H
#define _DNBCURRPARSER_H

View File

@ -1,3 +1,9 @@
/**
* @copyright Utilitech AS 2023
* License: Fair Source
*
*/
#ifndef _ENTSOEA44PARSER_H
#define _ENTSOEA44PARSER_H

View File

@ -1,3 +1,9 @@
/**
* @copyright Utilitech AS 2023
* License: Fair Source
*
*/
#ifndef _ENTSOEAPI_H
#define _ENTSOEAPI_H

View File

@ -1,3 +1,9 @@
/**
* @copyright Utilitech AS 2023
* License: Fair Source
*
*/
#ifndef _PRICESCONTAINER_H
#define _PRICESCONTAINER_H
struct PricesContainer {

View File

@ -1,3 +1,9 @@
/**
* @copyright Utilitech AS 2023
* License: Fair Source
*
*/
#include "DnbCurrParser.h"
#include "Arduino.h"

View File

@ -1,3 +1,9 @@
/**
* @copyright Utilitech AS 2023
* License: Fair Source
*
*/
#include "EntsoeA44Parser.h"
#include "HardwareSerial.h"

View File

@ -1,3 +1,9 @@
/**
* @copyright Utilitech AS 2023
* License: Fair Source
*
*/
#include "EntsoeApi.h"
#include <EEPROM.h>
#include "Uptime.h"

View File

@ -1,3 +1,9 @@
/**
* @copyright Utilitech AS 2023
* License: Fair Source
*
*/
#ifndef _FIRMWARE_VERSION_h
#define _FIRMWARE_VERSION_h

View File

@ -1,3 +1,9 @@
/**
* @copyright Utilitech AS 2023
* License: Fair Source
*
*/
#include "FirmwareVersion.h"
#include "generated_version.h"

View File

@ -1,3 +1,9 @@
/**
* @copyright Utilitech AS 2023
* License: Fair Source
*
*/
#ifndef _HOMEASSISTANTMQTTHANDLER_H
#define _HOMEASSISTANTMQTTHANDLER_H
@ -7,12 +13,12 @@
class HomeAssistantMqttHandler : public AmsMqttHandler {
public:
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);
HomeAssistantMqttHandler(MqttConfig& mqttConfig, RemoteDebug* debugger, char* buf, uint8_t boardType, HomeAssistantConfig config, HwTools* hw) : AmsMqttHandler(mqttConfig, debugger, buf) {
this->hw = hw;
l1Init = l2Init = l2eInit = l3Init = l3eInit = l4Init = l4eInit = rtInit = rteInit = pInit = sInit = false;
topic = String(mqttConfig.publishTopic);
if(strlen(config.discoveryNameTag) > 0) {
snprintf_P(buf, 128, PSTR("AMS reader (%s)"), config.discoveryNameTag);
deviceName = String(buf);
@ -56,11 +62,13 @@ public:
bool publishTemperatures(AmsConfiguration*, HwTools*);
bool publishPrices(EntsoeApi*);
bool publishSystem(HwTools* hw, EntsoeApi* eapi, EnergyAccounting* ea);
bool publishRaw(String data);
protected:
bool loop();
uint8_t getFormat();
private:
String topic;
String deviceName;
String deviceModel;
String deviceUid;
@ -74,8 +82,6 @@ private:
bool tInit[32] = {false};
bool prInit[38] = {false};
String clientId;
String topic;
HwTools* hw;
bool publishList1(AmsData* data, EnergyAccounting* ea);

View File

@ -1,3 +1,9 @@
/**
* @copyright Utilitech AS 2023
* License: Fair Source
*
*/
#ifndef _HOMEASSISTANTSTATIC_H
#define _HOMEASSISTANTSTATIC_H

View File

@ -1,3 +1,9 @@
/**
* @copyright Utilitech AS 2023
* License: Fair Source
*
*/
#include "HomeAssistantMqttHandler.h"
#include "hexutils.h"
#include "Uptime.h"
@ -16,7 +22,7 @@
#endif
bool HomeAssistantMqttHandler::publish(AmsData* data, AmsData* previousState, EnergyAccounting* ea, EntsoeApi* eapi) {
if(topic.isEmpty() || !mqtt->connected())
if(topic.isEmpty() || !mqtt.connected())
return false;
if(data->getListType() >= 3) { // publish energy counts
@ -45,7 +51,7 @@ bool HomeAssistantMqttHandler::publishList1(AmsData* data, EnergyAccounting* ea)
snprintf_P(json, BufferSize, HA1_JSON,
data->getActiveImportPower()
);
return mqtt->publish(topic + "/power", json);
return mqtt.publish(topic + "/power", json);
}
bool HomeAssistantMqttHandler::publishList2(AmsData* data, EnergyAccounting* ea) {
@ -66,7 +72,7 @@ bool HomeAssistantMqttHandler::publishList2(AmsData* data, EnergyAccounting* ea)
data->getL2Voltage(),
data->getL3Voltage()
);
return mqtt->publish(topic + "/power", json);
return mqtt.publish(topic + "/power", json);
}
bool HomeAssistantMqttHandler::publishList3(AmsData* data, EnergyAccounting* ea) {
@ -79,7 +85,7 @@ bool HomeAssistantMqttHandler::publishList3(AmsData* data, EnergyAccounting* ea)
data->getReactiveExportCounter(),
data->getMeterTimestamp()
);
return mqtt->publish(topic + "/energy", json);
return mqtt.publish(topic + "/energy", json);
}
bool HomeAssistantMqttHandler::publishList4(AmsData* data, EnergyAccounting* ea) {
@ -110,7 +116,7 @@ bool HomeAssistantMqttHandler::publishList4(AmsData* data, EnergyAccounting* ea)
data->getPowerFactor() == 0 ? 1 : data->getL2PowerFactor(),
data->getPowerFactor() == 0 ? 1 : data->getL3PowerFactor()
);
return mqtt->publish(topic + "/power", json);
return mqtt.publish(topic + "/power", json);
}
String HomeAssistantMqttHandler::getMeterModel(AmsData* data) {
@ -146,7 +152,7 @@ bool HomeAssistantMqttHandler::publishRealtime(AmsData* data, EnergyAccounting*
ea->getProducedThisMonth(),
ea->getIncomeThisMonth()
);
return mqtt->publish(topic + "/realtime", json);
return mqtt.publish(topic + "/realtime", json);
}
@ -174,13 +180,13 @@ bool HomeAssistantMqttHandler::publishTemperatures(AmsConfiguration* config, HwT
}
char* pos = buf+strlen(buf);
snprintf_P(count == 0 ? pos : pos-1, 8, PSTR("}}"));
bool ret = mqtt->publish(topic + "/temperatures", buf);
bool ret = mqtt.publish(topic + "/temperatures", buf);
loop();
return ret;
}
bool HomeAssistantMqttHandler::publishPrices(EntsoeApi* eapi) {
if(topic.isEmpty() || !mqtt->connected())
if(topic.isEmpty() || !mqtt.connected())
return false;
if(eapi->getValueForHour(0) == ENTSOE_NO_VALUE)
return false;
@ -310,13 +316,13 @@ bool HomeAssistantMqttHandler::publishPrices(EntsoeApi* eapi) {
ts3hr,
ts6hr
);
bool ret = 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())
if(topic.isEmpty() || !mqtt.connected())
return false;
publishSystemSensors();
@ -324,14 +330,14 @@ bool HomeAssistantMqttHandler::publishSystem(HwTools* hw, EntsoeApi* eapi, Energ
snprintf_P(json, BufferSize, JSONSYS_JSON,
WiFi.macAddress().c_str(),
clientId.c_str(),
mqttConfig.clientId,
(uint32_t) (millis64()/1000),
hw->getVcc(),
hw->getWifiRssi(),
hw->getTemperature(),
FirmwareVersion::VersionString
);
bool ret = mqtt->publish(topic + "/state", json);
bool ret = mqtt.publish(topic + "/state", json);
loop();
return ret;
}
@ -345,7 +351,7 @@ void HomeAssistantMqttHandler::publishSensor(const HomeAssistantSensor& sensor)
snprintf_P(json, BufferSize, HADISCOVER_JSON,
sensorNamePrefix.c_str(),
sensor.name,
topic.c_str(), sensor.topic,
mqttConfig.publishTopic, sensor.topic,
deviceUid.c_str(), uid.c_str(),
deviceUid.c_str(), uid.c_str(),
sensor.uom,
@ -363,7 +369,7 @@ void HomeAssistantMqttHandler::publishSensor(const HomeAssistantSensor& sensor)
strlen_P(sensor.stacl) > 0 ? (char *) FPSTR(sensor.stacl) : "",
strlen_P(sensor.stacl) > 0 ? "\"" : ""
);
mqtt->publish(discoveryTopic + deviceUid + "_" + uid.c_str() + "/config", json, true, 0);
mqtt.publish(discoveryTopic + deviceUid + "_" + uid.c_str() + "/config", json, true, 0);
loop();
}
@ -540,14 +546,10 @@ void HomeAssistantMqttHandler::publishSystemSensors() {
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;
uint8_t HomeAssistantMqttHandler::getFormat() {
return 4;
}
bool HomeAssistantMqttHandler::publishRaw(String data) {
return false;
}

View File

@ -1,3 +1,9 @@
/**
* @copyright Utilitech AS 2023
* License: Fair Source
*
*/
#ifndef _HWTOOLS_H
#define _HWTOOLS_H
@ -48,6 +54,7 @@ public:
bool ledOn(uint8_t color);
bool ledOff(uint8_t color);
bool ledBlink(uint8_t color, uint8_t blink);
void setBootSuccessful();
HwTools() {};
private:
@ -64,6 +71,8 @@ private:
uint8_t sensorCount = 0;
TempSensorData** tempSensors = NULL;
bool bootSuccessful = false;
bool writeLedPin(uint8_t color, uint8_t state);
bool isSensorAddressEqual(uint8_t a[8], uint8_t b[8]);
void getAdcChannel(uint8_t pin, AdcConfig&);

View File

@ -1,3 +1,9 @@
/**
* @copyright Utilitech AS 2023
* License: Fair Source
*
*/
#include "HwTools.h"
void HwTools::setup(GpioConfig* config, AmsConfiguration* amsConf) {
@ -92,6 +98,22 @@ void HwTools::setup(GpioConfig* config, AmsConfiguration* amsConf) {
} else {
config->ledPinBlue = 0xFF;
}
if(config->ledDisablePin > 0 && config->ledDisablePin < 40) {
pinMode(config->ledDisablePin, OUTPUT_OPEN_DRAIN);
switch(config->ledBehaviour) {
case LED_BEHAVIOUR_ERROR_ONLY:
case LED_BEHAVIOUR_OFF:
digitalWrite(config->ledDisablePin, LOW);
break;
case LED_BEHAVIOUR_BOOT:
if(bootSuccessful) {
digitalWrite(config->ledDisablePin, LOW);
}
break;
default:
digitalWrite(config->ledDisablePin, HIGH);
}
}
}
void HwTools::getAdcChannel(uint8_t pin, AdcConfig& config) {
@ -358,8 +380,7 @@ float HwTools::getTemperature() {
}
for(int x = 0; x < sensorCount; x++) {
TempSensorData data = *tempSensors[x];
TempSensorConfig* conf = amsConf->getTempSensorConfig(data.address);
if((conf == NULL || conf->common) && data.lastValidRead > -85) {
if(data.lastValidRead > -85) {
ret += data.lastValidRead;
c++;
}
@ -380,7 +401,19 @@ int HwTools::getWifiRssi() {
return isnan(rssi) ? -100.0 : rssi;
}
void HwTools::setBootSuccessful() {
if(bootSuccessful) return;
bootSuccessful = true;
if(config->ledBehaviour != LED_BEHAVIOUR_DEFAULT) {
digitalWrite(config->ledDisablePin, LOW);
}
}
bool HwTools::ledOn(uint8_t color) {
if(config->ledBehaviour == LED_BEHAVIOUR_OFF) return false;
if(config->ledBehaviour == LED_BEHAVIOUR_ERROR_ONLY && color != LED_RED) return false;
if(config->ledBehaviour == LED_BEHAVIOUR_BOOT && color != LED_RED && bootSuccessful) return false;
if(color == LED_INTERNAL) {
return writeLedPin(color, config->ledInverted ? LOW : HIGH);
} else {

View File

@ -1,3 +1,9 @@
/**
* @copyright Utilitech AS 2023
* License: Fair Source
*
*/
#ifndef _JSONMQTTHANDLER_H
#define _JSONMQTTHANDLER_H
@ -5,22 +11,18 @@
class JsonMqttHandler : public AmsMqttHandler {
public:
JsonMqttHandler(MQTTClient* mqtt, char* buf, const char* clientId, const char* topic, HwTools* hw) : AmsMqttHandler(mqtt, buf) {
this->clientId = clientId;
this->topic = String(topic);
JsonMqttHandler(MqttConfig& mqttConfig, RemoteDebug* debugger, char* buf, HwTools* hw) : AmsMqttHandler(mqttConfig, debugger, buf) {
this->hw = hw;
};
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);
bool publishRaw(String data);
protected:
bool loop();
uint8_t getFormat();
private:
String clientId;
String topic;
HwTools* hw;
bool publishList1(AmsData* data, EnergyAccounting* ea);

View File

@ -1,3 +1,9 @@
/**
* @copyright Utilitech AS 2023
* License: Fair Source
*
*/
#include "JsonMqttHandler.h"
#include "FirmwareVersion.h"
#include "hexutils.h"
@ -10,11 +16,18 @@
#include "json/jsonprices_json.h"
bool JsonMqttHandler::publish(AmsData* data, AmsData* previousState, EnergyAccounting* ea, EntsoeApi* eapi) {
if(topic.isEmpty() || !mqtt->connected())
if(strlen(mqttConfig.publishTopic) == 0) {
if(debugger->isActive(RemoteDebug::DEBUG)) debugger->printf_P(PSTR("Unable to publish data, no publish topic\n"));
return false;
}
if(!mqtt.connected()) {
if(debugger->isActive(RemoteDebug::DEBUG)) debugger->printf_P(PSTR("Unable to publish data, not connected\n"));
return false;
}
bool ret = false;
if(debugger->isActive(RemoteDebug::DEBUG)) debugger->printf_P(PSTR("Publishing list ID %d!\n"), data->getListType());
if(data->getListType() == 1) {
ret = publishList1(data, ea);
} else if(data->getListType() == 2) {
@ -31,7 +44,7 @@ bool JsonMqttHandler::publish(AmsData* data, AmsData* previousState, EnergyAccou
bool JsonMqttHandler::publishList1(AmsData* data, EnergyAccounting* ea) {
snprintf_P(json, BufferSize, JSON1_JSON,
WiFi.macAddress().c_str(),
clientId.c_str(),
mqttConfig.clientId,
(uint32_t) (millis64()/1000),
data->getPackageTimestamp(),
hw->getVcc(),
@ -45,13 +58,13 @@ bool JsonMqttHandler::publishList1(AmsData* data, EnergyAccounting* ea) {
ea->getProducedThisHour(),
ea->getProducedToday()
);
return mqtt->publish(topic, json);
return mqtt.publish(mqttConfig.publishTopic, json);
}
bool JsonMqttHandler::publishList2(AmsData* data, EnergyAccounting* ea) {
snprintf_P(json, BufferSize, JSON2_JSON,
WiFi.macAddress().c_str(),
clientId.c_str(),
mqttConfig.clientId,
(uint32_t) (millis64()/1000),
data->getPackageTimestamp(),
hw->getVcc(),
@ -77,13 +90,13 @@ bool JsonMqttHandler::publishList2(AmsData* data, EnergyAccounting* ea) {
ea->getProducedThisHour(),
ea->getProducedToday()
);
return mqtt->publish(topic, json);
return mqtt.publish(mqttConfig.publishTopic, json);
}
bool JsonMqttHandler::publishList3(AmsData* data, EnergyAccounting* ea) {
snprintf_P(json, BufferSize, JSON3_JSON,
WiFi.macAddress().c_str(),
clientId.c_str(),
mqttConfig.clientId,
(uint32_t) (millis64()/1000),
data->getPackageTimestamp(),
hw->getVcc(),
@ -114,13 +127,13 @@ bool JsonMqttHandler::publishList3(AmsData* data, EnergyAccounting* ea) {
ea->getProducedThisHour(),
ea->getProducedToday()
);
return mqtt->publish(topic, json);
return mqtt.publish(mqttConfig.publishTopic, json);
}
bool JsonMqttHandler::publishList4(AmsData* data, EnergyAccounting* ea) {
snprintf_P(json, BufferSize, JSON4_JSON,
WiFi.macAddress().c_str(),
clientId.c_str(),
mqttConfig.clientId,
(uint32_t) (millis64()/1000),
data->getPackageTimestamp(),
hw->getVcc(),
@ -161,7 +174,7 @@ bool JsonMqttHandler::publishList4(AmsData* data, EnergyAccounting* ea) {
ea->getProducedThisHour(),
ea->getProducedToday()
);
return mqtt->publish(topic, json);
return mqtt.publish(mqttConfig.publishTopic, json);
}
String JsonMqttHandler::getMeterModel(AmsData* data) {
@ -191,13 +204,13 @@ bool JsonMqttHandler::publishTemperatures(AmsConfiguration* config, HwTools* hw)
}
char* pos = json+strlen(json);
snprintf_P(count == 0 ? pos : pos-1, 8, PSTR("}}"));
bool ret = mqtt->publish(topic, json);
bool ret = mqtt.publish(mqttConfig.publishTopic, json);
loop();
return ret;
}
bool JsonMqttHandler::publishPrices(EntsoeApi* eapi) {
if(topic.isEmpty() || !mqtt->connected())
if(strlen(mqttConfig.publishTopic) == 0 || !mqtt.connected())
return false;
if(eapi->getValueForHour(0) == ENTSOE_NO_VALUE)
return false;
@ -325,37 +338,33 @@ bool JsonMqttHandler::publishPrices(EntsoeApi* eapi) {
ts3hr,
ts6hr
);
bool ret = mqtt->publish(topic, json);
bool ret = mqtt.publish(mqttConfig.publishTopic, json);
loop();
return ret;
}
bool JsonMqttHandler::publishSystem(HwTools* hw, EntsoeApi* eapi, EnergyAccounting* ea) {
if(topic.isEmpty() || !mqtt->connected())
if(strlen(mqttConfig.publishTopic) == 0 || !mqtt.connected())
return false;
snprintf_P(json, BufferSize, JSONSYS_JSON,
WiFi.macAddress().c_str(),
clientId.c_str(),
mqttConfig.clientId,
(uint32_t) (millis64()/1000),
hw->getVcc(),
hw->getWifiRssi(),
hw->getTemperature(),
FirmwareVersion::VersionString
);
bool ret = mqtt->publish(topic, json);
bool ret = mqtt.publish(mqttConfig.publishTopic, 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;
uint8_t JsonMqttHandler::getFormat() {
return 0;
}
bool JsonMqttHandler::publishRaw(String data) {
return false;
}

View File

@ -1,3 +1,9 @@
/**
* @copyright Utilitech AS 2023
* License: Fair Source
*
*/
#ifndef _RAWMQTTHANDLER_H
#define _RAWMQTTHANDLER_H
@ -5,21 +11,21 @@
class RawMqttHandler : public AmsMqttHandler {
public:
RawMqttHandler(MQTTClient* mqtt, char* buf, const char* topic, bool full) : AmsMqttHandler(mqtt, buf) {
this->topic = String(topic);
this->full = full;
RawMqttHandler(MqttConfig& mqttConfig, RemoteDebug* debugger, char* buf) : AmsMqttHandler(mqttConfig, debugger, buf) {
full = mqttConfig.payloadFormat == 2;
topic = String(mqttConfig.publishTopic);
};
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);
bool publishRaw(String data);
protected:
bool loop();
uint8_t getFormat();
private:
String topic;
bool full;
String topic;
bool publishList1(AmsData* data, AmsData* meterState);
bool publishList2(AmsData* data, AmsData* meterState);

View File

@ -1,13 +1,19 @@
/**
* @copyright Utilitech AS 2023
* License: Fair Source
*
*/
#include "RawMqttHandler.h"
#include "hexutils.h"
#include "Uptime.h"
bool RawMqttHandler::publish(AmsData* data, AmsData* meterState, EnergyAccounting* ea, EntsoeApi* eapi) {
if(topic.isEmpty() || !mqtt->connected())
if(topic.isEmpty() || !mqtt.connected())
return false;
if(data->getPackageTimestamp() > 0) {
mqtt->publish(topic + "/meter/dlms/timestamp", String(data->getPackageTimestamp()));
mqtt.publish(topic + "/meter/dlms/timestamp", String(data->getPackageTimestamp()));
}
switch(data->getListType()) {
case 4:
@ -32,7 +38,7 @@ bool RawMqttHandler::publish(AmsData* data, AmsData* meterState, EnergyAccountin
bool RawMqttHandler::publishList1(AmsData* data, AmsData* meterState) {
if(full || meterState->getActiveImportPower() != data->getActiveImportPower()) {
mqtt->publish(topic + "/meter/import/active", String(data->getActiveImportPower()));
mqtt.publish(topic + "/meter/import/active", String(data->getActiveImportPower()));
}
return true;
}
@ -40,101 +46,101 @@ bool RawMqttHandler::publishList1(AmsData* data, AmsData* meterState) {
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());
mqtt.publish(topic + "/meter/id", data->getMeterId());
}
if(full || meterState->getMeterModel() != data->getMeterModel()) {
mqtt->publish(topic + "/meter/type", 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));
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));
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));
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));
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));
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));
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()));
mqtt.publish(topic + "/meter/export/reactive", String(data->getReactiveExportPower()));
}
if(full || meterState->getActiveExportPower() != data->getActiveExportPower()) {
mqtt->publish(topic + "/meter/export/active", String(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()));
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);
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));
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));
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));
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));
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));
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));
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));
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));
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));
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));
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));
mqtt.publish(topic + "/realtime/import/hour", String(ea->getUseThisHour(), 3));
mqtt.publish(topic + "/realtime/import/day", String(ea->getUseToday(), 2));
mqtt.publish(topic + "/realtime/import/month", String(ea->getUseThisMonth(), 1));
uint8_t peakCount = ea->getConfig()->hours;
if(peakCount > 5) peakCount = 5;
for(uint8_t i = 1; i <= peakCount; i++) {
mqtt->publish(topic + "/realtime/import/peak/" + String(i, 10), String(ea->getPeak(i).value / 100.0, 10), true, 0);
mqtt.publish(topic + "/realtime/import/peak/" + String(i, 10), String(ea->getPeak(i).value / 100.0, 10), true, 0);
}
mqtt->publish(topic + "/realtime/import/threshold", String(ea->getCurrentThreshold(), 10), true, 0);
mqtt->publish(topic + "/realtime/import/monthmax", String(ea->getMonthMax(), 3), true, 0);
mqtt->publish(topic + "/realtime/export/hour", String(ea->getProducedThisHour(), 3));
mqtt->publish(topic + "/realtime/export/day", String(ea->getProducedToday(), 2));
mqtt->publish(topic + "/realtime/export/month", String(ea->getProducedThisMonth(), 1));
mqtt.publish(topic + "/realtime/import/threshold", String(ea->getCurrentThreshold(), 10), true, 0);
mqtt.publish(topic + "/realtime/import/monthmax", String(ea->getMonthMax(), 3), true, 0);
mqtt.publish(topic + "/realtime/export/hour", String(ea->getProducedThisHour(), 3));
mqtt.publish(topic + "/realtime/export/day", String(ea->getProducedToday(), 2));
mqtt.publish(topic + "/realtime/export/month", String(ea->getProducedThisMonth(), 1));
return true;
}
@ -144,7 +150,7 @@ bool RawMqttHandler::publishTemperatures(AmsConfiguration* config, HwTools* hw)
TempSensorData* data = hw->getTempSensorData(i);
if(data != NULL && data->lastValidRead > -85) {
if(data->changed || full) {
mqtt->publish(topic + "/temperature/" + toHex(data->address), String(data->lastValidRead, 2));
mqtt.publish(topic + "/temperature/" + toHex(data->address), String(data->lastValidRead, 2));
data->changed = false;
}
}
@ -153,7 +159,7 @@ bool RawMqttHandler::publishTemperatures(AmsConfiguration* config, HwTools* hw)
}
bool RawMqttHandler::publishPrices(EntsoeApi* eapi) {
if(topic.isEmpty() || !mqtt->connected())
if(topic.isEmpty() || !mqtt.connected())
return false;
if(eapi->getValueForHour(0) == ENTSOE_NO_VALUE)
return false;
@ -236,58 +242,54 @@ bool RawMqttHandler::publishPrices(EntsoeApi* eapi) {
for(int i = 0; i < 34; i++) {
float val = values[i];
if(val == ENTSOE_NO_VALUE) {
mqtt->publish(topic + "/price/" + String(i), "", true, 0);
mqtt.publish(topic + "/price/" + String(i), "", true, 0);
} else {
mqtt->publish(topic + "/price/" + String(i), String(val, 4), true, 0);
mqtt.publish(topic + "/price/" + String(i), String(val, 4), true, 0);
}
mqtt->loop();
mqtt.loop();
delay(10);
}
if(min != INT16_MAX) {
mqtt->publish(topic + "/price/min", String(min, 4), true, 0);
mqtt.publish(topic + "/price/min", String(min, 4), true, 0);
}
if(max != INT16_MIN) {
mqtt->publish(topic + "/price/max", String(max, 4), true, 0);
mqtt.publish(topic + "/price/max", String(max, 4), true, 0);
}
if(min1hrIdx != -1) {
mqtt->publish(topic + "/price/cheapest/1hr", String(ts1hr), true, 0);
mqtt.publish(topic + "/price/cheapest/1hr", String(ts1hr), true, 0);
}
if(min3hrIdx != -1) {
mqtt->publish(topic + "/price/cheapest/3hr", String(ts3hr), true, 0);
mqtt.publish(topic + "/price/cheapest/3hr", String(ts3hr), true, 0);
}
if(min6hrIdx != -1) {
mqtt->publish(topic + "/price/cheapest/6hr", String(ts6hr), true, 0);
mqtt.publish(topic + "/price/cheapest/6hr", String(ts6hr), true, 0);
}
return true;
}
bool RawMqttHandler::publishSystem(HwTools* hw, EntsoeApi* eapi, EnergyAccounting* ea) {
if(topic.isEmpty() || !mqtt->connected())
if(topic.isEmpty() || !mqtt.connected())
return false;
mqtt->publish(topic + "/id", WiFi.macAddress(), true, 0);
mqtt->publish(topic + "/uptime", String((uint32_t) (millis64()/1000)));
mqtt.publish(topic + "/id", WiFi.macAddress(), true, 0);
mqtt.publish(topic + "/uptime", String((uint32_t) (millis64()/1000)));
float vcc = hw->getVcc();
if(vcc > 0) {
mqtt->publish(topic + "/vcc", String(vcc, 2));
mqtt.publish(topic + "/vcc", String(vcc, 2));
}
mqtt->publish(topic + "/mem", String(ESP.getFreeHeap()));
mqtt->publish(topic + "/rssi", String(hw->getWifiRssi()));
mqtt.publish(topic + "/mem", String(ESP.getFreeHeap()));
mqtt.publish(topic + "/rssi", String(hw->getWifiRssi()));
if(hw->getTemperature() > -85) {
mqtt->publish(topic + "/temperature", String(hw->getTemperature(), 2));
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;
uint8_t RawMqttHandler::getFormat() {
return full ? 3 : 2;
}
bool RawMqttHandler::publishRaw(String data) {
return false;
}

View File

@ -0,0 +1,31 @@
/**
* @copyright Utilitech AS 2023
* License: Fair Source
*
*/
#ifndef _REALTIMEPLOT_H
#define _REALTIMEPLOT_H
#include <stdint.h>
#include "AmsData.h"
#define REALTIME_SAMPLE 10000
#define REALTIME_SIZE 360
class RealtimePlot {
public:
RealtimePlot();
void update(AmsData& data);
int32_t getValue(uint16_t req);
int16_t getSize();
private:
int8_t* values;
uint8_t* scaling;
unsigned long lastMillis = 0;
double lastReading = 0;
uint16_t lastPos = 0;
};
#endif

View File

@ -0,0 +1,82 @@
/**
* @copyright Utilitech AS 2023
* License: Fair Source
*
*/
#include "RealtimePlot.h"
#include <stdlib.h>
RealtimePlot::RealtimePlot() {
values = (int8_t*) malloc(REALTIME_SIZE);
scaling = (uint8_t*) malloc(REALTIME_SIZE);
memset(values, 0, REALTIME_SIZE);
memset(scaling, 0, REALTIME_SIZE);
}
void RealtimePlot::update(AmsData& data) {
unsigned long now = millis();
uint16_t pos = (now / REALTIME_SAMPLE) % REALTIME_SIZE;
if(lastMillis == 0) {
lastMillis = now;
lastReading = data.getActiveImportCounter() - data.getActiveExportCounter();
lastPos = pos;
return;
}
if(pos == lastPos && data.isCounterEstimated()) return;
unsigned long ms = now - lastMillis;
int32_t val; // A bit hacky this one, but just to avoid spikes at end of hour. Will mostly be correct
if(data.isCounterEstimated()) {
val = ((data.getActiveImportCounter() - data.getActiveExportCounter() - lastReading) * 1000) / (((float) ms) / 3600000.0);
} else {
val = data.getActiveImportPower() - data.getActiveExportPower();
}
uint8_t scale = 0;
int32_t update = val / pow(10, scale);
while(update > INT8_MAX || update < INT8_MIN) {
update = val / pow(10, ++scale);
}
if(pos < lastPos) {
for(uint16_t i = lastPos+1; i < REALTIME_SIZE; i++) {
values[i] = update;
scaling[i] = scale;
}
for(uint16_t i = 0; i <= pos; i++) {
values[i] = update;
scaling[i] = scale;
}
} else {
for(uint16_t i = lastPos+1; i <= pos; i++) {
values[i] = update;
scaling[i] = scale;
}
}
lastMillis = now;
lastReading = data.getActiveImportCounter() - data.getActiveExportCounter();
lastPos = pos;
}
int32_t RealtimePlot::getValue(uint16_t req) {
if(req > REALTIME_SAMPLE) return 0;
unsigned long now = millis();
if(req * REALTIME_SAMPLE > now) return 0;
unsigned long reqTime = now - (req * REALTIME_SAMPLE);
uint16_t pos = (now / REALTIME_SAMPLE) % REALTIME_SIZE;
uint16_t getPos;
if(reqTime > lastMillis) {
getPos = lastPos;
} else if(req > pos) {
getPos = REALTIME_SIZE + pos - req;
} else {
getPos = pos - req;
}
return values[getPos] * pow(10, scaling[getPos]);
}
int16_t RealtimePlot::getSize() {
return REALTIME_SIZE;
}

View File

@ -10,6 +10,8 @@ lerna-debug.log*
node_modules
*.local
vite.config.local.js
# Editor directories and files
.vscode/*
!.vscode/extensions.json

File diff suppressed because one or more lines are too long

View File

@ -9,7 +9,7 @@
<script type="module" crossorigin src="/index.js"></script>
<link rel="stylesheet" href="/index.css">
</head>
<body class="bg-gray-100">
<body class="bg-gray-100 dark:bg-gray-900">
<div id="app"></div>
</body>

File diff suppressed because one or more lines are too long

View File

@ -7,7 +7,7 @@
<link rel="mask-icon" href="/favicon.svg" color="#000000">
<title>AMS reader</title>
</head>
<body class="bg-gray-100">
<body class="bg-gray-100 dark:bg-gray-900">
<div id="app"></div>
<script type="module" src="/src/main.js"></script>
</body>

File diff suppressed because it is too large Load Diff

View File

@ -5,23 +5,25 @@
"type": "module",
"scripts": {
"dev": "vite",
"local": "vite --config vite.config.local.js",
"build": "vite build",
"preview": "vite preview"
},
"devDependencies": {
"@sveltejs/vite-plugin-svelte": "^1.0.1",
"@tailwindcss/forms": "^0.5.2",
"autoprefixer": "^10.4.7",
"http-proxy-middleware": "^2.0.1",
"@sveltejs/vite-plugin-svelte": "^2.1.0",
"@tailwindcss/forms": "^0.5.3",
"autoprefixer": "^10.4.14",
"http-proxy-middleware": "^2.0.6",
"postcss": "^8.4.31",
"postcss-load-config": "^4.0.1",
"svelte": "^3.49.0",
"svelte": "^3.58.0",
"svelte-navigator": "^3.2.2",
"svelte-preprocess": "^4.10.7",
"tailwindcss": "^3.1.5",
"vite": "^3.2.7"
"svelte-preprocess": "^5.0.3",
"svelte-qrcode": "^1.0.0",
"tailwindcss": "^3.3.1",
"vite": "^4.3.1"
},
"dependencies": {
"cssnano": "^5.1.14"
"cssnano": "^5.1.15"
}
}

View File

@ -1,3 +1,9 @@
/**
* @copyright Utilitech AS 2023
* License: Fair Source
*
*/
<script>
import { Router, Route, navigate } from "svelte-navigator";
import { getSysinfo, sysinfoStore, dataStore } from './lib/DataStores.js';
@ -21,6 +27,22 @@
} else if(sysinfo.fwconsent === 0) {
navigate("/consent");
}
if(sysinfo.ui.k === 1) {
console.log("dark");
document.documentElement.classList.add('dark')
} else if (sysinfo.ui.k === 0) {
console.log("light");
document.documentElement.classList.remove('dark')
} else {
if (window.matchMedia('(prefers-color-scheme: dark)').matches) {
console.log("dark auto");
document.documentElement.classList.add('dark')
} else {
console.log("light auto");
document.documentElement.classList.remove('dark')
}
}
});
getSysinfo();
let data = {};

View File

@ -8,7 +8,7 @@
}
.cnt {
@apply bg-white m-2 p-2 rounded shadow-lg;
@apply m-2 p-2 rounded dark:bg-gray-800 bg-white dark:text-white shadow-lg dark:shadow-xl dark:shadow-gray-900 dark:drop-shadow-md;
min-height: 268px;
}
@ -17,27 +17,28 @@
}
.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
@apply flex items-center bg-gray-100 dark:bg-gray-600 rounded-l-md border border-r-0 dark:border-gray-800 border-gray-300 px-3 whitespace-nowrap text-sm
}
.in-post {
@apply flex items-center bg-gray-100 rounded-r-md border border-l-0 border-gray-300 px-3 whitespace-nowrap text-sm
@apply flex items-center bg-gray-100 dark:bg-gray-600 rounded-r-md border border-l-0 dark:border-gray-800 border-gray-300 px-3 whitespace-nowrap text-sm
}
.in-txt {
@apply h-10 shadow-sm border-gray-300 disabled:bg-gray-200
@apply h-10 shadow-sm border-gray-300 dark:border-gray-800 disabled:bg-gray-200 dark:disabled:bg-gray-700 disabled:text-white
disabled:cursor-not-allowed dark:text-white dark:bg-gray-700 dark:shadow-lg dark:border dark:focus:ring-4 dark:drop-shadow-lg
}
.in-f {
@apply in-txt rounded-l-md
@apply in-txt rounded-l-md dark:placeholder:text-gray-400 dark:default:text-white disabled:text-white default:text-white
}
.in-m {
@apply in-txt border-l-0
}
.in-l {
@apply in-txt border-l-0 rounded-r-md
@apply in-txt border-l-0 rounded-r-md dark:placeholder-white
}
.in-s {
@apply in-txt rounded-md w-full
@apply in-txt rounded-md w-full dark:text-white placeholder:text-gray-400
}
.tr {
@apply text-right
@ -82,6 +83,7 @@
.pl-unt {
font-size: 1.0rem;
color: grey;
@apply dark:text-gray-200
}
.pl-sub {
padding-top: 10px;
@ -90,6 +92,7 @@
.pl-snt {
font-size: 0.7rem;
color: grey;
@apply dark:text-gray-200
}
.pl-lab {
font-size: 1.0rem;
@ -110,6 +113,7 @@ svg {
font-family: Helvetica, Arial;
font-size: 0.85em;
font-weight: 200;
@apply dark:fill-white text-white
}
.tick line {
@ -120,6 +124,7 @@ svg {
.tick text {
fill: #999;
text-anchor: start;
@apply dark:fill-white
}
.tick.tick-0 line {

View File

@ -27,6 +27,9 @@
<option value={201}>{boardtype(chip, 201)}</option>
<option value={202}>{boardtype(chip, 202)}</option>
<option value={203}>{boardtype(chip, 203)}</option>
<option value={241}>{boardtype(chip, 241)}</option>
<option value={242}>{boardtype(chip, 242)}</option>
<option value={243}>{boardtype(chip, 243)}</option>
<option value={200}>{boardtype(chip, 200)}</option>
</optgroup>
{/if}

View File

@ -10,6 +10,7 @@
import { Link, navigate } from 'svelte-navigator';
import SubnetOptions from './SubnetOptions.svelte';
import TrashIcon from './TrashIcon.svelte';
import QrCode from 'svelte-qrcode';
export let sysinfo = {}
@ -35,6 +36,9 @@
},{
name: 'Peaks',
key: 't'
},{
name: 'Realtime plot',
key: 'l'
},{
name: 'Price',
key: 'p'
@ -47,6 +51,9 @@
},{
name: 'Temperature plot',
key: 's'
},{
name: 'Dark mode',
key: 'k'
}];
let loading = true;
@ -93,11 +100,15 @@
a: null,
l: { p: null, i: false },
r: { r: null, g: null, b: null, i: false },
d: { d: null, b: 0 },
t: { d: null, a: null },
v: { p: null, d: { v: null, g: null }, o: null, m: null, b: null }
},
h: {
t: '', h: '', n: ''
},
c: {
e: false, i: null, s: null, es: null
}
};
configurationStore.subscribe(update => {
@ -329,6 +340,14 @@
<strong class="text-sm">Meter</strong>
<a href="{wiki('Meter-configuration')}" target="_blank" class="float-right"><HelpIcon/></a>
<input type="hidden" name="m" value="true"/>
<input type="hidden" name="mo" value="1"/>
<div class="my-1">
Communication<br/>
<select name="ma" bind:value={configuration.m.a} class="in-s">
<option value={0}>Passive (Push)</option>
<option value={9}>Kamstrup (Pull)</option>
</select>
</div>
<div class="my-1">
<span class="float-right">Buffer size</span>
<span>Serial conf.</span>
@ -412,41 +431,50 @@
{/if}
</div>
<div class="cnt">
<strong class="text-sm">WiFi</strong>
<strong class="text-sm">Connection</strong>
<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/>
<input name="ws" bind:value={configuration.w.s} type="text" class="in-s"/>
Connection<br/>
<select name="nc" class="in-s" bind:value={configuration.n.c}>
<option value={1}>WiFi</option>
<option value={2}>Access point</option>
{#if sysinfo.if && sysinfo.if.eth}
<option value={3}>Ethernet</option>
{/if}
</select>
</div>
<div class="my-1">
Password<br/>
<input name="wp" bind:value={configuration.w.p} type="password" class="in-s"/>
</div>
<div class="my-1 flex">
<div class="w-1/2">
Power saving<br/>
<select name="wz" bind:value={configuration.w.z} class="in-s">
<option value={255}>Default</option>
<option value={0}>Off</option>
<option value={1}>Minimum</option>
<option value={2}>Maximum</option>
</select>
{#if configuration.n.c == 1 || configuration.n.c == 2}
<div class="my-1">
SSID<br/>
<input name="ws" bind:value={configuration.w.s} type="text" class="in-s"/>
</div>
<div class="ml-2 w-1/2">
Power<br/>
<div class="flex">
<input name="ww" bind:value={configuration.w.w} type="number" min="0" max="20.5" step="0.5" class="in-f tr w-full"/>
<span class="in-post">dBm</span>
<div class="my-1">
Password<br/>
<input name="wp" bind:value={configuration.w.p} type="password" class="in-s"/>
</div>
<div class="my-1 flex">
<div class="w-1/2">
Power saving<br/>
<select name="wz" bind:value={configuration.w.z} class="in-s">
<option value={255}>Default</option>
<option value={0}>Off</option>
<option value={1}>Minimum</option>
<option value={2}>Maximum</option>
</select>
</div>
<div class="ml-2 w-1/2">
Power<br/>
<div class="flex">
<input name="ww" bind:value={configuration.w.w} type="number" min="0" max="20.5" step="0.5" class="in-f tr w-full"/>
<span class="in-post">dBm</span>
</div>
</div>
</div>
</div>
<div class="my-3">
<label><input type="checkbox" name="wa" value="true" bind:checked={configuration.w.a} class="rounded mb-1"/> Auto reboot on connection problem</label>
</div>
<div class="my-3">
<label><input type="checkbox" name="wb" value="true" bind:checked={configuration.w.b} class="rounded mb-1"/> Allow 802.11b legacy rates</label>
</div>
<div class="my-3">
<label><input type="checkbox" name="wb" value="true" bind:checked={configuration.w.b} class="rounded mb-1"/> Allow 802.11b legacy rates</label>
</div>
{/if}
</div>
<div class="cnt">
<strong class="text-sm">Network</strong>
@ -607,6 +635,36 @@
</div>
</div>
{/if}
{#if configuration.c.es != null}
<div class="cnt">
<strong class="text-sm">Cloud connections</strong>
<a href="{wiki('Cloud')}" target="_blank" class="float-right"><HelpIcon/></a>
<input type="hidden" name="c" value="true"/>
<div class="my-1">
<label><input type="checkbox" name="ce" value="true" bind:checked={configuration.c.e} class="rounded mb-1"/> Enable cloud upload</label>
</div>
<div class="my-1">
Client ID<br/>
<input name="ci" bind:value={configuration.c.i} type="text" class="in-s" pattern={configuration.c.e ? '[A-Z0-9]{16}' : '.*'} required={configuration.c.e}/>
</div>
<div class="my-1">
Client secret<br/>
<input name="cs" bind:value={configuration.c.s} type="text" class="in-s" pattern={configuration.c.e && configuration.c.s != '***' ? '[A-Z0-9]{16}' : '.*'}/>
</div>
<div class="my-1">
<label><input type="checkbox" class="rounded mb-1" name="ces" value="true" bind:checked={configuration.c.es}/> Energy Speedometer</label>
{#if configuration.c.es}
<div class="pl-5">MAC: {sysinfo.mac}</div>
<div class="pl-5">Meter ID: {sysinfo.meter.id ? sysinfo.meter.id : "missing, required"}</div>
{#if sysinfo.mac && sysinfo.meter.id}
<div class="pl-2">
<QrCode value='{'{'}"mac":"{sysinfo.mac}","meter":"{sysinfo.meter.id}"{'}'}'/>
</div>
{/if}
{/if}
</div>
</div>
{/if}
{#if configuration.p.r.startsWith("10YNO") || configuration.p.r == '10Y1001A1001A48H'}
<div class="cnt">
<strong class="text-sm">Tariff thresholds</strong>
@ -637,15 +695,15 @@
<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>
<option value={0}>Disable</option>
<option value={1}>Enable</option>
<option value={2}>Auto</option>
</select>
</div>
{/each}
</div>
</div>
{#if sysinfo.board > 20 || sysinfo.chip == 'esp8266'}
{#if sysinfo.board > 20 || sysinfo.chip == 'esp8266' || configuration.i.d.d > 0}
<div class="cnt">
<strong class="text-sm">Hardware</strong>
<a href="{wiki('GPIO-configuration')}" target="_blank" class="float-right"><HelpIcon/></a>
@ -653,21 +711,35 @@
<input type="hidden" name="i" value="true"/>
<div class="flex flex-wrap">
<div class="w-1/3">
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/>
HAN RX<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={gpioMax} class="in-m tr w-full"/>
HAN TX<br/>
<select name="iht" bind:value={configuration.i.h.t} class="in-l w-full">
<UartSelectOptions chip={sysinfo.chip}/>
</select>
</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/>
<label class="ml-2"><input name="ihu" value="true" bind:checked={configuration.i.h.u} type="checkbox" class="rounded mb-1"/> pullup</label>
</div>
</div>
<div class="flex flex-wrap">
<div class="w-1/3">
AP button<br/>
<input name="ia" bind:value={configuration.i.a} type="number" min="0" max={gpioMax} class="in-f tr w-full"/>
</div>
<div class="w-1/3">
LED<br/>
<div class="flex">
<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-1/3">
<label class="ml-4"><input name="ili" value="true" bind:checked={configuration.i.l.i} type="checkbox" class="rounded mb-1"/> inverted</label>
</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">
@ -676,6 +748,12 @@
<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="w-full">
<div class="my-1 pr-1 w-1/3">
LED dis. GPIO
<input name="idd" bind:value={configuration.i.d.d} type="number" min="0" max={gpioMax} class="in-s tr"/>
</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={gpioMax} class="in-f tr w-full"/>
@ -701,6 +779,15 @@
{/if}
</div>
{/if}
{#if configuration.i.d.d > 0}
<div class="my-1 w-full">
LED behaviour
<select name="idb" bind:value={configuration.i.d.b} class="in-s">
<option value={0}>Enabled</option>
<option value={1}>Disabled</option>
</select>
</div>
{/if}
{#if sysinfo.chip == 'esp8266'}
<input type="hidden" name="iv" value="true"/>
<div class="my-1 flex flex-wrap">

View File

@ -11,6 +11,7 @@
import MonthPlot from './MonthPlot.svelte';
import TemperaturePlot from './TemperaturePlot.svelte';
import TariffPeakChart from './TariffPeakChart.svelte';
import RealtimePlot from './RealtimePlot.svelte';
export let data = {}
export let sysinfo = {}
@ -80,6 +81,11 @@
<TariffPeakChart />
</div>
{/if}
{#if uiVisibility(sysinfo.ui.l)}
<div class="cnt gwf">
<RealtimePlot/>
</div>
{/if}
{#if uiVisibility(sysinfo.ui.p, data.pe && !Number.isNaN(data.p))}
<div class="cnt gwf">
<PricePlot json={prices} sysinfo={sysinfo}/>

View File

@ -218,3 +218,15 @@ export async function getGitHubReleases() {
releases = (await response.json())
gitHubReleaseStore.set(releases);
};
let realtime = {};
export async function getRealtime() {
const response = await fetchWithTimeout("/realtime.json");
realtime = (await response.json())
realtimeStore.set(realtime);
}
export const realtimeStore = writable(realtime, (set) => {
getRealtime();
return function stop() {}
});

View File

@ -41,7 +41,7 @@ export function metertype(mt) {
case 10:
return "Sagemcom";
default:
return "";
return "Unknown";
}
}
@ -79,6 +79,12 @@ export function boardtype(c, b) {
return "Adafruit HUZZAH32";
case 203:
return "DevKitC";
case 241:
return "LilyGO T-ETH-POE";
case 242:
return "M5 PoESP32";
case 243:
return "WT32-ETH01";
case 200:
return "Generic ESP32";
case 2:

View File

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

View File

@ -26,6 +26,6 @@
</script>
<svg viewBox="0 0 300 300" xmlns="http://www.w3.org/2000/svg" height="100%">
<path d="{ describeArc(150, 150, 115, 210, 510) }" stroke="#eee" fill="none" stroke-width="55"/>
<path d="{ describeArc(150, 150, 115, 210, 510) }" stroke="rgba(128, 128, 128, 0.15)" fill="none" stroke-width="55"/>
<path d="{ describeArc(150, 150, 115, 210, 210 + (300*pct/100)) }" stroke={color} fill="none" stroke-width="55"/>
</svg>

View File

@ -0,0 +1,145 @@
<script>
import { dataStore, realtimeStore } from './DataStores.js';
let realtime;
realtimeStore.subscribe(update => {
realtime = update;
});
let blankTimeout;
let lastUp = 0;
function setBlank() {
realtime.data.unshift(0);
realtime.data = realtime.data.slice(0,realtime.size);
lastUp += 10;
blankTimeout = setTimeout(setBlank, 10000);
}
dataStore.subscribe(update => {
if(lastUp > 0) {
if(realtime.data && update.u-lastUp >= 10) {
if(blankTimeout) clearTimeout(blankTimeout);
realtime.data.unshift(update.i-update.e);
realtime.data = realtime.data.slice(0,realtime.size);
lastUp += 10;
blankTimeout = setTimeout(setBlank, 10000);
}
} else {
lastUp = update.u;
}
});
let max;
let min;
let width;
let height;
let heightAvailable;
let widthAvailable;
let points;
let yScale;
let xScale;
let yTicks;
let xTicks;
let barWidth;
let labelSpacing = 12;
let unit
$:{
heightAvailable = parseInt(height) - 50;
widthAvailable = width - 35;
barWidth = widthAvailable / realtime.size;
min = 0;
max = 0;
if(realtime.data) {
for(let p in realtime.data) {
let val = realtime.data[p];
max = Math.max(Math.ceil(val/1000.0)*1000, max);
min = Math.min(Math.ceil(val/1000.0)*1000, min);
}
unit = max > 2500 ? 'kW' : 'W';
yTicks = [];
for(let i = min; i < max; i+= max/5) {
yTicks.push({
value: i,
label: max > 2500 ? (i / 1000).toFixed(1) : i
});
}
xTicks = [];
for(let i = min; i < realtime.size; i+=realtime.size/labelSpacing) {
xTicks.push({
value: i,
label: '-'+Math.round((realtime.size - i) / 6)+' min'
});
}
yScale = function(val) {
return Math.ceil(heightAvailable - ((val/max)* heightAvailable) ) - 25;
};
xScale = function(val) {
return 30 + Math.ceil((val/realtime.size) * (widthAvailable-35));
};
let i = realtime.size;
points = xScale(realtime.size)+","+yScale(0)+" "+xScale(1)+","+yScale(0);
for(let p in realtime.data) {
if(i < 0) {
break;
}
let val = realtime.data[p];
if(val != 0) {
points = xScale(i--)+","+yScale(val)+" "+points;
}
}
}
}
</script>
<div class="chart" bind:clientWidth={width} bind:clientHeight={height}>
<strong class="text-sm">Realtime ({unit})</strong>
{#if yTicks}
<svg viewBox="0 0 {widthAvailable} {heightAvailable}" height="100%">
<!-- y axis -->
<g class="axis y-axis">
{#each yTicks as tick}
{#if !isNaN(yScale(tick.value))}
<g class="tick tick-{tick.value}" transform="translate(0, {yScale(tick.value)})">
<line x2="100%"></line>
<text y="-4">{tick.label}</text>
</g>
{/if}
{/each}
</g>
<!-- x axis -->
<g class="axis x-axis">
{#each xTicks as point, i}
{#if !isNaN(xScale(point.value))}
{#if i%Math.round(6/barWidth) == 0}
<g class="tick" transform="translate({40+xScale(point.value)},{heightAvailable})">
<text x="{barWidth/2}" y="-4">{point.label}</text>
</g>
{/if}
{/if}
{/each}
</g>
<!-- Line -->
<polyline
opacity="0.9"
fill="#7c3aed"
stroke="#7c3aed"
stroke-width="1"
points={points}/>
</svg>
{/if}
</div>

View File

@ -6,6 +6,7 @@
export let sysinfo = {}
let staticIp = false;
let connectionMode = 1;
let loadingOrSaving = false;
let tries = 0;
@ -87,13 +88,25 @@
<input type="hidden" name="s" value="true"/>
<strong class="text-sm">Setup</strong>
<div class="my-3">
SSID<br/>
<input name="ss" type="text" class="in-s" required/>
</div>
<div class="my-3">
PSK<br/>
<input name="sp" type="password" class="in-s" autocomplete="off"/>
Connection<br/>
<select name="sc" class="in-s" bind:value={connectionMode}>
<option value={1}>Connect to WiFi</option>
<option value={2}>Standalone access point</option>
{#if sysinfo.if && sysinfo.if.eth}
<option value={3}>Ethernet</option>
{/if}
</select>
</div>
{#if connectionMode == 1 || connectionMode == 2}
<div class="my-3">
SSID<br/>
<input name="ss" type="text" class="in-s" required={connectionMode == 1 || connectionMode == 2}/>
</div>
<div class="my-3">
PSK<br/>
<input name="sp" type="password" class="in-s" autocomplete="off" required={connectionMode == 2}/>
</div>
{/if}
<div>
Hostname
<input name="sh" bind:value={sysinfo.hostname} type="text" class="in-s" maxlength="32" pattern="[a-z0-9_-]+" placeholder="Optional, ex.: ams-reader" autocomplete="off"/>

View File

@ -126,10 +126,10 @@
Manufacturer: {metertype(sysinfo.meter.mfg)}
</div>
<div class="my-2">
Model: {sysinfo.meter.model}
Model: {sysinfo.meter.model ? sysinfo.meter.model : "unknown"}
</div>
<div class="my-2">
ID: {sysinfo.meter.id}
ID: {sysinfo.meter.id ? sysinfo.meter.id : "unknown"}
</div>
</div>
{/if}

View File

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

View File

@ -8,6 +8,8 @@ const config = {
plugins: [
require('@tailwindcss/forms')
],
darkMode: 'class'
};
module.exports = config;

View File

@ -1,3 +1,9 @@
/**
* @copyright Utilitech AS 2023
* License: Fair Source
*
*/
static const char HEADER_CACHE_CONTROL[] PROGMEM = "Cache-Control";
static const char HEADER_CONTENT_ENCODING[] PROGMEM = "Content-Encoding";
static const char HEADER_PRAGMA[] PROGMEM = "Pragma";

View File

@ -1,8 +1,14 @@
/**
* @copyright Utilitech AS 2023
* License: Fair Source
*
*/
#ifndef _AMSWEBSERVER_h
#define _AMSWEBSERVER_h
#include "Arduino.h"
#include <MQTT.h>
#include "AmsMqttHandler.h"
#include "AmsConfiguration.h"
#include "HwTools.h"
#include "AmsData.h"
@ -12,6 +18,7 @@
#include "Uptime.h"
#include "RemoteDebug.h"
#include "EntsoeApi.h"
#include "RealtimePlot.h"
#if defined(ESP8266)
#include <ESP8266WiFi.h>
@ -32,29 +39,34 @@
class AmsWebServer {
public:
AmsWebServer(uint8_t* buf, RemoteDebug* Debug, HwTools* hw);
void setup(AmsConfiguration*, GpioConfig*, MeterConfig*, AmsData*, AmsDataStorage*, EnergyAccounting*);
void setup(AmsConfiguration*, GpioConfig*, AmsData*, AmsDataStorage*, EnergyAccounting*, RealtimePlot*);
void loop();
void setMqtt(MQTTClient* mqtt);
void setTimezone(Timezone* tz);
void setMqttEnabled(bool);
void setEntsoeApi(EntsoeApi* eapi);
void setPriceSettings(String region, String currency);
void setMeterConfig(uint8_t distributionSystem, uint16_t mainFuse, uint16_t productionCapacity);
void setMqttHandler(AmsMqttHandler* mqttHandler);
private:
RemoteDebug* debugger;
bool mqttEnabled = false;
int maxPwr = 0;
uint8_t distributionSystem = 0;
uint16_t mainFuse = 0, productionCapacity = 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;
RealtimePlot* rtp = NULL;
AmsMqttHandler* mqttHandler = NULL;
bool uploading = false;
File file;
bool performRestart = false;
@ -92,6 +104,7 @@ private:
void energyPriceJson();
void temperatureJson();
void tariffJson();
void realtimeJson();
void configurationJson();
void handleSave();

View File

@ -0,0 +1,6 @@
"c": {
"e" : %s,
"i" : "%s",
"s" : "%s",
"es": %s
}

View File

@ -1,7 +1,8 @@
"i": {
"h": {
"p": %s,
"u": %s
"u": %s,
"t": %s
},
"a": %s,
"l": {
@ -14,6 +15,10 @@
"b": %s,
"i": %s
},
"d": {
"d": %s,
"b": %d
},
"t": {
"d": %s,
"a": %s

View File

@ -2,4 +2,4 @@
"t" : "%s",
"h" : "%s",
"n" : "%s"
}
},

View File

@ -1,4 +1,6 @@
"m": {
"o": %d,
"a": %d,
"b": %d,
"p": %d,
"i": %s,

View File

@ -1,4 +1,5 @@
"n": {
"c": %d,
"m": "%s",
"i": "%s",
"s": "%s",

View File

@ -9,5 +9,7 @@
"p": %d,
"d": %d,
"m": %d,
"s": %d
"s": %d,
"l": %d,
"k": %d
},

View File

@ -3,6 +3,5 @@
"p": "%s",
"w": %.1f,
"z": %d,
"a": %s,
"b": %s
},

View File

@ -19,6 +19,9 @@
"dns1": "%s",
"dns2": "%s"
},
"if": {
"eth": %s
},
"meter": {
"mfg": %d,
"model": "%s",
@ -35,7 +38,9 @@
"p": %d,
"d": %d,
"m": %d,
"s": %d
"s": %d,
"l": %d,
"k": %d
},
"security": %d,
"boot_reason": %d,

View File

@ -1,3 +1,9 @@
/**
* @copyright Utilitech AS 2023
* License: Fair Source
*
*/
#include "AmsWebServer.h"
#include "AmsWebHeaders.h"
#include "FirmwareVersion.h"
@ -30,12 +36,13 @@
#include "html/conf_domoticz_json.h"
#include "html/conf_ha_json.h"
#include "html/conf_ui_json.h"
#include "html/conf_cloud_json.h"
#include "html/firmware_html.h"
#if defined(ESP32)
#include <esp_task_wdt.h>
#include <esp_wifi.h>
#include <esp_clk.h>
#include <esp32/clk.h>
#endif
@ -55,13 +62,13 @@ AmsWebServer::AmsWebServer(uint8_t* buf, RemoteDebug* Debug, HwTools* hw) {
this->buf = (char*) buf;
}
void AmsWebServer::setup(AmsConfiguration* config, GpioConfig* gpioConfig, MeterConfig* meterConfig, AmsData* meterState, AmsDataStorage* ds, EnergyAccounting* ea) {
void AmsWebServer::setup(AmsConfiguration* config, GpioConfig* gpioConfig, AmsData* meterState, AmsDataStorage* ds, EnergyAccounting* ea, RealtimePlot* rtp) {
this->config = config;
this->gpioConfig = gpioConfig;
this->meterConfig = meterConfig;
this->meterState = meterState;
this->ds = ds;
this->ea = ea;
this->rtp = rtp;
server.on(F("/"), HTTP_GET, std::bind(&AmsWebServer::indexHtml, this));
snprintf_P(buf, 32, PSTR("/index-%s.js"), FirmwareVersion::VersionString);
@ -87,6 +94,7 @@ void AmsWebServer::setup(AmsConfiguration* config, GpioConfig* gpioConfig, Meter
server.on(F("/energyprice.json"), HTTP_GET, std::bind(&AmsWebServer::energyPriceJson, this));
server.on(F("/temperature.json"), HTTP_GET, std::bind(&AmsWebServer::temperatureJson, this));
server.on(F("/tariff.json"), HTTP_GET, std::bind(&AmsWebServer::tariffJson, this));
server.on(F("/realtime.json"), HTTP_GET, std::bind(&AmsWebServer::realtimeJson, this));
server.on(F("/configuration.json"), HTTP_GET, std::bind(&AmsWebServer::configurationJson, this));
server.on(F("/save"), HTTP_POST, std::bind(&AmsWebServer::handleSave, this));
@ -124,11 +132,6 @@ void AmsWebServer::setup(AmsConfiguration* config, GpioConfig* gpioConfig, Meter
mqttEnabled = strlen(mqttConfig.host) > 0;
}
void AmsWebServer::setMqtt(MQTTClient* mqtt) {
this->mqtt = mqtt;
}
void AmsWebServer::setTimezone(Timezone* tz) {
this->tz = tz;
}
@ -136,28 +139,38 @@ void AmsWebServer::setTimezone(Timezone* tz) {
void AmsWebServer::setMqttEnabled(bool enabled) {
mqttEnabled = enabled;
}
void AmsWebServer::setMqttHandler(AmsMqttHandler* mqttHandler) {
this->mqttHandler = mqttHandler;
}
void AmsWebServer::setEntsoeApi(EntsoeApi* eapi) {
this->eapi = eapi;
}
void AmsWebServer::setMeterConfig(uint8_t distributionSystem, uint16_t mainFuse, uint16_t productionCapacity) {
maxPwr = 0;
this->distributionSystem = distributionSystem;
this->mainFuse = mainFuse;
this->productionCapacity = productionCapacity;
}
void AmsWebServer::loop() {
server.handleClient();
if(maxPwr == 0 && meterState->getListType() > 1 && meterConfig->mainFuse > 0 && meterConfig->distributionSystem > 0) {
int voltage = meterConfig->distributionSystem == 2 ? 400 : 230;
if(maxPwr == 0 && meterState->getListType() > 1 && mainFuse > 0 && distributionSystem > 0) {
int voltage = distributionSystem == 2 ? 400 : 230;
if(meterState->isThreePhase()) {
maxPwr = meterConfig->mainFuse * sqrt(3) * voltage;
maxPwr = mainFuse * sqrt(3) * voltage;
} else if(meterState->isTwoPhase()) {
maxPwr = meterConfig->mainFuse * voltage;
maxPwr = mainFuse * voltage;
} else {
maxPwr = meterConfig->mainFuse * 230;
maxPwr = mainFuse * 230;
}
}
}
bool AmsWebServer::checkSecurity(byte level, bool send401) {
bool access = WiFi.getMode() == WIFI_AP || webConfig.security < level;
bool access = WiFi.getMode() == WIFI_AP || WiFi.getMode() == WIFI_AP_STA || webConfig.security < level;
if(!access && webConfig.security >= level && server.hasHeader(F("Authorization"))) {
String expectedAuth = String(webConfig.username) + ":" + String(webConfig.password);
@ -218,9 +231,9 @@ void AmsWebServer::sysinfoJson() {
String hostname;
if(sys.userConfigured) {
WiFiConfig wifiConfig;
config->getWiFiConfig(wifiConfig);
hostname = String(wifiConfig.hostname);
NetworkConfig networkConfig;
config->getNetworkConfig(networkConfig);
hostname = String(networkConfig.hostname);
} else {
hostname = "ams-"+chipIdStr;
}
@ -303,6 +316,7 @@ void AmsWebServer::sysinfoJson() {
dns1 != INADDR_NONE ? dns1.toString().c_str() : "",
dns2 != INADDR_NONE ? dns2.toString().c_str() : "",
#endif
sys.boardType > 240 && sys.boardType < 250 ? "true" : "false",
meterState->getMeterType(),
meterModel.c_str(),
meterId.c_str(),
@ -317,6 +331,8 @@ void AmsWebServer::sysinfoJson() {
ui.showDayPlot,
ui.showMonthPlot,
ui.showTemperaturePlot,
ui.showRealtimePlot,
ui.darkMode,
webConfig.security,
#if defined(ESP32)
rtc_get_reset_reason(0),
@ -396,7 +412,7 @@ void AmsWebServer::dataJson() {
uint8_t hanStatus;
if(meterState->getLastError() != 0) {
hanStatus = 3;
} else if((meterConfig->baud == 0 || meterState->getLastUpdateMillis() == 0) && millis < 30000) {
} else if(meterState->getLastUpdateMillis() == 0 && millis < 30000) {
hanStatus = 0;
} else if(millis - meterState->getLastUpdateMillis() < 15000) {
hanStatus = 1;
@ -418,9 +434,9 @@ void AmsWebServer::dataJson() {
uint8_t mqttStatus;
if(!mqttEnabled) {
mqttStatus = 0;
} else if(mqtt != NULL && mqtt->connected()) {
} else if(mqttHandler != NULL && mqttHandler->connected()) {
mqttStatus = 1;
} else if(mqtt != NULL && mqtt->lastError() == 0) {
} else if(mqttHandler != NULL && mqttHandler->lastError() == 0) {
mqttStatus = 2;
} else {
mqttStatus = 3;
@ -438,8 +454,8 @@ void AmsWebServer::dataJson() {
snprintf_P(buf, BufferSize, DATA_JSON,
maxPwr == 0 ? meterState->isThreePhase() ? 20000 : 10000 : maxPwr,
meterConfig->productionCapacity,
meterConfig->mainFuse == 0 ? 40 : meterConfig->mainFuse,
productionCapacity,
mainFuse == 0 ? 40 : mainFuse,
meterState->getActiveImportPower(),
meterState->getActiveExportPower(),
meterState->getReactiveImportPower(),
@ -467,10 +483,10 @@ void AmsWebServer::dataJson() {
hanStatus,
wifiStatus,
mqttStatus,
mqtt == NULL ? 0 : (int) mqtt->lastError(),
mqttHandler == NULL ? 0 : (int) mqttHandler->lastError(),
price == ENTSOE_NO_VALUE ? "null" : String(price, 2).c_str(),
meterState->getMeterType(),
meterConfig->distributionSystem,
distributionSystem,
ea->getMonthMax(),
peaks.c_str(),
ea->getCurrentThreshold(),
@ -728,13 +744,12 @@ void AmsWebServer::temperatureJson() {
TempSensorData* data = hw->getTempSensorData(i);
if(data == NULL) continue;
TempSensorConfig* conf = config->getTempSensorConfig(data->address);
char* pos = buf+strlen(buf);
snprintf_P(pos, 72, TEMPSENSOR_JSON,
i,
toHex(data->address, 8).c_str(),
conf == NULL ? "" : String(conf->name).substring(0,16).c_str(),
conf == NULL || conf->common ? 1 : 0,
"",
1,
data->lastRead
);
yield();
@ -791,14 +806,19 @@ void AmsWebServer::configurationJson() {
if(!checkSecurity(1))
return;
MeterConfig meterConfig;
config->getMeterConfig(meterConfig);
SystemConfig sysConfig;
config->getSystemConfig(sysConfig);
NtpConfig ntpConfig;
config->getNtpConfig(ntpConfig);
WiFiConfig wifiConfig;
config->getWiFiConfig(wifiConfig);
NetworkConfig networkConfig;
config->getNetworkConfig(networkConfig);
bool encen = false;
for(uint8_t i = 0; i < 16; i++) {
if(meterConfig->encryptionKey[i] > 0) {
if(meterConfig.encryptionKey[i] > 0) {
encen = true;
}
}
@ -817,6 +837,8 @@ void AmsWebServer::configurationJson() {
config->getUiConfig(ui);
HomeAssistantConfig haconf;
config->getHomeAssistantConfig(haconf);
CloudConfig cloud;
config->getCloudConfig(cloud);
bool qsc = false;
bool qsr = false;
@ -839,28 +861,30 @@ void AmsWebServer::configurationJson() {
server.sendContent_P(PSTR("\","));
snprintf_P(buf, BufferSize, CONF_GENERAL_JSON,
ntpConfig.timezone,
wifiConfig.hostname,
networkConfig.hostname,
webConfig.security,
webConfig.username,
strlen(webConfig.password) > 0 ? "***" : ""
);
server.sendContent(buf);
snprintf_P(buf, BufferSize, CONF_METER_JSON,
meterConfig->baud,
meterConfig->parity,
meterConfig->invert ? "true" : "false",
meterConfig->bufferSize * 64,
meterConfig->distributionSystem,
meterConfig->mainFuse,
meterConfig->productionCapacity,
meterConfig.source,
meterConfig.parser,
meterConfig.baud,
meterConfig.parity,
meterConfig.invert ? "true" : "false",
meterConfig.bufferSize * 64,
meterConfig.distributionSystem,
meterConfig.mainFuse,
meterConfig.productionCapacity,
encen ? "true" : "false",
toHex(meterConfig->encryptionKey, 16).c_str(),
toHex(meterConfig->authenticationKey, 16).c_str(),
meterConfig->wattageMultiplier > 1 || meterConfig->voltageMultiplier > 1 || meterConfig->amperageMultiplier > 1 || meterConfig->accumulatedMultiplier > 1 ? "true" : "false",
meterConfig->wattageMultiplier / 1000.0,
meterConfig->voltageMultiplier / 1000.0,
meterConfig->amperageMultiplier / 1000.0,
meterConfig->accumulatedMultiplier / 1000.0
toHex(meterConfig.encryptionKey, 16).c_str(),
toHex(meterConfig.authenticationKey, 16).c_str(),
meterConfig.wattageMultiplier > 1 || meterConfig.voltageMultiplier > 1 || meterConfig.amperageMultiplier > 1 || meterConfig.accumulatedMultiplier > 1 ? "true" : "false",
meterConfig.wattageMultiplier / 1000.0,
meterConfig.voltageMultiplier / 1000.0,
meterConfig.amperageMultiplier / 1000.0,
meterConfig.accumulatedMultiplier / 1000.0
);
server.sendContent(buf);
@ -879,22 +903,22 @@ void AmsWebServer::configurationJson() {
);
server.sendContent(buf);
snprintf_P(buf, BufferSize, CONF_WIFI_JSON,
wifiConfig.ssid,
strlen(wifiConfig.psk) > 0 ? "***" : "",
wifiConfig.power / 10.0,
wifiConfig.sleep,
wifiConfig.autoreboot ? "true" : "false",
wifiConfig.use11b ? "true" : "false"
networkConfig.ssid,
strlen(networkConfig.psk) > 0 ? "***" : "",
networkConfig.power / 10.0,
networkConfig.sleep,
networkConfig.use11b ? "true" : "false"
);
server.sendContent(buf);
snprintf_P(buf, BufferSize, CONF_NET_JSON,
strlen(wifiConfig.ip) > 0 ? "static" : "dhcp",
wifiConfig.ip,
wifiConfig.subnet,
wifiConfig.gateway,
wifiConfig.dns1,
wifiConfig.dns2,
wifiConfig.mdns ? "true" : "false",
networkConfig.mode,
strlen(networkConfig.ip) > 0 ? "static" : "dhcp",
networkConfig.ip,
networkConfig.subnet,
networkConfig.gateway,
networkConfig.dns1,
networkConfig.dns2,
networkConfig.mdns ? "true" : "false",
ntpConfig.server,
ntpConfig.dhcp ? "true" : "false"
);
@ -929,8 +953,9 @@ void AmsWebServer::configurationJson() {
);
server.sendContent(buf);
snprintf_P(buf, BufferSize, CONF_GPIO_JSON,
gpioConfig->hanPin == 0xff ? "null" : String(gpioConfig->hanPin, 10).c_str(),
gpioConfig->hanPinPullup ? "true" : "false",
meterConfig.rxPin == 0xff ? "null" : String(meterConfig.rxPin, 10).c_str(),
meterConfig.rxPinPullup ? "true" : "false",
meterConfig.txPin == 0xff ? "null" : String(meterConfig.txPin, 10).c_str(),
gpioConfig->apPin == 0xff ? "null" : String(gpioConfig->apPin, 10).c_str(),
gpioConfig->ledPin == 0xff ? "null" : String(gpioConfig->ledPin, 10).c_str(),
gpioConfig->ledInverted ? "true" : "false",
@ -938,6 +963,8 @@ void AmsWebServer::configurationJson() {
gpioConfig->ledPinGreen == 0xff ? "null" : String(gpioConfig->ledPinGreen, 10).c_str(),
gpioConfig->ledPinBlue == 0xff ? "null" : String(gpioConfig->ledPinBlue, 10).c_str(),
gpioConfig->ledRgbInverted ? "true" : "false",
gpioConfig->ledDisablePin == 0xff ? "null" : String(gpioConfig->ledDisablePin, 10).c_str(),
gpioConfig->ledBehaviour,
gpioConfig->tempSensorPin == 0xff ? "null" : String(gpioConfig->tempSensorPin, 10).c_str(),
gpioConfig->tempAnalogSensorPin == 0xff ? "null" : String(gpioConfig->tempAnalogSensorPin, 10).c_str(),
gpioConfig->vccPin == 0xff ? "null" : String(gpioConfig->vccPin, 10).c_str(),
@ -959,7 +986,9 @@ void AmsWebServer::configurationJson() {
ui.showPricePlot,
ui.showDayPlot,
ui.showMonthPlot,
ui.showTemperaturePlot
ui.showTemperaturePlot,
ui.showRealtimePlot,
ui.darkMode
);
server.sendContent(buf);
snprintf_P(buf, BufferSize, CONF_DOMOTICZ_JSON,
@ -976,6 +1005,17 @@ void AmsWebServer::configurationJson() {
haconf.discoveryNameTag
);
server.sendContent(buf);
snprintf_P(buf, BufferSize, CONF_CLOUD_JSON,
cloud.enabled ? "true" : "false",
cloud.clientId,
strlen(cloud.clientSecret) > 0 ? "***" : "",
#if defined(ENERGY_SPEEDOMETER_PASS)
sysConfig.energyspeedometer == 7 ? "true" : "false"
#else
"null"
#endif
);
server.sendContent(buf);
server.sendContent_P(PSTR("}"));
}
@ -992,13 +1032,16 @@ void AmsWebServer::handleSave() {
config->clear();
}
MeterConfig meterConfig;
config->getMeterConfig(meterConfig);
#if defined(CONFIG_IDF_TARGET_ESP32S2)
switch(boardType) {
case 5: // Pow-K+
case 7: // Pow-U+
case 6: // Pow-P1
config->clearGpio(*gpioConfig);
gpioConfig->hanPin = 16;
meterConfig.rxPin = 16;
gpioConfig->apPin = 0;
gpioConfig->ledPinRed = 13;
gpioConfig->ledPinGreen = 14;
@ -1006,20 +1049,21 @@ void AmsWebServer::handleSave() {
gpioConfig->vccPin = 10;
gpioConfig->vccResistorGnd = 22;
gpioConfig->vccResistorVcc = 33;
gpioConfig->ledDisablePin = 6;
break;
case 51: // Wemos S2 mini
gpioConfig->ledPin = 15;
gpioConfig->ledInverted = false;
gpioConfig->apPin = 0;
gpioConfig->hanPin = hanPin > 0 ? hanPin : 18;
if(gpioConfig->hanPin != 18) {
meterConfig.rxPin = hanPin > 0 ? hanPin : 18;
if(meterConfig.rxPin != 18) {
gpioConfig->vccPin = 18;
gpioConfig->vccResistorGnd = 45;
gpioConfig->vccResistorVcc = 10;
}
break;
case 50: // Generic ESP32-S2
gpioConfig->hanPin = hanPin > 0 ? hanPin : 18;
meterConfig.rxPin = hanPin > 0 ? hanPin : 18;
break;
default:
success = false;
@ -1027,8 +1071,8 @@ void AmsWebServer::handleSave() {
#elif defined(CONFIG_IDF_TARGET_ESP32C3)
switch(boardType) {
case 8: // dbeinder: HAN mosquito
gpioConfig->hanPin = 7;
gpioConfig->hanPinPullup = false;
meterConfig.rxPin = 7;
meterConfig.rxPinPullup = false;
gpioConfig->apPin = 9;
gpioConfig->ledRgbInverted = true;
gpioConfig->ledPinRed = 5;
@ -1038,15 +1082,27 @@ void AmsWebServer::handleSave() {
case 71: // ESP32-C3-DevKitM-1
gpioConfig->apPin = 9;
case 70: // Generic ESP32-C3
gpioConfig->hanPin = hanPin > 0 ? hanPin : 7;
meterConfig.rxPin = hanPin > 0 ? hanPin : 7;
break;
default:
success = false;
}
#elif defined(ESP32)
switch(boardType) {
case 241: // LilyGO T-ETH-POE
gpioConfig->apPin = 0;
meterConfig.rxPin = hanPin > 0 ? hanPin : 39;
gpioConfig->ledPin = 2;
gpioConfig->ledInverted = true;
break;
case 242: // M5 PoESP32
meterConfig.rxPin = hanPin > 0 ? hanPin : 16;
break;
case 243: // WT32-ETH01
meterConfig.rxPin = hanPin > 0 ? hanPin : 39;
break;
case 201: // D32
gpioConfig->hanPin = hanPin > 0 ? hanPin : 16;
meterConfig.rxPin = hanPin > 0 ? hanPin : 16;
gpioConfig->apPin = 4;
gpioConfig->ledPin = 5;
gpioConfig->ledInverted = true;
@ -1054,7 +1110,7 @@ void AmsWebServer::handleSave() {
case 202: // Feather
case 203: // DevKitC
case 200: // ESP32
gpioConfig->hanPin = hanPin > 0 ? hanPin : 16;
meterConfig.rxPin = hanPin > 0 ? hanPin : 16;
gpioConfig->ledPin = 2;
gpioConfig->ledInverted = false;
break;
@ -1066,7 +1122,7 @@ void AmsWebServer::handleSave() {
case 2: // spenceme
config->clearGpio(*gpioConfig);
gpioConfig->vccBootLimit = 32;
gpioConfig->hanPin = 3;
meterConfig.rxPin = 3;
gpioConfig->apPin = 0;
gpioConfig->ledPin = 2;
gpioConfig->ledInverted = true;
@ -1074,7 +1130,7 @@ void AmsWebServer::handleSave() {
break;
case 0: // roarfred
config->clearGpio(*gpioConfig);
gpioConfig->hanPin = 3;
meterConfig.rxPin = 3;
gpioConfig->apPin = 0;
gpioConfig->ledPin = 2;
gpioConfig->ledInverted = true;
@ -1084,7 +1140,7 @@ void AmsWebServer::handleSave() {
case 3: // Pow-K UART0
case 4: // Pow-U UART0
config->clearGpio(*gpioConfig);
gpioConfig->hanPin = 3;
meterConfig.rxPin = 3;
gpioConfig->apPin = 0;
gpioConfig->ledPin = 2;
gpioConfig->ledInverted = true;
@ -1095,7 +1151,7 @@ void AmsWebServer::handleSave() {
case 5: // Pow-K GPIO12
case 7: // Pow-U GPIO12
config->clearGpio(*gpioConfig);
gpioConfig->hanPin = 12;
meterConfig.rxPin = 12;
gpioConfig->apPin = 0;
gpioConfig->ledPin = 2;
gpioConfig->ledInverted = true;
@ -1104,14 +1160,14 @@ void AmsWebServer::handleSave() {
gpioConfig->ledRgbInverted = true;
break;
case 101: // D1
gpioConfig->hanPin = hanPin > 0 ? hanPin : 5;
meterConfig.rxPin = hanPin > 0 ? hanPin : 5;
gpioConfig->apPin = 4;
gpioConfig->ledPin = 2;
gpioConfig->ledInverted = true;
gpioConfig->vccMultiplier = 1100;
break;
case 100: // ESP8266
gpioConfig->hanPin = hanPin > 0 ? hanPin : 3;
meterConfig.rxPin = hanPin > 0 ? hanPin : 3;
gpioConfig->ledPin = 2;
gpioConfig->ledInverted = true;
break;
@ -1130,69 +1186,72 @@ void AmsWebServer::handleSave() {
}
}
if(server.hasArg(F("s")) && server.arg(F("s")) == F("true") && server.hasArg(F("ss")) && !server.arg(F("ss")).isEmpty()) {
if(server.hasArg(F("s")) && server.arg(F("s")) == F("true")) {
SystemConfig sys;
config->getSystemConfig(sys);
MeterConfig meterConfig;
config->getMeterConfig(meterConfig);
config->clear();
WiFiConfig wifi;
config->clearWifi(wifi);
NetworkConfig network;
config->clearNetworkConfig(network);
strcpy(wifi.ssid, server.arg(F("ss")).c_str());
strcpy(network.ssid, server.arg(F("ss")).c_str());
String psk = server.arg(F("sp"));
if(!psk.equals("***")) {
strcpy(wifi.psk, psk.c_str());
strcpy(network.psk, psk.c_str());
}
network.mode = server.arg(F("sc")).toInt();
if(network.mode > 3 || network.mode == 0) network.mode = 1; // WiFi Client
if(server.hasArg(F("sm")) && server.arg(F("sm")) == "static") {
strcpy(wifi.ip, server.arg(F("si")).c_str());
strcpy(wifi.gateway, server.arg(F("sg")).c_str());
strcpy(wifi.subnet, server.arg(F("su")).c_str());
strcpy(wifi.dns1, server.arg(F("sd")).c_str());
if(network.mode == 3 || strlen(network.ssid) > 0) {
if(server.hasArg(F("sm")) && server.arg(F("sm")) == "static") {
strcpy(network.ip, server.arg(F("si")).c_str());
strcpy(network.gateway, server.arg(F("sg")).c_str());
strcpy(network.subnet, server.arg(F("su")).c_str());
strcpy(network.dns1, server.arg(F("sd")).c_str());
}
if(server.hasArg(F("sh")) && !server.arg(F("sh")).isEmpty()) {
strcpy(network.hostname, server.arg(F("sh")).c_str());
network.mdns = true;
} else {
network.mdns = false;
}
switch(sys.boardType) {
case 6: // Pow-P1
meterConfig.baud = 115200;
meterConfig.parity = 3; // 8N1
break;
case 3: // Pow-K UART0
case 5: // Pow-K+
meterConfig.baud = 2400;
meterConfig.parity = 3; // 8N1
case 2: // spenceme
case 8: // dbeinder: HAN mosquito
case 50: // Generic ESP32-S2
case 51: // Wemos S2 mini
case 70: // Generic ESP32-C3
case 71: // ESP32-C3-DevKitM-1
network.sleep = 1; // Modem sleep
break;
case 4: // Pow-U UART0
case 7: // Pow-U+
network.sleep = 2; // Light sleep
break;
}
config->setNetworkConfig(network);
config->setMeterConfig(meterConfig);
sys.userConfigured = success;
sys.dataCollectionConsent = 0;
config->setSystemConfig(sys);
performRestart = true;
}
if(server.hasArg(F("sh")) && !server.arg(F("sh")).isEmpty()) {
strcpy(wifi.hostname, server.arg(F("sh")).c_str());
wifi.mdns = true;
} else {
wifi.mdns = false;
}
switch(sys.boardType) {
case 6: // Pow-P1
meterConfig->baud = 115200;
meterConfig->parity = 3; // 8N1
break;
case 3: // Pow-K UART0
case 5: // Pow-K+
meterConfig->baud = 2400;
meterConfig->parity = 3; // 8N1
case 2: // spenceme
case 50: // Generic ESP32-S2
case 51: // Wemos S2 mini
case 70: // Generic ESP32-C3
case 71: // ESP32-C3-DevKitM-1
wifi.sleep = 1; // Modem sleep
break;
case 4: // Pow-U UART0
case 7: // Pow-U+
wifi.sleep = 2; // Light sleep
break;
case 8: // dbeinder: HAN mosquito
wifi.sleep = 1; // Modem sleep
wifi.use11b = 0;
break;
}
config->setWiFiConfig(wifi);
config->setMeterConfig(*meterConfig);
sys.userConfigured = success;
sys.dataCollectionConsent = 0;
config->setSystemConfig(sys);
performRestart = true;
} else if(server.hasArg(F("sf")) && !server.arg(F("sf")).isEmpty()) {
SystemConfig sys;
config->getSystemConfig(sys);
@ -1202,76 +1261,80 @@ void AmsWebServer::handleSave() {
if(server.hasArg(F("m")) && server.arg(F("m")) == F("true")) {
if(debugger->isActive(RemoteDebug::DEBUG)) debugger->printf_P(PSTR("Received meter config\n"));
config->getMeterConfig(*meterConfig);
meterConfig->baud = server.arg(F("mb")).toInt();
meterConfig->parity = server.arg(F("mp")).toInt();
meterConfig->invert = server.hasArg(F("mi")) && server.arg(F("mi")) == F("true");
meterConfig->distributionSystem = server.arg(F("md")).toInt();
meterConfig->mainFuse = server.arg(F("mf")).toInt();
meterConfig->productionCapacity = server.arg(F("mr")).toInt();
meterConfig->bufferSize = min((double) 64, ceil((server.arg(F("ms")).toInt()) / 64));
MeterConfig meterConfig;
config->getMeterConfig(meterConfig);
meterConfig.source = server.arg(F("mo")).toInt();
meterConfig.parser = server.arg(F("ma")).toInt();
meterConfig.baud = server.arg(F("mb")).toInt();
meterConfig.parity = server.arg(F("mp")).toInt();
meterConfig.invert = server.hasArg(F("mi")) && server.arg(F("mi")) == F("true");
meterConfig.distributionSystem = server.arg(F("md")).toInt();
meterConfig.mainFuse = server.arg(F("mf")).toInt();
meterConfig.productionCapacity = server.arg(F("mr")).toInt();
meterConfig.bufferSize = min((double) 64, ceil((server.arg(F("ms")).toInt()) / 64));
maxPwr = 0;
if(server.hasArg(F("me")) && server.arg(F("me")) == F("true")) {
String encryptionKeyHex = server.arg(F("mek"));
if(!encryptionKeyHex.isEmpty()) {
encryptionKeyHex.replace(F("0x"), F(""));
fromHex(meterConfig->encryptionKey, encryptionKeyHex, 16);
fromHex(meterConfig.encryptionKey, encryptionKeyHex, 16);
} else {
memset(meterConfig->encryptionKey, 0, 16);
memset(meterConfig.encryptionKey, 0, 16);
}
String authenticationKeyHex = server.arg(F("mea"));
if(!authenticationKeyHex.isEmpty()) {
authenticationKeyHex.replace(F("0x"), F(""));
fromHex(meterConfig->authenticationKey, authenticationKeyHex, 16);
fromHex(meterConfig.authenticationKey, authenticationKeyHex, 16);
} else {
memset(meterConfig->authenticationKey, 0, 16);
memset(meterConfig.authenticationKey, 0, 16);
}
} else {
memset(meterConfig->encryptionKey, 0, 16);
memset(meterConfig->authenticationKey, 0, 16);
memset(meterConfig.encryptionKey, 0, 16);
memset(meterConfig.authenticationKey, 0, 16);
}
meterConfig->wattageMultiplier = server.arg(F("mmw")).toFloat() * 1000;
meterConfig->voltageMultiplier = server.arg(F("mmv")).toFloat() * 1000;
meterConfig->amperageMultiplier = server.arg(F("mma")).toFloat() * 1000;
meterConfig->accumulatedMultiplier = server.arg(F("mmc")).toFloat() * 1000;
config->setMeterConfig(*meterConfig);
meterConfig.wattageMultiplier = server.arg(F("mmw")).toFloat() * 1000;
meterConfig.voltageMultiplier = server.arg(F("mmv")).toFloat() * 1000;
meterConfig.amperageMultiplier = server.arg(F("mma")).toFloat() * 1000;
meterConfig.accumulatedMultiplier = server.arg(F("mmc")).toFloat() * 1000;
config->setMeterConfig(meterConfig);
}
if(server.hasArg(F("w")) && server.arg(F("w")) == F("true")) {
if(debugger->isActive(RemoteDebug::DEBUG)) debugger->printf_P(PSTR("Received WiFi config\n"));
WiFiConfig wifi;
config->getWiFiConfig(wifi);
strcpy(wifi.ssid, server.arg(F("ws")).c_str());
NetworkConfig network;
config->getNetworkConfig(network);
strcpy(network.ssid, server.arg(F("ws")).c_str());
String psk = server.arg(F("wp"));
if(!psk.equals("***")) {
strcpy(wifi.psk, psk.c_str());
strcpy(network.psk, psk.c_str());
}
wifi.power = server.arg(F("ww")).toFloat() * 10;
wifi.sleep = server.arg(F("wz")).toInt();
wifi.autoreboot = server.hasArg(F("wa")) && server.arg(F("wa")) == F("true");
wifi.use11b = server.hasArg(F("wb")) && server.arg(F("wb")) == F("true");
config->setWiFiConfig(wifi);
network.power = server.arg(F("ww")).toFloat() * 10;
network.sleep = server.arg(F("wz")).toInt();
network.use11b = server.hasArg(F("wb")) && server.arg(F("wb")) == F("true");
network.mode = server.arg(F("nc")).toInt();
if(network.mode > 3) network.mode = 1; // WiFi Client
config->setNetworkConfig(network);
if(server.hasArg(F("nm"))) {
if(server.arg(F("nm")) == "static") {
strcpy(wifi.ip, server.arg(F("ni")).c_str());
strcpy(wifi.gateway, server.arg(F("ng")).c_str());
strcpy(wifi.subnet, server.arg(F("ns")).c_str());
strcpy(wifi.dns1, server.arg(F("nd1")).c_str());
strcpy(wifi.dns2, server.arg(F("nd2")).c_str());
strcpy(network.ip, server.arg(F("ni")).c_str());
strcpy(network.gateway, server.arg(F("ng")).c_str());
strcpy(network.subnet, server.arg(F("ns")).c_str());
strcpy(network.dns1, server.arg(F("nd1")).c_str());
strcpy(network.dns2, server.arg(F("nd2")).c_str());
} else if(server.arg(F("nm")) == "dhcp") {
strcpy(wifi.ip, "");
strcpy(wifi.gateway, "");
strcpy(wifi.subnet, "");
strcpy(wifi.dns1, "");
strcpy(wifi.dns2, "");
strcpy(network.ip, "");
strcpy(network.gateway, "");
strcpy(network.subnet, "");
strcpy(network.dns1, "");
strcpy(network.dns2, "");
}
}
wifi.mdns = server.hasArg(F("nd")) && server.arg(F("nd")) == F("true");
config->setWiFiConfig(wifi);
network.mdns = server.hasArg(F("nd")) && server.arg(F("nd")) == F("true");
config->setNetworkConfig(network);
}
if(server.hasArg(F("ntp")) && server.arg(F("ntp")) == F("true")) {
@ -1353,12 +1416,12 @@ void AmsWebServer::handleSave() {
}
config->setWebConfig(webConfig);
WiFiConfig wifi;
config->getWiFiConfig(wifi);
NetworkConfig network;
config->getNetworkConfig(network);
if(server.hasArg(F("gh")) && !server.arg(F("gh")).isEmpty()) {
strcpy(wifi.hostname, server.arg(F("gh")).c_str());
strcpy(network.hostname, server.arg(F("gh")).c_str());
}
config->setWiFiConfig(wifi);
config->setNetworkConfig(network);
NtpConfig ntp;
config->getNtpConfig(ntp);
@ -1368,8 +1431,13 @@ void AmsWebServer::handleSave() {
if(server.hasArg(F("i")) && server.arg(F("i")) == F("true")) {
if(debugger->isActive(RemoteDebug::DEBUG)) debugger->printf_P(PSTR("Received GPIO config\n"));
gpioConfig->hanPin = server.hasArg(F("ihp")) && !server.arg(F("ihp")).isEmpty() ? server.arg(F("ihp")).toInt() : 3;
gpioConfig->hanPinPullup = server.hasArg(F("ihu")) && server.arg(F("ihu")) == F("true");
MeterConfig meterConfig;
config->getMeterConfig(meterConfig);
meterConfig.rxPin = server.hasArg(F("ihp")) && !server.arg(F("ihp")).isEmpty() ? server.arg(F("ihp")).toInt() : 3;
meterConfig.rxPinPullup = server.hasArg(F("ihu")) && server.arg(F("ihu")) == F("true");
meterConfig.txPin = server.hasArg(F("iht")) && !server.arg(F("iht")).isEmpty() ? server.arg(F("iht")).toInt() : 1;
config->setMeterConfig(meterConfig);
gpioConfig->ledPin = server.hasArg(F("ilp")) && !server.arg(F("ilp")).isEmpty() ? server.arg(F("ilp")).toInt() : 0xFF;
gpioConfig->ledInverted = server.hasArg(F("ili")) && server.arg(F("ili")) == F("true");
gpioConfig->ledPinRed = server.hasArg(F("irr")) && !server.arg(F("irr")).isEmpty() ? server.arg(F("irr")).toInt() : 0xFF;
@ -1382,6 +1450,12 @@ void AmsWebServer::handleSave() {
gpioConfig->vccPin = server.hasArg(F("ivp")) && !server.arg(F("ivp")).isEmpty() ? server.arg(F("ivp")).toInt() : 0xFF;
gpioConfig->vccResistorGnd = server.hasArg(F("ivdg")) && !server.arg(F("ivdg")).isEmpty() ? server.arg(F("ivdg")).toInt() : 0;
gpioConfig->vccResistorVcc = server.hasArg(F("ivdv")) && !server.arg(F("ivdv")).isEmpty() ? server.arg(F("ivdv")).toInt() : 0;
gpioConfig->ledDisablePin = server.hasArg(F("idd")) && !server.arg(F("idd")).isEmpty() ? server.arg(F("idd")).toInt() : 0;
config->setGpioConfig(*gpioConfig);
}
if(server.hasArg(F("idb"))) {
gpioConfig->ledBehaviour = server.hasArg(F("idb")) && !server.arg(F("idb")).isEmpty() ? server.arg(F("idb")).toInt() : 0;
config->setGpioConfig(*gpioConfig);
}
@ -1410,9 +1484,9 @@ void AmsWebServer::handleSave() {
debugger->setPassword(F(""));
}
debugger->setSerialEnabled(debug.serial);
WiFiConfig wifi;
if(config->getWiFiConfig(wifi) && strlen(wifi.hostname) > 0) {
debugger->begin(wifi.hostname, (uint8_t) debug.level);
NetworkConfig network;
if(config->getNetworkConfig(network) && strlen(network.hostname) > 0) {
debugger->begin(network.hostname, (uint8_t) debug.level);
if(!debug.telnet) {
debugger->stop();
}
@ -1437,6 +1511,8 @@ void AmsWebServer::handleSave() {
ui.showDayPlot = server.arg(F("ud")).toInt();
ui.showMonthPlot = server.arg(F("um")).toInt();
ui.showTemperaturePlot = server.arg(F("us")).toInt();
ui.showRealtimePlot = server.arg(F("ul")).toInt();
ui.darkMode = server.arg(F("uk")).toInt();
config->setUiConfig(ui);
}
@ -1471,11 +1547,38 @@ void AmsWebServer::handleSave() {
config->setEnergyAccountingConfig(eac);
}
if(server.hasArg(F("c")) && server.arg(F("c")) == F("true")) {
if(debugger->isActive(RemoteDebug::DEBUG)) debugger->printf_P(PSTR("Received cloud config\n"));
SystemConfig sys;
config->getSystemConfig(sys);
sys.energyspeedometer = server.hasArg(F("ces")) && server.arg(F("ces")) == F("true") ? 7 : 0;
config->setSystemConfig(sys);
CloudConfig cloud;
config->getCloudConfig(cloud);
cloud.enabled = server.hasArg(F("ce")) && server.arg(F("ce")) == F("true");
if(cloud.enabled) {
String host = server.arg("ch");
if(!host.isEmpty()) {
strcpy(cloud.hostname, host.c_str());
}
String clientId = server.arg(F("ci")).c_str();
if(!clientId.isEmpty()) {
strcpy(cloud.clientId, clientId.c_str());
}
String secret = server.arg(F("cs"));
if(!secret.isEmpty() && !secret.equals("***")) {
strcpy(cloud.clientSecret, secret.c_str());
}
}
config->setCloudConfig(cloud);
}
if(debugger->isActive(RemoteDebug::INFO)) debugger->printf_P(PSTR("Saving configuration now...\n"));
if (config->save()) {
if(debugger->isActive(RemoteDebug::INFO)) debugger->printf_P(PSTR("Successfully saved.\n"));
if(config->isWifiChanged() || performRestart) {
if(config->isNetworkConfigChanged() || performRestart) {
performRestart = true;
} else {
hw->setup(gpioConfig, config);
@ -1702,14 +1805,8 @@ HTTPUpload& AmsWebServer::uploadFile(const char* path) {
}
}
} else if(upload.status == UPLOAD_FILE_WRITE) {
if(debugger->isActive(RemoteDebug::DEBUG)) {
debugger->printf_P(PSTR("handleFileUpload Writing: %u\n"), upload.currentSize);
}
if(file) {
size_t written = file.write(upload.buf, upload.currentSize);
if(debugger->isActive(RemoteDebug::DEBUG)) {
debugger->printf_P(PSTR("handleFileUpload Written: %u\n"), written);
}
delay(1);
if(written != upload.currentSize) {
file.flush();
@ -1942,6 +2039,30 @@ void AmsWebServer::tariffJson() {
server.send(200, MIME_JSON, buf);
}
void AmsWebServer::realtimeJson() {
if(debugger->isActive(RemoteDebug::DEBUG)) debugger->printf_P(PSTR("Serving /realtime.json over http...\n"));
if(rtp == NULL) {
server.send_P(500, MIME_PLAIN, PSTR("500: Not available"));
return;
}
server.sendHeader(HEADER_CACHE_CONTROL, CACHE_CONTROL_NO_CACHE);
server.sendHeader(HEADER_PRAGMA, PRAGMA_NO_CACHE);
server.sendHeader(HEADER_EXPIRES, EXPIRES_OFF);
server.setContentLength(CONTENT_LENGTH_UNKNOWN);
snprintf_P(buf, BufferSize, PSTR("{\"size\":\"%d\",\"data\":["), rtp->getSize());
server.send(200, MIME_JSON, buf);
bool first = true;
for(uint16_t i = 0; i < rtp->getSize(); i++) {
snprintf_P(buf, BufferSize, PSTR("%s%d"), first ? "" : ",", rtp->getValue(i));
server.sendContent(buf);
first = false;
}
snprintf_P(buf, BufferSize, PSTR("]}"));
server.sendContent(buf);
}
void AmsWebServer::setPriceSettings(String region, String currency) {
this->priceRegion = region;
this->priceCurrency = currency;
@ -1977,20 +2098,21 @@ void AmsWebServer::configFileDownload() {
server.sendContent(buf, snprintf_P(buf, BufferSize, PSTR("boardType %d\n"), sys.boardType));
if(includeWifi) {
WiFiConfig wifi;
config->getWiFiConfig(wifi);
if(includeSecrets) server.sendContent(buf, snprintf_P(buf, BufferSize, PSTR("hostname %s\n"), wifi.hostname));
if(includeSecrets) server.sendContent(buf, snprintf_P(buf, BufferSize, PSTR("ssid %s\n"), wifi.ssid));
if(includeSecrets) server.sendContent(buf, snprintf_P(buf, BufferSize, PSTR("psk %s\n"), wifi.psk));
if(strlen(wifi.ip) > 0) {
server.sendContent(buf, snprintf_P(buf, BufferSize, PSTR("ip %s\n"), wifi.ip));
if(strlen(wifi.gateway) > 0) server.sendContent(buf, snprintf_P(buf, BufferSize, PSTR("gateway %s\n"), wifi.gateway));
if(strlen(wifi.subnet) > 0) server.sendContent(buf, snprintf_P(buf, BufferSize, PSTR("subnet %s\n"), wifi.subnet));
if(strlen(wifi.dns1) > 0) server.sendContent(buf, snprintf_P(buf, BufferSize, PSTR("dns1 %s\n"), wifi.dns1));
if(strlen(wifi.dns2) > 0) server.sendContent(buf, snprintf_P(buf, BufferSize, PSTR("dns2 %s\n"), wifi.dns2));
NetworkConfig network;
config->getNetworkConfig(network);
server.sendContent(buf, snprintf_P(buf, BufferSize, PSTR("netmode %d\n"), network.mode));
server.sendContent(buf, snprintf_P(buf, BufferSize, PSTR("hostname %s\n"), network.hostname));
if(includeSecrets) server.sendContent(buf, snprintf_P(buf, BufferSize, PSTR("ssid %s\n"), network.ssid));
if(includeSecrets) server.sendContent(buf, snprintf_P(buf, BufferSize, PSTR("psk %s\n"), network.psk));
if(strlen(network.ip) > 0) {
server.sendContent(buf, snprintf_P(buf, BufferSize, PSTR("ip %s\n"), network.ip));
if(strlen(network.gateway) > 0) server.sendContent(buf, snprintf_P(buf, BufferSize, PSTR("gateway %s\n"), network.gateway));
if(strlen(network.subnet) > 0) server.sendContent(buf, snprintf_P(buf, BufferSize, PSTR("subnet %s\n"), network.subnet));
if(strlen(network.dns1) > 0) server.sendContent(buf, snprintf_P(buf, BufferSize, PSTR("dns1 %s\n"), network.dns1));
if(strlen(network.dns2) > 0) server.sendContent(buf, snprintf_P(buf, BufferSize, PSTR("dns2 %s\n"), network.dns2));
}
server.sendContent(buf, snprintf_P(buf, BufferSize, PSTR("mdns %d\n"), wifi.mdns ? 1 : 0));
server.sendContent(buf, snprintf_P(buf, BufferSize, PSTR("use11b %d\n"), wifi.use11b ? 1 : 0));
server.sendContent(buf, snprintf_P(buf, BufferSize, PSTR("mdns %d\n"), network.mdns ? 1 : 0));
server.sendContent(buf, snprintf_P(buf, BufferSize, PSTR("use11b %d\n"), network.use11b ? 1 : 0));
}
if(includeMqtt) {
@ -2065,10 +2187,12 @@ void AmsWebServer::configFileDownload() {
}
if(includeGpio) {
MeterConfig meter;
config->getMeterConfig(meter);
GpioConfig gpio;
config->getGpioConfig(gpio);
if(gpio.hanPin != 0xFF) server.sendContent(buf, snprintf_P(buf, BufferSize, PSTR("gpioHanPin %d\n"), gpio.hanPin));
if(gpio.hanPin != 0xFF) server.sendContent(buf, snprintf_P(buf, BufferSize, PSTR("gpioHanPinPullup %d\n"), gpio.hanPinPullup ? 1 : 0));
if(meter.rxPin != 0xFF) server.sendContent(buf, snprintf_P(buf, BufferSize, PSTR("gpioHanPin %d\n"), meter.rxPin));
if(meter.rxPin != 0xFF) server.sendContent(buf, snprintf_P(buf, BufferSize, PSTR("gpioHanPinPullup %d\n"), meter.rxPinPullup ? 1 : 0));
if(gpio.apPin != 0xFF) server.sendContent(buf, snprintf_P(buf, BufferSize, PSTR("gpioApPin %d\n"), gpio.apPin));
if(gpio.ledPin != 0xFF) server.sendContent(buf, snprintf_P(buf, BufferSize, PSTR("gpioLedPin %d\n"), gpio.ledPin));
if(gpio.ledPin != 0xFF) server.sendContent(buf, snprintf_P(buf, BufferSize, PSTR("gpioLedInverted %d\n"), gpio.ledInverted ? 1 : 0));

View File

@ -1,3 +1,9 @@
/**
* @copyright Utilitech AS 2023
* License: Fair Source
*
*/
#ifndef _UPTIME_H
#define _UPTIME_H

View File

@ -1,3 +1,9 @@
/**
* @copyright Utilitech AS 2023
* License: Fair Source
*
*/
#include "Uptime.h"
uint32_t _uptime_last_value = 0;

View File

@ -2,7 +2,7 @@
extra_configs = platformio-user.ini
[common]
lib_deps = EEPROM, LittleFS, DNSServer, https://github.com/256dpi/arduino-mqtt.git, OneWireNg@0.10.0, DallasTemperature@3.9.1, EspSoftwareSerial@6.14.1, https://github.com/gskjold/RemoteDebug.git, Time@1.6.1, Timezone@1.2.4, FirmwareVersion, AmsConfiguration, AmsData, AmsDataStorage, HwTools, Uptime, AmsDecoder, EntsoePriceApi, EnergyAccounting, RawMqttHandler, JsonMqttHandler, DomoticzMqttHandler, HomeAssistantMqttHandler, SvelteUi
lib_deps = EEPROM, LittleFS, DNSServer, https://github.com/256dpi/arduino-mqtt.git, OneWireNg@0.10.0, DallasTemperature@3.9.1, EspSoftwareSerial@6.14.1, https://github.com/gskjold/RemoteDebug.git, Time@1.6.1, Timezone@1.2.4, FirmwareVersion, AmsConfiguration, AmsData, AmsDataStorage, HwTools, Uptime, AmsDecoder, EntsoePriceApi, EnergyAccounting, AmsMqttHandler, RawMqttHandler, JsonMqttHandler, DomoticzMqttHandler, HomeAssistantMqttHandler, RealtimePlot, SvelteUi
lib_ignore = OneWire
extra_scripts =
pre:scripts/addversion.py
@ -17,7 +17,7 @@ build_flags =
-fexceptions
[esp32]
lib_deps = WiFi, ESPmDNS, WiFiClientSecure, HTTPClient, FS, Update, HTTPUpdate, WebServer, ${common.lib_deps}
lib_deps = WiFi, Ethernet, ESPmDNS, WiFiClientSecure, HTTPClient, FS, Update, HTTPUpdate, WebServer, ${common.lib_deps}, CloudConnector
[env:esp8266]
platform = espressif8266@4.2.0

File diff suppressed because it is too large Load Diff

View File

@ -1,39 +0,0 @@
#ifndef _AMSTOMQTTBRIDGE_H
#define _AMSTOMQTTBRIDGE_H
#define WIFI_CONNECTION_TIMEOUT 30000
#define INVALID_BUTTON_PIN 0xFFFFFFFF
#define MAX_PEM_SIZE 4096
#define METER_SOURCE_NONE 0
#define METER_SOURCE_SERIAL 1
#define METER_SOURCE_MQTT 2
#define METER_SOURCE_ESPNOW 3
#define METER_ERROR_NO_DATA 90
#define METER_ERROR_BREAK 91
#define METER_ERROR_BUFFER 92
#define METER_ERROR_FIFO 93
#define METER_ERROR_FRAME 94
#define METER_ERROR_PARITY 95
#define METER_ERROR_RX 96
#define METER_ERROR_EXCEPTION 98
#define METER_ERROR_AUTODETECT 99
#include <SoftwareSerial.h>
#if defined(ESP8266)
#include <ESP8266WiFi.h>
#include <ESP8266mDNS.h>
#elif defined(ESP32)
#include <WiFi.h>
#include <WiFiClientSecure.h>
#include <ESPmDNS.h>
#include "Update.h"
#endif
#include "LittleFS.h"
#endif

41
src/ConnectionHandler.h Normal file
View File

@ -0,0 +1,41 @@
/**
* @copyright Utilitech AS 2023
* License: Fair Source
*
*/
#ifndef _CONNECTIONHANDLER_H
#define _CONNECTIONHANDLER_H
#include "AmsConfiguration.h"
#if defined(ESP8266)
#include <ESP8266WiFi.h>
#elif defined(ESP32)
#include <WiFi.h>
#endif
#define NETWORK_MODE_WIFI_CLIENT 1
#define NETWORK_MODE_WIFI_AP 2
#define NETWORK_MODE_ETH_CLIENT 3
class ConnectionHandler {
public:
virtual ~ConnectionHandler() {};
virtual bool connect(NetworkConfig config, SystemConfig sys);
virtual void disconnect(unsigned long reconnectDelay);
virtual bool isConnected();
virtual bool isConfigChanged();
virtual void getCurrentConfig(NetworkConfig& networkConfig);
#if defined(ESP32)
virtual void eventHandler(WiFiEvent_t event, WiFiEventInfo_t info);
#endif
uint8_t getMode() {
return this->mode;
}
protected:
uint8_t mode;
};
#endif

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