You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

583 lines
20 KiB

//-----------------------------------------------------------------------------
// 2024 Ahoy, https://ahoydtu.de
// Creative Commons - https://creativecommons.org/licenses/by-nc-sa/4.0/deed
//-----------------------------------------------------------------------------
#if defined(PLUGIN_ZEROEXPORT)
#ifndef __POWERMETER_H__
#define __POWERMETER_H__
#include <AsyncJson.h>
#include <HTTPClient.h>
#include "config/settings.h"
#if defined(ZEROEXPORT_POWERMETER_TIBBER)
#include <base64.h>
#include <string.h>
#include <list>
#include "plugins/zeroExport/lib/sml.h"
typedef struct {
const unsigned char OBIS[6];
void (*Fn)(double &);
float *Arg;
} OBISHandler;
#endif
class powermeter {
public:
/** powermeter
* constructor
*/
powermeter() {}
/** ~powermeter
* destructor
*/
~powermeter() {}
/** setup
* Initialisierung
* @param *cfg
* @param *mqtt
* @param *log
* @returns void
*/
bool setup(zeroExport_t *cfg, PubMqttType *mqtt, JsonObject *log) {
mCfg = cfg;
mMqtt = mqtt;
mLog = log;
return true;
}
/** loop
* Arbeitsschleife
* @param void
* @returns void
* @todo emergency
*/
void loop(void) {
if (millis() - mPreviousTsp <= 1000) return; // skip when it is to fast
mPreviousTsp = millis();
if (mCfg->debug) DBGPRINTLN(F("pm Takt:"));
bool result = false;
float power = 0.0;
for (u_short group = 0; group < ZEROEXPORT_MAX_GROUPS; group++) {
if ((!mCfg->groups[group].enabled) || (mCfg->groups[group].sleep)) continue;
if ((millis() - mCfg->groups[group].pm_peviousTsp) < ((uint16_t)mCfg->groups[group].pm_refresh * 1000)) continue;
mCfg->groups[group].pm_peviousTsp = millis();
if (mCfg->debug) DBGPRINTLN(F("pm Do:"));
result = false;
power = 0.0;
switch (mCfg->groups[group].pm_type) {
#if defined(ZEROEXPORT_POWERMETER_SHELLY)
case zeroExportPowermeterType_t::Shelly:
result = getPowermeterWattsShelly(*mLog, group, &power);
break;
#endif
#if defined(ZEROEXPORT_POWERMETER_TASMOTA)
case zeroExportPowermeterType_t::Tasmota:
result = getPowermeterWattsTasmota(*mLog, group, &power);
break;
#endif
#if defined(ZEROEXPORT_POWERMETER_HICHI)
case zeroExportPowermeterType_t::Hichi:
result = getPowermeterWattsHichi(*mLog, group, &power);
break;
#endif
#if defined(ZEROEXPORT_POWERMETER_TIBBER)
case zeroExportPowermeterType_t::Tibber:
result = getPowermeterWattsTibber(*mLog, group, &power);
mPreviousTsp += 2000; // Zusätzliche Pause
break;
#endif
#if defined(ZEROEXPORT_POWERMETER_SHRDZM)
case zeroExportPowermeterType_t::Shrdzm:
result = getPowermeterWattsShrdzm(*mLog, group, &power);
break;
#endif
}
if (result) {
bufferWrite(power, group);
// MQTT - Powermeter
// if (mCfg->debug) {
if (mMqtt->isConnected()) {
mMqtt->publish(String("zero/state/groups/" + String(group) + "/powermeter/P").c_str(), String(ah::round1(power)).c_str(), false);
}
// }
}
}
}
/** getDataAVG
* Holt die Daten vom Powermeter
* @param group
* @returns value
*/
float getDataAVG(uint8_t group) {
float avg = 0.0;
for (int i = 0; i < 5; i++) {
avg += mPowermeterBuffer[group][i];
}
avg = avg / 5.0;
return avg;
}
/** getDataMIN
* Holt die Daten vom Powermeter
* @param group
* @returns value
*/
float getDataMIN(uint8_t group) {
float min = 0.0;
for (int i = 0; i < 5; i++) {
if (i == 0)
min = mPowermeterBuffer[group][i];
if (min > mPowermeterBuffer[group][i])
min = mPowermeterBuffer[group][i];
}
return min;
}
/** onMqttConnect
*
*/
void onMqttConnect(void) {
#if defined(ZEROEXPORT_POWERMETER_MQTT)
for (uint8_t group = 0; group < ZEROEXPORT_MAX_GROUPS; group++) {
if (!strcmp(mCfg->groups[group].pm_src, "")) continue;
if (!mCfg->groups[group].enabled) continue;
if (mCfg->groups[group].pm_type == zeroExportPowermeterType_t::Mqtt) {
mMqtt->subscribeExtern(String(mCfg->groups[group].pm_src).c_str(), QOS_2);
}
}
#endif /*defined(ZEROEXPORT_POWERMETER_MQTT)*/
}
/** onMqttMessage
*
*/
void onMqttMessage(JsonObject obj) {
String topic = String(obj["topic"]);
#if defined(ZEROEXPORT_POWERMETER_MQTT)
for (uint8_t group = 0; group < ZEROEXPORT_MAX_GROUPS; group++) {
if (!mCfg->groups[group].enabled) continue;
if (!mCfg->groups[group].pm_type == zeroExportPowermeterType_t::Mqtt) continue;
if (!strcmp(mCfg->groups[group].pm_src, "")) continue;
if (strcmp(mCfg->groups[group].pm_src, String(topic).c_str())) continue;
float power = 0.0;
power = (uint16_t)obj["val"];
bufferWrite(power, group);
// MQTT - Powermeter
if (mCfg->debug) {
if (mMqtt->isConnected()) {
mMqtt->publish(String("zero/state/groups/" + String(group) + "/powermeter/P").c_str(), String(ah::round1(power)).c_str(), false);
}
}
return;
}
#endif /*defined(ZEROEXPORT_POWERMETER_MQTT)*/
}
private:
/** mqttSubscribe
* when a MQTT Msg is needed to subscribe, then a publish is leading
* @param gr
* @param payload
* @returns void
*/
void mqttSubscribe(String gr, String payload) {
// mqttPublish(gr, payload);
mMqtt->subscribe(gr.c_str(), QOS_2);
}
/** mqttPublish
* when a MQTT Msg is needed to Publish, but not to subscribe.
* @param gr
* @param payload
* @param retain
* @returns void
*/
void mqttPublish(String gr, String payload, bool retain = false) {
mMqtt->publish(gr.c_str(), payload.c_str(), retain);
}
HTTPClient http;
zeroExport_t *mCfg;
PubMqttType *mMqtt = nullptr;
JsonObject *mLog;
unsigned long mPreviousTsp = 0;
float mPowermeterBuffer[ZEROEXPORT_MAX_GROUPS][5] = {0};
short mPowermeterBufferPos[ZEROEXPORT_MAX_GROUPS] = {0};
StaticJsonDocument<512> mqttDoc; // DynamicJsonDocument mqttDoc(512);
JsonObject mqttObj = mqttDoc.to<JsonObject>();
/** setHeader
*
*/
void setHeader(HTTPClient *h) {
h->setFollowRedirects(HTTPC_STRICT_FOLLOW_REDIRECTS);
h->setUserAgent("Ahoy-Agent");
// TODO: Ahoy-0.8.850024-zero
h->setConnectTimeout(500);
h->setTimeout(1000);
h->addHeader("Content-Type", "application/json");
h->addHeader("Accept", "application/json");
}
#if defined(ZEROEXPORT_POWERMETER_SHELLY)
/** getPowermeterWattsShelly
* ...
* @param logObj
* @param group
* @returns true/false
*/
bool getPowermeterWattsShelly(JsonObject logObj, uint8_t group, float *power) {
logObj["mod"] = "getPowermeterWattsShelly";
setHeader(&http);
String url = String("http://") + String(mCfg->groups[group].pm_src) + String("/") + String(mCfg->groups[group].pm_jsonPath);
logObj["HTTP_URL"] = url;
http.begin(url);
if (http.GET() == HTTP_CODE_OK) {
// Parsing
DynamicJsonDocument doc(2048);
DeserializationError error = deserializeJson(doc, http.getString());
if (error) {
logObj["err"] = "deserializeJson: " + String(error.c_str());
return false;
} else {
switch (mCfg->groups[group].pm_target) {
case zeroExportPowermeterTarget::L1:
if (doc.containsKey(F("emeters"))) {
// Shelly 3EM
*power = doc["emeters"][0]["power"];
} else if (doc.containsKey(F("em:0"))) {
// Shelly pro 3EM
*power = doc["em:0"]["a_act_power"];
} else if (doc.containsKey(F("a_act_power"))) {
// Shelly pro 3EM
*power = doc["a_act_power"];
}
break;
case zeroExportPowermeterTarget::L2:
if (doc.containsKey(F("emeters"))) {
// Shelly 3EM
*power = doc["emeters"][1]["power"];
} else if (doc.containsKey(F("em:0"))) {
// Shelly pro 3EM
*power = doc["em:0"]["b_act_power"];
} else if (doc.containsKey(F("b_act_power"))) {
// Shelly pro 3EM
*power = doc["b_act_power"];
}
break;
case zeroExportPowermeterTarget::L3:
if (doc.containsKey(F("emeters"))) {
// Shelly 3EM
*power = doc["emeters"][2]["power"];
} else if (doc.containsKey(F("em:0"))) {
// Shelly pro 3EM
*power = doc["em:0"]["c_act_power"];
} else if (doc.containsKey(F("c_act_power"))) {
// Shelly pro 3EM
*power = doc["c_act_power"];
}
break;
case zeroExportPowermeterTarget::Sum:
default:
if (doc.containsKey(F("total_power"))) {
// Shelly 3EM
*power = doc["total_power"];
} else if (doc.containsKey(F("em:0"))) {
// Shelly pro 3EM
*power = doc["em:0"]["total_act_power"];
} else if (doc.containsKey(F("total_act_power"))) {
// Shelly pro 3EM
*power = doc["total_act_power"];
}
break;
}
}
}
http.end();
return true;
}
#endif
#if defined(ZEROEXPORT_POWERMETER_TASMOTA)
/** getPowermeterWattsTasmota
* ...
* @param logObj
* @param group
* @returns true/false
*/
bool getPowermeterWattsTasmota(JsonObject logObj, uint8_t group, float *power) {
logObj["mod"] = "getPowermeterWattsTasmota";
/*
// TODO: nicht komplett
HTTPClient http;
http.setFollowRedirects(HTTPC_STRICT_FOLLOW_REDIRECTS);
http.setUserAgent("Ahoy-Agent");
// TODO: Ahoy-0.8.850024-zero
http.setConnectTimeout(500);
http.setTimeout(500);
// TODO: Timeout von 1000 reduzieren?
http.addHeader("Content-Type", "application/json");
http.addHeader("Accept", "application/json");
// String url = String("http://") + String(mCfg->groups[group].pm_src) + String("/") + String(mCfg->groups[group].pm_jsonPath);
String url = String(mCfg->groups[group].pm_src);
logObj["HTTP_URL"] = url;
http.begin(url);
if (http.GET() == HTTP_CODE_OK)
{
// Parsing
DynamicJsonDocument doc(2048);
DeserializationError error = deserializeJson(doc, http.getString());
if (error)
{
logObj["error"] = "deserializeJson() failed: " + String(error.c_str());
return result;
}
// TODO: Sum
result = true;
// TODO: L1
// TODO: L2
// TODO: L3
/*
JsonObject Tasmota_ENERGY = doc["StatusSNS"]["ENERGY"];
int Tasmota_Power = Tasmota_ENERGY["Power"]; // 0
return Tasmota_Power;
*/
/*
String url = "http://" + String(TASMOTA_IP) + "/cm?cmnd=status%2010";
ParsedData = http.get(url).json();
int Watts = ParsedData[TASMOTA_JSON_STATUS][TASMOTA_JSON_PAYLOAD_MQTT_PREFIX][TASMOTA_JSON_POWER_MQTT_LABEL].toInt();
return Watts;
*/
/*
logObj["P"] = mCfg->groups[group].pmPower;
logObj["P1"] = mCfg->groups[group].pmPowerL1;
logObj["P2"] = mCfg->groups[group].pmPowerL2;
logObj["P3"] = mCfg->groups[group].pmPowerL3;
}
http.end();
*/
return false;
}
#endif
#if defined(ZEROEXPORT_POWERMETER_HICHI)
/** getPowermeterWattsHichi
* ...
* @param logObj
* @param group
* @returns true/false
*/
bool getPowermeterWattsHichi(JsonObject logObj, uint8_t group, float *power) {
logObj["mod"] = "getPowermeterWattsHichi";
// Hier neuer Code - Anfang
// TODO: Noch nicht komplett
// Hier neuer Code - Ende
return false;
}
#endif
#if defined(ZEROEXPORT_POWERMETER_TIBBER)
/** getPowermeterWattsTibber
* ...
* @param logObj
* @param group
* @returns true/false
* @TODO: Username & Passwort wird mittels base64 verschlüsselt. Dies wird für die Authentizierung benötigt. Wichtig diese im WebUI unkenntlich zu machen und base64 im eeprom zu speichern, statt klartext.
* @TODO: Abfrage Interval einbauen. Info: Datei-Size kann auch mal 0-bytes sein!
*/
sml_states_t currentState;
float _powerMeterTotal = 0.0;
float _powerMeter1Power = 0.0;
float _powerMeter2Power = 0.0;
float _powerMeter3Power = 0.0;
float _powerMeterImport = 0.0;
float _powerMeterExport = 0.0;
/*
07 81 81 c7 82 03 ff #objName: OBIS Kennzahl für den Hersteller
07 01 00 01 08 00 ff #objName: OBIS Kennzahl für Wirkenergie Bezug gesamt tariflos
07 01 00 01 08 01 ff #objName: OBIS-Kennzahl für Wirkenergie Bezug Tarif1
07 01 00 01 08 02 ff #objName: OBIS-Kennzahl für Wirkenergie Bezug Tarif2
07 01 00 02 08 00 ff #objName: OBIS-Kennzahl für Wirkenergie Einspeisung gesamt tariflos
07 01 00 02 08 01 ff #objName: OBIS-Kennzahl für Wirkenergie Einspeisung Tarif1
07 01 00 02 08 02 ff #objName: OBIS-Kennzahl für Wirkenergie Einspeisung Tarif2
*/
const std::list<OBISHandler> smlHandlerList{
{{0x01, 0x00, 0x10, 0x07, 0x00, 0xff}, &smlOBISW, &_powerMeterTotal}, // total - OBIS-Kennzahl für momentane Gesamtwirkleistung
{{0x01, 0x00, 0x24, 0x07, 0x00, 0xff}, &smlOBISW, &_powerMeter1Power}, // OBIS-Kennzahl für momentane Wirkleistung in Phase L1
{{0x01, 0x00, 0x38, 0x07, 0x00, 0xff}, &smlOBISW, &_powerMeter2Power}, // OBIS-Kennzahl für momentane Wirkleistung in Phase L2
{{0x01, 0x00, 0x4c, 0x07, 0x00, 0xff}, &smlOBISW, &_powerMeter3Power}, // OBIS-Kennzahl für momentane Wirkleistung in Phase L3
{{0x01, 0x00, 0x01, 0x08, 0x00, 0xff}, &smlOBISWh, &_powerMeterImport},
{{0x01, 0x00, 0x02, 0x08, 0x00, 0xff}, &smlOBISWh, &_powerMeterExport}};
bool getPowermeterWattsTibber(JsonObject logObj, uint8_t group, float *power) {
mPreviousTsp = mPreviousTsp + 2000; // Zusätzliche Pause
bool result = false;
logObj["mod"] = "getPowermeterWattsTibber";
String auth;
if (strlen(mCfg->groups[group].pm_user) > 0 && strlen(mCfg->groups[group].pm_pass) > 0) {
auth = base64::encode(String(mCfg->groups[group].pm_user) + String(":") + String(mCfg->groups[group].pm_pass));
snprintf(mCfg->groups[group].pm_user, ZEROEXPORT_GROUP_MAX_LEN_PM_USER, "%s", DEF_ZEXPORT);
snprintf(mCfg->groups[group].pm_pass, ZEROEXPORT_GROUP_MAX_LEN_PM_PASS, "%s", auth.c_str());
//@TODO:mApp->saveSettings(false);
} else {
auth = mCfg->groups[group].pm_pass;
}
String url = String("http://") + mCfg->groups[group].pm_src + String("/") + String(mCfg->groups[group].pm_jsonPath);
setHeader(&http);
http.begin(url);
http.addHeader("Authorization", "Basic " + auth);
if (http.GET() == HTTP_CODE_OK && http.getSize() > 0) {
String myString = http.getString();
double readVal = 0;
unsigned char c;
for (int i = 0; i < http.getSize(); ++i) {
c = myString[i];
sml_states_t smlCurrentState = smlState(c);
switch (smlCurrentState) {
case SML_FINAL:
*power = _powerMeterTotal;
result = true;
break;
case SML_LISTEND:
// check handlers on last received list
for (auto &handler : smlHandlerList) {
if (smlOBISCheck(handler.OBIS)) {
handler.Fn(readVal);
*handler.Arg = readVal;
}
}
break;
}
}
}
http.end();
return result;
}
#endif
#if defined(ZEROEXPORT_POWERMETER_SHRDZM)
/** getPowermeterWattsShrdzm
* ...
* @param logObj
* @param group
* @returns true/false
* @TODO: Username & Passwort wird mittels base64 verschlüsselt. Dies wird für die Authentizierung benötigt. Wichtig diese im WebUI unkenntlich zu machen und base64 im eeprom zu speichern, statt klartext.
* @TODO: Abfrage Interval einbauen. Info: Datei-Size kann auch mal 0-bytes sein?
*/
bool getPowermeterWattsShrdzm(JsonObject logObj, uint8_t group, float *power) {
logObj["mod"] = "getPowermeterWattsShrdzm";
setHeader(&http);
String url =
String("http://") + String(mCfg->groups[group].pm_src) +
String("/") + String(mCfg->groups[group].pm_jsonPath + String("?user=") + String(mCfg->groups[group].pm_user) + String("&password=") + String(mCfg->groups[group].pm_pass));
http.begin(url);
if (http.GET() == HTTP_CODE_OK && http.getSize() > 0) {
// Parsing
DynamicJsonDocument doc(512);
DeserializationError error = deserializeJson(doc, http.getString());
if (error) {
logObj["err"] = "deserializeJson: " + String(error.c_str());
return false;
} else {
if (doc.containsKey(F("16.7.0"))) {
*power = doc["16.7.0"];
}
}
}
http.end();
return true;
}
#endif
/**
*
*/
void bufferWrite(float raw, short group) {
mPowermeterBuffer[group][mPowermeterBufferPos[group]] = raw;
mPowermeterBufferPos[group]++;
if (mPowermeterBufferPos[group] >= 5) mPowermeterBufferPos[group] = 0;
}
};
#endif /*__POWERMETER_H__*/
#endif /* #if defined(PLUGIN_ZEROEXPORT) */