Skip to content

Commit

Permalink
Implement HTTP(s) + JSON type Power Meter support
Browse files Browse the repository at this point in the history
  • Loading branch information
Bernhard Kaszt committed Apr 1, 2023
1 parent b3c17c8 commit cf40e78
Show file tree
Hide file tree
Showing 16 changed files with 637 additions and 81 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ This is a fork from the Hoymiles project OpenDTU.
This project is still under development and adds following features:

* Support Victron's Ve.Direct protocol on the same chip (cable based serial interface!). Additional information about Ve.direct can be downloaded from https://www.victronenergy.com/support-and-downloads/technical-information.
* Dynamically sets the Hoymiles power limited according to the currently used energy in the household (needs an MQTT based power meter like Shelly 3EM)
* Dynamically sets the Hoymiles power limited according to the currently used energy in the household. Needs an HTTP JSON based power meter (e.g. Tasmota), an MQTT based power meter like Shelly 3EM or an SDM power meter.
* Battery support: Read the voltage from Victron MPPT charge controller or from the Hoymiles DC inputs and starts/stops the power producing based on configurable voltage thresholds
* Voltage correction that takes the voltage drop because of the current output load into account (not 100% reliable calculation)
* Can read the current solar panel power from the Victron MPPT and adjust the limiter accordingly to not save energy in the battery (for increased system efficiency). Increases the battery lifespan and reduces energy loses.
Expand Down
20 changes: 19 additions & 1 deletion include/Configuration.h
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,13 @@

#define DEV_MAX_MAPPING_NAME_STRLEN 63

#define POWERMETER_MAX_PHASES 3
#define POWERMETER_MAX_HTTP_URL_STRLEN 1024
#define POWERMETER_MAX_HTTP_HEADER_KEY_STRLEN 64
#define POWERMETER_MAX_HTTP_HEADER_VALUE_STRLEN 256
#define POWERMETER_MAX_HTTP_JSON_PATH_STRLEN 256
#define POWERMETER_HTTP_TIMEOUT 1000

#define JSON_BUFFER_SIZE 12288

