OpenDTU-old/src/WebApi_ws_live.cpp
Bernhard Kirchen d5d1a9982f Fix: force websocket clients to authenticate
when changing the security settings (disabling read-only access or
changing the password), existing websocket connections are now closed,
forcing the respective clients to authenticate (with the new password).
otherwise, existing websocket clients keep connected even though the
security settings now expect authentication with a (changed) password.
2024-09-30 15:54:55 +02:00

281 lines
11 KiB
C++

// SPDX-License-Identifier: GPL-2.0-or-later
/*
* Copyright (C) 2022-2024 Thomas Basler and others
*/
#include "WebApi_ws_live.h"
#include "Datastore.h"
#include "MessageOutput.h"
#include "Utils.h"
#include "WebApi.h"
#include "defaults.h"
#include <AsyncJson.h>
WebApiWsLiveClass::WebApiWsLiveClass()
: _ws("/livedata")
, _wsCleanupTask(1 * TASK_SECOND, TASK_FOREVER, std::bind(&WebApiWsLiveClass::wsCleanupTaskCb, this))
, _sendDataTask(1 * TASK_SECOND, TASK_FOREVER, std::bind(&WebApiWsLiveClass::sendDataTaskCb, this))
{
}
void WebApiWsLiveClass::init(AsyncWebServer& server, Scheduler& scheduler)
{
using std::placeholders::_1;
using std::placeholders::_2;
using std::placeholders::_3;
using std::placeholders::_4;
using std::placeholders::_5;
using std::placeholders::_6;
server.on("/api/livedata/status", HTTP_GET, std::bind(&WebApiWsLiveClass::onLivedataStatus, this, _1));
server.addHandler(&_ws);
_ws.onEvent(std::bind(&WebApiWsLiveClass::onWebsocketEvent, this, _1, _2, _3, _4, _5, _6));
scheduler.addTask(_wsCleanupTask);
_wsCleanupTask.enable();
scheduler.addTask(_sendDataTask);
_sendDataTask.enable();
_simpleDigestAuth.setUsername(AUTH_USERNAME);
_simpleDigestAuth.setRealm("live websocket");
reload();
}
void WebApiWsLiveClass::reload()
{
_ws.removeMiddleware(&_simpleDigestAuth);
auto const& config = Configuration.get();
if (config.Security.AllowReadonly) { return; }
_ws.enable(false);
_simpleDigestAuth.setPassword(config.Security.Password);
_ws.addMiddleware(&_simpleDigestAuth);
_ws.closeAll();
_ws.enable(true);
}
void WebApiWsLiveClass::wsCleanupTaskCb()
{
// see: https://github.com/me-no-dev/ESPAsyncWebServer#limiting-the-number-of-web-socket-clients
_ws.cleanupClients();
}
void WebApiWsLiveClass::sendDataTaskCb()
{
// do nothing if no WS client is connected
if (_ws.count() == 0) {
return;
}
// Loop all inverters
for (uint8_t i = 0; i < Hoymiles.getNumInverters(); i++) {
auto inv = Hoymiles.getInverterByPos(i);
if (inv == nullptr) {
continue;
}
const uint32_t lastUpdateInternal = inv->Statistics()->getLastUpdateFromInternal();
if (!((lastUpdateInternal > 0 && lastUpdateInternal > _lastPublishStats[i]) || (millis() - _lastPublishStats[i] > (10 * 1000)))) {
continue;
}
_lastPublishStats[i] = millis();
try {
std::lock_guard<std::mutex> lock(_mutex);
JsonDocument root;
JsonVariant var = root;
auto invArray = var["inverters"].to<JsonArray>();
auto invObject = invArray.add<JsonObject>();
generateCommonJsonResponse(var);
generateInverterCommonJsonResponse(invObject, inv);
generateInverterChannelJsonResponse(invObject, inv);
if (!Utils::checkJsonAlloc(root, __FUNCTION__, __LINE__)) {
continue;
}
String buffer;
serializeJson(root, buffer);
_ws.textAll(buffer);
} catch (const std::bad_alloc& bad_alloc) {
MessageOutput.printf("Call to /api/livedata/status temporarely out of resources. Reason: \"%s\".\r\n", bad_alloc.what());
} catch (const std::exception& exc) {
MessageOutput.printf("Unknown exception in /api/livedata/status. Reason: \"%s\".\r\n", exc.what());
}
}
}
void WebApiWsLiveClass::generateCommonJsonResponse(JsonVariant& root)
{
auto totalObj = root["total"].to<JsonObject>();
addTotalField(totalObj, "Power", Datastore.getTotalAcPowerEnabled(), "W", Datastore.getTotalAcPowerDigits());
addTotalField(totalObj, "YieldDay", Datastore.getTotalAcYieldDayEnabled(), "Wh", Datastore.getTotalAcYieldDayDigits());
addTotalField(totalObj, "YieldTotal", Datastore.getTotalAcYieldTotalEnabled(), "kWh", Datastore.getTotalAcYieldTotalDigits());
JsonObject hintObj = root["hints"].to<JsonObject>();
struct tm timeinfo;
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;
}
void WebApiWsLiveClass::generateInverterCommonJsonResponse(JsonObject& root, std::shared_ptr<InverterAbstract> inv)
{
const INVERTER_CONFIG_T* inv_cfg = Configuration.getInverterConfig(inv->serial());
if (inv_cfg == nullptr) {
return;
}
root["serial"] = inv->serialString();
root["name"] = inv->name();
root["order"] = inv_cfg->Order;
root["data_age"] = (millis() - inv->Statistics()->getLastUpdate()) / 1000;
root["poll_enabled"] = inv->getEnablePolling();
root["reachable"] = inv->isReachable();
root["producing"] = inv->isProducing();
root["limit_relative"] = inv->SystemConfigPara()->getLimitPercent();
if (inv->DevInfo()->getMaxPower() > 0) {
root["limit_absolute"] = inv->SystemConfigPara()->getLimitPercent() * inv->DevInfo()->getMaxPower() / 100.0;
} else {
root["limit_absolute"] = -1;
}
root["radio_stats"]["tx_request"] = inv->RadioStats.TxRequestData;
root["radio_stats"]["tx_re_request"] = inv->RadioStats.TxReRequestFragment;
root["radio_stats"]["rx_success"] = inv->RadioStats.RxSuccess;
root["radio_stats"]["rx_fail_nothing"] = inv->RadioStats.RxFailNoAnswer;
root["radio_stats"]["rx_fail_partial"] = inv->RadioStats.RxFailPartialAnswer;
root["radio_stats"]["rx_fail_corrupt"] = inv->RadioStats.RxFailCorruptData;
}
void WebApiWsLiveClass::generateInverterChannelJsonResponse(JsonObject& root, std::shared_ptr<InverterAbstract> inv)
{
const INVERTER_CONFIG_T* inv_cfg = Configuration.getInverterConfig(inv->serial());
if (inv_cfg == nullptr) {
return;
}
// Loop all channels
for (auto& t : inv->Statistics()->getChannelTypes()) {
auto chanTypeObj = root[inv->Statistics()->getChannelTypeName(t)].to<JsonObject>();
for (auto& c : inv->Statistics()->getChannelsByType(t)) {
if (t == TYPE_DC) {
chanTypeObj[String(static_cast<uint8_t>(c))]["name"]["u"] = inv_cfg->channel[c].Name;
}
addField(chanTypeObj, inv, t, c, FLD_PAC);
addField(chanTypeObj, inv, t, c, FLD_UAC);
addField(chanTypeObj, inv, t, c, FLD_IAC);
if (t == TYPE_INV) {
addField(chanTypeObj, inv, t, c, FLD_PDC, "Power DC");
} else {
addField(chanTypeObj, inv, t, c, FLD_PDC);
}
addField(chanTypeObj, inv, t, c, FLD_UDC);
addField(chanTypeObj, inv, t, c, FLD_IDC);
addField(chanTypeObj, inv, t, c, FLD_YD);
addField(chanTypeObj, inv, t, c, FLD_YT);
addField(chanTypeObj, inv, t, c, FLD_F);
addField(chanTypeObj, inv, t, c, FLD_T);
addField(chanTypeObj, inv, t, c, FLD_PF);
addField(chanTypeObj, inv, t, c, FLD_Q);
addField(chanTypeObj, inv, t, c, FLD_EFF);
if (t == TYPE_DC && inv->Statistics()->getStringMaxPower(c) > 0) {
addField(chanTypeObj, inv, t, c, FLD_IRR);
chanTypeObj[String(c)][inv->Statistics()->getChannelFieldName(t, c, FLD_IRR)]["max"] = inv->Statistics()->getStringMaxPower(c);
}
}
}
if (inv->Statistics()->hasChannelFieldValue(TYPE_INV, CH0, FLD_EVT_LOG)) {
root["events"] = inv->EventLog()->getEntryCount();
} else {
root["events"] = -1;
}
}
void WebApiWsLiveClass::addField(JsonObject& root, std::shared_ptr<InverterAbstract> inv, const ChannelType_t type, const ChannelNum_t channel, const FieldId_t fieldId, String topic)
{
if (inv->Statistics()->hasChannelFieldValue(type, channel, fieldId)) {
String chanName;
if (topic == "") {
chanName = inv->Statistics()->getChannelFieldName(type, channel, fieldId);
} else {
chanName = topic;
}
String chanNum;
chanNum = channel;
root[chanNum][chanName]["v"] = inv->Statistics()->getChannelFieldValue(type, channel, fieldId);
root[chanNum][chanName]["u"] = inv->Statistics()->getChannelFieldUnit(type, channel, fieldId);
root[chanNum][chanName]["d"] = inv->Statistics()->getChannelFieldDigits(type, channel, fieldId);
}
}
void WebApiWsLiveClass::addTotalField(JsonObject& root, const String& name, const float value, const String& unit, const uint8_t digits)
{
root[name]["v"] = value;
root[name]["u"] = unit;
root[name]["d"] = digits;
}
void WebApiWsLiveClass::onWebsocketEvent(AsyncWebSocket* server, AsyncWebSocketClient* client, AwsEventType type, void* arg, uint8_t* data, size_t len)
{
if (type == WS_EVT_CONNECT) {
MessageOutput.printf("Websocket: [%s][%u] connect\r\n", server->url(), client->id());
} else if (type == WS_EVT_DISCONNECT) {
MessageOutput.printf("Websocket: [%s][%u] disconnect\r\n", server->url(), client->id());
}
}
void WebApiWsLiveClass::onLivedataStatus(AsyncWebServerRequest* request)
{
if (!WebApi.checkCredentialsReadonly(request)) {
return;
}
try {
std::lock_guard<std::mutex> lock(_mutex);
AsyncJsonResponse* response = new AsyncJsonResponse();
auto& root = response->getRoot();
auto invArray = root["inverters"].to<JsonArray>();
auto serial = WebApi.parseSerialFromRequest(request);
if (serial > 0) {
auto inv = Hoymiles.getInverterBySerial(serial);
if (inv != nullptr) {
JsonObject invObject = invArray.add<JsonObject>();
generateInverterCommonJsonResponse(invObject, inv);
generateInverterChannelJsonResponse(invObject, inv);
}
} else {
// Loop all inverters
for (uint8_t i = 0; i < Hoymiles.getNumInverters(); i++) {
auto inv = Hoymiles.getInverterByPos(i);
if (inv == nullptr) {
continue;
}
JsonObject invObject = invArray.add<JsonObject>();
generateInverterCommonJsonResponse(invObject, inv);
}
}
generateCommonJsonResponse(root);
WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__);
} catch (const std::bad_alloc& bad_alloc) {
MessageOutput.printf("Call to /api/livedata/status temporarely out of resources. Reason: \"%s\".\r\n", bad_alloc.what());
WebApi.sendTooManyRequests(request);
} catch (const std::exception& exc) {
MessageOutput.printf("Unknown exception in /api/livedata/status. Reason: \"%s\".\r\n", exc.what());
WebApi.sendTooManyRequests(request);
}
}