Skip to content

Commit

Permalink
Feature: live view: update with respective frequency
Browse files Browse the repository at this point in the history
the update frequency of Victron MPPT charger data, the battery Soc, the
huawei charger power, and the power meter differ from one another, and
differ in particular from the inverter update frequency.

the OnBattery-specific data is now handled in a new method, outside the
upstream code, which merely call the new function(s). the new function
will update the websocket independently from inverter updates. also, it
adds the respective data if it actually changed since it was last
updated through the websocket.

for the webapp to be able to recover in case of errors, all values are
also written to the websocket with a fixed interval of 10 seconds.
  • Loading branch information
schlimmchen committed Mar 5, 2024
1 parent e432f0e commit 50635ee
Show file tree
Hide file tree
Showing 3 changed files with 81 additions and 25 deletions.
9 changes: 9 additions & 0 deletions include/WebApi_ws_live.h
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@ class WebApiWsLiveClass {
static void generateInverterChannelJsonResponse(JsonObject& root, std::shared_ptr<InverterAbstract> inv);
static void generateCommonJsonResponse(JsonVariant& root);

void generateOnBatteryJsonResponse(JsonVariant& root, bool all);
void sendOnBatteryStats();

static void addField(JsonObject& root, std::shared_ptr<InverterAbstract> inv, const ChannelType_t type, const ChannelNum_t channel, const FieldId_t fieldId, String topic = "");
static void addTotalField(JsonObject& root, const String& name, const float value, const String& unit, const uint8_t digits);

Expand All @@ -25,6 +28,12 @@ class WebApiWsLiveClass {

AsyncWebSocket _ws;

uint32_t _lastPublishOnBatteryFull = 0;
uint32_t _lastPublishVictron = 0;
uint32_t _lastPublishHuawei = 0;
uint32_t _lastPublishBattery = 0;
uint32_t _lastPublishPowerMeter = 0;

uint32_t _lastPublishStats[INV_MAX_COUNT] = { 0 };

std::mutex _mutex;
Expand Down
85 changes: 64 additions & 21 deletions src/WebApi_ws_live.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -54,13 +54,75 @@ void WebApiWsLiveClass::wsCleanupTaskCb()
}
}

void WebApiWsLiveClass::generateOnBatteryJsonResponse(JsonVariant& root, bool all)
{
auto constexpr halfOfAllMillis = std::numeric_limits<uint32_t>::max() / 2;

if (all || (millis() - _lastPublishVictron) > VictronMppt.getDataAgeMillis()) {
JsonObject vedirectObj = root.createNestedObject("vedirect");
vedirectObj["enabled"] = Configuration.get().Vedirect.Enabled;
JsonObject totalVeObj = vedirectObj.createNestedObject("total");

addTotalField(totalVeObj, "Power", VictronMppt.getPanelPowerWatts(), "W", 1);
addTotalField(totalVeObj, "YieldDay", VictronMppt.getYieldDay() * 1000, "Wh", 0);
addTotalField(totalVeObj, "YieldTotal", VictronMppt.getYieldTotal(), "kWh", 2);

if (!all) { _lastPublishVictron = millis(); }
}

if (all || (HuaweiCan.getLastUpdate() - _lastPublishHuawei) < halfOfAllMillis ) {
JsonObject huaweiObj = root.createNestedObject("huawei");
huaweiObj["enabled"] = Configuration.get().Huawei.Enabled;
const RectifierParameters_t * rp = HuaweiCan.get();
addTotalField(huaweiObj, "Power", rp->output_power, "W", 2);

if (!all) { _lastPublishHuawei = millis(); }
}

auto spStats = Battery.getStats();
if (all || spStats->updateAvailable(_lastPublishBattery)) {
JsonObject batteryObj = root.createNestedObject("battery");
batteryObj["enabled"] = Configuration.get().Battery.Enabled;
addTotalField(batteryObj, "soc", spStats->getSoC(), "%", 0);

if (!all) { _lastPublishBattery = millis(); }
}

if (all || (PowerMeter.getLastPowerMeterUpdate() - _lastPublishPowerMeter) < halfOfAllMillis) {
JsonObject powerMeterObj = root.createNestedObject("power_meter");
powerMeterObj["enabled"] = Configuration.get().PowerMeter.Enabled;
addTotalField(powerMeterObj, "Power", PowerMeter.getPowerTotal(false), "W", 1);

if (!all) { _lastPublishPowerMeter = millis(); }
}
}