struct CHANNEL_CONFIG_T {
Expand All @@ -47,6 +54,15 @@ struct INVERTER_CONFIG_T {
CHANNEL_CONFIG_T channel[INV_MAX_CHAN_COUNT];
};

struct POWERMETER_HTTP_PHASE_CONFIG_T {
bool Enabled;
char Url[POWERMETER_MAX_HTTP_URL_STRLEN + 1];
char HeaderKey[POWERMETER_MAX_HTTP_HEADER_KEY_STRLEN + 1];
char HeaderValue[POWERMETER_MAX_HTTP_HEADER_VALUE_STRLEN + 1];
uint16_t Timeout;
char JsonPath[POWERMETER_MAX_HTTP_JSON_PATH_STRLEN + 1];
};

struct CONFIG_T {
uint32_t Cfg_Version;
uint Cfg_SaveCount;
Expand Down Expand Up @@ -107,7 +123,9 @@ struct CONFIG_T {
char PowerMeter_MqttTopicPowerMeter3[MQTT_MAX_TOPIC_STRLEN + 1];
uint32_t PowerMeter_SdmBaudrate;
uint32_t PowerMeter_SdmAddress;

uint32_t PowerMeter_HttpInterval;
bool PowerMeter_HttpIndividualRequests;
POWERMETER_HTTP_PHASE_CONFIG_T Powermeter_Http_Phase[POWERMETER_MAX_PHASES];

bool PowerLimiter_Enabled;
bool PowerLimiter_SolarPassTroughEnabled;
Expand Down
19 changes: 19 additions & 0 deletions include/HttpPowerMeter.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once

#include <stdint.h>

class HttpPowerMeterClass {
public:
void init();
bool updateValues();
float getPower(int8_t phase);
bool httpRequest(const char* url, const char* httpHeader, const char* httpValue, uint32_t timeout,
char* response, uint32_t responseSize, char* error, uint32_t errorSize);
float getFloatValueByJsonPath(const char* jsonString, const char* jsonPath, float &value);

private:
float power[POWERMETER_MAX_PHASES];
};

extern HttpPowerMeterClass HttpPowerMeter;
8 changes: 8 additions & 0 deletions include/PowerMeter.h
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,12 @@

class PowerMeterClass {
public:
enum SOURCE {
SOURCE_MQTT = 0,
SOURCE_SDM1PH = 1,
SOURCE_SDM3PH = 2,
SOURCE_HTTP = 3,
};
void init();
void mqtt();
void loop();
Expand All @@ -27,6 +33,8 @@ class PowerMeterClass {

private:
uint32_t _interval;
uint32_t _lastPowerMeterCheck;
// Used in Power limiter for safety check
uint32_t _lastPowerMeterUpdate;

float _powerMeter1Power = 0.0;
Expand Down
3 changes: 2 additions & 1 deletion include/WebApi_powermeter.h
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ class WebApiPowerMeterClass {
void onStatus(AsyncWebServerRequest* request);
void onAdminGet(AsyncWebServerRequest* request);
void onAdminPost(AsyncWebServerRequest* request);
void onTestHttpRequest(AsyncWebServerRequest* request);

AsyncWebServer* _server;
};
};
3 changes: 2 additions & 1 deletion platformio.ini
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ lib_deps =
olikraus/U8g2 @ ^2.34.16
buelowp/sunset @ ^1.1.7
https://github.com/coryjfowler/MCP_CAN_lib
mobizt/FirebaseJson @ ^3.0.6

extra_scripts =
pre:auto_firmware_version.py
Expand Down Expand Up @@ -180,4 +181,4 @@ build_flags = ${env.build_flags}
-DHOYMILES_PIN_SCLK=12
-DHOYMILES_PIN_CS=10
-DHOYMILES_PIN_IRQ=4
-DHOYMILES_PIN_CE=5
-DHOYMILES_PIN_CE=5
27 changes: 26 additions & 1 deletion src/Configuration.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,19 @@ bool ConfigurationClass::write()
powermeter["mqtt_topic_powermeter_3"] = config.PowerMeter_MqttTopicPowerMeter3;
powermeter["sdmbaudrate"] = config.PowerMeter_SdmBaudrate;
powermeter["sdmaddress"] = config.PowerMeter_SdmAddress;
powermeter["http_individual_requests"] = config.PowerMeter_HttpIndividualRequests;

JsonArray powermeter_http_phases = powermeter.createNestedArray("http_phases");
for (uint8_t i = 0; i < POWERMETER_MAX_PHASES; i++) {
JsonObject powermeter_phase = powermeter_http_phases.createNestedObject();

powermeter_phase["enabled"] = config.Powermeter_Http_Phase[i].Enabled;
powermeter_phase["url"] = config.Powermeter_Http_Phase[i].Url;
powermeter_phase["header_key"] = config.Powermeter_Http_Phase[i].HeaderKey;
powermeter_phase["header_value"] = config.Powermeter_Http_Phase[i].HeaderValue;
powermeter_phase["timeout"] = config.Powermeter_Http_Phase[i].Timeout;
powermeter_phase["json_path"] = config.Powermeter_Http_Phase[i].JsonPath;
}

JsonObject powerlimiter = doc.createNestedObject("powerlimiter");
powerlimiter["enabled"] = config.PowerLimiter_Enabled;
Expand Down Expand Up @@ -300,7 +313,19 @@ bool ConfigurationClass::read()
strlcpy(config.PowerMeter_MqttTopicPowerMeter3, powermeter["mqtt_topic_powermeter_3"] | "", sizeof(config.PowerMeter_MqttTopicPowerMeter3));
config.PowerMeter_SdmBaudrate = powermeter["sdmbaudrate"] | POWERMETER_SDMBAUDRATE;
config.PowerMeter_SdmAddress = powermeter["sdmaddress"] | POWERMETER_SDMADDRESS;

config.PowerMeter_HttpIndividualRequests = powermeter["http_individual_requests"] | false;

JsonArray powermeter_http_phases = powermeter["http_phases"];
for (uint8_t i = 0; i < POWERMETER_MAX_PHASES; i++) {
JsonObject powermeter_phase = powermeter_http_phases[i].as<JsonObject>();

config.Powermeter_Http_Phase[i].Enabled = powermeter_phase["enabled"] | (i == 0);
strlcpy(config.Powermeter_Http_Phase[i].Url, powermeter_phase["url"] | "", sizeof(config.Powermeter_Http_Phase[i].Url));
strlcpy(config.Powermeter_Http_Phase[i].HeaderKey, powermeter_phase["header_key"] | "", sizeof(config.Powermeter_Http_Phase[i].HeaderKey));
strlcpy(config.Powermeter_Http_Phase[i].HeaderValue, powermeter_phase["header_value"] | "", sizeof(config.Powermeter_Http_Phase[i].HeaderValue));
config.Powermeter_Http_Phase[i].Timeout = powermeter_phase["timeout"] | POWERMETER_HTTP_TIMEOUT;
strlcpy(config.Powermeter_Http_Phase[i].JsonPath, powermeter_phase["json_path"] | "", sizeof(config.Powermeter_Http_Phase[i].JsonPath));
}

JsonObject powerlimiter = doc["powerlimiter"];
config.PowerLimiter_Enabled = powerlimiter["enabled"] | POWERLIMITER_ENABLED;
Expand Down
127 changes: 127 additions & 0 deletions src/HttpPowerMeter.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
// SPDX-License-Identifier: GPL-2.0-or-later
#include "Configuration.h"
#include "HttpPowerMeter.h"
#include "MessageOutput.h"
#include <WiFiClientSecure.h>
#include <HTTPClient.h>
#include <FirebaseJson.h>

void HttpPowerMeterClass::init()
{
}

float HttpPowerMeterClass::getPower(int8_t phase)
{
return power[phase - 1];
}

bool HttpPowerMeterClass::updateValues()
{
const CONFIG_T& config = Configuration.get();

char response[2000],
errorMessage[256];
bool success = true;

for (uint8_t i = 0; i < POWERMETER_MAX_PHASES; i++) {
POWERMETER_HTTP_PHASE_CONFIG_T phaseConfig = config.Powermeter_Http_Phase[i];

if (!phaseConfig.Enabled || !success) {
power[i] = 0.0;
continue;
}

if (i == 0 || config.PowerMeter_HttpIndividualRequests) {
if (!httpRequest(phaseConfig.Url, phaseConfig.HeaderKey, phaseConfig.HeaderValue, phaseConfig.Timeout,
response, sizeof(response), errorMessage, sizeof(errorMessage))) {
MessageOutput.printf("[HttpPowerMeter] Getting the power of phase %d failed. Error: %s\n",
i + 1, errorMessage);
success = false;
}
}

if (!getFloatValueByJsonPath(response, phaseConfig.JsonPath, power[i])) {
MessageOutput.printf("[HttpPowerMeter] Couldn't find a value with Json query \"%s\"\n", phaseConfig.JsonPath);
success = false;
}
}

return success;
}

bool HttpPowerMeterClass::httpRequest(const char* url, const char* httpHeader, const char* httpValue, uint32_t timeout,
char* response, uint32_t responseSize, char* error, uint32_t errorSize)
{
WiFiClient* wifiClient = NULL;
HTTPClient httpClient;

response[0] = '\0';
error[0] = '\0';

if (String(url).substring(0, 6) == "https:") {
wifiClient = new WiFiClientSecure;
((WiFiClientSecure*)wifiClient)->setInsecure();
} else {
wifiClient = new WiFiClient;
}

if (!httpClient.begin(*wifiClient, url)) {
snprintf_P(error, errorSize, "httpClient.begin failed");
delete wifiClient;
return false;
}

httpClient.setFollowRedirects(HTTPC_STRICT_FOLLOW_REDIRECTS);
httpClient.setUserAgent("OpenDTU-OnBattery");
httpClient.setConnectTimeout(timeout);
httpClient.setTimeout(timeout);
httpClient.addHeader("Content-Type", "application/json");
httpClient.addHeader("Accept", "application/json");

if (strlen(httpHeader) > 0) {
httpClient.addHeader(httpHeader, httpValue);
}

int httpCode = httpClient.GET();


if (httpCode == HTTP_CODE_OK) {
if (httpClient.getSize() > (responseSize - 1)) {
snprintf_P(error, errorSize, "Response too large!");
} else {
strcpy(response, httpClient.getString().c_str());
}
} else if (httpCode <= 0) {
snprintf_P(error, errorSize, "Error: %s", httpClient.errorToString(httpCode).c_str());
} else if (httpCode != HTTP_CODE_OK) {
snprintf_P(error, errorSize, "Bad HTTP code: %d", httpCode);
}

httpClient.end();
delete wifiClient;

if (error[0] != '\0') {
return false;
}

return true;
}

float HttpPowerMeterClass::getFloatValueByJsonPath(const char* jsonString, const char* jsonPath, float& value)
{
FirebaseJson firebaseJson;
firebaseJson.setJsonData(jsonString);

FirebaseJsonData firebaseJsonResult;
if (!firebaseJson.get(firebaseJsonResult, jsonPath)) {
return false;
}

value = firebaseJsonResult.to<float>();

firebaseJson.clear();

return true;
}

HttpPowerMeterClass HttpPowerMeter;
2 changes: 0 additions & 2 deletions src/Huawei_can.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ HuaweiCanClass HuaweiCan;

void HuaweiCanClass::init(uint8_t huawei_miso, uint8_t huawei_mosi, uint8_t huawei_clk, uint8_t huawei_irq, uint8_t huawei_cs, uint8_t huawei_power)
{

spi = new SPIClass(VSPI);
spi->begin(huawei_clk, huawei_miso, huawei_mosi, huawei_cs);
pinMode(huawei_cs, OUTPUT);
Expand Down Expand Up @@ -141,7 +140,6 @@ void HuaweiCanClass::onReceive(uint8_t* frame, uint8_t len)

void HuaweiCanClass::loop()
{

long unsigned int rxId;
unsigned char len = 0;
unsigned char rxBuf[8];
Expand Down
3 changes: 2 additions & 1 deletion src/PowerLimiter.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -175,7 +175,8 @@ int32_t PowerLimiterClass::calcPowerLimit(std::shared_ptr<InverterAbstract> inve

MessageOutput.printf("[PowerLimiterClass::loop] victronChargePower: %d, efficiency: %.2f, consumeSolarPowerOnly: %s, powerConsumption: %d \r\n",
victronChargePower, efficency, consumeSolarPowerOnly ? "true" : "false", newPowerLimit);


// Safety check: Are the power meter values not too old?
if (millis() - PowerMeter.getLastPowerMeterUpdate() < (30 * 1000)) {
if (config.PowerLimiter_IsInverterBehindPowerMeter) {
// If the inverter the behind the power meter (part of measurement),
Expand Down
Loading

0 comments on commit cf40e78

Please sign in to comment.