Skip to content

Commit e5d260a

Browse files
authored
Zmartcharge support (#1007)
* ZC initial implementation * ZmartCharge * Fixed zc bug * Adjustments to ZmartCharge connection
1 parent 6336718 commit e5d260a

File tree

10 files changed

+315
-13
lines changed

10 files changed

+315
-13
lines changed

lib/AmsConfiguration/include/AmsConfiguration.h

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@
1313
#define EEPROM_CHECK_SUM 104 // Used to check if config is stored. Change if structure changes
1414
#define EEPROM_CLEARED_INDICATOR 0xFC
1515
#define EEPROM_CONFIG_ADDRESS 0
16-
#define EEPROM_TEMP_CONFIG_ADDRESS 2048
1716

1817
#define CONFIG_SYSTEM_START 8
1918
#define CONFIG_NETWORK_START 40
@@ -30,6 +29,7 @@
3029
#define CONFIG_UI_START 1720
3130
#define CONFIG_CLOUD_START 1742
3231
#define CONFIG_UPGRADE_INFO_START 1934
32+
#define CONFIG_ZC_START 2000
3333

3434
#define CONFIG_METER_START_103 32
3535
#define CONFIG_UPGRADE_INFO_START_103 216
@@ -254,6 +254,12 @@ struct CloudConfig {
254254
uint8_t proto;
255255
}; // 88
256256

257+
struct ZmartChargeConfig {
258+
bool enabled;
259+
char token[21];
260+
char baseUrl[64];
261+
}; // 86
262+
257263
class AmsConfiguration {
258264
public:
259265
bool hasConfig();
@@ -347,6 +353,13 @@ class AmsConfiguration {
347353
void clearCloudConfig(CloudConfig&);
348354
bool isCloudChanged();
349355
void ackCloudConfig();
356+
357+
bool getZmartChargeConfig(ZmartChargeConfig&);
358+
bool setZmartChargeConfig(ZmartChargeConfig&);
359+
void clearZmartChargeConfig(ZmartChargeConfig&);
360+
bool isZmartChargeConfigChanged();
361+
void ackZmartChargeConfig();
362+
350363

351364
void clear();
352365

@@ -355,7 +368,7 @@ class AmsConfiguration {
355368
private:
356369
uint8_t configVersion = 0;
357370

358-
bool sysChanged = false, networkChanged = false, mqttChanged = false, webChanged = false, meterChanged = true, ntpChanged = true, priceChanged = false, energyAccountingChanged = true, cloudChanged = true, uiLanguageChanged = false;
371+
bool sysChanged = false, networkChanged = false, mqttChanged = false, webChanged = false, meterChanged = true, ntpChanged = true, priceChanged = false, energyAccountingChanged = true, cloudChanged = true, uiLanguageChanged = false, zcChanged = true;
359372

360373
bool relocateConfig103(); // 2.2.12, until, but not including 2.3
361374

lib/AmsConfiguration/src/AmsConfiguration.cpp

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -896,6 +896,65 @@ void AmsConfiguration::ackCloudConfig() {
896896
cloudChanged = false;
897897
}
898898

899+
bool AmsConfiguration::getZmartChargeConfig(ZmartChargeConfig& config) {
900+
if(hasConfig()) {
901+
EEPROM.begin(EEPROM_SIZE);
902+
EEPROM.get(CONFIG_ZC_START, config);
903+
EEPROM.end();
904+
stripNonAscii((uint8_t*) config.token, 21);
905+
stripNonAscii((uint8_t*) config.baseUrl, 64);
906+
if(strncmp_P(config.token, PSTR(" "), 1) == 0) {
907+
config.enabled = false;
908+
memset(config.token, 0, 64);
909+
memset(config.baseUrl, 0, 64);
910+
}
911+
if(strncmp_P(config.baseUrl, PSTR("https"), 5) != 0) {
912+
memset(config.baseUrl, 0, 64);
913+
snprintf_P(config.baseUrl, 64, PSTR("https://main.zmartcharge.com/api"));
914+
}
915+
return true;
916+
} else {
917+
clearZmartChargeConfig(config);
918+
return false;
919+
}
920+
}
921+
922+
bool AmsConfiguration::setZmartChargeConfig(ZmartChargeConfig& config) {
923+
ZmartChargeConfig existing;
924+
if(getZmartChargeConfig(existing)) {
925+
zcChanged |= config.enabled != existing.enabled;
926+
zcChanged |= memcmp(config.token, existing.token, 21) != 0;
927+
zcChanged |= memcmp(config.token, existing.baseUrl, 64) != 0;
928+
} else {
929+
zcChanged = true;
930+
}
931+
932+
stripNonAscii((uint8_t*) config.token, 21);
933+
stripNonAscii((uint8_t*) config.baseUrl, 64);
934+
if(strncmp_P(config.baseUrl, PSTR("https"), 5) != 0) {
935+
memset(config.baseUrl, 0, 64);
936+
}
937+
938+
EEPROM.begin(EEPROM_SIZE);
939+
EEPROM.put(CONFIG_ZC_START, config);
940+
bool ret = EEPROM.commit();
941+
EEPROM.end();
942+
return ret;
943+
}
944+
945+
void AmsConfiguration::clearZmartChargeConfig(ZmartChargeConfig& config) {
946+
config.enabled = false;
947+
memset(config.token, 0, 21);
948+
}
949+
950+
bool AmsConfiguration::isZmartChargeConfigChanged() {
951+
return zcChanged;
952+
}
953+
954+
void AmsConfiguration::ackZmartChargeConfig() {
955+
zcChanged = false;
956+
}
957+
899958
void AmsConfiguration::setUiLanguageChanged() {
900959
uiLanguageChanged = true;
901960
}
@@ -1097,6 +1156,10 @@ bool AmsConfiguration::relocateConfig103() {
10971156
clearCloudConfig(cloud);
10981157
EEPROM.put(CONFIG_CLOUD_START, cloud);
10991158

1159+
ZmartChargeConfig zcc;
1160+
clearZmartChargeConfig(zcc);
1161+
EEPROM.put(CONFIG_ZC_START, zcc);
1162+
11001163
EEPROM.put(EEPROM_CONFIG_ADDRESS, 104);
11011164
bool ret = EEPROM.commit();
11021165
EEPROM.end();

lib/SvelteUi/app/dist/index.js

Lines changed: 6 additions & 6 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

lib/SvelteUi/app/src/lib/ConfigurationPanel.svelte

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -757,6 +757,16 @@
757757
{/if}
758758
{/if}
759759
</div>
760+
{#if sysinfo?.features?.includes('zc')}
761+
<div class="my-1">
762+
<label><input type="checkbox" name="cze" value="true" bind:checked={configuration.c.ze} class="rounded mb-1"/> ZmartCharge</label>
763+
</div>
764+
{#if configuration.c.ze}
765+
<div class="my-1">
766+
<input name="czt" bind:value={configuration.c.zt} type="text" class="in-s" placeholder="ZmartCharge token"/>
767+
</div>
768+
{/if}
769+
{/if}
760770
</div>
761771
{/if}
762772
{#if configuration?.p?.r?.startsWith("NO") || configuration?.p?.r?.startsWith("10YNO") || configuration?.p?.r?.startsWith('10Y1001A1001A4')}

lib/SvelteUi/json/conf_cloud.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
"c": {
22
"e" : %s,
33
"p" : %d,
4-
"es": %s
4+
"es": %s,
5+
"ze": %s,
6+
"zt" : "%s"
57
}

lib/SvelteUi/src/AmsWebServer.cpp

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -397,6 +397,10 @@ void AmsWebServer::sysinfoJson() {
397397
if(!features.isEmpty()) features += ",";
398398
features += "\"cloud\"";
399399
#endif
400+
#if defined(ZMART_CHARGE)
401+
if(!features.isEmpty()) features += ",";
402+
features += "\"zc\"";
403+
#endif
400404

401405
int size = snprintf_P(buf, BufferSize, SYSINFO_JSON,
402406
FirmwareVersion::VersionString,
@@ -879,6 +883,9 @@ void AmsWebServer::configurationJson() {
879883
config->getHomeAssistantConfig(haconf);
880884
CloudConfig cloud;
881885
config->getCloudConfig(cloud);
886+
ZmartChargeConfig zcc;
887+
config->getZmartChargeConfig(zcc);
888+
stripNonAscii((uint8_t*) zcc.token, 21);
882889

883890
bool qsc = false;
884891
bool qsr = false;
@@ -1058,10 +1065,12 @@ void AmsWebServer::configurationJson() {
10581065
cloud.enabled ? "true" : "false",
10591066
cloud.proto,
10601067
#if defined(ESP32) && defined(ENERGY_SPEEDOMETER_PASS)
1061-
sysConfig.energyspeedometer == 7 ? "true" : "false"
1068+
sysConfig.energyspeedometer == 7 ? "true" : "false",
10621069
#else
1063-
"null"
1070+
"null",
10641071
#endif
1072+
zcc.enabled ? "true" : "false",
1073+
zcc.token
10651074
);
10661075
server.sendContent(buf);
10671076
server.sendContent_P(PSTR("}"));
@@ -1578,6 +1587,16 @@ void AmsWebServer::handleSave() {
15781587
cloud.enabled = server.hasArg(F("ce")) && server.arg(F("ce")) == F("true");
15791588
cloud.proto = server.arg(F("cp")).toInt();
15801589
config->setCloudConfig(cloud);
1590+
1591+
ZmartChargeConfig zcc;
1592+
config->getZmartChargeConfig(zcc);
1593+
zcc.enabled = server.hasArg(F("cze")) && server.arg(F("cze")) == F("true");
1594+
String token = server.arg(F("czt"));
1595+
strcpy(zcc.token, token.c_str());
1596+
if(server.hasArg(F("czu")) && server.arg(F("czu")).startsWith(F("https"))) {
1597+
strcpy(zcc.baseUrl, server.arg(F("czu")).c_str());
1598+
}
1599+
config->setZmartChargeConfig(zcc);
15811600
}
15821601

15831602
if(server.hasArg(F("r")) && server.arg(F("r")) == F("true")) {
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
#pragma once
2+
3+
#include "RemoteDebug.h"
4+
#include "AmsData.h"
5+
#include "FirmwareVersion.h"
6+
7+
#if defined(ESP8266)
8+
#include <ESP8266HTTPClient.h>
9+
#elif defined(ESP32) // ARDUINO_ARCH_ESP32
10+
#include <HTTPClient.h>
11+
#else
12+
#warning "Unsupported board type"
13+
#endif
14+
15+
static const char ZC_LB_JSON[] PROGMEM = "{\"LBToken\":\"%s\",\"L1A\":\"%.1f\",\"L2A\":\"%.1f\",\"L3A\":\"%.1f\",\"HighConsumption\":\"%d\",\"CurrentPower\":\"%d\"}";
16+
17+
class ZmartChargeCloudConnector {
18+
public:
19+
ZmartChargeCloudConnector(RemoteDebug* debugger, char* buf) {
20+
this->debugger = debugger;
21+
this->json = buf;
22+
http = new HTTPClient();
23+
http->setFollowRedirects(HTTPC_STRICT_FOLLOW_REDIRECTS);
24+
http->setReuse(false);
25+
http->setTimeout(10000);
26+
http->setUserAgent("ams2mqtt/" + String(FirmwareVersion::VersionString));
27+
};
28+
void setup(const char* baseUrl, const char* token);
29+
void update(AmsData& data);
30+
31+
bool isConfigChanged();
32+
void ackConfigChanged();
33+
34+
const char* getBaseUrl();
35+
36+
private:
37+
RemoteDebug* debugger;
38+
char baseUrl[64];
39+
char token[21];
40+
uint16_t BufferSize = 2048;
41+
char* json;
42+
43+
bool configChanged = false;
44+
45+
bool lastFailed = false;
46+
uint64_t lastUpdate = 0;
47+
HTTPClient* http = NULL;
48+
49+
uint16_t heartbeat = 30;
50+
uint16_t heartbeatFast = 10;
51+
uint16_t heartbeatFastThreshold = 32;
52+
};
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
#include "ZmartChargeCloudConnector.h"
2+
#include "Uptime.h"
3+
#include "ArduinoJson.h"
4+
5+
void ZmartChargeCloudConnector::setup(const char* baseUrl, const char* token) {
6+
memset(this->baseUrl, 0, 64);
7+
memset(this->token, 0, 21);
8+
strcpy(this->baseUrl, baseUrl);
9+
strcpy(this->token, token);
10+
}
11+
12+
bool ZmartChargeCloudConnector::isConfigChanged() {
13+
return configChanged;
14+
}
15+
16+
void ZmartChargeCloudConnector::ackConfigChanged() {
17+
configChanged = false;
18+
}
19+
20+
const char* ZmartChargeCloudConnector::getBaseUrl() {
21+
return baseUrl;
22+
}
23+
24+
25+
void ZmartChargeCloudConnector::update(AmsData& data) {
26+
if(strlen(token) == 0) return;
27+
28+
uint64_t now = millis64();
29+
30+
float maximum = max(max(data.getL1Current(), data.getL1Current()), data.getL3Current());
31+
bool fast = maximum > heartbeatFastThreshold;
32+
33+
if(now - lastUpdate < (fast ? heartbeatFast : heartbeat) * 1000) return;
34+
35+
if(strlen(token) != 20) {
36+
lastUpdate = now;
37+
if(debugger->isActive(RemoteDebug::WARNING)) debugger->printf_P(PSTR("(ZmartCharge) Token defined, but is incorrect length (%s, %d)\n"), token, strlen(token));
38+
return;
39+
}
40+
41+
if(((now - lastUpdate) / 1000) > (fast || lastFailed ? heartbeatFast : heartbeat)) {
42+
if(debugger->isActive(RemoteDebug::DEBUG)) debugger->printf_P(PSTR("(ZmartCharge) Preparing to update cloud\n"));
43+
memset(json, 0, BufferSize);
44+
snprintf_P(json, BufferSize, ZC_LB_JSON,
45+
token,
46+
data.getL1Current(),
47+
data.getL2Current(),
48+
data.getL3Current(),
49+
fast ? 1 : 0,
50+
data.getActiveImportPower()
51+
);
52+
lastFailed = true;
53+
char url[128];
54+
memset(url, 0, 128);
55+
snprintf_P(url, 128, PSTR("%s/loadbalancer"), baseUrl);
56+
if(debugger->isActive(RemoteDebug::VERBOSE)) debugger->printf_P(PSTR("(ZmartCharge) Connecting to: %s\n"), baseUrl);
57+
if(http->begin(url)) {
58+
if(debugger->isActive(RemoteDebug::VERBOSE)) debugger->printf_P(PSTR("(ZmartCharge) Sending data: %s\n"), json);
59+
int status = http->POST(json);
60+
if(status == 200) {
61+
lastFailed = false;
62+
JsonDocument doc;
63+
String body = http->getString();
64+
if(debugger->isActive(RemoteDebug::VERBOSE)) debugger->printf_P(PSTR("(ZmartCharge) Received data: %s\n"), body.c_str());
65+
deserializeJson(doc, body);
66+
if(doc.containsKey("Settings")) {
67+
if(doc["Settings"].containsKey("HeartBeatTime")) {
68+
heartbeat = doc["Settings"]["HeartBeatTime"].as<long>();
69+
}
70+
if(doc["Settings"].containsKey("HearBeatTimeFast")) {
71+
heartbeatFast = doc["Settings"]["HearBeatTimeFast"].as<long>();
72+
}
73+
if(doc["Settings"].containsKey("HeartBeatTimeFastThreshold")) {
74+
heartbeatFastThreshold = doc["Settings"]["HeartBeatTimeFastThreshold"].as<long>();
75+
}
76+
if(doc["Settings"].containsKey("ZmartChargeUrl")) {
77+
String newBaseUrl = doc["Settings"]["ZmartChargeUrl"].as<String>();
78+
if(newBaseUrl.startsWith("https:") && strncmp(newBaseUrl.c_str(), baseUrl, strlen(baseUrl)) != 0) {
79+
newBaseUrl.replace("\\/", "/");
80+
if(debugger->isActive(RemoteDebug::INFO)) debugger->printf_P(PSTR("(ZmartCharge) Received new URL: %s\n"), newBaseUrl.c_str());
81+
memset(baseUrl, 0, 64);
82+
memcpy(baseUrl, newBaseUrl.c_str(), strlen(newBaseUrl.c_str()));
83+
configChanged = true;
84+
}
85+
}
86+
}
87+
http->end();
88+
} else {
89+
if(debugger->isActive(RemoteDebug::ERROR)) debugger->printf_P(PSTR("(ZmartCharge) Communication error, returned status: %d\n"), status);
90+
if(debugger->isActive(RemoteDebug::ERROR)) debugger->printf(http->errorToString(status).c_str());
91+
if(debugger->isActive(RemoteDebug::DEBUG)) debugger->printf(http->getString().c_str());
92+
93+
http->end();
94+
}
95+
} else {
96+
if(debugger->isActive(RemoteDebug::ERROR)) debugger->printf_P(PSTR("(ZmartCharge) Unable to establish connection with cloud\n"));
97+
}
98+
lastUpdate = now;
99+
}
100+
}

0 commit comments

Comments
 (0)