void WebApiWsLiveClass::sendOnBatteryStats()
{
DynamicJsonDocument root(512);
if (!Utils::checkJsonAlloc(root, __FUNCTION__, __LINE__)) { return; }

JsonVariant var = root;

bool all = (millis() - _lastPublishOnBatteryFull) > 10 * 1000;
if (all) { _lastPublishOnBatteryFull = millis(); }
generateOnBatteryJsonResponse(var, all);

String buffer;
serializeJson(root, buffer);

_ws.textAll(buffer);
}

void WebApiWsLiveClass::sendDataTaskCb()
{
// do nothing if no WS client is connected
if (_ws.count() == 0) {
return;
}

sendOnBatteryStats();

// Loop all inverters
for (uint8_t i = 0; i < Hoymiles.getNumInverters(); i++) {
auto inv = Hoymiles.getInverterByPos(i);
Expand Down Expand Up @@ -115,27 +177,6 @@ void WebApiWsLiveClass::generateCommonJsonResponse(JsonVariant& root)
hintObj["time_sync"] = !getLocalTime(&timeinfo, 5);
hintObj["radio_problem"] = (Hoymiles.getRadioNrf()->isInitialized() && (!Hoymiles.getRadioNrf()->isConnected() || !Hoymiles.getRadioNrf()->isPVariant())) || (Hoymiles.getRadioCmt()->isInitialized() && (!Hoymiles.getRadioCmt()->isConnected()));
hintObj["default_password"] = strcmp(Configuration.get().Security.Password, ACCESS_POINT_PASSWORD) == 0;

JsonObject vedirectObj = root.createNestedObject("vedirect");
vedirectObj["enabled"] = Configuration.get().Vedirect.Enabled;
JsonObject totalVeObj = vedirectObj.createNestedObject("total");

addTotalField(totalVeObj, "Power", VictronMppt.getPanelPowerWatts(), "W", 1);
addTotalField(totalVeObj, "YieldDay", VictronMppt.getYieldDay() * 1000, "Wh", 0);
addTotalField(totalVeObj, "YieldTotal", VictronMppt.getYieldTotal(), "kWh", 2);

JsonObject huaweiObj = root.createNestedObject("huawei");
huaweiObj["enabled"] = Configuration.get().Huawei.Enabled;
const RectifierParameters_t * rp = HuaweiCan.get();
addTotalField(huaweiObj, "Power", rp->output_power, "W", 2);

JsonObject batteryObj = root.createNestedObject("battery");
batteryObj["enabled"] = Configuration.get().Battery.Enabled;
addTotalField(batteryObj, "soc", Battery.getStats()->getSoC(), "%", 0);

JsonObject powerMeterObj = root.createNestedObject("power_meter");
powerMeterObj["enabled"] = Configuration.get().PowerMeter.Enabled;
addTotalField(powerMeterObj, "Power", PowerMeter.getPowerTotal(false), "W", 1);
}

void WebApiWsLiveClass::generateInverterCommonJsonResponse(JsonObject& root, std::shared_ptr<InverterAbstract> inv)
Expand Down Expand Up @@ -279,6 +320,8 @@ void WebApiWsLiveClass::onLivedataStatus(AsyncWebServerRequest* request)

generateCommonJsonResponse(root);

generateOnBatteryJsonResponse(root, true);

response->setLength();
request->send(response);

Expand Down
12 changes: 8 additions & 4 deletions webapp/src/views/HomeView.vue
Original file line number Diff line number Diff line change
Expand Up @@ -461,12 +461,16 @@ export default defineComponent({
console.log(event);
if (event.data != "{}") {
const newData = JSON.parse(event.data);
if (typeof newData.vedirect !== 'undefined') { Object.assign(this.liveData.vedirect, newData.vedirect); }
if (typeof newData.huawei !== 'undefined') { Object.assign(this.liveData.huawei, newData.huawei); }
if (typeof newData.battery !== 'undefined') { Object.assign(this.liveData.battery, newData.battery); }
if (typeof newData.power_meter !== 'undefined') { Object.assign(this.liveData.power_meter, newData.power_meter); }
if (typeof newData.total === 'undefined') { return; }
Object.assign(this.liveData.total, newData.total);
Object.assign(this.liveData.hints, newData.hints);
Object.assign(this.liveData.vedirect, newData.vedirect);
Object.assign(this.liveData.huawei, newData.huawei);
Object.assign(this.liveData.battery, newData.battery);
Object.assign(this.liveData.power_meter, newData.power_meter);
const foundIdx = this.liveData.inverters.findIndex((element) => element.serial == newData.inverters[0].serial);
if (foundIdx == -1) {
Expand Down

0 comments on commit 50635ee

Please sign in to comment.