Refactored MQTT payload handling into separate classes

This commit is contained in:
Gunnar Skjold 2021-01-17 20:11:04 +01:00
parent 53573184f3
commit 33070af111
23 changed files with 803 additions and 502 deletions

View File

@ -4,7 +4,7 @@ extra_configs = platformio-user.ini
[common]
framework = arduino
lib_deps = file://lib/HanReader, file://lib/Timezone, ArduinoJson@6.14.1, MQTT@2.4.7, DallasTemperature@3.8.1, EspSoftwareSerial@6.7.1, RemoteDebug@3.0.5, Time@1.6
lib_deps = file://lib/HanReader, file://lib/Timezone, MQTT@2.4.7, DallasTemperature@3.8.1, EspSoftwareSerial@6.7.1, RemoteDebug@3.0.5, Time@1.6
[env:esp8266]
platform = espressif8266@2.5.1

View File

@ -105,6 +105,7 @@ bool AmsConfiguration::setMqttConfig(MqttConfig& config) {
mqttChanged |= strcmp(config.subscribeTopic, existing.subscribeTopic) != 0;
mqttChanged |= strcmp(config.username, existing.username) != 0;
mqttChanged |= strcmp(config.password, existing.password) != 0;
mqttChanged |= config.payloadFormat != existing.payloadFormat;
mqttChanged |= config.ssl != existing.ssl;
} else {
mqttChanged = true;
@ -254,6 +255,7 @@ bool AmsConfiguration::setDomoticzConfig(DomoticzConfig& config) {
} else {
domoChanged = true;
}
mqttChanged = domoChanged;
EEPROM.begin(EEPROM_SIZE);
EEPROM.put(CONFIG_DOMOTICZ_START, config);
bool ret = EEPROM.commit();

View File

@ -19,7 +19,6 @@
#include "AmsToMqttBridge.h"
#include "AmsStorage.h"
#define ARDUINOJSON_POSITIVE_EXPONENTIATION_THRESHOLD 1e9
#include <ArduinoJson.h>
#include <MQTT.h>
#include <DNSServer.h>
#include <lwip/apps/sntp.h>
@ -34,7 +33,11 @@ ADC_MODE(ADC_VCC);
#include "web/AmsWebServer.h"
#include "AmsConfiguration.h"
#include "HanReader.h"
#include "HanToJson.h"
#include "mqtt/AmsMqttHandler.h"
#include "mqtt/JsonMqttHandler.h"
#include "mqtt/RawMqttHandler.h"
#include "mqtt/DomoticzMqttHandler.h"
#include "Aidon.h"
#include "Kaifa.h"
@ -61,6 +64,7 @@ Timezone* tz;
AmsWebServer ws(&Debug, &hw, &eapi);
MQTTClient mqtt(512);
AmsMqttHandler* mqttHandler = NULL;
HanReader hanReader;
@ -68,10 +72,7 @@ Stream *hanSerial;
GpioConfig gpioConfig;
MeterConfig meterConfig;
DomoticzConfig* domoConfig = NULL;
float energy = 0.0;
bool mqttEnabled = false;
String clientId = "ams-reader";
uint8_t payloadFormat = 0;
String topic = "ams";
AmsData meterState;
@ -290,18 +291,6 @@ unsigned long lastSuccessfulRead = 0;
unsigned long lastErrorBlink = 0;
int lastError = 0;
String toHex(uint8_t* in) {
String hex;
for(int i = 0; i < sizeof(in)*2; i++) {
if(in[i] < 0x10) {
hex += '0';
}
hex += String(in[i], HEX);
}
hex.toUpperCase();
return hex;
}
void loop() {
Debug.handle();
unsigned long now = millis();
@ -333,38 +322,8 @@ void loop() {
hw.updateTemperatures();
lastTemperatureRead = now;
uint8_t c = hw.getTempSensorCount();
if(WiFi.getMode() != WIFI_AP && WiFi.status() == WL_CONNECTED && mqtt.connected() && !topic.isEmpty()) {
if(payloadFormat == 1 || payloadFormat == 2) {
for(int i = 0; i < c; i++) {
TempSensorData* data = hw.getTempSensorData(i);
if(data->lastValidRead > -85) {
if(data->changed || payloadFormat == 2) {
mqtt.publish(topic + "/temperature/" + toHex(data->address), String(data->lastValidRead, 2));
data->changed = false;
}
}
}
} else if(payloadFormat == 0) {
StaticJsonDocument<512> json;
JsonObject temps = json.createNestedObject("temperatures");
for(int i = 0; i < c; i++) {
TempSensorData* data = hw.getTempSensorData(i);
if(data->lastValidRead > -85) {
TempSensorConfig* conf = config.getTempSensorConfig(data->address);
JsonObject obj = temps.createNestedObject(toHex(data->address));
if(conf != NULL) {
obj["name"] = conf->name;
}
obj["value"] = serialized(String(data->lastValidRead, 2));
}
data->changed = false;
}
String msg;
serializeJson(json, msg);
mqtt.publish(topic, msg.c_str());
}
if(mqttHandler != NULL && WiFi.getMode() != WIFI_AP && WiFi.status() == WL_CONNECTED && mqtt.connected() && !topic.isEmpty()) {
mqttHandler->publishTemperatures(&config, &hw);
}
debugD("Used %d ms to update temperature", millis()-start);
}
@ -412,6 +371,7 @@ void loop() {
MqttConfig mqttConfig;
if(config.getMqttConfig(mqttConfig)) {
mqttEnabled = strlen(mqttConfig.host) > 0;
ws.setMqttEnabled(mqttEnabled);
}
}
if(config.isNtpChanged()) {
@ -443,9 +403,6 @@ void loop() {
if(!mqtt.connected() || config.isMqttChanged()) {
MQTT_connect();
}
if(payloadFormat == 1) {
sendSystemStatusToMqtt();
}
} else if(mqtt.connected()) {
mqtt.disconnect();
}
@ -470,8 +427,8 @@ void loop() {
delay(1);
readHanPort();
if(WiFi.status() == WL_CONNECTED) {
if(eapi.loop()) {
sendPricesToMqtt();
if(eapi.loop() && mqttHandler != NULL) {
mqttHandler->publishPrices(&eapi);
}
}
ws.loop();
@ -620,150 +577,6 @@ void mqttMessageReceived(String &topic, String &payload)
// Ideas could be to query for values or to initiate OTA firmware update
}
void sendPricesToMqtt() {
if(!mqttEnabled || topic.isEmpty() || !mqtt.connected())
return;
if(strcmp(eapi.getToken(), "") != 0)
return;
time_t now = time(nullptr);
float min1hr, min3hr, min6hr;
uint8_t min1hrIdx = -1, min3hrIdx = -1, min6hrIdx = -1;
float min = INT16_MAX, max = INT16_MIN;
float values[24] = {0};
for(uint8_t i = 0; i < 24; i++) {
float val = eapi.getValueForHour(now, i);
values[i] = val;
if(val == ENTSOE_NO_VALUE) break;
if(val < min) min = val;
if(val > max) max = val;
if(min1hrIdx == -1 || min1hr > val) {
min1hr = val;
min1hrIdx = i;
}
if(i >= 2) {
i -= 2;
float val1 = values[i++];
float val2 = values[i++];
float val3 = val;
if(val1 == ENTSOE_NO_VALUE || val2 == ENTSOE_NO_VALUE || val3 == ENTSOE_NO_VALUE) continue;
float val3hr = val1+val2+val3;
if(min3hrIdx == -1 || min3hr > val3hr) {
min3hr = val3hr;
min3hrIdx = i-2;
}
}
if(i >= 5) {
i -= 5;
float val1 = values[i++];
float val2 = values[i++];
float val3 = values[i++];
float val4 = values[i++];
float val5 = values[i++];
float val6 = val;
if(val1 == ENTSOE_NO_VALUE || val2 == ENTSOE_NO_VALUE || val3 == ENTSOE_NO_VALUE || val4 == ENTSOE_NO_VALUE || val5 == ENTSOE_NO_VALUE || val6 == ENTSOE_NO_VALUE) continue;
float val6hr = val1+val2+val3+val4+val5+val6;
if(min6hrIdx == -1 || min6hr > val6hr) {
min6hr = val6hr;
min6hrIdx = i-5;
}
}
}
char ts1hr[21];
if(min1hrIdx != -1) {
tmElements_t tm;
breakTime(now + (SECS_PER_HOUR * min1hrIdx), tm);
sprintf(ts1hr, "%04d-%02d-%02dT%02d:00:00Z", tm.Year+1970, tm.Month, tm.Day, tm.Hour);
}
char ts3hr[21];
if(min3hrIdx != -1) {
tmElements_t tm;
breakTime(now + (SECS_PER_HOUR * min3hrIdx), tm);
sprintf(ts3hr, "%04d-%02d-%02dT%02d:00:00Z", tm.Year+1970, tm.Month, tm.Day, tm.Hour);
}
char ts6hr[21];
if(min6hrIdx != -1) {
tmElements_t tm;
breakTime(now + (SECS_PER_HOUR * min6hrIdx), tm);
sprintf(ts6hr, "%04d-%02d-%02dT%02d:00:00Z", tm.Year+1970, tm.Month, tm.Day, tm.Hour);
}
switch(payloadFormat) {
case 0: // JSON
{
StaticJsonDocument<384> json;
json["id"] = WiFi.macAddress();
JsonObject jp = json.createNestedObject("prices");
for(int i = 0; i < 24; i++) {
float val = values[i];
if(val == ENTSOE_NO_VALUE) break;
jp[String(i)] = serialized(String(val, 4));
}
if(min != INT16_MAX) {
jp["min"] = serialized(String(min, 4));
}
if(max != INT16_MIN) {
jp["max"] = serialized(String(max, 4));
}
if(min1hrIdx != -1) {
jp["cheapest1hr"] = String(ts1hr);
}
if(min3hrIdx != -1) {
jp["cheapest3hr"] = String(ts3hr);
}
if(min6hrIdx != -1) {
jp["cheapest6hr"] = String(ts6hr);
}
String msg;
serializeJson(json, msg);
mqtt.publish(topic, msg.c_str());
break;
}
case 1: // RAW
case 2:
{
for(int i = 0; i < 24; i++) {
float val = values[i];
if(val == ENTSOE_NO_VALUE) {
mqtt.publish(topic + "/price/" + String(i), "");
break;
} else {
mqtt.publish(topic + "/price/" + String(i), String(val, 4));
}
mqtt.loop();
delay(10);
}
if(min != INT16_MAX) {
mqtt.publish(topic + "/price/min", String(min, 4));
}
if(max != INT16_MIN) {
mqtt.publish(topic + "/price/max", String(max, 4));
}
if(min1hrIdx != -1) {
mqtt.publish(topic + "/price/cheapest/1hr", String(ts1hr));
}
if(min3hrIdx != -1) {
mqtt.publish(topic + "/price/cheapest/3hr", String(ts3hr));
}
if(min6hrIdx != -1) {
mqtt.publish(topic + "/price/cheapest/6hr", String(ts6hr));
}
}
break;
}
}
int currentMeterType = 0;
void readHanPort() {
if (hanReader.read()) {
@ -775,163 +588,13 @@ void readHanPort() {
AmsData data(meterConfig.type, meterConfig.substituteMissing, hanReader);
if(data.getListType() > 0) {
if(mqttEnabled && !topic.isEmpty()) {
if(payloadFormat == 0) {
StaticJsonDocument<512> json;
hanToJson(json, data, hw, hw.getTemperature(), clientId);
if (Debug.isActive(RemoteDebug::INFO)) {
debugI("Sending data to MQTT");
if (Debug.isActive(RemoteDebug::DEBUG)) {
serializeJsonPretty(json, Debug);
}
if(mqttEnabled && mqttHandler != NULL) {
if(mqttHandler->publish(&data, &meterState)) {
if(data.getListType() == 3) {
mqttHandler->publishPrices(&eapi);
}
String msg;
serializeJson(json, msg);
mqtt.publish(topic, msg.c_str());
//
// Start DOMOTICZ
//
} else if(payloadFormat == 3) {
debugI("Sending data to MQTT");
if(config.isDomoChanged()) {
if(domoConfig == NULL)
domoConfig = new DomoticzConfig();
if(config.getDomoticzConfig(*domoConfig)) {
config.ackDomoChange();
}
}
if (domoConfig->elidx > 0) {
if(data.getActiveImportCounter() > 1.0) {
energy = data.getActiveImportCounter();
}
if(energy > 0.0) {
String PowerEnergy;
int p;
StaticJsonDocument<200> json_PE;
p = data.getActiveImportPower();
PowerEnergy = String((double) p/1.0) + ";" + String(energy*1000.0, 0);
json_PE["command"] = "udevice";
json_PE["idx"] = domoConfig->elidx;
json_PE["nvalue"] = 0;
json_PE["svalue"] = PowerEnergy;
String msg_PE;
serializeJson(json_PE, msg_PE);
mqtt.publish("domoticz/in", msg_PE.c_str());
}
}
if (domoConfig->vl1idx > 0){
if (data.getL1Voltage() > 0.1){
StaticJsonDocument<200> json_u1;
json_u1["command"] = "udevice";
json_u1["idx"] = domoConfig->vl1idx;
json_u1["nvalue"] = 0;
json_u1["svalue"] = String(data.getL1Voltage(), 2);
String msg_u1;
serializeJson(json_u1, msg_u1);
mqtt.publish("domoticz/in", msg_u1.c_str());
}
}
if (domoConfig->vl2idx > 0){
if (data.getL2Voltage() > 0.1){
StaticJsonDocument<200> json_u2;
json_u2["command"] = "udevice";
json_u2["idx"] = domoConfig->vl2idx;
json_u2["nvalue"] = 0;
json_u2["svalue"] = String(data.getL2Voltage(), 2);
String msg_u2;
serializeJson(json_u2, msg_u2);
mqtt.publish("domoticz/in", msg_u2.c_str());
}
}
if (domoConfig->vl3idx > 0){
if (data.getL3Voltage() > 0.1){
StaticJsonDocument<200> json_u3;
json_u3["command"] = "udevice";
json_u3["idx"] = domoConfig->vl3idx;
json_u3["nvalue"] = 0;
json_u3["svalue"] = String(data.getL3Voltage());
String msg_u3;
serializeJson(json_u3, msg_u3);
mqtt.publish("domoticz/in", msg_u3.c_str());
}
}
if (domoConfig->cl1idx > 0){
if(data.getL1Current() > 0.0) {
StaticJsonDocument<200> json_i1;
String Ampere3;
Ampere3 = String(data.getL1Current(), 1) + ";" + String(data.getL2Current(), 1) + ";" + String(data.getL3Current(), 1) ;
json_i1["command"] = "udevice";
json_i1["idx"] = domoConfig->cl1idx;
json_i1["nvalue"] = 0;
json_i1["svalue"] = Ampere3;
String msg_i1;
serializeJson(json_i1, msg_i1);
mqtt.publish("domoticz/in", msg_i1.c_str());
}
}
//
// End DOMOTICZ
//
} else if(payloadFormat == 1 || payloadFormat == 2) {
if(data.getPackageTimestamp() > 0) {
mqtt.publish(topic + "/meter/dlms/timestamp", String(data.getPackageTimestamp()));
}
switch(data.getListType()) {
case 3:
// ID and type belongs to List 2, but I see no need to send that every 10s
mqtt.publish(topic + "/meter/id", data.getMeterId());
mqtt.publish(topic + "/meter/type", data.getMeterType());
mqtt.publish(topic + "/meter/clock", String(data.getMeterTimestamp()));
mqtt.publish(topic + "/meter/import/reactive/accumulated", String(data.getReactiveImportCounter(), 2));
mqtt.publish(topic + "/meter/import/active/accumulated", String(data.getActiveImportCounter(), 2));
mqtt.publish(topic + "/meter/export/reactive/accumulated", String(data.getReactiveExportCounter(), 2));
mqtt.publish(topic + "/meter/export/active/accumulated", String(data.getActiveExportCounter(), 2));
sendPricesToMqtt();
case 2:
// Only send data if changed. ID and Type is sent on the 10s interval only if changed
if(meterState.getMeterId() != data.getMeterId() || payloadFormat == 2) {
mqtt.publish(topic + "/meter/id", data.getMeterId());
}
if(meterState.getMeterType() != data.getMeterType() || payloadFormat == 2) {
mqtt.publish(topic + "/meter/type", data.getMeterType());
}
if(meterState.getL1Current() != data.getL1Current() || payloadFormat == 2) {
mqtt.publish(topic + "/meter/l1/current", String(data.getL1Current(), 2));
}
if(meterState.getL1Voltage() != data.getL1Voltage() || payloadFormat == 2) {
mqtt.publish(topic + "/meter/l1/voltage", String(data.getL1Voltage(), 2));
}
if(meterState.getL2Current() != data.getL2Current() || payloadFormat == 2) {
mqtt.publish(topic + "/meter/l2/current", String(data.getL2Current(), 2));
}
if(meterState.getL2Voltage() != data.getL2Voltage() || payloadFormat == 2) {
mqtt.publish(topic + "/meter/l2/voltage", String(data.getL2Voltage(), 2));
}
if(meterState.getL3Current() != data.getL3Current() || payloadFormat == 2) {
mqtt.publish(topic + "/meter/l3/current", String(data.getL3Current(), 2));
}
if(meterState.getL3Voltage() != data.getL3Voltage() || payloadFormat == 2) {
mqtt.publish(topic + "/meter/l3/voltage", String(data.getL3Voltage(), 2));
}
if(meterState.getReactiveExportPower() != data.getReactiveExportPower() || payloadFormat == 2) {
mqtt.publish(topic + "/meter/export/reactive", String(data.getReactiveExportPower()));
}
if(meterState.getActiveExportPower() != data.getActiveExportPower() || payloadFormat == 2) {
mqtt.publish(topic + "/meter/export/active", String(data.getActiveExportPower()));
}
if(meterState.getReactiveImportPower() != data.getReactiveImportPower() || payloadFormat == 2) {
mqtt.publish(topic + "/meter/import/reactive", String(data.getReactiveImportPower()));
}
case 1:
if(meterState.getActiveImportPower() != data.getActiveImportPower() || payloadFormat == 2) {
mqtt.publish(topic + "/meter/import/active", String(data.getActiveImportPower()));
}
if(data.getListType() >= 2) {
mqttHandler->publishSystem(&hw);
}
}
mqtt.loop();
@ -1044,6 +707,7 @@ void MQTT_connect() {
if(!config.getMqttConfig(mqttConfig) || strlen(mqttConfig.host) == 0) {
if(Debug.isActive(RemoteDebug::WARNING)) debugW("No MQTT config");
mqttEnabled = false;
ws.setMqttEnabled(false);
config.ackMqttChange();
return;
}
@ -1053,8 +717,8 @@ void MQTT_connect() {
}
lastMqttRetry = millis();
clientId = String(mqttConfig.clientId);
mqttEnabled = true;
ws.setMqttEnabled(true);
payloadFormat = mqttConfig.payloadFormat;
topic = String(mqttConfig.publishTopic);
@ -1065,6 +729,26 @@ void MQTT_connect() {
mqtt.disconnect();
yield();
if(mqttHandler != NULL) {
delete mqttHandler;
mqttHandler = NULL;
}
switch(mqttConfig.payloadFormat) {
case 0:
mqttHandler = new JsonMqttHandler(&mqtt, mqttConfig.clientId, mqttConfig.publishTopic, &hw);
break;
case 1:
case 2:
mqttHandler = new RawMqttHandler(&mqtt, mqttConfig.publishTopic, mqttConfig.payloadFormat == 2);
break;
case 3:
DomoticzConfig domo;
config.getDomoticzConfig(domo);
mqttHandler = new DomoticzMqttHandler(&mqtt, domo);
break;
}
WiFiClientSecure *secureClient = NULL;
Client *client = NULL;
if(mqttConfig.ssl) {
@ -1127,10 +811,8 @@ void MQTT_connect() {
if (Debug.isActive(RemoteDebug::INFO)) debugI(" Subscribing to [%s]\r\n", mqttConfig.subscribeTopic);
}
if(payloadFormat == 0) {
sendMqttData("Connected!");
} else if(payloadFormat == 1 || payloadFormat == 2) {
sendSystemStatusToMqtt();
if(mqttHandler != NULL) {
mqttHandler->publishSystem(&hw);
}
} else {
if (Debug.isActive(RemoteDebug::ERROR)) {
@ -1146,52 +828,3 @@ void MQTT_connect() {
}
yield();
}
// Send a simple string embedded in json over MQTT
void sendMqttData(String data)
{
// Make sure we have configured a publish topic
if (topic.isEmpty())
return;
// Build a json with the message in a "data" attribute
StaticJsonDocument<128> json;
json["id"] = WiFi.macAddress();
json["up"] = millis64()/1000;
json["data"] = data;
double vcc = hw.getVcc();
if(vcc > 0) {
json["vcc"] = vcc;
}
json["rssi"] = hw.getWifiRssi();
// Stringify the json
String msg;
serializeJson(json, msg);
// Send the json over MQTT
mqtt.publish(topic, msg.c_str());
if (Debug.isActive(RemoteDebug::INFO)) debugI("Sending MQTT data");
if (Debug.isActive(RemoteDebug::DEBUG)) debugD("[%s]", data.c_str());
}
unsigned long lastSystemDataSent = -10000;
void sendSystemStatusToMqtt() {
if (topic.isEmpty())
return;
if(millis() - lastSystemDataSent < 10000)
return;
lastSystemDataSent = millis();
mqtt.publish(topic + "/id", WiFi.macAddress());
mqtt.publish(topic + "/uptime", String((unsigned long) millis64()/1000));
double vcc = hw.getVcc();
if(vcc > 0) {
mqtt.publish(topic + "/vcc", String(vcc, 2));
}
mqtt.publish(topic + "/rssi", String(hw.getWifiRssi()));
if(hw.getTemperature() > -85) {
mqtt.publish(topic + "/temperature", String(hw.getTemperature(), 2));
}
}

View File

@ -1,47 +0,0 @@
#include "HanToJson.h"
void hanToJson(JsonDocument& json, AmsData& data, HwTools& hw, double temperature, String name) {
json["id"] = WiFi.macAddress();
json["name"] = name;
json["up"] = millis();
json["t"] = data.getPackageTimestamp();
double vcc = hw.getVcc();
if(vcc > 0) {
json["vcc"] = serialized(String(vcc, 3));
}
json["rssi"] = hw.getWifiRssi();
if(temperature != DEVICE_DISCONNECTED_C) {
json["temp"] = serialized(String(temperature, 2));
}
// Add a sub-structure to the json object,
// to keep the data from the meter itself
JsonObject jd = json.createNestedObject("data");
switch(data.getListType()) {
case 3:
jd["rtc"] = data.getMeterTimestamp();
jd["tPI"] = data.getActiveImportCounter();
jd["tPO"] = data.getActiveExportCounter();
jd["tQI"] = data.getReactiveImportCounter();
jd["tQO"] = data.getReactiveExportCounter();
case 2:
jd["lv"] = data.getListId();
jd["id"] = data.getMeterId();
jd["type"] = data.getMeterType();
jd["Q"] = data.getReactiveImportPower();
jd["PO"] = data.getActiveExportPower();
jd["QO"] = data.getReactiveExportPower();
jd["I1"] = data.getL1Current();
jd["I2"] = data.getL2Current();
jd["I3"] = data.getL3Current();
jd["U1"] = data.getL1Voltage();
jd["U2"] = data.getL2Voltage();
jd["U3"] = data.getL3Voltage();
case 1:
jd["P"] = data.getActiveImportPower();
}
}

View File

@ -1,16 +0,0 @@
#ifndef _HANTOJSON_h
#define _HANTOJSON_h
#if defined(ARDUINO) && ARDUINO >= 100
#include "Arduino.h"
#else
#include "WProgram.h"
#endif
#include <ArduinoJson.h>
#include "AmsData.h"
#include "HwTools.h"
void hanToJson(JsonDocument& json, AmsData& data, HwTools& hw, double temperature, String name);
#endif

23
src/hexutils.cpp Normal file
View File

@ -0,0 +1,23 @@
#include "hexutils.h";
String toHex(uint8_t* in) {
return toHex(in, sizeof(in)*2);
}
String toHex(uint8_t* in, uint8_t size) {
String hex;
for(int i = 0; i < size; i++) {
if(in[i] < 0x10) {
hex += '0';
}
hex += String(in[i], HEX);
}
hex.toUpperCase();
return hex;
}
void fromHex(uint8_t *out, String in, uint8_t size) {
for(int i = 0; i < size*2; i += 2) {
out[i/2] = strtol(in.substring(i, i+2).c_str(), 0, 16);
}
}

11
src/hexutils.h Normal file
View File

@ -0,0 +1,11 @@
#ifndef _HEXUTILS_H
#define _HEXUTILS_H
#include <stdint.h>
#include "Arduino.h"
String toHex(uint8_t* in);
String toHex(uint8_t* in, uint8_t size);
void fromHex(uint8_t *out, String in, uint8_t size);
#endif

26
src/mqtt/AmsMqttHandler.h Normal file
View File

@ -0,0 +1,26 @@
#ifndef _AMSMQTTHANDLER_H
#define _AMSMQTTHANDLER_H
#include "Arduino.h"
#include <MQTT.h>
#include "AmsData.h"
#include "AmsConfiguration.h"
#include "HwTools.h"
#include "entsoe/EntsoeApi.h"
class AmsMqttHandler {
public:
AmsMqttHandler(MQTTClient* mqtt) {
this->mqtt = mqtt;
};
virtual bool publish(AmsData* data, AmsData* previousState);
virtual bool publishTemperatures(AmsConfiguration*, HwTools*);
virtual bool publishPrices(EntsoeApi* eapi);
virtual bool publishSystem(HwTools*);
protected:
MQTTClient* mqtt;
};
#endif

View File

@ -0,0 +1,89 @@
#include "DomoticzMqttHandler.h"
#include "web/root/domoticz_json.h"
bool DomoticzMqttHandler::publish(AmsData* data, AmsData* previousState) {
bool ret = false;
if (config.elidx > 0) {
if(data->getActiveImportCounter() > 1.0) {
energy = data->getActiveImportCounter();
}
if(energy > 0.0) {
char val[16];
snprintf(val, 16, "%.1f;%.1f", (data->getActiveImportPower()/1.0), energy*1000.0);
char json[192];
snprintf_P(json, sizeof(json), DOMOTICZ_JSON,
config.elidx,
val
);
ret = mqtt->publish("domoticz/in", json);
}
}
if(data->getListType() == 1)
return ret;
if (config.vl1idx > 0){
if (data->getL1Voltage() > 0.1){
char val[16];
snprintf(val, 16, "%.2f", data->getL1Voltage());
char json[192];
snprintf_P(json, sizeof(json), DOMOTICZ_JSON,
config.vl1idx,
val
);
ret |= mqtt->publish("domoticz/in", json);
}
}
if (config.vl2idx > 0){
if (data->getL2Voltage() > 0.1){
char val[16];
snprintf(val, 16, "%.2f", data->getL2Voltage());
char json[192];
snprintf_P(json, sizeof(json), DOMOTICZ_JSON,
config.vl2idx,
val
);
ret |= mqtt->publish("domoticz/in", json);
}
}
if (config.vl3idx > 0){
if (data->getL3Voltage() > 0.1){
char val[16];
snprintf(val, 16, "%.2f", data->getL3Voltage());
char json[192];
snprintf_P(json, sizeof(json), DOMOTICZ_JSON,
config.vl3idx,
val
);
ret |= mqtt->publish("domoticz/in", json);
}
}
if (config.cl1idx > 0){
if(data->getL1Current() > 0.0) {
char val[16];
snprintf(val, 16, "%.1f;%.1f;%.1f", data->getL1Current(), data->getL2Current(), data->getL3Current());
char json[192];
snprintf_P(json, sizeof(json), DOMOTICZ_JSON,
config.cl1idx,
val
);
ret |= mqtt->publish("domoticz/in", json);
}
}
return ret;
}
bool DomoticzMqttHandler::publishTemperatures(AmsConfiguration* config, HwTools* hw) {
return false;
}
bool DomoticzMqttHandler::publishPrices(EntsoeApi* eapi) {
return false;
}
bool DomoticzMqttHandler::publishSystem(HwTools* hw) {
return false;
}

View File

@ -0,0 +1,21 @@
#ifndef _DOMOTICZMQTTHANDLER_H
#define _DOMOTICZMQTTHANDLER_H
#include "AmsMqttHandler.h"
#include "AmsConfiguration.h"
class DomoticzMqttHandler : public AmsMqttHandler {
public:
DomoticzMqttHandler(MQTTClient* mqtt, DomoticzConfig config) : AmsMqttHandler(mqtt) {
this->config = config;
};
bool publish(AmsData* data, AmsData* previousState);
bool publishTemperatures(AmsConfiguration*, HwTools*);
bool publishPrices(EntsoeApi*);
bool publishSystem(HwTools*);
private:
DomoticzConfig config;
int energy = 0.0;
};
#endif

View File

@ -0,0 +1,227 @@
#include "JsonMqttHandler.h"
#include "hexutils.h"
#include "Uptime.h"
#include "web/root/json1_json.h"
#include "web/root/json2_json.h"
#include "web/root/json3_json.h"
#include "web/root/jsonsys_json.h"
#include "web/root/jsonprices_json.h"
bool JsonMqttHandler::publish(AmsData* data, AmsData* previousState) {
if(topic.isEmpty() || !mqtt->connected())
return false;
double vcc = hw->getVcc();
if(data->getListType() == 1) {
char json[192];
snprintf_P(json, sizeof(json), JSON1_JSON,
WiFi.macAddress().c_str(),
clientId.c_str(),
(uint32_t) (millis64()/1000),
data->getPackageTimestamp(),
vcc,
hw->getWifiRssi(),
hw->getTemperature(),
data->getActiveImportPower()
);
return mqtt->publish(topic, json);
} else if(data->getListType() == 2) {
char json[256];
snprintf_P(json, sizeof(json), JSON2_JSON,
WiFi.macAddress().c_str(),
clientId.c_str(),
(uint32_t) (millis64()/1000),
data->getPackageTimestamp(),
vcc,
hw->getWifiRssi(),
hw->getTemperature(),
data->getListId().c_str(),
data->getMeterId().c_str(),
data->getMeterType().c_str(),
data->getActiveImportPower(),
data->getReactiveImportPower(),
data->getActiveExportPower(),
data->getReactiveExportPower(),
data->getL1Current(),
data->getL2Current(),
data->getL3Current(),
data->getL1Voltage(),
data->getL2Voltage(),
data->getL3Voltage()
);
return mqtt->publish(topic, json);
} else if(data->getListType() == 3) {
char json[384];
snprintf_P(json, sizeof(json), JSON3_JSON,
WiFi.macAddress().c_str(),
clientId.c_str(),
(uint32_t) (millis64()/1000),
data->getPackageTimestamp(),
vcc,
hw->getWifiRssi(),
hw->getTemperature(),
data->getListId().c_str(),
data->getMeterId().c_str(),
data->getMeterType().c_str(),
data->getActiveImportPower(),
data->getReactiveImportPower(),
data->getActiveExportPower(),
data->getReactiveExportPower(),
data->getL1Current(),
data->getL2Current(),
data->getL3Current(),
data->getL1Voltage(),
data->getL2Voltage(),
data->getL3Voltage(),
data->getActiveImportCounter(),
data->getActiveExportCounter(),
data->getReactiveImportCounter(),
data->getReactiveExportCounter(),
data->getMeterTimestamp()
);
return mqtt->publish(topic, json);
}
}
bool JsonMqttHandler::publishTemperatures(AmsConfiguration* config, HwTools* hw) {
int count = hw->getTempSensorCount();
if(count == 0)
return false;
int size = 32 + (count * 26);
char buf[size];
snprintf(buf, 24, "{\"temperatures\":{");
for(int i = 0; i < count; i++) {
TempSensorData* data = hw->getTempSensorData(i);
TempSensorConfig* conf = config->getTempSensorConfig(data->address);
char* pos = buf+strlen(buf);
snprintf(pos, 26, "\"%s\":%.2f,",
toHex(data->address, 8).c_str(),
data->lastRead
);
data->changed = false;
delay(1);
}
char* pos = buf+strlen(buf);
snprintf(count == 0 ? pos : pos-1, 8, "}}");
return mqtt->publish(topic, buf);
}
bool JsonMqttHandler::publishPrices(EntsoeApi* eapi) {
if(topic.isEmpty() || !mqtt->connected())
return false;
if(strcmp(eapi->getToken(), "") != 0)
return false;
time_t now = time(nullptr);
float min1hr, min3hr, min6hr;
uint8_t min1hrIdx = -1, min3hrIdx = -1, min6hrIdx = -1;
float min = INT16_MAX, max = INT16_MIN;
float values[24] = {0};
for(uint8_t i = 0; i < 24; i++) {
float val = eapi->getValueForHour(now, i);
values[i] = val;
if(val == ENTSOE_NO_VALUE) break;
if(val < min) min = val;
if(val > max) max = val;
if(min1hrIdx == -1 || min1hr > val) {
min1hr = val;
min1hrIdx = i;
}
if(i >= 2) {
i -= 2;
float val1 = values[i++];
float val2 = values[i++];
float val3 = val;
if(val1 == ENTSOE_NO_VALUE || val2 == ENTSOE_NO_VALUE || val3 == ENTSOE_NO_VALUE) continue;
float val3hr = val1+val2+val3;
if(min3hrIdx == -1 || min3hr > val3hr) {
min3hr = val3hr;
min3hrIdx = i-2;
}
}
if(i >= 5) {
i -= 5;
float val1 = values[i++];
float val2 = values[i++];
float val3 = values[i++];
float val4 = values[i++];
float val5 = values[i++];
float val6 = val;
if(val1 == ENTSOE_NO_VALUE || val2 == ENTSOE_NO_VALUE || val3 == ENTSOE_NO_VALUE || val4 == ENTSOE_NO_VALUE || val5 == ENTSOE_NO_VALUE || val6 == ENTSOE_NO_VALUE) continue;
float val6hr = val1+val2+val3+val4+val5+val6;
if(min6hrIdx == -1 || min6hr > val6hr) {
min6hr = val6hr;
min6hrIdx = i-5;
}
}
}
char ts1hr[21];
if(min1hrIdx != -1) {
tmElements_t tm;
breakTime(now + (SECS_PER_HOUR * min1hrIdx), tm);
sprintf(ts1hr, "%04d-%02d-%02dT%02d:00:00Z", tm.Year+1970, tm.Month, tm.Day, tm.Hour);
}
char ts3hr[21];
if(min3hrIdx != -1) {
tmElements_t tm;
breakTime(now + (SECS_PER_HOUR * min3hrIdx), tm);
sprintf(ts3hr, "%04d-%02d-%02dT%02d:00:00Z", tm.Year+1970, tm.Month, tm.Day, tm.Hour);
}
char ts6hr[21];
if(min6hrIdx != -1) {
tmElements_t tm;
breakTime(now + (SECS_PER_HOUR * min6hrIdx), tm);
sprintf(ts6hr, "%04d-%02d-%02dT%02d:00:00Z", tm.Year+1970, tm.Month, tm.Day, tm.Hour);
}
char json[384];
snprintf_P(json, sizeof(json), JSONPRICES_JSON,
WiFi.macAddress().c_str(),
values[0],
values[1],
values[2],
values[3],
values[4],
values[5],
values[6],
values[7],
values[8],
values[9],
values[10],
values[11],
min == INT16_MAX ? 0.0 : min,
max == INT16_MIN ? 0.0 : max,
ts1hr,
ts3hr,
ts6hr
);
return mqtt->publish(topic, json);
}
bool JsonMqttHandler::publishSystem(HwTools* hw) {
if(init || topic.isEmpty() || !mqtt->connected())
return false;
char json[192];
snprintf_P(json, sizeof(json), JSONSYS_JSON,
WiFi.macAddress().c_str(),
clientId.c_str(),
(uint32_t) (millis64()/1000),
hw->getVcc(),
hw->getWifiRssi(),
hw->getTemperature()
);
init = mqtt->publish(topic, json);
return init;
}

View File

@ -0,0 +1,24 @@
#ifndef _JSONMQTTHANDLER_H
#define _JSONMQTTHANDLER_H
#include "AmsMqttHandler.h"
class JsonMqttHandler : public AmsMqttHandler {
public:
JsonMqttHandler(MQTTClient* mqtt, const char* clientId, const char* topic, HwTools* hw) : AmsMqttHandler(mqtt) {
this->clientId = clientId;
this->topic = String(topic);
this->hw = hw;
};
bool publish(AmsData* data, AmsData* previousState);
bool publishTemperatures(AmsConfiguration*, HwTools*);
bool publishPrices(EntsoeApi*);
bool publishSystem(HwTools*);
private:
String clientId;
String topic;
HwTools* hw;
bool init = false;
};
#endif

200
src/mqtt/RawMqttHandler.cpp Normal file
View File

@ -0,0 +1,200 @@
#include "RawMqttHandler.h"
#include "hexutils.h"
#include "Uptime.h"
bool RawMqttHandler::publish(AmsData* data, AmsData* meterState) {
if(topic.isEmpty() || !mqtt->connected())
return false;
if(data->getPackageTimestamp() > 0) {
mqtt->publish(topic + "/meter/dlms/timestamp", String(data->getPackageTimestamp()));
}
switch(data->getListType()) {
case 3:
// ID and type belongs to List 2, but I see no need to send that every 10s
mqtt->publish(topic + "/meter/id", data->getMeterId());
mqtt->publish(topic + "/meter/type", data->getMeterType());
mqtt->publish(topic + "/meter/clock", String(data->getMeterTimestamp()));
mqtt->publish(topic + "/meter/import/reactive/accumulated", String(data->getReactiveImportCounter(), 2));
mqtt->publish(topic + "/meter/import/active/accumulated", String(data->getActiveImportCounter(), 2));
mqtt->publish(topic + "/meter/export/reactive/accumulated", String(data->getReactiveExportCounter(), 2));
mqtt->publish(topic + "/meter/export/active/accumulated", String(data->getActiveExportCounter(), 2));
case 2:
// Only send data if changed. ID and Type is sent on the 10s interval only if changed
if(full || meterState->getMeterId() != data->getMeterId()) {
mqtt->publish(topic + "/meter/id", data->getMeterId());
}
if(full || meterState->getMeterType() != data->getMeterType()) {
mqtt->publish(topic + "/meter/type", data->getMeterType());
}
if(full || meterState->getL1Current() != data->getL1Current()) {
mqtt->publish(topic + "/meter/l1/current", String(data->getL1Current(), 2));
}
if(full || meterState->getL1Voltage() != data->getL1Voltage()) {
mqtt->publish(topic + "/meter/l1/voltage", String(data->getL1Voltage(), 2));
}
if(full || meterState->getL2Current() != data->getL2Current()) {
mqtt->publish(topic + "/meter/l2/current", String(data->getL2Current(), 2));
}
if(full || meterState->getL2Voltage() != data->getL2Voltage()) {
mqtt->publish(topic + "/meter/l2/voltage", String(data->getL2Voltage(), 2));
}
if(full || meterState->getL3Current() != data->getL3Current()) {
mqtt->publish(topic + "/meter/l3/current", String(data->getL3Current(), 2));
}
if(full || meterState->getL3Voltage() != data->getL3Voltage()) {
mqtt->publish(topic + "/meter/l3/voltage", String(data->getL3Voltage(), 2));
}
if(full || meterState->getReactiveExportPower() != data->getReactiveExportPower()) {
mqtt->publish(topic + "/meter/export/reactive", String(data->getReactiveExportPower()));
}
if(full || meterState->getActiveExportPower() != data->getActiveExportPower()) {
mqtt->publish(topic + "/meter/export/active", String(data->getActiveExportPower()));
}
if(full || meterState->getReactiveImportPower() != data->getReactiveImportPower()) {
mqtt->publish(topic + "/meter/import/reactive", String(data->getReactiveImportPower()));
}
case 1:
if(full || meterState->getActiveImportPower() != data->getActiveImportPower()) {
mqtt->publish(topic + "/meter/import/active", String(data->getActiveImportPower()));
}
}
return true;
}
bool RawMqttHandler::publishTemperatures(AmsConfiguration* config, HwTools* hw) {
uint8_t c = hw->getTempSensorCount();
for(int i = 0; i < c; i++) {
TempSensorData* data = hw->getTempSensorData(i);
if(data->lastValidRead > -85) {
if(data->changed || full) {
mqtt->publish(topic + "/temperature/" + toHex(data->address), String(data->lastValidRead, 2));
data->changed = false;
}
}
}
return c > 0;
}
bool RawMqttHandler::publishPrices(EntsoeApi* eapi) {
if(topic.isEmpty() || !mqtt->connected())
return false;
if(strcmp(eapi->getToken(), "") != 0)
return false;
time_t now = time(nullptr);
float min1hr, min3hr, min6hr;
uint8_t min1hrIdx = -1, min3hrIdx = -1, min6hrIdx = -1;
float min = INT16_MAX, max = INT16_MIN;
float values[24] = {0};
for(uint8_t i = 0; i < 24; i++) {
float val = eapi->getValueForHour(now, i);
values[i] = val;
if(val == ENTSOE_NO_VALUE) break;
if(val < min) min = val;
if(val > max) max = val;
if(min1hrIdx == -1 || min1hr > val) {
min1hr = val;
min1hrIdx = i;
}
if(i >= 2) {
i -= 2;
float val1 = values[i++];
float val2 = values[i++];
float val3 = val;
if(val1 == ENTSOE_NO_VALUE || val2 == ENTSOE_NO_VALUE || val3 == ENTSOE_NO_VALUE) continue;
float val3hr = val1+val2+val3;
if(min3hrIdx == -1 || min3hr > val3hr) {
min3hr = val3hr;
min3hrIdx = i-2;
}
}
if(i >= 5) {
i -= 5;
float val1 = values[i++];
float val2 = values[i++];
float val3 = values[i++];
float val4 = values[i++];
float val5 = values[i++];
float val6 = val;
if(val1 == ENTSOE_NO_VALUE || val2 == ENTSOE_NO_VALUE || val3 == ENTSOE_NO_VALUE || val4 == ENTSOE_NO_VALUE || val5 == ENTSOE_NO_VALUE || val6 == ENTSOE_NO_VALUE) continue;
float val6hr = val1+val2+val3+val4+val5+val6;
if(min6hrIdx == -1 || min6hr > val6hr) {
min6hr = val6hr;
min6hrIdx = i-5;
}
}
}
char ts1hr[21];
if(min1hrIdx != -1) {
tmElements_t tm;
breakTime(now + (SECS_PER_HOUR * min1hrIdx), tm);
sprintf(ts1hr, "%04d-%02d-%02dT%02d:00:00Z", tm.Year+1970, tm.Month, tm.Day, tm.Hour);
}
char ts3hr[21];
if(min3hrIdx != -1) {
tmElements_t tm;
breakTime(now + (SECS_PER_HOUR * min3hrIdx), tm);
sprintf(ts3hr, "%04d-%02d-%02dT%02d:00:00Z", tm.Year+1970, tm.Month, tm.Day, tm.Hour);
}
char ts6hr[21];
if(min6hrIdx != -1) {
tmElements_t tm;
breakTime(now + (SECS_PER_HOUR * min6hrIdx), tm);
sprintf(ts6hr, "%04d-%02d-%02dT%02d:00:00Z", tm.Year+1970, tm.Month, tm.Day, tm.Hour);
}
for(int i = 0; i < 24; i++) {
float val = values[i];
if(val == ENTSOE_NO_VALUE) {
mqtt->publish(topic + "/price/" + String(i), "");
break;
} else {
mqtt->publish(topic + "/price/" + String(i), String(val, 4));
}
mqtt->loop();
delay(10);
}
if(min != INT16_MAX) {
mqtt->publish(topic + "/price/min", String(min, 4));
}
if(max != INT16_MIN) {
mqtt->publish(topic + "/price/max", String(max, 4));
}
if(min1hrIdx != -1) {
mqtt->publish(topic + "/price/cheapest/1hr", String(ts1hr));
}
if(min3hrIdx != -1) {
mqtt->publish(topic + "/price/cheapest/3hr", String(ts3hr));
}
if(min6hrIdx != -1) {
mqtt->publish(topic + "/price/cheapest/6hr", String(ts6hr));
}
return true;
}
bool RawMqttHandler::publishSystem(HwTools* hw) {
if(topic.isEmpty() || !mqtt->connected())
return false;
mqtt->publish(topic + "/id", WiFi.macAddress());
mqtt->publish(topic + "/uptime", String((unsigned long) millis64()/1000));
float vcc = hw->getVcc();
if(vcc > 0) {
mqtt->publish(topic + "/vcc", String(vcc, 2));
}
mqtt->publish(topic + "/rssi", String(hw->getWifiRssi()));
if(hw->getTemperature() > -85) {
mqtt->publish(topic + "/temperature", String(hw->getTemperature(), 2));
}
return true;
}

21
src/mqtt/RawMqttHandler.h Normal file
View File

@ -0,0 +1,21 @@
#ifndef _RAWMQTTHANDLER_H
#define _RAWMQTTHANDLER_H
#include "AmsMqttHandler.h"
class RawMqttHandler : public AmsMqttHandler {
public:
RawMqttHandler(MQTTClient* mqtt, const char* topic, bool full) : AmsMqttHandler(mqtt) {
this->topic = String(topic);
this->full = full;
};
bool publish(AmsData* data, AmsData* previousState);
bool publishTemperatures(AmsConfiguration*, HwTools*);
bool publishPrices(EntsoeApi*);
bool publishSystem(HwTools*);
private:
String topic;
bool full;
};
#endif

View File

@ -1,6 +1,7 @@
#include "AmsWebServer.h"
#include "version.h"
#include "AmsStorage.h"
#include "hexutils.h"
#include "root/head_html.h"
#include "root/foot_html.h"
@ -101,6 +102,10 @@ void AmsWebServer::setTimezone(Timezone* tz) {
this->tz = tz;
}
void AmsWebServer::setMqttEnabled(bool enabled) {
mqttEnabled = enabled;
}
void AmsWebServer::loop() {
server.handleClient();
@ -192,7 +197,7 @@ void AmsWebServer::temperatureJson() {
int size = 32 + (count * 72);
char buf[size];
snprintf_P(buf, 16, "{\"c\":%d,\"s\":[", count);
snprintf(buf, 16, "{\"c\":%d,\"s\":[", count);
for(int i = 0; i < count; i++) {
TempSensorData* data = hw->getTempSensorData(i);
@ -208,7 +213,7 @@ void AmsWebServer::temperatureJson() {
delay(1);
}
char* pos = buf+strlen(buf);
snprintf_P(count == 0 ? pos : pos-1, 8, "]}");
snprintf(count == 0 ? pos : pos-1, 8, "]}");
server.sendHeader("Cache-Control", "no-cache, no-store, must-revalidate");
server.sendHeader("Pragma", "no-cache");
@ -391,24 +396,6 @@ void AmsWebServer::configMeterHtml() {
server.sendContent_P(FOOT_HTML);
}
String AmsWebServer::toHex(uint8_t* in, uint8_t size) {
String hex;
for(int i = 0; i < size; i++) {
if(in[i] < 0x10) {
hex += '0';
}
hex += String(in[i], HEX);
}
hex.toUpperCase();
return hex;
}
void AmsWebServer::fromHex(uint8_t *out, String in, uint8_t size) {
for(int i = 0; i < size*2; i += 2) {
out[i/2] = strtol(in.substring(i, i+2).c_str(), 0, 16);
}
}
void AmsWebServer::configWifiHtml() {
printD("Serving /wifi.html over http...");

View File

@ -31,6 +31,7 @@ public:
void setup(AmsConfiguration*, GpioConfig*, MeterConfig*, AmsData*, MQTTClient*);
void loop();
void setTimezone(Timezone* tz);
void setMqttEnabled(bool);
private:
RemoteDebug* debugger;
@ -109,9 +110,6 @@ private:
void notFound();
String toHex(uint8_t* in, uint8_t size);
void fromHex(uint8_t *out, String in, uint8_t size);
void printD(String fmt, ...);
void printI(String fmt, ...);
void printW(String fmt, ...);

View File

@ -272,9 +272,9 @@ var fetch = function() {
}
setStatus("esp", json.em);
setStatus("han", json.em == 3 ? 0 : json.hm);
setStatus("wifi", json.em == 3 ? 0 : json.wm);
setStatus("mqtt", json.em == 3 ? 0 : json.mm);
setStatus("han", json.hm);
setStatus("wifi", json.wm);
setStatus("mqtt", json.mm);
if(im && im.gaugeMeter) {
@ -348,10 +348,10 @@ var fetch = function() {
}).fail(function() {
setTimeout(fetch, interval*4);
setStatus("mqtt", "secondary");
setStatus("wifi", "secondary");
setStatus("han", "secondary");
setStatus("esp", "danger");
setStatus("mqtt", 0);
setStatus("wifi", 0);
setStatus("han", 0);
setStatus("esp", 3);
});
}

6
web/domoticz.json Normal file
View File

@ -0,0 +1,6 @@
{
"command" : "udevice",
"idx" : %d,
"nvalue" : 0,
"svalue" : "%s"
}

12
web/json1.json Normal file
View File

@ -0,0 +1,12 @@
{
"id" : "%s",
"name" : "%s",
"up" : %d,
"t" : %d,
"vcc" : %.3f,
"rssi": %d,
"temp": %.2f,
"data" : {
"P" : %d
}
}

24
web/json2.json Normal file
View File

@ -0,0 +1,24 @@
{
"id" : "%s",
"name" : "%s",
"up" : %d,
"t" : %d,
"vcc" : %.3f,
"rssi": %d,
"temp": %.2f,
"data" : {
"lv" : "%s",
"id" : "%s",
"type" : "%s",
"P" : %d,
"Q" : %d,
"PO" : %d,
"QO" : %d,
"I1" : %.2f,
"I2" : %.2f,
"I3" : %.2f,
"U1" : %.2f,
"U2" : %.2f,
"U3" : %.2f
}
}

29
web/json3.json Normal file
View File

@ -0,0 +1,29 @@
{
"id" : "%s",
"name" : "%s",
"up" : %d,
"t" : %d,
"vcc" : %.3f,
"rssi": %d,
"temp": %.2f,
"data" : {
"lv" : "%s",
"id" : "%s",
"type" : "%s",
"P" : %d,
"Q" : %d,
"PO" : %d,
"QO" : %d,
"I1" : %.2f,
"I2" : %.2f,
"I3" : %.2f,
"U1" : %.2f,
"U2" : %.2f,
"U3" : %.2f,
"tPI" : %.1f,
"tPO" : %.1f,
"tQI" : %.1f,
"tQO" : %.1f,
"rtc" : %d
}
}

23
web/jsonprices.json Normal file
View File

@ -0,0 +1,23 @@
{
"id" : "%s",
"prices" : {
"0" : %.4f,
"1" : %.4f,
"2" : %.4f,
"3" : %.4f,
"4" : %.4f,
"5" : %.4f,
"6" : %.4f,
"7" : %.4f,
"8" : %.4f,
"9" : %.4f,
"10" : %.4f,
"11" : %.4f,
"12" : %.4f,
"min" : %.4f,
"max" : %.4f,
"cheapest1hr" : "%s",
"cheapest3hr" : "%s",
"cheapest6hr" : "%s"
}
}

8
web/jsonsys.json Normal file
View File

@ -0,0 +1,8 @@
{
"id" : "%s",
"name" : "%s",
"up" : %d,
"vcc" : %.3f,
"rssi": %d,
"temp": %.2f
}