From a7fdf6408194ed21e9ce19d8468ee54ef3704176 Mon Sep 17 00:00:00 2001 From: Snoopy-HSS Date: Sun, 6 Oct 2024 21:14:26 +0200 Subject: [PATCH 1/7] Changes to be committed: modified: include/Configuration.h new file: include/ShellyACPlug.h modified: include/WebApi.h new file: include/WebApi_Shelly.h modified: include/defaults.h modified: src/Configuration.cpp new file: src/ShellyACPlug.cpp modified: src/WebApi.cpp new file: src/WebApi_Shelly.cpp modified: webapp/src/locales/de.json modified: webapp/src/locales/en.json modified: webapp/src/locales/fr.json modified: webapp/src/types/AcChargerConfig.ts modified: webapp/src/views/AcChargerAdminView.vue --- include/Configuration.h | 15 ++++ include/ShellyACPlug.h | 30 +++++++ include/WebApi.h | 3 + include/WebApi_Shelly.h | 17 ++++ include/defaults.h | 4 + src/Configuration.cpp | 16 ++++ src/ShellyACPlug.cpp | 38 ++++++++ src/WebApi.cpp | 2 +- src/WebApi_Shelly.cpp | 111 ++++++++++++++++++++++++ webapp/src/locales/de.json | 4 + webapp/src/locales/en.json | 4 + webapp/src/locales/fr.json | 4 + webapp/src/types/AcChargerConfig.ts | 9 ++ webapp/src/views/AcChargerAdminView.vue | 104 ++++++++++++++++++++++ 14 files changed, 360 insertions(+), 1 deletion(-) create mode 100644 include/ShellyACPlug.h create mode 100644 include/WebApi_Shelly.h create mode 100644 src/ShellyACPlug.cpp create mode 100644 src/WebApi_Shelly.cpp diff --git a/include/Configuration.h b/include/Configuration.h index 3b99c38bb..5c950ecdc 100644 --- a/include/Configuration.h +++ b/include/Configuration.h @@ -128,6 +128,12 @@ struct POWERMETER_HTTP_SML_CONFIG_T { }; using PowerMeterHttpSmlConfig = struct POWERMETER_HTTP_SML_CONFIG_T; +struct ShellyACPlug_HTTP_CONFIG_T { + uint32_t PollingInterval; + HttpRequestConfig HttpRequest; +}; +using ShellyACPlugConfig = struct ShellyACPlug_HTTP_CONFIG_T ; + enum BatteryVoltageUnit { Volts = 0, DeciVolts = 1, CentiVolts = 2, MilliVolts = 3 }; enum BatteryAmperageUnit { Amps = 0, MilliAmps = 1 }; @@ -324,6 +330,15 @@ 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; + char ip[15]; + } 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..08cdc5112 --- /dev/null +++ b/include/ShellyACPlug.h @@ -0,0 +1,30 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#pragma once +#include +#include +#include "HttpGetter.h" +#include "Configuration.h" +#include +#include +#include "PowerMeterProvider.h" + + +#include +#include +#include +#include + + +class ShellyACPlug : public PowerMeterProvider { +public: + explicit ShellyACPlug(ShellyACPlugConfig const& cfg) + : _cfg(cfg) { } + ~ShellyACPlug(); + bool init(); + void loop(); + +private: + std::unique_ptr _upHttpGetter; + ShellyACPlugConfig const& _cfg; + std::condition_variable _cv; +}; diff --git a/include/WebApi.h b/include/WebApi.h index c995ecfca..e056fdfe8 100644 --- a/include/WebApi.h +++ b/include/WebApi.h @@ -31,9 +31,11 @@ #include "WebApi_ws_Huawei.h" #include "WebApi_Huawei.h" #include "WebApi_ws_battery.h" +#include "WebApi_Shelly.h" #include #include + class WebApiClass { public: WebApiClass(); @@ -82,6 +84,7 @@ class WebApiClass { WebApiHuaweiClass _webApiHuaweiClass; WebApiWsHuaweiLiveClass _webApiWsHuaweiLive; WebApiWsBatteryLiveClass _webApiWsBatteryLive; + WebApiShellyClass _webApiShellyClass; }; 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/defaults.h b/include/defaults.h index 67b1d122e..cb3fbde84 100644 --- a/include/defaults.h +++ b/include/defaults.h @@ -167,4 +167,8 @@ #define HUAWEI_AUTO_POWER_STOP_BATTERYSOC_THRESHOLD 95 #define HUAWEI_AUTO_POWER_TARGET_POWER_CONSUMPTION 0 +#define SHELLY_ENABLED false +#define SHELLY_STOP_BATTERYSOC_THRESHOLD 95 +#define SHELLY_IPADDRESS "192.168.2.100" + #define VERBOSE_LOGGING true diff --git a/src/Configuration.cpp b/src/Configuration.cpp index 61ee7758d..bc4c3b68b 100644 --- a/src/Configuration.cpp +++ b/src/Configuration.cpp @@ -292,6 +292,14 @@ 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["ip"] =config.Shelly.ip; + if (!Utils::checkJsonAlloc(doc, __FUNCTION__, __LINE__)) { return false; } @@ -657,6 +665,14 @@ 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; + strlcpy(config.Shelly.ip, shelly["ip"] | SHELLY_IPADDRESS, sizeof(config.Shelly.ip)); + f.close(); return true; } diff --git a/src/ShellyACPlug.cpp b/src/ShellyACPlug.cpp new file mode 100644 index 000000000..83027baa0 --- /dev/null +++ b/src/ShellyACPlug.cpp @@ -0,0 +1,38 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#include "ShellyACPlug.h" +#include "MessageOutput.h" +#include +#include +#include + +ShellyACPlug::~ShellyACPlug() +{ + _cv.notify_all(); +} + +bool ShellyACPlug::init() +{ + _upHttpGetter = std::make_unique(_cfg.HttpRequest); + + if (_upHttpGetter->init()) { return true; } + + MessageOutput.printf("[ShellyACPlug] Initializing HTTP getter failed:\r\n"); + MessageOutput.printf("[ShellyACPlug] %s\r\n", _upHttpGetter->getErrorText()); + + _upHttpGetter = nullptr; + + return false; +} + +void ShellyACPlug::loop() +{ + + + + + + uint32_t constexpr stackSize = 3072; + +} + + diff --git a/src/WebApi.cpp b/src/WebApi.cpp index 47ce7657e..98df060ba 100644 --- a/src/WebApi.cpp +++ b/src/WebApi.cpp @@ -43,7 +43,7 @@ void WebApiClass::init(Scheduler& scheduler) _webApiWsHuaweiLive.init(_server, scheduler); _webApiHuaweiClass.init(_server, scheduler); _webApiWsBatteryLive.init(_server, scheduler); - + _webApiShellyClass.init(_server, scheduler); _server.begin(); } diff --git a/src/WebApi_Shelly.cpp b/src/WebApi_Shelly.cpp new file mode 100644 index 000000000..679e974e7 --- /dev/null +++ b/src/WebApi_Shelly.cpp @@ -0,0 +1,111 @@ +// 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 + +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(); + auto& root = response->getRoot(); + + 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["ip"] = config.Shelly.ip; + response->setLength(); + request->send(response); + MessageOutput.println("Read Shelly AC charger config... "); +} + +void WebApiShellyClass::onAdminPost(AsyncWebServerRequest* request) +{ + MessageOutput.println("Write Shelly AC charger config... 1"); + 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; + } + + CONFIG_T& config = Configuration.get(); + 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"]; + strlcpy( config.Shelly.ip, root["ip"].as().c_str(), sizeof(config.Shelly.ip)); + MessageOutput.println("Write Shelly AC charger config... 2 "); + WebApi.writeConfig(retMsg); + WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); + + + yield(); + delay(1000); + yield(); + //ESP.restart(); + + // Properly turn this on + if (config.Shelly.Enabled) { + MessageOutput.println("Initialize Shelly AC charger interface... "); + } + + // Properly turn this off + if (!config.Shelly.Enabled) { + //HuaweiCan.setValue(0, HUAWEI_ONLINE_CURRENT); + delay(500); + //HuaweiCan.setMode(HUAWEI_MODE_OFF); + return; + } +} diff --git a/webapp/src/locales/de.json b/webapp/src/locales/de.json index d050860b8..394ae5172 100644 --- a/webapp/src/locales/de.json +++ b/webapp/src/locales/de.json @@ -898,6 +898,10 @@ "ChargerSettings": "AC Ladegerät Einstellungen", "Configuration": "AC Ladegerät Konfiguration", "EnableHuawei": "Huawei R4850G2 an CAN Bus Interface aktiv", + "EnableShelly": "AC Charger an Shelly Plug S", + "ShellySettings": "Shelly Plug S Konfiguration", + "ShellyAddress": "IP-Adresse oder Hostname", + "ShellyAddressHint": "Ip oder voller Hostname des Shelly Plug S", "VerboseLogging": "@:base.VerboseLogging", "CanControllerFrequency": "Frequenz des Quarzes am CAN Controller", "EnableAutoPower": "Automatische Leistungssteuerung", diff --git a/webapp/src/locales/en.json b/webapp/src/locales/en.json index bb4d5f299..ff1128386 100644 --- a/webapp/src/locales/en.json +++ b/webapp/src/locales/en.json @@ -902,6 +902,10 @@ "ChargerSettings": "AC Charger Settings", "Configuration": "AC Charger Configuration", "EnableHuawei": "Enable Huawei R4850G2 on CAN Bus Interface", + "EnableShelly": "AC Charger with Shelly Plug S", + "ShellySettings": "Shelly Plug S configuration", + "ShellyAddress": "IP-Address or Hostname", + "ShellyAddressHint": "Ip or full Hostname of Shelly Plug S", "VerboseLogging": "@:base.VerboseLogging", "CanControllerFrequency": "CAN controller quarz frequency", "EnableAutoPower": "Automatic power control", diff --git a/webapp/src/locales/fr.json b/webapp/src/locales/fr.json index aed0a9b88..1a90bcdc9 100644 --- a/webapp/src/locales/fr.json +++ b/webapp/src/locales/fr.json @@ -870,6 +870,10 @@ "ChargerSettings": "AC Charger Settings", "Configuration": "AC Charger Configuration", "EnableHuawei": "Enable Huawei R4850G2 on CAN Bus Interface", + "EnableShelly": "AC Charger with Shelly Plug S", + "ShellySettings": "Shelly Plug S configuration", + "ShellyAddress": "IP-Address or Hostname", + "ShellyAddressHint": "Ip or full Hostname of Shelly Plug S", "VerboseLogging": "@:base.VerboseLogging", "CanControllerFrequency": "CAN controller quarz frequency", "EnableAutoPower": "Automatic power control", diff --git a/webapp/src/types/AcChargerConfig.ts b/webapp/src/types/AcChargerConfig.ts index ec9fc7a44..76ea26dff 100644 --- a/webapp/src/types/AcChargerConfig.ts +++ b/webapp/src/types/AcChargerConfig.ts @@ -12,3 +12,12 @@ export interface AcChargerConfig { stop_batterysoc_threshold: number; target_power_consumption: number; } + +export interface AcChargerShellyConfig { + enabled: boolean; + verbose_logging: boolean; + auto_power_batterysoc_limits_enabled: boolean; + emergency_charge_enabled: boolean; + stop_batterysoc_threshold: number; + ip: String; +} diff --git a/webapp/src/views/AcChargerAdminView.vue b/webapp/src/views/AcChargerAdminView.vue index d8ae820e6..9366e1dac 100644 --- a/webapp/src/views/AcChargerAdminView.vue +++ b/webapp/src/views/AcChargerAdminView.vue @@ -12,6 +12,12 @@ type="checkbox" wide /> +
+ + + + + +
+ +
+
+ + +
+
+
+ +
+ +
+
+ + % +
+
+
+
+
@@ -217,6 +298,7 @@ import FormFooter from '@/components/FormFooter.vue'; import InputElement from '@/components/InputElement.vue'; import { BIconInfoCircle } from 'bootstrap-icons-vue'; import type { AcChargerConfig } from '@/types/AcChargerConfig'; +import type { AcChargerShellyConfig } from '@/types/AcChargerConfig'; import { authHeader, handleResponse } from '@/utils/authentication'; import { defineComponent } from 'vue'; @@ -233,6 +315,7 @@ export default defineComponent({ return { dataLoading: true, acChargerConfigList: {} as AcChargerConfig, + acChargerShellyConfigList: {} as AcChargerShellyConfig, alertMessage: '', alertType: 'info', showAlert: false, @@ -254,12 +337,32 @@ export default defineComponent({ this.acChargerConfigList = data; this.dataLoading = false; }); + fetch('/api/shelly/config', { headers: authHeader() }) + .then((response) => handleResponse(response, this.$emitter, this.$router)) + .then((data) => { + this.acChargerShellyConfigList = data; + this.dataLoading = false; + }); }, saveChargerConfig(e: Event) { e.preventDefault(); const formData = new FormData(); + const formDataShelly = new FormData(); formData.append('data', JSON.stringify(this.acChargerConfigList)); + formDataShelly.append('data', JSON.stringify(this.acChargerShellyConfigList)); + + fetch('/api/shelly/config', { + method: 'POST', + headers: authHeader(), + body: formDataShelly, + }) + .then((response) => handleResponse(response, this.$emitter, this.$router)) + .then((response) => { + this.alertMessage = this.$t('apiresponse.' + response.code, response.param); + this.alertType = response.type; + this.showAlert = true; + }); fetch('/api/huawei/config', { method: 'POST', @@ -272,6 +375,7 @@ export default defineComponent({ this.alertType = response.type; this.showAlert = true; }); + }, }, }); From 42da0802e124704c8ff4d1236e5b3797836301b5 Mon Sep 17 00:00:00 2001 From: Snoopy-HSS Date: Mon, 7 Oct 2024 07:48:37 +0200 Subject: [PATCH 2/7] Changes to be committed: modified: webapp/src/locales/de.json modified: webapp/src/locales/en.json modified: webapp/src/locales/fr.json --- webapp/src/locales/de.json | 4 ++++ webapp/src/locales/en.json | 4 ++++ webapp/src/locales/fr.json | 4 ++++ 3 files changed, 12 insertions(+) diff --git a/webapp/src/locales/de.json b/webapp/src/locales/de.json index 394ae5172..00491c78f 100644 --- a/webapp/src/locales/de.json +++ b/webapp/src/locales/de.json @@ -899,9 +899,13 @@ "Configuration": "AC Ladegerät Konfiguration", "EnableHuawei": "Huawei R4850G2 an CAN Bus Interface aktiv", "EnableShelly": "AC Charger an Shelly Plug S", + "ShellyStartThreshold": "Powermeter Start-Schwellwert", + "ShellyStopThreshold": "Powermeter Stop-Schwellwert", "ShellySettings": "Shelly Plug S Konfiguration", "ShellyAddress": "IP-Adresse oder Hostname", "ShellyAddressHint": "Ip oder voller Hostname des Shelly Plug S", + "ShellyStartThresholdHint": "Wert ab welcher Netzleistung mit AC Charger geladen werden soll", + "ShellyStopThresholdHint": "Wert ab welcher Netzleistung der AC Charger gestoppt werden soll", "VerboseLogging": "@:base.VerboseLogging", "CanControllerFrequency": "Frequenz des Quarzes am CAN Controller", "EnableAutoPower": "Automatische Leistungssteuerung", diff --git a/webapp/src/locales/en.json b/webapp/src/locales/en.json index ff1128386..802da0bc5 100644 --- a/webapp/src/locales/en.json +++ b/webapp/src/locales/en.json @@ -903,9 +903,13 @@ "Configuration": "AC Charger Configuration", "EnableHuawei": "Enable Huawei R4850G2 on CAN Bus Interface", "EnableShelly": "AC Charger with Shelly Plug S", + "ShellyStartThreshold": "Powermeter Start-Threshold", + "ShellyStopThreshold": "Powermeter Stop-Threshold", "ShellySettings": "Shelly Plug S configuration", "ShellyAddress": "IP-Address or Hostname", "ShellyAddressHint": "Ip or full Hostname of Shelly Plug S", + "ShellyStartThresholdHint": "Value where AC Charger should start", + "ShellyStopThresholdHint": "Value where AC Charger should stop", "VerboseLogging": "@:base.VerboseLogging", "CanControllerFrequency": "CAN controller quarz frequency", "EnableAutoPower": "Automatic power control", diff --git a/webapp/src/locales/fr.json b/webapp/src/locales/fr.json index 1a90bcdc9..6d34bfb91 100644 --- a/webapp/src/locales/fr.json +++ b/webapp/src/locales/fr.json @@ -871,9 +871,13 @@ "Configuration": "AC Charger Configuration", "EnableHuawei": "Enable Huawei R4850G2 on CAN Bus Interface", "EnableShelly": "AC Charger with Shelly Plug S", + "ShellyStartThreshold": "Powermeter Start-Threshold", + "ShellyStopThreshold": "Powermeter Stop-Threshold", "ShellySettings": "Shelly Plug S configuration", "ShellyAddress": "IP-Address or Hostname", "ShellyAddressHint": "Ip or full Hostname of Shelly Plug S", + "ShellyStartThresholdHint": "Value where AC Charger should start", + "ShellyStopThresholdHint": "Value where AC Charger should stop", "VerboseLogging": "@:base.VerboseLogging", "CanControllerFrequency": "CAN controller quarz frequency", "EnableAutoPower": "Automatic power control", From 46ed857d918f698161036ef1974da4a4d5a8a2db Mon Sep 17 00:00:00 2001 From: Snoopy-HSS Date: Mon, 14 Oct 2024 14:13:44 +0200 Subject: [PATCH 3/7] Changes to be committed: modified: include/Configuration.h modified: include/ShellyACPlug.h modified: include/defaults.h modified: src/Configuration.cpp modified: src/ShellyACPlug.cpp modified: src/WebApi_Shelly.cpp modified: src/main.cpp modified: webapp/src/types/AcChargerConfig.ts modified: webapp/src/views/AcChargerAdminView.vue --- include/Configuration.h | 10 +-- include/ShellyACPlug.h | 33 ++++---- include/defaults.h | 2 + src/Configuration.cpp | 18 +++-- src/ShellyACPlug.cpp | 100 +++++++++++++++++++----- src/WebApi_Shelly.cpp | 5 ++ src/main.cpp | 5 ++ webapp/src/types/AcChargerConfig.ts | 2 + webapp/src/views/AcChargerAdminView.vue | 40 ++++++++++ 9 files changed, 167 insertions(+), 48 deletions(-) diff --git a/include/Configuration.h b/include/Configuration.h index 5c950ecdc..d3fd96a70 100644 --- a/include/Configuration.h +++ b/include/Configuration.h @@ -128,12 +128,6 @@ struct POWERMETER_HTTP_SML_CONFIG_T { }; using PowerMeterHttpSmlConfig = struct POWERMETER_HTTP_SML_CONFIG_T; -struct ShellyACPlug_HTTP_CONFIG_T { - uint32_t PollingInterval; - HttpRequestConfig HttpRequest; -}; -using ShellyACPlugConfig = struct ShellyACPlug_HTTP_CONFIG_T ; - enum BatteryVoltageUnit { Volts = 0, DeciVolts = 1, CentiVolts = 2, MilliVolts = 3 }; enum BatteryAmperageUnit { Amps = 0, MilliAmps = 1 }; @@ -337,6 +331,10 @@ struct CONFIG_T { bool Emergency_Charge_Enabled; uint8_t stop_batterysoc_threshold; char ip[15]; + int32_t POWER_ON_threshold; + int32_t POWER_OFF_threshold; + bool POWER_ON; + bool POWER_OFF; } Shelly; diff --git a/include/ShellyACPlug.h b/include/ShellyACPlug.h index 08cdc5112..c20a88246 100644 --- a/include/ShellyACPlug.h +++ b/include/ShellyACPlug.h @@ -6,25 +6,26 @@ #include "Configuration.h" #include #include -#include "PowerMeterProvider.h" - - #include #include #include #include - -class ShellyACPlug : public PowerMeterProvider { -public: - explicit ShellyACPlug(ShellyACPlugConfig const& cfg) - : _cfg(cfg) { } - ~ShellyACPlug(); - bool init(); - void loop(); - -private: - std::unique_ptr _upHttpGetter; - ShellyACPlugConfig const& _cfg; - std::condition_variable _cv; +class ShellyACPlugClass { + public: + bool init(Scheduler& scheduler); + void loop(); + void PowerON(); + void PowerOFF(); + private: + bool _initialized = false; + Task _loopTask; + const uint16_t _period = 2000; + float _acPower; + float _SoC; + bool _emergcharge; + void send_http(String uri); + std::unique_ptr _upHttpGetter; }; + +extern ShellyACPlugClass ShellyACPlug; diff --git a/include/defaults.h b/include/defaults.h index cb3fbde84..fb0d2aff8 100644 --- a/include/defaults.h +++ b/include/defaults.h @@ -168,6 +168,8 @@ #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_IPADDRESS "192.168.2.100" diff --git a/src/Configuration.cpp b/src/Configuration.cpp index bc4c3b68b..b798a29a2 100644 --- a/src/Configuration.cpp +++ b/src/Configuration.cpp @@ -294,11 +294,15 @@ bool ConfigurationClass::write() 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["ip"] =config.Shelly.ip; + 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["ip"] = config.Shelly.ip; + shelly["power_on_threshold"] = config.Shelly.POWER_ON_threshold; + shelly["power_off_threshold"] = config.Shelly.POWER_OFF_threshold; + shelly["power_on"] = config.Shelly.POWER_ON; + shelly["power_off"] = config.Shelly.POWER_OFF; if (!Utils::checkJsonAlloc(doc, __FUNCTION__, __LINE__)) { return false; @@ -672,6 +676,10 @@ bool ConfigurationClass::read() config.Shelly.Emergency_Charge_Enabled = shelly["emergency_charge_enabled"] | false; config.Shelly.stop_batterysoc_threshold = shelly["stop_batterysoc_threshold"] | SHELLY_STOP_BATTERYSOC_THRESHOLD; strlcpy(config.Shelly.ip, shelly["ip"] | SHELLY_IPADDRESS, sizeof(config.Shelly.ip)); + 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; + config.Shelly.POWER_ON = shelly["power_on"] | false; + config.Shelly.POWER_OFF = shelly["power_off"] | false; f.close(); return true; diff --git a/src/ShellyACPlug.cpp b/src/ShellyACPlug.cpp index 83027baa0..31e6c744e 100644 --- a/src/ShellyACPlug.cpp +++ b/src/ShellyACPlug.cpp @@ -4,35 +4,93 @@ #include #include #include +#include "Configuration.h" +#include "Datastore.h" +#include "PowerMeter.h" +#include "Battery.h" -ShellyACPlug::~ShellyACPlug() -{ - _cv.notify_all(); -} - -bool ShellyACPlug::init() -{ - _upHttpGetter = std::make_unique(_cfg.HttpRequest); - - if (_upHttpGetter->init()) { return true; } +ShellyACPlugClass ShellyACPlug; - MessageOutput.printf("[ShellyACPlug] Initializing HTTP getter failed:\r\n"); - MessageOutput.printf("[ShellyACPlug] %s\r\n", _upHttpGetter->getErrorText()); - - _upHttpGetter = nullptr; +bool ShellyACPlugClass::init(Scheduler& scheduler) +{ + MessageOutput.printf("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 ShellyACPlug::loop() +void ShellyACPlugClass::loop() { + const CONFIG_T& config = Configuration.get(); + if (!config.Shelly.Enabled || !_initialized || !Configuration.get().PowerMeter.Enabled ) { + return; + } + _loopTask.setInterval(_period); + _acPower = PowerMeter.getPowerTotal(); + _SoC = Battery.getStats()->getSoC(); + _emergcharge = Battery.getStats()->getImmediateChargingRequest(); + if ((_acPower < config.Shelly.POWER_ON_threshold && !config.Shelly.POWER_ON && _SoC < config.Shelly.stop_batterysoc_threshold) || (_emergcharge && config.Shelly.Emergency_Charge_Enabled)) + { + PowerON(); + } + else if ((_acPower > config.Shelly.POWER_OFF_threshold && !config.Shelly.POWER_OFF) || (_SoC >= config.Shelly.stop_batterysoc_threshold && !config.Shelly.POWER_OFF)) + { + PowerOFF(); + } + bool _verboseLogging = config.Shelly.VerboseLogging; + if (_verboseLogging) { + MessageOutput.print("[ShellyACPlug] Loop \r\n"); + MessageOutput.printf("[ShellyACPlug] %f W \r\n", _acPower ); + MessageOutput.printf("[ShellyACPlug] ON: %d OFF: %d \r\n", config.Shelly.POWER_ON, config.Shelly.POWER_OFF ); + MessageOutput.printf("[ShellyACPlug] Battery SoC %f \r\n", _SoC); + } +} - - - - - uint32_t constexpr stackSize = 3072; - +void ShellyACPlugClass::PowerON() +{ + CONFIG_T& config = Configuration.get(); + config.Shelly.POWER_ON = true; + config.Shelly.POWER_OFF = false; + Configuration.write(); + send_http("http://192.168.2.153/relay/0?turn=on"); + bool _verboseLogging = config.Shelly.VerboseLogging; + if (_verboseLogging) { + MessageOutput.print("[ShellyACPlug] Power ON \r\n"); + } } +void ShellyACPlugClass::PowerOFF() +{ + CONFIG_T& config = Configuration.get(); + config.Shelly.POWER_ON = false; + config.Shelly.POWER_OFF = true; + Configuration.write(); + send_http("http://192.168.2.153/relay/0?turn=off"); + bool _verboseLogging = config.Shelly.VerboseLogging; + if (_verboseLogging) { + MessageOutput.print("[ShellyACPlug] Power OFF \r\n"); + } +} +void ShellyACPlugClass::send_http(String uri) +{ + JsonDocument doc; + JsonObject obj = doc["http_request"].add(); + obj["Url"] = uri; + obj["Timeout"] = 60; + HttpRequestConfig HttpRequest; + JsonObject source_http_config = doc["http_request"]; + strlcpy(HttpRequest.Url, source_http_config["url"], sizeof(HttpRequest.Url)); + HttpRequest.Timeout = source_http_config["timeout"] | 60; + _upHttpGetter = std::make_unique(HttpRequest); + _upHttpGetter->init(); + _upHttpGetter->performGetRequest(); + MessageOutput.printf("[ShellyACPlug] Initializing:\r\n"); + MessageOutput.printf("[ShellyACPlug] %s\r\n", _upHttpGetter->getErrorText()); + _upHttpGetter = nullptr; +} diff --git a/src/WebApi_Shelly.cpp b/src/WebApi_Shelly.cpp index 679e974e7..96c6dbab9 100644 --- a/src/WebApi_Shelly.cpp +++ b/src/WebApi_Shelly.cpp @@ -50,6 +50,9 @@ void WebApiShellyClass::onAdminGet(AsyncWebServerRequest* request) root["emergency_charge_enabled"] = config.Shelly.Emergency_Charge_Enabled; root["stop_batterysoc_threshold"] = config.Shelly.stop_batterysoc_threshold; root["ip"] = config.Shelly.ip; + 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... "); @@ -86,6 +89,8 @@ void WebApiShellyClass::onAdminPost(AsyncWebServerRequest* request) config.Shelly.Emergency_Charge_Enabled = root["emergency_charge_enabled"].as(); config.Shelly.stop_batterysoc_threshold = root["stop_batterysoc_threshold"]; strlcpy( config.Shelly.ip, root["ip"].as().c_str(), sizeof(config.Shelly.ip)); + config.Shelly.POWER_ON_threshold = root["power_on_threshold"]; + config.Shelly.POWER_OFF_threshold = root["power_off_threshold"]; MessageOutput.println("Write Shelly AC charger config... 2 "); WebApi.writeConfig(retMsg); WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); diff --git a/src/main.cpp b/src/main.cpp index 9e5f93c3d..0a41e1009 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -39,6 +39,7 @@ #include #include #include +#include "ShellyACPlug.h" #include @@ -204,6 +205,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/types/AcChargerConfig.ts b/webapp/src/types/AcChargerConfig.ts index 76ea26dff..41aff6727 100644 --- a/webapp/src/types/AcChargerConfig.ts +++ b/webapp/src/types/AcChargerConfig.ts @@ -20,4 +20,6 @@ export interface AcChargerShellyConfig { emergency_charge_enabled: boolean; stop_batterysoc_threshold: number; ip: String; + power_on_threshold: number; + power_off_threshold: number; } diff --git a/webapp/src/views/AcChargerAdminView.vue b/webapp/src/views/AcChargerAdminView.vue index 9366e1dac..813bb40cd 100644 --- a/webapp/src/views/AcChargerAdminView.vue +++ b/webapp/src/views/AcChargerAdminView.vue @@ -253,6 +253,46 @@ +
+ +
+
+ + W +
+
+
+
+ +
+
+ + W +
+
+
Date: Sat, 19 Oct 2024 21:22:58 +0200 Subject: [PATCH 4/7] Changes to be committed: modified: include/Configuration.h modified: include/ShellyACPlug.h modified: include/WebApi.h new file: include/WebApi_ws_Shelly.h modified: include/WebApi_ws_live.h modified: include/defaults.h modified: src/Configuration.cpp modified: src/ShellyACPlug.cpp modified: src/WebApi.cpp modified: src/WebApi_Shelly.cpp new file: src/WebApi_ws_Shelly.cpp modified: src/WebApi_ws_live.cpp modified: webapp/src/components/InverterTotalInfo.vue modified: webapp/src/locales/de.json modified: webapp/src/locales/en.json modified: webapp/src/locales/fr.json modified: webapp/src/types/AcChargerConfig.ts modified: webapp/src/types/LiveDataStatus.ts modified: webapp/src/views/AcChargerAdminView.vue modified: webapp/src/views/HomeView.vue --- include/Configuration.h | 2 +- include/ShellyACPlug.h | 6 +- include/WebApi.h | 2 + include/WebApi_ws_Shelly.h | 31 +++++ include/WebApi_ws_live.h | 2 +- include/defaults.h | 4 +- src/Configuration.cpp | 4 +- src/ShellyACPlug.cpp | 102 +++++++++++--- src/WebApi.cpp | 2 + src/WebApi_Shelly.cpp | 15 +- src/WebApi_ws_Shelly.cpp | 144 ++++++++++++++++++++ src/WebApi_ws_live.cpp | 13 +- webapp/src/components/InverterTotalInfo.vue | 16 ++- webapp/src/locales/de.json | 7 +- webapp/src/locales/en.json | 7 +- webapp/src/locales/fr.json | 3 +- webapp/src/types/AcChargerConfig.ts | 2 +- webapp/src/types/LiveDataStatus.ts | 6 + webapp/src/views/AcChargerAdminView.vue | 10 +- webapp/src/views/HomeView.vue | 6 +- 20 files changed, 327 insertions(+), 57 deletions(-) create mode 100644 include/WebApi_ws_Shelly.h create mode 100644 src/WebApi_ws_Shelly.cpp diff --git a/include/Configuration.h b/include/Configuration.h index d3fd96a70..7994c8e3e 100644 --- a/include/Configuration.h +++ b/include/Configuration.h @@ -330,7 +330,7 @@ struct CONFIG_T { bool Auto_Power_BatterySoC_Limits_Enabled; bool Emergency_Charge_Enabled; uint8_t stop_batterysoc_threshold; - char ip[15]; + char url[1025]; int32_t POWER_ON_threshold; int32_t POWER_OFF_threshold; bool POWER_ON; diff --git a/include/ShellyACPlug.h b/include/ShellyACPlug.h index c20a88246..d73819d34 100644 --- a/include/ShellyACPlug.h +++ b/include/ShellyACPlug.h @@ -17,6 +17,7 @@ class ShellyACPlugClass { void loop(); void PowerON(); void PowerOFF(); + float _readpower; private: bool _initialized = false; Task _loopTask; @@ -24,8 +25,9 @@ class ShellyACPlugClass { float _acPower; float _SoC; bool _emergcharge; - void send_http(String uri); - std::unique_ptr _upHttpGetter; + bool send_http(String uri); + float read_http(String uri); + std::unique_ptr _HttpGetter; }; extern ShellyACPlugClass ShellyACPlug; diff --git a/include/WebApi.h b/include/WebApi.h index e056fdfe8..3973e685c 100644 --- a/include/WebApi.h +++ b/include/WebApi.h @@ -32,6 +32,7 @@ #include "WebApi_Huawei.h" #include "WebApi_ws_battery.h" #include "WebApi_Shelly.h" +#include "WebApi_ws_Shelly.h" #include #include @@ -85,6 +86,7 @@ class WebApiClass { WebApiWsHuaweiLiveClass _webApiWsHuaweiLive; WebApiWsBatteryLiveClass _webApiWsBatteryLive; WebApiShellyClass _webApiShellyClass; + WebApiWsShellyLiveClass _webApiWsShellyLive; }; extern WebApiClass WebApi; 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 fc02d7816..10c635abb 100644 --- a/include/defaults.h +++ b/include/defaults.h @@ -171,6 +171,8 @@ #define SHELLY_POWER_ON_THRESHOLD -500 #define SHELLY_POWER_OFF_THRESHOLD -100 #define SHELLY_STOP_BATTERYSOC_THRESHOLD 95 -#define SHELLY_IPADDRESS "192.168.2.100" +#define SHELLY_IPADDRESS "http://192.168.2.100" +#define SHELLY_POWER_ON false +#define SHELLY_POWER_OFF false #define VERBOSE_LOGGING true diff --git a/src/Configuration.cpp b/src/Configuration.cpp index b798a29a2..f7b12ea48 100644 --- a/src/Configuration.cpp +++ b/src/Configuration.cpp @@ -298,7 +298,7 @@ bool ConfigurationClass::write() 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["ip"] = config.Shelly.ip; + shelly["url"] = config.Shelly.url; shelly["power_on_threshold"] = config.Shelly.POWER_ON_threshold; shelly["power_off_threshold"] = config.Shelly.POWER_OFF_threshold; shelly["power_on"] = config.Shelly.POWER_ON; @@ -675,7 +675,7 @@ bool ConfigurationClass::read() 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; - strlcpy(config.Shelly.ip, shelly["ip"] | SHELLY_IPADDRESS, sizeof(config.Shelly.ip)); + strlcpy(config.Shelly.url, shelly["url"] | SHELLY_IPADDRESS, sizeof(config.Shelly.url)); 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; config.Shelly.POWER_ON = shelly["power_on"] | false; diff --git a/src/ShellyACPlug.cpp b/src/ShellyACPlug.cpp index 31e6c744e..5b5573a2e 100644 --- a/src/ShellyACPlug.cpp +++ b/src/ShellyACPlug.cpp @@ -1,4 +1,5 @@ // SPDX-License-Identifier: GPL-2.0-or-later +#include "Utils.h" #include "ShellyACPlug.h" #include "MessageOutput.h" #include @@ -34,6 +35,7 @@ void ShellyACPlugClass::loop() _acPower = PowerMeter.getPowerTotal(); _SoC = Battery.getStats()->getSoC(); _emergcharge = Battery.getStats()->getImmediateChargingRequest(); + _readpower = read_http("/rpc/Switch.GetStatus?id=0"); if ((_acPower < config.Shelly.POWER_ON_threshold && !config.Shelly.POWER_ON && _SoC < config.Shelly.stop_batterysoc_threshold) || (_emergcharge && config.Shelly.Emergency_Charge_Enabled)) { PowerON(); @@ -42,55 +44,111 @@ void ShellyACPlugClass::loop() { PowerOFF(); } - bool _verboseLogging = config.Shelly.VerboseLogging; - if (_verboseLogging) { + if (config.Shelly.VerboseLogging) { MessageOutput.print("[ShellyACPlug] Loop \r\n"); MessageOutput.printf("[ShellyACPlug] %f W \r\n", _acPower ); MessageOutput.printf("[ShellyACPlug] ON: %d OFF: %d \r\n", config.Shelly.POWER_ON, config.Shelly.POWER_OFF ); MessageOutput.printf("[ShellyACPlug] Battery SoC %f \r\n", _SoC); + MessageOutput.printf("[ShellyACPlug] Verbrauch %f W \r\n", _readpower ); } } void ShellyACPlugClass::PowerON() { + if (!send_http("/relay/0?turn=on")) + { + return; + } CONFIG_T& config = Configuration.get(); config.Shelly.POWER_ON = true; config.Shelly.POWER_OFF = false; Configuration.write(); - send_http("http://192.168.2.153/relay/0?turn=on"); - bool _verboseLogging = config.Shelly.VerboseLogging; - if (_verboseLogging) { + if (config.Shelly.VerboseLogging) { MessageOutput.print("[ShellyACPlug] Power ON \r\n"); } } void ShellyACPlugClass::PowerOFF() { + if (!send_http("/relay/0?turn=off")) + { + return; + }; CONFIG_T& config = Configuration.get(); config.Shelly.POWER_ON = false; config.Shelly.POWER_OFF = true; Configuration.write(); - send_http("http://192.168.2.153/relay/0?turn=off"); - bool _verboseLogging = config.Shelly.VerboseLogging; - if (_verboseLogging) { + if (config.Shelly.VerboseLogging) { MessageOutput.print("[ShellyACPlug] Power OFF \r\n"); } } -void ShellyACPlugClass::send_http(String uri) +bool ShellyACPlugClass::send_http(String uri) { - JsonDocument doc; - JsonObject obj = doc["http_request"].add(); - obj["Url"] = uri; - obj["Timeout"] = 60; + CONFIG_T& config = Configuration.get(); + String url = config.Shelly.url; + url += uri; HttpRequestConfig HttpRequest; - JsonObject source_http_config = doc["http_request"]; - strlcpy(HttpRequest.Url, source_http_config["url"], sizeof(HttpRequest.Url)); - HttpRequest.Timeout = source_http_config["timeout"] | 60; - _upHttpGetter = std::make_unique(HttpRequest); - _upHttpGetter->init(); - _upHttpGetter->performGetRequest(); - MessageOutput.printf("[ShellyACPlug] Initializing:\r\n"); - MessageOutput.printf("[ShellyACPlug] %s\r\n", _upHttpGetter->getErrorText()); - _upHttpGetter = nullptr; + 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 Initializing: %s\r\n",url.c_str()); + } + if (!_HttpGetter->init()) { + MessageOutput.printf("[ShellyACPlug] INIT %s\r\n", _HttpGetter->getErrorText()); + return false; + } + if (!_HttpGetter->performGetRequest()) { + MessageOutput.printf("[ShellyACPlug] GET %s\r\n", _HttpGetter->getErrorText()); + return false; + } + _HttpGetter = nullptr; + return true; } +float ShellyACPlugClass::read_http(String uri) +{ + 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 Initializing: %s\r\n",url.c_str()); + } + if (!_HttpGetter->init()) { + MessageOutput.printf("[ShellyACPlug] INIT %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] GET %s\r\n", _HttpGetter->getErrorText()); + return 0; + } + auto pStream = res.getStream(); + if (!pStream) { + MessageOutput.printf("Programmer error: HTTP request yields no stream"); + return 0; + } + + const DeserializationError error = deserializeJson(jsonResponse, *pStream); + if (error) { + String msg("[ShellyACPlug] Unable to parse server response as JSON: "); + MessageOutput.printf((msg + error.c_str()).c_str()); + return 0; + } + auto pathResolutionResult = Utils::getJsonValueByPath(jsonResponse, "apower"); + if (!pathResolutionResult.second.isEmpty()) { + MessageOutput.printf("[ShellyACPlug] second %s\r\n",pathResolutionResult.second.c_str()); + } + + _HttpGetter = nullptr; + return pathResolutionResult.first; +} + + diff --git a/src/WebApi.cpp b/src/WebApi.cpp index fcd1d121d..f45d87358 100644 --- a/src/WebApi.cpp +++ b/src/WebApi.cpp @@ -44,6 +44,7 @@ void WebApiClass::init(Scheduler& scheduler) _webApiHuaweiClass.init(_server, scheduler); _webApiWsBatteryLive.init(_server, scheduler); _webApiShellyClass.init(_server, scheduler); + _webApiWsShellyLive.init(_server, scheduler); _server.begin(); } @@ -54,6 +55,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 index 96c6dbab9..c9ec02917 100644 --- a/src/WebApi_Shelly.cpp +++ b/src/WebApi_Shelly.cpp @@ -9,6 +9,7 @@ #include "WebApi_errors.h" #include #include +#include "ShellyACPlug.h" void WebApiShellyClass::init(AsyncWebServer& server, Scheduler& scheduler) { @@ -28,7 +29,6 @@ void WebApiShellyClass::onStatus(AsyncWebServerRequest* request) } AsyncJsonResponse* response = new AsyncJsonResponse(); - auto& root = response->getRoot(); response->setLength(); request->send(response); @@ -49,7 +49,7 @@ void WebApiShellyClass::onAdminGet(AsyncWebServerRequest* request) 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["ip"] = config.Shelly.ip; + root["url"] = config.Shelly.url; root["power_on_threshold"] = config.Shelly.POWER_ON_threshold; root["power_off_threshold"] = config.Shelly.POWER_OFF_threshold; @@ -60,7 +60,6 @@ void WebApiShellyClass::onAdminGet(AsyncWebServerRequest* request) void WebApiShellyClass::onAdminPost(AsyncWebServerRequest* request) { - MessageOutput.println("Write Shelly AC charger config... 1"); if (!WebApi.checkCredentials(request)) { return; } @@ -88,10 +87,9 @@ void WebApiShellyClass::onAdminPost(AsyncWebServerRequest* request) 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"]; - strlcpy( config.Shelly.ip, root["ip"].as().c_str(), sizeof(config.Shelly.ip)); + strlcpy( config.Shelly.url, root["url"].as().c_str(), sizeof(config.Shelly.url)); config.Shelly.POWER_ON_threshold = root["power_on_threshold"]; config.Shelly.POWER_OFF_threshold = root["power_off_threshold"]; - MessageOutput.println("Write Shelly AC charger config... 2 "); WebApi.writeConfig(retMsg); WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); @@ -99,18 +97,13 @@ void WebApiShellyClass::onAdminPost(AsyncWebServerRequest* request) yield(); delay(1000); yield(); - //ESP.restart(); - // Properly turn this on if (config.Shelly.Enabled) { MessageOutput.println("Initialize Shelly AC charger interface... "); } - // Properly turn this off if (!config.Shelly.Enabled) { - //HuaweiCan.setValue(0, HUAWEI_ONLINE_CURRENT); - delay(500); - //HuaweiCan.setMode(HUAWEI_MODE_OFF); + 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 e3fc7570f..7444c0833 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(); @@ -367,7 +378,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/webapp/src/components/InverterTotalInfo.vue b/webapp/src/components/InverterTotalInfo.vue index a1f0a3157..53f446f41 100644 --- a/webapp/src/components/InverterTotalInfo.vue +++ b/webapp/src/components/InverterTotalInfo.vue @@ -182,11 +182,24 @@ +
+ +

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

+
+
diff --git a/webapp/src/locales/de.json b/webapp/src/locales/de.json index dd5c61d3c..f244f25ba 100644 --- a/webapp/src/locales/de.json +++ b/webapp/src/locales/de.json @@ -429,7 +429,8 @@ "BatteryCharge": "Batterie Ladezustand", "BatteryPower": "Batterie Leistung", "HomePower": "Leistung / Netz", - "HuaweiPower": "Huawei AC Leistung" + "HuaweiPower": "Huawei AC Leistung", + "ShellyPower": "AC Ladegerät Leistung" }, "inverterchannelproperty": { "Power": "Leistung", @@ -924,8 +925,8 @@ "ShellyStartThreshold": "Powermeter Start-Schwellwert", "ShellyStopThreshold": "Powermeter Stop-Schwellwert", "ShellySettings": "Shelly Plug S Konfiguration", - "ShellyAddress": "IP-Adresse oder Hostname", - "ShellyAddressHint": "Ip oder voller Hostname des Shelly Plug S", + "ShellyAddress": "Url mit IP-Adresse oder Hostname", + "ShellyAddressHint": "URL mit Ip oder voller Hostname des Shelly Plug S z.b. http://192.168.2.100 oder https://shelly123.domain.local", "ShellyStartThresholdHint": "Wert ab welcher Netzleistung mit AC Charger geladen werden soll", "ShellyStopThresholdHint": "Wert ab welcher Netzleistung der AC Charger gestoppt werden soll", "VerboseLogging": "@:base.VerboseLogging", diff --git a/webapp/src/locales/en.json b/webapp/src/locales/en.json index 2feedd6fa..c29011012 100644 --- a/webapp/src/locales/en.json +++ b/webapp/src/locales/en.json @@ -431,7 +431,8 @@ "BatteryCharge": "Battery Charge", "BatteryPower": "Battery Power", "HomePower": "Grid Power", - "HuaweiPower": "Huawei AC Power" + "HuaweiPower": "Huwawei AC Power", + "ShellyPower": "AC Charger Power" }, "inverterchannelproperty": { "Power": "Power", @@ -928,8 +929,8 @@ "ShellyStartThreshold": "Powermeter Start-Threshold", "ShellyStopThreshold": "Powermeter Stop-Threshold", "ShellySettings": "Shelly Plug S configuration", - "ShellyAddress": "IP-Address or Hostname", - "ShellyAddressHint": "Ip or full Hostname of Shelly Plug S", + "ShellyAddress": "url with IP-Address or Hostname", + "ShellyAddressHint": "url with Ip or full Hostname of Shelly Plug S", "ShellyStartThresholdHint": "Value where AC Charger should start", "ShellyStopThresholdHint": "Value where AC Charger should stop", "VerboseLogging": "@:base.VerboseLogging", diff --git a/webapp/src/locales/fr.json b/webapp/src/locales/fr.json index 9fda30520..58c5482f5 100644 --- a/webapp/src/locales/fr.json +++ b/webapp/src/locales/fr.json @@ -446,7 +446,8 @@ "BatteryCharge": "Battery Charge", "BatteryPower": "Battery Power", "HomePower": "Grid Power", - "HuaweiPower": "Huawei AC Power" + "HuaweiPower": "Huawei AC Power", + "ShellyPower": "AC Charger Power" }, "inverterchannelproperty": { "Power": "Puissance", diff --git a/webapp/src/types/AcChargerConfig.ts b/webapp/src/types/AcChargerConfig.ts index 41aff6727..29171d12e 100644 --- a/webapp/src/types/AcChargerConfig.ts +++ b/webapp/src/types/AcChargerConfig.ts @@ -19,7 +19,7 @@ export interface AcChargerShellyConfig { auto_power_batterysoc_limits_enabled: boolean; emergency_charge_enabled: boolean; stop_batterysoc_threshold: number; - ip: String; + url: String; power_on_threshold: number; power_off_threshold: number; } diff --git a/webapp/src/types/LiveDataStatus.ts b/webapp/src/types/LiveDataStatus.ts index 591e01129..84df82693 100644 --- a/webapp/src/types/LiveDataStatus.ts +++ b/webapp/src/types/LiveDataStatus.ts @@ -70,6 +70,11 @@ export interface Huawei { Power: ValueObject; } +export interface Shelly { + enabled: boolean; + Power: ValueObject; +} + export interface Battery { enabled: boolean; soc?: ValueObject; @@ -91,4 +96,5 @@ export interface LiveData { huawei: Huawei; battery: Battery; power_meter: PowerMeter; + shelly: Shelly; } diff --git a/webapp/src/views/AcChargerAdminView.vue b/webapp/src/views/AcChargerAdminView.vue index 813bb40cd..d8359052b 100644 --- a/webapp/src/views/AcChargerAdminView.vue +++ b/webapp/src/views/AcChargerAdminView.vue @@ -243,13 +243,13 @@ - + diff --git a/webapp/src/views/HomeView.vue b/webapp/src/views/HomeView.vue index ba4eb0509..d00f26709 100644 --- a/webapp/src/views/HomeView.vue +++ b/webapp/src/views/HomeView.vue @@ -14,6 +14,7 @@ :totalBattData="liveData.battery" :powerMeterData="liveData.power_meter" :huaweiData="liveData.huawei" + :shellyData="liveData.shelly" />
@@ -702,20 +703,21 @@ 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.shelly !== 'undefined') { + Object.assign(this.liveData.shelly, newData.shelly); + } 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; } From 2d5b999b8f3c19255dd03b01292b214954dc7fe9 Mon Sep 17 00:00:00 2001 From: Snoopy-HSS Date: Sat, 19 Oct 2024 21:27:09 +0200 Subject: [PATCH 5/7] Changes to be committed: modified: webapp/src/types/AcChargerConfig.ts string in lower case --- webapp/src/types/AcChargerConfig.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/webapp/src/types/AcChargerConfig.ts b/webapp/src/types/AcChargerConfig.ts index 29171d12e..dfb35a770 100644 --- a/webapp/src/types/AcChargerConfig.ts +++ b/webapp/src/types/AcChargerConfig.ts @@ -19,7 +19,7 @@ export interface AcChargerShellyConfig { auto_power_batterysoc_limits_enabled: boolean; emergency_charge_enabled: boolean; stop_batterysoc_threshold: number; - url: String; + url: string; power_on_threshold: number; power_off_threshold: number; } From 4e1f2015a342f2ea0f65d589d489e20422a06cd2 Mon Sep 17 00:00:00 2001 From: Snoopy-HSS Date: Sat, 19 Oct 2024 21:34:57 +0200 Subject: [PATCH 6/7] Changes to be committed: modified: webapp/src/views/AcChargerAdminView.vue Prettier --- webapp/src/views/AcChargerAdminView.vue | 312 +++++++----------------- 1 file changed, 89 insertions(+), 223 deletions(-) diff --git a/webapp/src/views/AcChargerAdminView.vue b/webapp/src/views/AcChargerAdminView.vue index d8359052b..de61b1182 100644 --- a/webapp/src/views/AcChargerAdminView.vue +++ b/webapp/src/views/AcChargerAdminView.vue @@ -6,18 +6,10 @@
- - + +
- + - + - + v-model="acChargerConfigList.auto_power_batterysoc_limits_enabled" type="checkbox" wide /> - + - +
-
- + ">
-
- - - - + + + +
-
- -
-
- - W -
+ +
+
+ + W
-
- -
-
- - W -
+
+
+ +
+
+ + W
- +
+
-
@@ -415,7 +282,6 @@ export default defineComponent({ this.alertType = response.type; this.showAlert = true; }); - }, }, }); From b7702135b2d54495fc55a56a85312d5a807e1b4b Mon Sep 17 00:00:00 2001 From: Snoopy-HSS Date: Sat, 19 Oct 2024 21:50:14 +0200 Subject: [PATCH 7/7] Changes to be committed: modified: webapp/src/views/AcChargerAdminView.vue yarn prettier --- webapp/src/views/AcChargerAdminView.vue | 283 +++++++++++++++++------- 1 file changed, 209 insertions(+), 74 deletions(-) diff --git a/webapp/src/views/AcChargerAdminView.vue b/webapp/src/views/AcChargerAdminView.vue index de61b1182..cdab173ee 100644 --- a/webapp/src/views/AcChargerAdminView.vue +++ b/webapp/src/views/AcChargerAdminView.vue @@ -6,10 +6,18 @@ - - + +
- + - + - + v-model="acChargerConfigList.auto_power_batterysoc_limits_enabled" + type="checkbox" + wide + /> - + - +
-
- + " + >
-
- - - - + + + +
-
-
-
- +
-