Zmartcharge support (#1007)

* ZC initial implementation

* ZmartCharge

* Fixed zc bug

* Adjustments to ZmartCharge connection
This commit is contained in:
Gunnar Skjold 2025-09-25 11:38:05 +02:00 committed by GitHub
parent 633671851e
commit e5d260ae3e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 315 additions and 13 deletions

View File

@ -13,7 +13,6 @@
#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_NETWORK_START 40
@ -30,6 +29,7 @@
#define CONFIG_UI_START 1720
#define CONFIG_CLOUD_START 1742
#define CONFIG_UPGRADE_INFO_START 1934
#define CONFIG_ZC_START 2000
#define CONFIG_METER_START_103 32
#define CONFIG_UPGRADE_INFO_START_103 216
@ -254,6 +254,12 @@ struct CloudConfig {
uint8_t proto;
}; // 88
struct ZmartChargeConfig {
bool enabled;
char token[21];
char baseUrl[64];
}; // 86
class AmsConfiguration {
public:
bool hasConfig();
@ -347,6 +353,13 @@ public:
void clearCloudConfig(CloudConfig&);
bool isCloudChanged();
void ackCloudConfig();
bool getZmartChargeConfig(ZmartChargeConfig&);
bool setZmartChargeConfig(ZmartChargeConfig&);
void clearZmartChargeConfig(ZmartChargeConfig&);
bool isZmartChargeConfigChanged();
void ackZmartChargeConfig();
void clear();
@ -355,7 +368,7 @@ protected:
private:
uint8_t configVersion = 0;
bool sysChanged = false, networkChanged = false, mqttChanged = false, webChanged = false, meterChanged = true, ntpChanged = true, priceChanged = false, energyAccountingChanged = true, cloudChanged = true, uiLanguageChanged = false;
bool sysChanged = false, networkChanged = false, mqttChanged = false, webChanged = false, meterChanged = true, ntpChanged = true, priceChanged = false, energyAccountingChanged = true, cloudChanged = true, uiLanguageChanged = false, zcChanged = true;
bool relocateConfig103(); // 2.2.12, until, but not including 2.3

View File

@ -896,6 +896,65 @@ void AmsConfiguration::ackCloudConfig() {
cloudChanged = false;
}
bool AmsConfiguration::getZmartChargeConfig(ZmartChargeConfig& config) {
if(hasConfig()) {
EEPROM.begin(EEPROM_SIZE);
EEPROM.get(CONFIG_ZC_START, config);
EEPROM.end();
stripNonAscii((uint8_t*) config.token, 21);
stripNonAscii((uint8_t*) config.baseUrl, 64);
if(strncmp_P(config.token, PSTR(" "), 1) == 0) {
config.enabled = false;
memset(config.token, 0, 64);
memset(config.baseUrl, 0, 64);
}
if(strncmp_P(config.baseUrl, PSTR("https"), 5) != 0) {
memset(config.baseUrl, 0, 64);
snprintf_P(config.baseUrl, 64, PSTR("https://main.zmartcharge.com/api"));
}
return true;
} else {
clearZmartChargeConfig(config);
return false;
}
}
bool AmsConfiguration::setZmartChargeConfig(ZmartChargeConfig& config) {
ZmartChargeConfig existing;
if(getZmartChargeConfig(existing)) {
zcChanged |= config.enabled != existing.enabled;
zcChanged |= memcmp(config.token, existing.token, 21) != 0;
zcChanged |= memcmp(config.token, existing.baseUrl, 64) != 0;
} else {
zcChanged = true;
}
stripNonAscii((uint8_t*) config.token, 21);
stripNonAscii((uint8_t*) config.baseUrl, 64);
if(strncmp_P(config.baseUrl, PSTR("https"), 5) != 0) {
memset(config.baseUrl, 0, 64);
}
EEPROM.begin(EEPROM_SIZE);
EEPROM.put(CONFIG_ZC_START, config);
bool ret = EEPROM.commit();
EEPROM.end();
return ret;
}
void AmsConfiguration::clearZmartChargeConfig(ZmartChargeConfig& config) {
config.enabled = false;
memset(config.token, 0, 21);
}
bool AmsConfiguration::isZmartChargeConfigChanged() {
return zcChanged;
}
void AmsConfiguration::ackZmartChargeConfig() {
zcChanged = false;
}
void AmsConfiguration::setUiLanguageChanged() {
uiLanguageChanged = true;
}
@ -1097,6 +1156,10 @@ bool AmsConfiguration::relocateConfig103() {
clearCloudConfig(cloud);
EEPROM.put(CONFIG_CLOUD_START, cloud);
ZmartChargeConfig zcc;
clearZmartChargeConfig(zcc);
EEPROM.put(CONFIG_ZC_START, zcc);
EEPROM.put(EEPROM_CONFIG_ADDRESS, 104);
bool ret = EEPROM.commit();
EEPROM.end();

File diff suppressed because one or more lines are too long

View File

@ -757,6 +757,16 @@
{/if}
{/if}
</div>
{#if sysinfo?.features?.includes('zc')}
<div class="my-1">
<label><input type="checkbox" name="cze" value="true" bind:checked={configuration.c.ze} class="rounded mb-1"/> ZmartCharge</label>
</div>
{#if configuration.c.ze}
<div class="my-1">
<input name="czt" bind:value={configuration.c.zt} type="text" class="in-s" placeholder="ZmartCharge token"/>
</div>
{/if}
{/if}
</div>
{/if}
{#if configuration?.p?.r?.startsWith("NO") || configuration?.p?.r?.startsWith("10YNO") || configuration?.p?.r?.startsWith('10Y1001A1001A4')}

View File

@ -1,5 +1,7 @@
"c": {
"e" : %s,
"p" : %d,
"es": %s
"es": %s,
"ze": %s,
"zt" : "%s"
}

View File

@ -397,6 +397,10 @@ void AmsWebServer::sysinfoJson() {
if(!features.isEmpty()) features += ",";
features += "\"cloud\"";
#endif
#if defined(ZMART_CHARGE)
if(!features.isEmpty()) features += ",";
features += "\"zc\"";
#endif
int size = snprintf_P(buf, BufferSize, SYSINFO_JSON,
FirmwareVersion::VersionString,
@ -879,6 +883,9 @@ void AmsWebServer::configurationJson() {
config->getHomeAssistantConfig(haconf);
CloudConfig cloud;
config->getCloudConfig(cloud);
ZmartChargeConfig zcc;
config->getZmartChargeConfig(zcc);
stripNonAscii((uint8_t*) zcc.token, 21);
bool qsc = false;
bool qsr = false;
@ -1058,10 +1065,12 @@ void AmsWebServer::configurationJson() {
cloud.enabled ? "true" : "false",
cloud.proto,
#if defined(ESP32) && defined(ENERGY_SPEEDOMETER_PASS)
sysConfig.energyspeedometer == 7 ? "true" : "false"
sysConfig.energyspeedometer == 7 ? "true" : "false",
#else
"null"
"null",
#endif
zcc.enabled ? "true" : "false",
zcc.token
);
server.sendContent(buf);
server.sendContent_P(PSTR("}"));
@ -1578,6 +1587,16 @@ void AmsWebServer::handleSave() {
cloud.enabled = server.hasArg(F("ce")) && server.arg(F("ce")) == F("true");
cloud.proto = server.arg(F("cp")).toInt();
config->setCloudConfig(cloud);
ZmartChargeConfig zcc;
config->getZmartChargeConfig(zcc);
zcc.enabled = server.hasArg(F("cze")) && server.arg(F("cze")) == F("true");
String token = server.arg(F("czt"));
strcpy(zcc.token, token.c_str());
if(server.hasArg(F("czu")) && server.arg(F("czu")).startsWith(F("https"))) {
strcpy(zcc.baseUrl, server.arg(F("czu")).c_str());
}
config->setZmartChargeConfig(zcc);
}
if(server.hasArg(F("r")) && server.arg(F("r")) == F("true")) {

View File

@ -0,0 +1,52 @@
#pragma once
#include "RemoteDebug.h"
#include "AmsData.h"
#include "FirmwareVersion.h"
#if defined(ESP8266)
#include <ESP8266HTTPClient.h>
#elif defined(ESP32) // ARDUINO_ARCH_ESP32
#include <HTTPClient.h>
#else
#warning "Unsupported board type"
#endif
static const char ZC_LB_JSON[] PROGMEM = "{\"LBToken\":\"%s\",\"L1A\":\"%.1f\",\"L2A\":\"%.1f\",\"L3A\":\"%.1f\",\"HighConsumption\":\"%d\",\"CurrentPower\":\"%d\"}";
class ZmartChargeCloudConnector {
public:
ZmartChargeCloudConnector(RemoteDebug* debugger, char* buf) {
this->debugger = debugger;
this->json = buf;
http = new HTTPClient();
http->setFollowRedirects(HTTPC_STRICT_FOLLOW_REDIRECTS);
http->setReuse(false);
http->setTimeout(10000);
http->setUserAgent("ams2mqtt/" + String(FirmwareVersion::VersionString));
};
void setup(const char* baseUrl, const char* token);
void update(AmsData& data);
bool isConfigChanged();
void ackConfigChanged();
const char* getBaseUrl();
private:
RemoteDebug* debugger;
char baseUrl[64];
char token[21];
uint16_t BufferSize = 2048;
char* json;
bool configChanged = false;
bool lastFailed = false;
uint64_t lastUpdate = 0;
HTTPClient* http = NULL;
uint16_t heartbeat = 30;
uint16_t heartbeatFast = 10;
uint16_t heartbeatFastThreshold = 32;
};

View File

@ -0,0 +1,100 @@
#include "ZmartChargeCloudConnector.h"
#include "Uptime.h"
#include "ArduinoJson.h"
void ZmartChargeCloudConnector::setup(const char* baseUrl, const char* token) {
memset(this->baseUrl, 0, 64);
memset(this->token, 0, 21);
strcpy(this->baseUrl, baseUrl);
strcpy(this->token, token);
}
bool ZmartChargeCloudConnector::isConfigChanged() {
return configChanged;
}
void ZmartChargeCloudConnector::ackConfigChanged() {
configChanged = false;
}
const char* ZmartChargeCloudConnector::getBaseUrl() {
return baseUrl;
}
void ZmartChargeCloudConnector::update(AmsData& data) {
if(strlen(token) == 0) return;
uint64_t now = millis64();
float maximum = max(max(data.getL1Current(), data.getL1Current()), data.getL3Current());
bool fast = maximum > heartbeatFastThreshold;
if(now - lastUpdate < (fast ? heartbeatFast : heartbeat) * 1000) return;
if(strlen(token) != 20) {
lastUpdate = now;
if(debugger->isActive(RemoteDebug::WARNING)) debugger->printf_P(PSTR("(ZmartCharge) Token defined, but is incorrect length (%s, %d)\n"), token, strlen(token));
return;
}
if(((now - lastUpdate) / 1000) > (fast || lastFailed ? heartbeatFast : heartbeat)) {
if(debugger->isActive(RemoteDebug::DEBUG)) debugger->printf_P(PSTR("(ZmartCharge) Preparing to update cloud\n"));
memset(json, 0, BufferSize);
snprintf_P(json, BufferSize, ZC_LB_JSON,
token,
data.getL1Current(),
data.getL2Current(),
data.getL3Current(),
fast ? 1 : 0,
data.getActiveImportPower()
);
lastFailed = true;
char url[128];
memset(url, 0, 128);
snprintf_P(url, 128, PSTR("%s/loadbalancer"), baseUrl);
if(debugger->isActive(RemoteDebug::VERBOSE)) debugger->printf_P(PSTR("(ZmartCharge) Connecting to: %s\n"), baseUrl);
if(http->begin(url)) {
if(debugger->isActive(RemoteDebug::VERBOSE)) debugger->printf_P(PSTR("(ZmartCharge) Sending data: %s\n"), json);
int status = http->POST(json);
if(status == 200) {
lastFailed = false;
JsonDocument doc;
String body = http->getString();
if(debugger->isActive(RemoteDebug::VERBOSE)) debugger->printf_P(PSTR("(ZmartCharge) Received data: %s\n"), body.c_str());
deserializeJson(doc, body);
if(doc.containsKey("Settings")) {
if(doc["Settings"].containsKey("HeartBeatTime")) {
heartbeat = doc["Settings"]["HeartBeatTime"].as<long>();
}
if(doc["Settings"].containsKey("HearBeatTimeFast")) {
heartbeatFast = doc["Settings"]["HearBeatTimeFast"].as<long>();
}
if(doc["Settings"].containsKey("HeartBeatTimeFastThreshold")) {
heartbeatFastThreshold = doc["Settings"]["HeartBeatTimeFastThreshold"].as<long>();
}
if(doc["Settings"].containsKey("ZmartChargeUrl")) {
String newBaseUrl = doc["Settings"]["ZmartChargeUrl"].as<String>();
if(newBaseUrl.startsWith("https:") && strncmp(newBaseUrl.c_str(), baseUrl, strlen(baseUrl)) != 0) {
newBaseUrl.replace("\\/", "/");
if(debugger->isActive(RemoteDebug::INFO)) debugger->printf_P(PSTR("(ZmartCharge) Received new URL: %s\n"), newBaseUrl.c_str());
memset(baseUrl, 0, 64);
memcpy(baseUrl, newBaseUrl.c_str(), strlen(newBaseUrl.c_str()));
configChanged = true;
}
}
}
http->end();
} else {
if(debugger->isActive(RemoteDebug::ERROR)) debugger->printf_P(PSTR("(ZmartCharge) 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();
}
} else {
if(debugger->isActive(RemoteDebug::ERROR)) debugger->printf_P(PSTR("(ZmartCharge) Unable to establish connection with cloud\n"));
}
lastUpdate = now;
}
}

View File

@ -2,7 +2,7 @@
extra_configs = platformio-user.ini
[common]
lib_deps = EEPROM, LittleFS, DNSServer, 256dpi/MQTT@2.5.2, OneWireNg@0.13.3, DallasTemperature@4.0.4, https://github.com/gskjold/RemoteDebug.git, PaulStoffregen/Time@1.6.1, JChristensen/Timezone@1.2.4, FirmwareVersion, AmsConfiguration, AmsData, AmsDataStorage, HwTools, Uptime, AmsDecoder, PriceService, EnergyAccounting, AmsFirmwareUpdater, AmsMqttHandler, RawMqttHandler, JsonMqttHandler, DomoticzMqttHandler, HomeAssistantMqttHandler, PassthroughMqttHandler, RealtimePlot, ConnectionHandler, MeterCommunicators
lib_deps = EEPROM, LittleFS, DNSServer, 256dpi/MQTT@2.5.2, OneWireNg@0.13.3, DallasTemperature@4.0.4, https://github.com/gskjold/RemoteDebug.git, PaulStoffregen/Time@1.6.1, JChristensen/Timezone@1.2.4, bblanchon/ArduinoJson@7.0.4, FirmwareVersion, AmsConfiguration, AmsData, AmsDataStorage, HwTools, Uptime, AmsDecoder, PriceService, EnergyAccounting, AmsFirmwareUpdater, AmsMqttHandler, RawMqttHandler, JsonMqttHandler, DomoticzMqttHandler, HomeAssistantMqttHandler, PassthroughMqttHandler, RealtimePlot, ConnectionHandler, MeterCommunicators
lib_ignore = OneWire
extra_scripts =
pre:scripts/addversion.py
@ -19,7 +19,7 @@ build_flags =
-fexceptions
[esp32]
lib_deps = WiFi, Ethernet, ESPmDNS, WiFiClientSecure, HTTPClient, FS, WebServer, ESP32 Async UDP, ESP32SSDP, mulmer89/ESPRandom@1.5.0, ${common.lib_deps}, CloudConnector, SvelteUi
lib_deps = WiFi, Ethernet, ESPmDNS, WiFiClientSecure, HTTPClient, FS, WebServer, ESP32 Async UDP, ESP32SSDP, mulmer89/ESPRandom@1.5.0, ${common.lib_deps}, CloudConnector, ZmartCharge, SvelteUi
[env:esp8266]
platform = espressif8266@4.2.1
@ -44,6 +44,7 @@ build_flags =
-D AMS_REMOTE_DEBUG=1
-D AMS_CLOUD=1
-D AMS_KMP=1
-D ZMART_CHARGE=1
-L precompiled/esp32
-lKmpTalker
lib_ldf_mode = off
@ -70,6 +71,7 @@ build_flags =
-D AMS_REMOTE_DEBUG=1
-D AMS_CLOUD=1
-D AMS_KMP=1
-D ZMART_CHARGE=1
-L precompiled/esp32s2
-lKmpTalker
lib_ldf_mode = off
@ -90,6 +92,7 @@ build_flags =
-D AMS_REMOTE_DEBUG=1
-D AMS_CLOUD=1
-D AMS_KMP=1
-D ZMART_CHARGE=1
-L precompiled/esp32
-lKmpTalker
lib_ldf_mode = off
@ -109,6 +112,7 @@ build_flags =
-D AMS_REMOTE_DEBUG=1
-D AMS_CLOUD=1
-D AMS_KMP=1
-D ZMART_CHARGE=1
-L precompiled/esp32c3
-lKmpTalker
lib_ldf_mode = off
@ -127,6 +131,7 @@ build_flags =
-D AMS_REMOTE_DEBUG=1
-D AMS_CLOUD=1
-D AMS_KMP=1
-D ZMART_CHARGE=1
-L precompiled/esp32s3
-lKmpTalker
lib_ldf_mode = off

View File

@ -26,6 +26,9 @@ ADC_MODE(ADC_VCC);
#if defined(AMS_CLOUD)
#include "CloudConnector.h"
#endif
#if defined(ZMART_CHARGE)
#include "ZmartChargeCloudConnector.h"
#endif
#define WDT_TIMEOUT 120
#if defined(SLOW_PROC_TRIGGER_MS)
@ -182,6 +185,11 @@ AmsFirmwareUpdater updater(&Debug, &hw, &meterState);
AmsDataStorage ds(&Debug);
#if defined(_CLOUDCONNECTOR_H)
CloudConnector *cloud = NULL;
#endif
#if defined(ZMART_CHARGE)
ZmartChargeCloudConnector *zcloud = NULL;
#endif
#if defined(ESP32)
__NOINIT_ATTR EnergyAccountingRealtimeData rtd;
#else
EnergyAccountingRealtimeData rtd;
@ -694,6 +702,36 @@ void loop() {
cloud->update(meterState, ea);
}
#endif
#if defined(ZMART_CHARGE)
if(config.isZmartChargeConfigChanged()) {
ZmartChargeConfig zcc;
if(config.getZmartChargeConfig(zcc) && zcc.enabled) {
if(zcloud == NULL) {
zcloud = new ZmartChargeCloudConnector(&Debug, (char*) commonBuffer);
}
zcloud->setup(zcc.baseUrl, zcc.token);
} else if(zcloud != NULL) {
delete zcloud;
zcloud = NULL;
}
config.ackZmartChargeConfig();
}
if(zcloud != NULL) {
zcloud->update(meterState);
if(zcloud->isConfigChanged()) {
ZmartChargeConfig zcc;
if(config.getZmartChargeConfig(zcc)) {
const char* newBaseUrl = zcloud->getBaseUrl();
memset(zcc.baseUrl, 0, 64);
memcpy(zcc.baseUrl, newBaseUrl, strlen(newBaseUrl));
config.setZmartChargeConfig(zcc);
config.ackZmartChargeConfig();
}
zcloud->ackConfigChanged();
}
}
#endif
start = millis();
handleUiLanguage();
end = millis();