|
8 | 8 | #include "AmsStorage.h" |
9 | 9 | #include "LittleFS.h" |
10 | 10 | #include "FirmwareVersion.h" |
| 11 | +#include "AmsData.h" |
| 12 | +#include "PriceService.h" |
| 13 | +#include "NtpStatus.h" |
| 14 | +#include "Uptime.h" |
| 15 | +#include "mqtt/AmsMqttHandler.h" |
| 16 | +#if defined(AMS_CLOUD) |
| 17 | +#include "cloud/CloudConnector.h" |
| 18 | +#endif |
| 19 | +#if defined(ZMART_CHARGE) |
| 20 | +#include "cloud/ZmartChargeCloudConnector.h" |
| 21 | +#endif |
| 22 | + |
| 23 | +// SNTP resyncs roughly hourly; flag the NTP service as degraded if no sync has |
| 24 | +// landed in this long, allowing a couple of missed cycles before warning. |
| 25 | +#define NTP_STALE_AFTER_SECONDS 10800 |
| 26 | + |
| 27 | +uint8_t AmsJsonGenerator::hanState(AmsData* meterState) { |
| 28 | + if(meterState == NULL) return 2; |
| 29 | + uint64_t millis = millis64(); |
| 30 | + if(meterState->getLastError() != 0) return 3; |
| 31 | + // State 0 means disabled, which the HAN port never is. Waiting for the first |
| 32 | + // frame after boot is the connecting state. |
| 33 | + if(meterState->getLastUpdateMillis() == 0 && millis < 30000) return 2; |
| 34 | + if(millis - meterState->getLastUpdateMillis() < 15000) return 1; |
| 35 | + if(millis - meterState->getLastUpdateMillis() < 30000) return 2; |
| 36 | + return 3; |
| 37 | +} |
| 38 | + |
| 39 | +uint8_t AmsJsonGenerator::mqttHandlerState(AmsMqttHandler* h) { |
| 40 | + if(h == NULL) return 2; |
| 41 | + if(h->connected()) return 1; |
| 42 | + return h->lastError() == 0 ? 2 : 3; |
| 43 | +} |
| 44 | + |
| 45 | +// Formats one entry of the services array. Kept in one place so the web payload |
| 46 | +// and the MQTT payload (#1128) cannot drift apart. |
| 47 | +static void appendServiceEntry(String& out, const char* key, uint8_t state, int16_t err, const char* detail, const char* name) { |
| 48 | + char entry[320]; |
| 49 | + snprintf_P(entry, sizeof(entry), PSTR("{\"k\":\"%s\",\"s\":%d,\"e\":%d%s%s%s,\"d\":\"%s\"}"), |
| 50 | + key, state, err, |
| 51 | + name != NULL ? ",\"n\":\"" : "", name != NULL ? name : "", name != NULL ? "\"" : "", |
| 52 | + detail == NULL ? "" : detail); |
| 53 | + if(!out.isEmpty()) out += ","; |
| 54 | + out += entry; |
| 55 | +} |
| 56 | + |
| 57 | +String AmsJsonGenerator::generateServicesJson(const ServiceStatusContext& ctx) { |
| 58 | + String out = ""; |
| 59 | + if(ctx.config == NULL) return out; |
| 60 | + |
| 61 | + { |
| 62 | + String meterModel = ctx.meterState == NULL ? String("") : String(ctx.meterState->getMeterModel()); |
| 63 | + if(!meterModel.isEmpty()) |
| 64 | + meterModel.replace(F("\\"), F("\\\\")); |
| 65 | + appendServiceEntry(out, "han", hanState(ctx.meterState), |
| 66 | + ctx.meterState == NULL ? 0 : ctx.meterState->getLastError(), meterModel.c_str(), NULL); |
| 67 | + } |
| 68 | + |
| 69 | + MqttConfig mqttConfig; |
| 70 | + bool haveMqttConfig = ctx.config->getMqttConfig(mqttConfig); |
| 71 | + if(haveMqttConfig && strlen(mqttConfig.host) > 0) { |
| 72 | + uint8_t s; |
| 73 | + int16_t err = 0; |
| 74 | + if(!ctx.mqttEnabled) { |
| 75 | + s = 0; |
| 76 | + } else { |
| 77 | + s = mqttHandlerState(ctx.mqttHandler); |
| 78 | + if(ctx.mqttHandler != NULL) err = (int16_t) ctx.mqttHandler->lastError(); |
| 79 | + } |
| 80 | + appendServiceEntry(out, "mqtt", s, err, mqttConfig.host, NULL); |
| 81 | + } |
| 82 | + |
| 83 | + #if defined(CUSTOM_MQTT_HOST) |
| 84 | + { |
| 85 | + uint8_t s = mqttHandlerState(ctx.customMqttHandler); |
| 86 | + int16_t err = ctx.customMqttHandler == NULL ? 0 : (int16_t) ctx.customMqttHandler->lastError(); |
| 87 | + #if defined(CUSTOM_MQTT_NAME) |
| 88 | + appendServiceEntry(out, "mqtt_c", s, err, CUSTOM_MQTT_HOST, CUSTOM_MQTT_NAME); |
| 89 | + #else |
| 90 | + appendServiceEntry(out, "mqtt_c", s, err, CUSTOM_MQTT_HOST, NULL); |
| 91 | + #endif |
| 92 | + } |
| 93 | + #endif |
| 94 | + |
| 95 | + #if defined(ESP32) && defined(ENERGY_SPEEDOMETER_PASS) |
| 96 | + { |
| 97 | + SystemConfig sys; |
| 98 | + ctx.config->getSystemConfig(sys); |
| 99 | + if(sys.energyspeedometer == 7) { |
| 100 | + uint8_t s = mqttHandlerState(ctx.energySpeedometer); |
| 101 | + int16_t err = ctx.energySpeedometer == NULL ? 0 : (int16_t) ctx.energySpeedometer->lastError(); |
| 102 | + appendServiceEntry(out, "mqtt_es", s, err, "", NULL); |
| 103 | + } |
| 104 | + } |
| 105 | + #endif |
| 106 | + |
| 107 | + PriceServiceConfig priceCfg; |
| 108 | + if(ctx.config->getPriceServiceConfig(priceCfg) && priceCfg.enabled && strlen(priceCfg.area) > 0) { |
| 109 | + uint8_t s; |
| 110 | + int16_t err = ctx.ps == NULL ? 0 : ctx.ps->getLastError(); |
| 111 | + if(ctx.ps == NULL) { |
| 112 | + s = 2; |
| 113 | + } else if(err != 0) { |
| 114 | + s = 3; |
| 115 | + } else if(ctx.ps->hasPrice()) { |
| 116 | + s = 1; |
| 117 | + } else { |
| 118 | + s = 2; |
| 119 | + } |
| 120 | + appendServiceEntry(out, "price", s, err, priceCfg.area, NULL); |
| 121 | + } |
| 122 | + |
| 123 | + { |
| 124 | + NtpConfig ntp; |
| 125 | + if(ctx.config->getNtpConfig(ntp) && ntp.enable) { |
| 126 | + const char* server = strlen(ntp.server) > 0 ? ntp.server : "pool.ntp.org"; |
| 127 | + // A set-but-stale clock (NTP stopped resyncing) silently corrupts |
| 128 | + // day-boundary accounting, so flag staleness rather than only |
| 129 | + // reporting whether the clock was ever set. |
| 130 | + uint64_t lastSync = ntpLastSyncMillis(); |
| 131 | + uint8_t s; |
| 132 | + if(lastSync == 0) { |
| 133 | + s = 2; // No SNTP sync since boot yet |
| 134 | + } else { |
| 135 | + uint32_t ageSec = (uint32_t) ((millis64() - lastSync) / 1000); |
| 136 | + s = ageSec > NTP_STALE_AFTER_SECONDS ? 2 : 1; |
| 137 | + } |
| 138 | + appendServiceEntry(out, "ntp", s, 0, server, NULL); |
| 139 | + } |
| 140 | + } |
| 141 | + |
| 142 | + #if defined(AMS_CLOUD) |
| 143 | + { |
| 144 | + CloudConfig cc; |
| 145 | + if(ctx.config->getCloudConfig(cc) && cc.enabled) { |
| 146 | + uint8_t s; |
| 147 | + int16_t err = ctx.cloud == NULL ? 0 : ctx.cloud->getLastError(); |
| 148 | + if(ctx.cloud == NULL || !ctx.cloud->isInitialized()) { |
| 149 | + s = 2; |
| 150 | + } else { |
| 151 | + unsigned long since = millis() - ctx.cloud->getLastUpdate(); |
| 152 | + uint32_t maxAge = ((uint32_t) cc.interval) * 3000; |
| 153 | + s = (ctx.cloud->getLastUpdate() > 0 && since > maxAge) ? 3 : 1; |
| 154 | + } |
| 155 | + appendServiceEntry(out, "cloud", s, err, cc.hostname, NULL); |
| 156 | + } |
| 157 | + } |
| 158 | + #endif |
| 159 | + |
| 160 | + #if defined(ZMART_CHARGE) |
| 161 | + { |
| 162 | + ZmartChargeConfig zc; |
| 163 | + if(ctx.config->getZmartChargeConfig(zc) && zc.enabled) { |
| 164 | + uint8_t s; |
| 165 | + int16_t err = ctx.zcloud == NULL ? 0 : ctx.zcloud->getLastError(); |
| 166 | + if(ctx.zcloud == NULL || ctx.zcloud->getLastUpdate() == 0) { |
| 167 | + s = 2; |
| 168 | + } else { |
| 169 | + s = ctx.zcloud->isLastFailed() ? 3 : 1; |
| 170 | + } |
| 171 | + appendServiceEntry(out, "zc", s, err, zc.baseUrl, NULL); |
| 172 | + } |
| 173 | + } |
| 174 | + #endif |
| 175 | + |
| 176 | + return out; |
| 177 | +} |
11 | 178 |
|
12 | 179 | void AmsJsonGenerator::generateDayPlotJson(AmsDataStorage* ds, char* buf, size_t bufSize) { |
13 | 180 | uint16_t pos = snprintf_P(buf, bufSize, PSTR("{\"unit\":\"kwh\"")); |
|
0 commit comments