diff --git a/include/Configuration.h b/include/Configuration.h index 54f912d2a..13319cadf 100644 --- a/include/Configuration.h +++ b/include/Configuration.h @@ -342,6 +342,22 @@ struct CONFIG_T { float Auto_Power_Target_Power_Consumption; } Huawei; + struct { + bool Enabled; + bool VerboseLogging; + bool Auto_Power_BatterySoC_Limits_Enabled; + bool Emergency_Charge_Enabled; + uint8_t stop_batterysoc_threshold; + uint8_t start_batterysoc_threshold; + char url[1025]; + char uri_on[1025]; + char uri_off[1025]; + char uri_stats[1025]; + char uri_powerparam[256]; + int32_t POWER_ON_threshold; + int32_t POWER_OFF_threshold; + } Shelly; + INVERTER_CONFIG_T Inverter[INV_MAX_COUNT]; char Dev_PinMapping[DEV_MAX_MAPPING_NAME_STRLEN + 1]; diff --git a/include/ShellyACPlug.h b/include/ShellyACPlug.h new file mode 100644 index 000000000..bc1bc5f80 --- /dev/null +++ b/include/ShellyACPlug.h @@ -0,0 +1,37 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#pragma once +#include +#include +#include "HttpGetter.h" +#include "Configuration.h" +#include +#include +#include +#include +#include +#include + +class ShellyACPlugClass { + public: + bool init(Scheduler& scheduler); + void loop(); + void PowerON(); + void PowerOFF(); + float _readpower; + private: + bool _initialized = false; + Task _loopTask; + const uint16_t _period = 2001; + float _acPower; + float _SoC; + bool _emergcharge; + bool send_http(String uri); + float read_http(String uri); + std::unique_ptr _HttpGetter; + bool powerstate = false; + bool verboselogging; + String uri_on; + String uri_off; +}; + +extern ShellyACPlugClass ShellyACPlug; diff --git a/include/WebApi.h b/include/WebApi.h index 07ffaa22b..023236e3a 100644 --- a/include/WebApi.h +++ b/include/WebApi.h @@ -32,9 +32,12 @@ #include "WebApi_ws_Huawei.h" #include "WebApi_Huawei.h" #include "WebApi_ws_battery.h" +#include "WebApi_Shelly.h" +#include "WebApi_ws_Shelly.h" #include #include + class WebApiClass { public: WebApiClass(); @@ -84,6 +87,8 @@ class WebApiClass { WebApiHuaweiClass _webApiHuaweiClass; WebApiWsHuaweiLiveClass _webApiWsHuaweiLive; WebApiWsBatteryLiveClass _webApiWsBatteryLive; + WebApiShellyClass _webApiShellyClass; + WebApiWsShellyLiveClass _webApiWsShellyLive; }; extern WebApiClass WebApi; diff --git a/include/WebApi_Shelly.h b/include/WebApi_Shelly.h new file mode 100644 index 000000000..1bdf4056d --- /dev/null +++ b/include/WebApi_Shelly.h @@ -0,0 +1,17 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#pragma once + +#include +#include +#include + +class WebApiShellyClass { +public: + void init(AsyncWebServer& server, Scheduler& scheduler); +private: + void onStatus(AsyncWebServerRequest* request); + void onAdminGet(AsyncWebServerRequest* request); + void onAdminPost(AsyncWebServerRequest* request); + + AsyncWebServer* _server; +}; diff --git a/include/WebApi_ws_Shelly.h b/include/WebApi_ws_Shelly.h new file mode 100644 index 000000000..f8963a609 --- /dev/null +++ b/include/WebApi_ws_Shelly.h @@ -0,0 +1,31 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#pragma once + +#include "ArduinoJson.h" +#include +#include +#include + +class WebApiWsShellyLiveClass { +public: + WebApiWsShellyLiveClass(); + void init(AsyncWebServer& server, Scheduler& scheduler); + void reload(); + +private: + void generateCommonJsonResponse(JsonVariant& root); + void onLivedataStatus(AsyncWebServerRequest* request); + void onWebsocketEvent(AsyncWebSocket* server, AsyncWebSocketClient* client, AwsEventType type, void* arg, uint8_t* data, size_t len); + + AsyncWebServer* _server; + AsyncWebSocket _ws; + AuthenticationMiddleware _simpleDigestAuth; + + std::mutex _mutex; + + Task _wsCleanupTask; + void wsCleanupTaskCb(); + + Task _sendDataTask; + void sendDataTaskCb(); +}; diff --git a/include/WebApi_ws_live.h b/include/WebApi_ws_live.h index e02f9a8c1..fca820097 100644 --- a/include/WebApi_ws_live.h +++ b/include/WebApi_ws_live.h @@ -35,7 +35,7 @@ class WebApiWsLiveClass { uint32_t _lastPublishHuawei = 0; uint32_t _lastPublishBattery = 0; uint32_t _lastPublishPowerMeter = 0; - + uint32_t _lastPublishShelly = 0; uint32_t _lastPublishStats[INV_MAX_COUNT] = { 0 }; std::mutex _mutex; diff --git a/include/defaults.h b/include/defaults.h index 7d8a50a16..e67638572 100644 --- a/include/defaults.h +++ b/include/defaults.h @@ -169,4 +169,15 @@ #define HUAWEI_AUTO_POWER_STOP_BATTERYSOC_THRESHOLD 95 #define HUAWEI_AUTO_POWER_TARGET_POWER_CONSUMPTION 0 +#define SHELLY_ENABLED false +#define SHELLY_POWER_ON_THRESHOLD -500 +#define SHELLY_POWER_OFF_THRESHOLD -100 +#define SHELLY_STOP_BATTERYSOC_THRESHOLD 95 +#define SHELLY_START_BATTERYSOC_THRESHOLD 90 +#define SHELLY_IPADDRESS "http://192.168.2.100" +#define SHELLY_URION "/relay/0?turn=on" +#define SHELLY_URIOFF "/relay/0?turn=off" +#define SHELLY_URISTATS "/rpc/Switch.GetStatus?id=0" +#define SHELLY_URIPOWERPARAM "apower" + #define VERBOSE_LOGGING true diff --git a/src/Configuration.cpp b/src/Configuration.cpp index facf0ab7b..e5bb77b9f 100644 --- a/src/Configuration.cpp +++ b/src/Configuration.cpp @@ -339,6 +339,21 @@ bool ConfigurationClass::write() huawei["stop_batterysoc_threshold"] = config.Huawei.Auto_Power_Stop_BatterySoC_Threshold; huawei["target_power_consumption"] = config.Huawei.Auto_Power_Target_Power_Consumption; + JsonObject shelly = doc["shelly"].to(); + shelly["enabled"] = config.Shelly.Enabled; + shelly["verbose_logging"] = config.Shelly.VerboseLogging; + shelly["auto_power_batterysoc_limits_enabled"]= config.Shelly.Auto_Power_BatterySoC_Limits_Enabled ; + shelly["emergency_charge_enabled"]= config.Shelly.Emergency_Charge_Enabled; + shelly["stop_batterysoc_threshold"] = config.Shelly.stop_batterysoc_threshold; + shelly["start_batterysoc_threshold"] = config.Shelly.start_batterysoc_threshold; + shelly["url"] = config.Shelly.url; + shelly["uri_on"] = config.Shelly.uri_on; + shelly["uri_off"] = config.Shelly.uri_off; + shelly["uri_stats"] = config.Shelly.uri_stats; + shelly["uri_powerparam"] = config.Shelly.uri_powerparam; + shelly["power_on_threshold"] = config.Shelly.POWER_ON_threshold; + shelly["power_off_threshold"] = config.Shelly.POWER_OFF_threshold; + if (!Utils::checkJsonAlloc(doc, __FUNCTION__, __LINE__)) { return false; } @@ -690,6 +705,22 @@ bool ConfigurationClass::read() config.Huawei.Auto_Power_Stop_BatterySoC_Threshold = huawei["stop_batterysoc_threshold"] | HUAWEI_AUTO_POWER_STOP_BATTERYSOC_THRESHOLD; config.Huawei.Auto_Power_Target_Power_Consumption = huawei["target_power_consumption"] | HUAWEI_AUTO_POWER_TARGET_POWER_CONSUMPTION; + JsonObject shelly = doc["shelly"]; + config.Shelly.Enabled = shelly["enabled"] | SHELLY_ENABLED; + config.Shelly.VerboseLogging = shelly["verbose_logging"] | VERBOSE_LOGGING; + config.Shelly.Auto_Power_BatterySoC_Limits_Enabled = shelly["auto_power_batterysoc_limits_enabled"] | false; + config.Shelly.Emergency_Charge_Enabled = shelly["emergency_charge_enabled"] | false; + config.Shelly.stop_batterysoc_threshold = shelly["stop_batterysoc_threshold"] | SHELLY_STOP_BATTERYSOC_THRESHOLD; + config.Shelly.start_batterysoc_threshold = shelly["start_batterysoc_threshold"] | SHELLY_START_BATTERYSOC_THRESHOLD; + strlcpy(config.Shelly.url, shelly["url"] | SHELLY_IPADDRESS, sizeof(config.Shelly.url)); + strlcpy(config.Shelly.uri_on, shelly["uri_on"] | SHELLY_URION, sizeof(config.Shelly.uri_on)); + strlcpy(config.Shelly.uri_off, shelly["uri_off"] | SHELLY_URIOFF, sizeof(config.Shelly.uri_off)); + strlcpy(config.Shelly.uri_stats, shelly["uri_stats"] | SHELLY_URIOFF, sizeof(config.Shelly.uri_stats)); + strlcpy(config.Shelly.uri_powerparam, shelly["uri_powerparam"] | SHELLY_URIOFF, sizeof(config.Shelly.uri_powerparam)); + config.Shelly.POWER_ON_threshold = shelly["power_on_threshold"] | SHELLY_POWER_ON_THRESHOLD; + config.Shelly.POWER_OFF_threshold = shelly["power_off_threshold"] | SHELLY_POWER_OFF_THRESHOLD; + + f.close(); // Check for default DTU serial diff --git a/src/ShellyACPlug.cpp b/src/ShellyACPlug.cpp new file mode 100644 index 000000000..36eb5b7aa --- /dev/null +++ b/src/ShellyACPlug.cpp @@ -0,0 +1,152 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#include "Utils.h" +#include "ShellyACPlug.h" +#include "MessageOutput.h" +#include +#include +#include +#include "Configuration.h" +#include "Datastore.h" +#include "PowerMeter.h" +#include "Battery.h" + +ShellyACPlugClass ShellyACPlug; + + +bool ShellyACPlugClass::init(Scheduler& scheduler) +{ + MessageOutput.printf("[ShellyACPlug::init] ShellyACPlug Initializing ...\r\n"); + _initialized = true; + scheduler.addTask(_loopTask); + _loopTask.setCallback([this] { loop(); }); + _loopTask.setIterations(TASK_FOREVER); + _loopTask.setInterval(_period); + _loopTask.enable(); + return false; +} + +void ShellyACPlugClass::loop() +{ + const CONFIG_T& config = Configuration.get(); + verboselogging=config.Shelly.VerboseLogging; + uri_on=config.Shelly.uri_on; + uri_off=config.Shelly.uri_off; + if (!config.Shelly.Enabled || !_initialized || !config.PowerMeter.Enabled ) { + return; + } + _loopTask.setInterval(_period); + _acPower = PowerMeter.getPowerTotal(); + _SoC = Battery.getStats()->getSoC(); + _emergcharge = Battery.getStats()->getImmediateChargingRequest(); + _readpower = read_http(config.Shelly.uri_stats); + if (_readpower>0) + { + powerstate=true; + } + if ((_acPower < config.Shelly.POWER_ON_threshold && !powerstate && _SoC <= config.Shelly.start_batterysoc_threshold) || (_emergcharge && config.Shelly.Emergency_Charge_Enabled)) + { + PowerON(); + } + else if ((_acPower > config.Shelly.POWER_OFF_threshold && powerstate) || (_SoC >= config.Shelly.stop_batterysoc_threshold && powerstate)) + { + PowerOFF(); + } + if (verboselogging) { + MessageOutput.printf("[ShellyACPlug::loop] Power reported by the Smart Plug%f W\r\n", _acPower ); + MessageOutput.printf("[ShellyACPlug::loop] State of the Smart Plug ON/OFF %d \r\n", powerstate ); + MessageOutput.printf("[ShellyACPlug::loop] Current Battery SoC %f \r\n", _SoC); + MessageOutput.printf("[ShellyACPlug::loop] Current Power consumed by the household %f W\r\n", _readpower ); + } +} + +void ShellyACPlugClass::PowerON() +{ + if (!send_http(uri_on)) + { + return; + } + powerstate=true; + if (verboselogging) { + MessageOutput.print("[ShellyACPlug::PowerON] Power ON\r\n"); + } +} + +void ShellyACPlugClass::PowerOFF() +{ + if (!send_http(uri_off)) + { + return; + }; + powerstate=false; + if (verboselogging) { + MessageOutput.print("[ShellyACPlug::PowerOFF] Power OFF\r\n"); + } +} + +bool ShellyACPlugClass::send_http(String uri) +{ + const CONFIG_T& config = Configuration.get(); + String url = config.Shelly.url; + url += uri; + HttpRequestConfig HttpRequest; + strlcpy(HttpRequest.Url, url.c_str(), sizeof(HttpRequest.Url)); + HttpRequest.Timeout = 60; + _HttpGetter = std::make_unique(HttpRequest); + if (config.Shelly.VerboseLogging) { + MessageOutput.printf("[ShellyACPlug::send_http]] Start sending to URL: %s\r\n",url.c_str()); + } + if (!_HttpGetter->init()) { + MessageOutput.printf("[ShellyACPlug::send_http]] ERROR INIT HttpGetter %s\r\n", _HttpGetter->getErrorText()); + return false; + } + if (!_HttpGetter->performGetRequest()) { + MessageOutput.printf("[ShellyACPlug::send_http] ERROR GET HttpGetter %s\r\n", _HttpGetter->getErrorText()); + return false; + } + _HttpGetter = nullptr; + return true; +} +float ShellyACPlugClass::read_http(String uri) +{ + const CONFIG_T& config = Configuration.get(); + String url = config.Shelly.url; + url += uri; + HttpRequestConfig HttpRequest; + JsonDocument jsonResponse; + strlcpy(HttpRequest.Url, url.c_str(), sizeof(HttpRequest.Url)); + HttpRequest.Timeout = 60; + _HttpGetter = std::make_unique(HttpRequest); + if (config.Shelly.VerboseLogging) { + MessageOutput.printf("[ShellyACPlug::read_http] Start reading from URL: %s\r\n",url.c_str()); + } + if (!_HttpGetter->init()) { + MessageOutput.printf("[ShellyACPlug::read_http] ERROR INIT HttpGetter %s\r\n", _HttpGetter->getErrorText()); + return 0; + } + _HttpGetter->addHeader("Content-Type", "application/json"); + _HttpGetter->addHeader("Accept", "application/json"); + auto res = _HttpGetter->performGetRequest(); + if (!res) { + MessageOutput.printf("[ShellyACPlug::read_http] ERROR GET HttpGetter %s\r\n", _HttpGetter->getErrorText()); + return 0; + } + auto pStream = res.getStream(); + if (!pStream) { + MessageOutput.printf("[ShellyACPlug::read_http] Programmer error: HTTP request yields no stream"); + return 0; + } + + const DeserializationError error = deserializeJson(jsonResponse, *pStream); + if (error) { + String msg("[ShellyACPlug::read_http] Unable to parse server response as JSON: "); + MessageOutput.printf((msg + error.c_str()).c_str()); + return 0; + } + auto pathResolutionResult = Utils::getJsonValueByPath(jsonResponse, config.Shelly.uri_powerparam); + if (!pathResolutionResult.second.isEmpty()) { + MessageOutput.printf("[ShellyACPlug::read_http] ERROR reading AC Power from Smart Plug %s\r\n",pathResolutionResult.second.c_str()); + } + + _HttpGetter = nullptr; + return pathResolutionResult.first; +} diff --git a/src/WebApi.cpp b/src/WebApi.cpp index 08026c4ec..a18b2ce6b 100644 --- a/src/WebApi.cpp +++ b/src/WebApi.cpp @@ -44,7 +44,8 @@ void WebApiClass::init(Scheduler& scheduler) _webApiWsHuaweiLive.init(_server, scheduler); _webApiHuaweiClass.init(_server, scheduler); _webApiWsBatteryLive.init(_server, scheduler); - + _webApiShellyClass.init(_server, scheduler); + _webApiWsShellyLive.init(_server, scheduler); _server.begin(); } @@ -55,6 +56,7 @@ void WebApiClass::reload() _webApiWsBatteryLive.reload(); _webApiWsVedirectLive.reload(); _webApiWsHuaweiLive.reload(); + _webApiWsShellyLive.reload(); } bool WebApiClass::checkCredentials(AsyncWebServerRequest* request) diff --git a/src/WebApi_Shelly.cpp b/src/WebApi_Shelly.cpp new file mode 100644 index 000000000..f6e98ff61 --- /dev/null +++ b/src/WebApi_Shelly.cpp @@ -0,0 +1,120 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Copyright (C) 2022-2024 HSS + */ +#include "WebApi_Shelly.h" +#include "Configuration.h" +#include "MessageOutput.h" +#include "WebApi.h" +#include "WebApi_errors.h" +#include +#include +#include "ShellyACPlug.h" + +void WebApiShellyClass::init(AsyncWebServer& server, Scheduler& scheduler) +{ + using std::placeholders::_1; + + _server = &server; + + _server->on("/api/shelly/status", HTTP_GET, std::bind(&WebApiShellyClass::onStatus, this, _1)); + _server->on("/api/shelly/config", HTTP_GET, std::bind(&WebApiShellyClass::onAdminGet, this, _1)); + _server->on("/api/shelly/config", HTTP_POST, std::bind(&WebApiShellyClass::onAdminPost, this, _1)); +} + +void WebApiShellyClass::onStatus(AsyncWebServerRequest* request) +{ + if (!WebApi.checkCredentialsReadonly(request)) { + return; + } + + AsyncJsonResponse* response = new AsyncJsonResponse(); + + response->setLength(); + request->send(response); +} + +void WebApiShellyClass::onAdminGet(AsyncWebServerRequest* request) +{ + if (!WebApi.checkCredentials(request)) { + return; + } + + AsyncJsonResponse* response = new AsyncJsonResponse(); + auto& root = response->getRoot(); + const CONFIG_T& config = Configuration.get(); + + root["enabled"] = config.Shelly.Enabled; + root["verbose_logging"] = config.Shelly.VerboseLogging; + root["auto_power_batterysoc_limits_enabled"] = config.Shelly.Auto_Power_BatterySoC_Limits_Enabled; + root["emergency_charge_enabled"] = config.Shelly.Emergency_Charge_Enabled; + root["stop_batterysoc_threshold"] = config.Shelly.stop_batterysoc_threshold; + root["start_batterysoc_threshold"] = config.Shelly.start_batterysoc_threshold; + root["url"] = config.Shelly.url; + root["uri_on"] = config.Shelly.uri_on; + root["uri_off"] = config.Shelly.uri_off; + root["uri_stats"] = config.Shelly.uri_stats; + root["uri_powerparam"] = config.Shelly.uri_powerparam; + root["power_on_threshold"] = config.Shelly.POWER_ON_threshold; + root["power_off_threshold"] = config.Shelly.POWER_OFF_threshold; + + response->setLength(); + request->send(response); + MessageOutput.println("Read Shelly AC charger config... "); +} + +void WebApiShellyClass::onAdminPost(AsyncWebServerRequest* request) +{ + if (!WebApi.checkCredentials(request)) { + return; + } + + AsyncJsonResponse* response = new AsyncJsonResponse(); + JsonDocument root; + if (!WebApi.parseRequestData(request, response, root)) { + return; + } + + auto& retMsg = response->getRoot(); + + if (!(root["enabled"].is()) || + !(root["emergency_charge_enabled"].is())){ + retMsg["message"] = "Values are missing!"; + retMsg["code"] = WebApiError::GenericValueMissing; + response->setLength(); + request->send(response); + return; + } + + auto guard = Configuration.getWriteGuard(); + auto& config = guard.getConfig(); + config.Shelly.Enabled = root["enabled"].as(); + config.Shelly.VerboseLogging = root["verbose_logging"]; + config.Shelly.Auto_Power_BatterySoC_Limits_Enabled = root["auto_power_batterysoc_limits_enabled"].as(); + config.Shelly.Emergency_Charge_Enabled = root["emergency_charge_enabled"].as(); + config.Shelly.stop_batterysoc_threshold = root["stop_batterysoc_threshold"]; + config.Shelly.start_batterysoc_threshold = root["start_batterysoc_threshold"]; + strlcpy( config.Shelly.url, root["url"].as().c_str(), sizeof(config.Shelly.url)); + strlcpy( config.Shelly.uri_on, root["uri_on"].as().c_str(), sizeof(config.Shelly.uri_on)); + strlcpy( config.Shelly.uri_off, root["uri_off"].as().c_str(), sizeof(config.Shelly.uri_off)); + strlcpy( config.Shelly.uri_stats, root["uri_stats"].as().c_str(), sizeof(config.Shelly.uri_stats)); + strlcpy( config.Shelly.uri_powerparam, root["uri_powerparam"].as().c_str(), sizeof(config.Shelly.uri_powerparam)); + config.Shelly.POWER_ON_threshold = root["power_on_threshold"]; + config.Shelly.POWER_OFF_threshold = root["power_off_threshold"]; + WebApi.writeConfig(retMsg); + WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); + + + yield(); + delay(1000); + yield(); + + if (config.Shelly.Enabled) { + MessageOutput.println("[ShellyACPlug::WebApi] Initialize Shelly AC charger interface... "); + } + + if (!config.Shelly.Enabled) { + ShellyACPlug.PowerOFF(); + return; + } +} diff --git a/src/WebApi_ws_Shelly.cpp b/src/WebApi_ws_Shelly.cpp new file mode 100644 index 000000000..80ed90dc1 --- /dev/null +++ b/src/WebApi_ws_Shelly.cpp @@ -0,0 +1,144 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Copyright (C) 2022-2024 Thomas Basler and others + */ +#include "WebApi_ws_Shelly.h" +#include "AsyncJson.h" +#include "ShellyACPlug.h" +#include "Configuration.h" +#include "MessageOutput.h" +#include "Utils.h" +#include "WebApi.h" +#include "defaults.h" + +WebApiWsShellyLiveClass::WebApiWsShellyLiveClass() + : _ws("/shellylivedata") +{ +} + +void WebApiWsShellyLiveClass::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 = &server; + _server->on("/api/shellylivedata/status", HTTP_GET, std::bind(&WebApiWsShellyLiveClass::onLivedataStatus, this, _1)); + + _server->addHandler(&_ws); + _ws.onEvent(std::bind(&WebApiWsShellyLiveClass::onWebsocketEvent, this, _1, _2, _3, _4, _5, _6)); + + scheduler.addTask(_wsCleanupTask); + _wsCleanupTask.setCallback(std::bind(&WebApiWsShellyLiveClass::wsCleanupTaskCb, this)); + _wsCleanupTask.setIterations(TASK_FOREVER); + _wsCleanupTask.setInterval(1 * TASK_SECOND); + _wsCleanupTask.enable(); + + scheduler.addTask(_sendDataTask); + _sendDataTask.setCallback(std::bind(&WebApiWsShellyLiveClass::sendDataTaskCb, this)); + _sendDataTask.setIterations(TASK_FOREVER); + _sendDataTask.setInterval(1 * TASK_SECOND); + _sendDataTask.enable(); + + _simpleDigestAuth.setUsername(AUTH_USERNAME); + _simpleDigestAuth.setRealm("AC charger websocket"); + + reload(); +} + +void WebApiWsShellyLiveClass::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 WebApiWsShellyLiveClass::wsCleanupTaskCb() +{ + // see: https://github.com/me-no-dev/ESPAsyncWebServer#limiting-the-number-of-web-socket-clients + _ws.cleanupClients(); +} + +void WebApiWsShellyLiveClass::sendDataTaskCb() +{ + // do nothing if no WS client is connected + if (_ws.count() == 0) { + return; + } + + try { + std::lock_guard lock(_mutex); + JsonDocument root; + JsonVariant var = root; + + generateCommonJsonResponse(var); + + if (Utils::checkJsonAlloc(root, __FUNCTION__, __LINE__)) { + String buffer; + serializeJson(root, buffer); + + _ws.textAll(buffer); + } + } catch (std::bad_alloc& bad_alloc) { + MessageOutput.printf("Calling /api/shellylivedata/status has temporarily run out of resources. Reason: \"%s\".\r\n", bad_alloc.what()); + } catch (const std::exception& exc) { + MessageOutput.printf("Unknown exception in /api/shellylivedata/status. Reason: \"%s\".\r\n", exc.what()); + } +} + +void WebApiWsShellyLiveClass::generateCommonJsonResponse(JsonVariant& root) +{ + root["input_power"]["v"] = ShellyACPlug._readpower; + root["input_power"]["u"] = "W"; + root["enabled"] = true; + +} + +void WebApiWsShellyLiveClass::onWebsocketEvent(AsyncWebSocket* server, AsyncWebSocketClient* client, AwsEventType type, void* arg, uint8_t* data, size_t len) +{ + if (type == WS_EVT_CONNECT) { + char str[64]; + snprintf(str, sizeof(str), "Websocket: [%s][%u] connect", server->url(), client->id()); + Serial.println(str); + MessageOutput.println(str); + } else if (type == WS_EVT_DISCONNECT) { + char str[64]; + snprintf(str, sizeof(str), "Websocket: [%s][%u] disconnect", server->url(), client->id()); + Serial.println(str); + MessageOutput.println(str); + } +} + +void WebApiWsShellyLiveClass::onLivedataStatus(AsyncWebServerRequest* request) +{ + if (!WebApi.checkCredentialsReadonly(request)) { + return; + } + try { + std::lock_guard lock(_mutex); + AsyncJsonResponse* response = new AsyncJsonResponse(); + auto& root = response->getRoot(); + + generateCommonJsonResponse(root); + + WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); + + } catch (std::bad_alloc& bad_alloc) { + MessageOutput.printf("Calling /api/shellylivedata/status has temporarily run out of resources. Reason: \"%s\".\r\n", bad_alloc.what()); + WebApi.sendTooManyRequests(request); + } catch (const std::exception& exc) { + MessageOutput.printf("Unknown exception in /api/shellylivedata/status. Reason: \"%s\".\r\n", exc.what()); + WebApi.sendTooManyRequests(request); + } +} diff --git a/src/WebApi_ws_live.cpp b/src/WebApi_ws_live.cpp index 36031a357..a2a65f326 100644 --- a/src/WebApi_ws_live.cpp +++ b/src/WebApi_ws_live.cpp @@ -13,6 +13,7 @@ #include "VictronMppt.h" #include "defaults.h" #include +#include "ShellyACPlug.h" WebApiWsLiveClass::WebApiWsLiveClass() : _ws("/livedata") @@ -99,6 +100,16 @@ void WebApiWsLiveClass::generateOnBatteryJsonResponse(JsonVariant& root, bool al if (!all) { _lastPublishHuawei = millis(); } } + if (all || config.Shelly.Enabled ) { + auto shellyObj = root["shelly"].to(); + shellyObj["enabled"] = config.Shelly.Enabled; + + if (config.Shelly.Enabled) { + addTotalField(shellyObj, "Power", ShellyACPlug._readpower, "W", 2); + } + if (!all) { _lastPublishShelly = millis(); } + } + auto spStats = Battery.getStats(); if (all || spStats->updateAvailable(_lastPublishBattery)) { auto batteryObj = root["battery"].to(); @@ -370,7 +381,7 @@ void WebApiWsLiveClass::onLivedataStatus(AsyncWebServerRequest* request) generateOnBatteryJsonResponse(root, true); WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); - + } catch (const std::bad_alloc& bad_alloc) { MessageOutput.printf("Calling /api/livedata/status has temporarily run out of resources. Reason: \"%s\".\r\n", bad_alloc.what()); WebApi.sendTooManyRequests(request); diff --git a/src/main.cpp b/src/main.cpp index 9a4ac2506..388a4182a 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -40,6 +40,7 @@ #include #include #include +#include "ShellyACPlug.h" void setup() { @@ -196,6 +197,10 @@ void setup() MessageOutput.println("Invalid pin config"); } + // Initialize Shelly AC-charger + MessageOutput.println("Initialize Shelly AC charger interface... "); + ShellyACPlug.init(scheduler); + Battery.init(scheduler); } diff --git a/webapp/src/components/InverterTotalInfo.vue b/webapp/src/components/InverterTotalInfo.vue index 6cb609e7d..8cc68b550 100644 --- a/webapp/src/components/InverterTotalInfo.vue +++ b/webapp/src/components/InverterTotalInfo.vue @@ -188,13 +188,26 @@ +
+ +

+ {{ + $n(shellyData.Power.v, 'decimal', { + minimumFractionDigits: shellyData.Power.d, + maximumFractionDigits: shellyData.Power.d, + }) + }} + {{ shellyData.Power.u }} +

+
+