diff --git a/include/Configuration.h b/include/Configuration.h index 7955bdc..ed0ddd7 100644 --- a/include/Configuration.h +++ b/include/Configuration.h @@ -27,6 +27,8 @@ #define CHAN_MAX_NAME_STRLEN 31 +#define DEV_MAX_MAPPING_NAME_STRLEN 31 + #define JSON_BUFFER_SIZE 6144 struct CHANNEL_CONFIG_T { @@ -88,6 +90,8 @@ struct CONFIG_T { char Security_Password[WIFI_MAX_PASSWORD_STRLEN + 1]; bool Security_AllowReadonly; + + char Dev_PinMapping[DEV_MAX_MAPPING_NAME_STRLEN + 1]; }; class ConfigurationClass { diff --git a/include/PinMapping.h b/include/PinMapping.h new file mode 100644 index 0000000..8eac016 --- /dev/null +++ b/include/PinMapping.h @@ -0,0 +1,33 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#pragma once + +#include +#include + +#define PINMAPPING_FILENAME "/pin_mapping.json" + +#define MAPPING_NAME_STRLEN 31 + +struct PinMapping_t { + char name[MAPPING_NAME_STRLEN + 1]; + int8_t nrf24_miso; + int8_t nrf24_mosi; + int8_t nrf24_clk; + int8_t nrf24_irq; + int8_t nrf24_en; + int8_t nrf24_cs; +}; + +class PinMappingClass { +public: + PinMappingClass(); + bool init(const String& deviceMapping); + PinMapping_t& get(); + + bool isValidNrf24Config(); + +private: + PinMapping_t _pinMapping; +}; + +extern PinMappingClass PinMapping; \ No newline at end of file diff --git a/include/WebApi.h b/include/WebApi.h index 9cef1c2..94275aa 100644 --- a/include/WebApi.h +++ b/include/WebApi.h @@ -6,6 +6,7 @@ #include "WebApi_dtu.h" #include "WebApi_eventlog.h" #include "WebApi_firmware.h" +#include "WebApi_device.h" #include "WebApi_inverter.h" #include "WebApi_limit.h" #include "WebApi_maintenance.h" @@ -35,6 +36,7 @@ private: AsyncEventSource _events; WebApiConfigClass _webApiConfig; + WebApiDeviceClass _webApiDevice; WebApiDevInfoClass _webApiDevInfo; WebApiDtuClass _webApiDtu; WebApiEventlogClass _webApiEventlog; diff --git a/include/WebApi_device.h b/include/WebApi_device.h new file mode 100644 index 0000000..ae76edd --- /dev/null +++ b/include/WebApi_device.h @@ -0,0 +1,16 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#pragma once + +#include + +class WebApiDeviceClass { +public: + void init(AsyncWebServer* server); + void loop(); + +private: + void onDeviceAdminGet(AsyncWebServerRequest* request); + void onDeviceAdminPost(AsyncWebServerRequest* request); + + AsyncWebServer* _server; +}; \ No newline at end of file diff --git a/include/WebApi_errors.h b/include/WebApi_errors.h index 0e4099a..9a53759 100644 --- a/include/WebApi_errors.h +++ b/include/WebApi_errors.h @@ -81,4 +81,7 @@ enum WebApiError { PowerBase = 11000, PowerSerialZero, PowerInvalidInverter, + + HardwareBase = 12000, + HardwarePinMappingLength, }; \ No newline at end of file diff --git a/include/defaults.h b/include/defaults.h index 2422001..1009ca5 100644 --- a/include/defaults.h +++ b/include/defaults.h @@ -77,4 +77,6 @@ #define MQTT_HASS_EXPIRE true #define MQTT_HASS_RETAIN true #define MQTT_HASS_TOPIC "homeassistant/" -#define MQTT_HASS_INDIVIDUALPANELS false \ No newline at end of file +#define MQTT_HASS_INDIVIDUALPANELS false + +#define DEV_PINMAPPING "" \ No newline at end of file diff --git a/src/Configuration.cpp b/src/Configuration.cpp index 158364c..9998cf5 100644 --- a/src/Configuration.cpp +++ b/src/Configuration.cpp @@ -80,6 +80,9 @@ bool ConfigurationClass::write() security["password"] = config.Security_Password; security["allow_readonly"] = config.Security_AllowReadonly; + JsonObject device = doc.createNestedObject("device"); + device["pinmapping"] = config.Dev_PinMapping; + JsonArray inverters = doc.createNestedArray("inverters"); for (uint8_t i = 0; i < INV_MAX_COUNT; i++) { JsonObject inv = inverters.createNestedObject(); @@ -201,6 +204,9 @@ bool ConfigurationClass::read() strlcpy(config.Security_Password, security["password"] | ACCESS_POINT_PASSWORD, sizeof(config.Security_Password)); config.Security_AllowReadonly = security["allow_readonly"] | SECURITY_ALLOW_READONLY; + JsonObject device = doc["device"]; + strlcpy(config.Dev_PinMapping, device["pinmapping"] | DEV_PINMAPPING, sizeof(config.Dev_PinMapping)); + JsonArray inverters = doc["inverters"]; for (uint8_t i = 0; i < INV_MAX_COUNT; i++) { JsonObject inv = inverters[i].as(); diff --git a/src/PinMapping.cpp b/src/PinMapping.cpp new file mode 100644 index 0000000..b95d6d0 --- /dev/null +++ b/src/PinMapping.cpp @@ -0,0 +1,72 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Copyright (C) 2022 - 2023 Thomas Basler and others + */ +#include "PinMapping.h" +#include "MessageOutput.h" +#include +#include +#include + +#define JSON_BUFFER_SIZE 6144 + +PinMappingClass PinMapping; + +PinMappingClass::PinMappingClass() +{ + memset(&_pinMapping, 0x0, sizeof(_pinMapping)); + _pinMapping.nrf24_clk = HOYMILES_PIN_SCLK; + _pinMapping.nrf24_cs = HOYMILES_PIN_CS; + _pinMapping.nrf24_en = HOYMILES_PIN_CE; + _pinMapping.nrf24_irq = HOYMILES_PIN_IRQ; + _pinMapping.nrf24_miso = HOYMILES_PIN_MISO; + _pinMapping.nrf24_mosi = HOYMILES_PIN_MOSI; +} + +PinMapping_t& PinMappingClass::get() +{ + return _pinMapping; +} + + +bool PinMappingClass::init(const String& deviceMapping) +{ + File f = LittleFS.open(PINMAPPING_FILENAME, "r", false); + + if (!f) { + return false; + } + + DynamicJsonDocument doc(JSON_BUFFER_SIZE); + // Deserialize the JSON document + DeserializationError error = deserializeJson(doc, f); + if (error) { + MessageOutput.println(F("Failed to read file, using default configuration")); + } + + for (uint8_t i = 1; i <= doc.size(); i++) { + String devName = doc[i]["name"] | ""; + if (devName == deviceMapping) { + strlcpy(_pinMapping.name, devName.c_str(), sizeof(_pinMapping.name)); + _pinMapping.nrf24_clk = doc[i]["nrf24"]["clk"] | HOYMILES_PIN_SCLK; + _pinMapping.nrf24_cs = doc[i]["nrf24"]["cs"] | HOYMILES_PIN_CS; + _pinMapping.nrf24_en = doc[i]["nrf24"]["en"] | HOYMILES_PIN_CE; + _pinMapping.nrf24_irq = doc[i]["nrf24"]["irq"] | HOYMILES_PIN_IRQ; + _pinMapping.nrf24_miso = doc[i]["nrf24"]["miso"] | HOYMILES_PIN_MISO; + _pinMapping.nrf24_mosi = doc[i]["nrf24"]["mosi"] | HOYMILES_PIN_MOSI; + return true; + } + } + + return false; +} + +bool PinMappingClass::isValidNrf24Config() +{ + return _pinMapping.nrf24_clk > 0 + && _pinMapping.nrf24_cs > 0 + && _pinMapping.nrf24_en > 0 + && _pinMapping.nrf24_irq > 0 + && _pinMapping.nrf24_miso > 0 + && _pinMapping.nrf24_mosi > 0; +} \ No newline at end of file diff --git a/src/WebApi.cpp b/src/WebApi.cpp index f7b5683..349fb58 100644 --- a/src/WebApi.cpp +++ b/src/WebApi.cpp @@ -18,6 +18,7 @@ void WebApiClass::init() _server.addHandler(&_events); _webApiConfig.init(&_server); + _webApiDevice.init(&_server); _webApiDevInfo.init(&_server); _webApiDtu.init(&_server); _webApiEventlog.init(&_server); @@ -42,6 +43,7 @@ void WebApiClass::init() void WebApiClass::loop() { _webApiConfig.loop(); + _webApiDevice.loop(); _webApiDevInfo.loop(); _webApiDtu.loop(); _webApiEventlog.loop(); diff --git a/src/WebApi_device.cpp b/src/WebApi_device.cpp new file mode 100644 index 0000000..96b9557 --- /dev/null +++ b/src/WebApi_device.cpp @@ -0,0 +1,113 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Copyright (C) 2022 Thomas Basler and others + */ +#include "WebApi_device.h" +#include "Configuration.h" +#include "WebApi.h" +#include "WebApi_errors.h" +#include "helper.h" +#include + +void WebApiDeviceClass::init(AsyncWebServer* server) +{ + using std::placeholders::_1; + + _server = server; + + _server->on("/api/device/config", HTTP_GET, std::bind(&WebApiDeviceClass::onDeviceAdminGet, this, _1)); + _server->on("/api/device/config", HTTP_POST, std::bind(&WebApiDeviceClass::onDeviceAdminPost, this, _1)); +} + +void WebApiDeviceClass::loop() +{ +} + +void WebApiDeviceClass::onDeviceAdminGet(AsyncWebServerRequest* request) +{ + if (!WebApi.checkCredentials(request)) { + return; + } + + AsyncJsonResponse* response = new AsyncJsonResponse(false, MQTT_JSON_DOC_SIZE); + JsonObject root = response->getRoot(); + const CONFIG_T& config = Configuration.get(); + + root[F("dev_pinmapping")] = config.Dev_PinMapping; + + response->setLength(); + request->send(response); +} + +void WebApiDeviceClass::onDeviceAdminPost(AsyncWebServerRequest* request) +{ + if (!WebApi.checkCredentials(request)) { + return; + } + + AsyncJsonResponse* response = new AsyncJsonResponse(false, MQTT_JSON_DOC_SIZE); + JsonObject retMsg = response->getRoot(); + retMsg[F("type")] = F("warning"); + + if (!request->hasParam("data", true)) { + retMsg[F("message")] = F("No values found!"); + retMsg[F("code")] = WebApiError::GenericNoValueFound; + response->setLength(); + request->send(response); + return; + } + + String json = request->getParam("data", true)->value(); + + if (json.length() > MQTT_JSON_DOC_SIZE) { + retMsg[F("message")] = F("Data too large!"); + retMsg[F("code")] = WebApiError::GenericDataTooLarge; + response->setLength(); + request->send(response); + return; + } + + DynamicJsonDocument root(MQTT_JSON_DOC_SIZE); + DeserializationError error = deserializeJson(root, json); + + if (error) { + retMsg[F("message")] = F("Failed to parse data!"); + retMsg[F("code")] = WebApiError::GenericParseError; + response->setLength(); + request->send(response); + return; + } + + if (!(root.containsKey("dev_pinmapping"))) { + retMsg[F("message")] = F("Values are missing!"); + retMsg[F("code")] = WebApiError::GenericValueMissing; + response->setLength(); + request->send(response); + return; + } + + if (root[F("dev_pinmapping")].as().length() == 0 || root[F("dev_pinmapping")].as().length() > DEV_MAX_MAPPING_NAME_STRLEN) { + retMsg[F("message")] = F("Pin mapping must between 1 and " STR(DEV_MAX_MAPPING_NAME_STRLEN) " characters long!"); + retMsg[F("code")] = WebApiError::HardwarePinMappingLength; + retMsg[F("param")][F("max")] = DEV_MAX_MAPPING_NAME_STRLEN; + response->setLength(); + request->send(response); + return; + } + + CONFIG_T& config = Configuration.get(); + strlcpy(config.Dev_PinMapping, root[F("dev_pinmapping")].as().c_str(), sizeof(config.Dev_PinMapping)); + Configuration.write(); + + retMsg[F("type")] = F("success"); + retMsg[F("message")] = F("Settings saved!"); + retMsg[F("code")] = WebApiError::GenericSuccess; + + response->setLength(); + request->send(response); + + yield(); + delay(1000); + yield(); + ESP.restart(); +} \ No newline at end of file diff --git a/src/main.cpp b/src/main.cpp index 08d3893..c74b45d 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -10,6 +10,7 @@ #include "MqttSettings.h" #include "NetworkSettings.h" #include "NtpSettings.h" +#include "PinMapping.h" #include "Utils.h" #include "WebApi.h" #include "defaults.h" @@ -56,6 +57,15 @@ void setup() } MessageOutput.println(F("done")); + // Load PinMapping + MessageOutput.print(F("Reading PinMapping... ")); + if (PinMapping.init(String(Configuration.get().Dev_PinMapping))) { + MessageOutput.print(F("found valid mapping ")); + } else { + MessageOutput.print(F("using default config ")); + } + MessageOutput.println(F("done")); + // Initialize WiFi MessageOutput.print(F("Initialize Network... ")); NetworkSettings.init(); @@ -96,39 +106,44 @@ void setup() // Initialize inverter communication MessageOutput.print(F("Initialize Hoymiles interface... ")); - SPIClass* spiClass = new SPIClass(HSPI); - spiClass->begin(HOYMILES_PIN_SCLK, HOYMILES_PIN_MISO, HOYMILES_PIN_MOSI, HOYMILES_PIN_CS); - Hoymiles.setMessageOutput(&MessageOutput); - Hoymiles.init(spiClass, HOYMILES_PIN_CE, HOYMILES_PIN_IRQ); + if (PinMapping.isValidNrf24Config()) { + SPIClass* spiClass = new SPIClass(HSPI); + PinMapping_t& pin = PinMapping.get(); + spiClass->begin(pin.nrf24_clk, pin.nrf24_miso, pin.nrf24_mosi, pin.nrf24_cs); + Hoymiles.setMessageOutput(&MessageOutput); + Hoymiles.init(spiClass, pin.nrf24_en, pin.nrf24_irq); - MessageOutput.println(F(" Setting radio PA level... ")); - Hoymiles.getRadio()->setPALevel((rf24_pa_dbm_e)config.Dtu_PaLevel); + MessageOutput.println(F(" Setting radio PA level... ")); + Hoymiles.getRadio()->setPALevel((rf24_pa_dbm_e)config.Dtu_PaLevel); - MessageOutput.println(F(" Setting DTU serial... ")); - Hoymiles.getRadio()->setDtuSerial(config.Dtu_Serial); + MessageOutput.println(F(" Setting DTU serial... ")); + Hoymiles.getRadio()->setDtuSerial(config.Dtu_Serial); - MessageOutput.println(F(" Setting poll interval... ")); - Hoymiles.setPollInterval(config.Dtu_PollInterval); + MessageOutput.println(F(" Setting poll interval... ")); + Hoymiles.setPollInterval(config.Dtu_PollInterval); - for (uint8_t i = 0; i < INV_MAX_COUNT; i++) { - if (config.Inverter[i].Serial > 0) { - MessageOutput.print(F(" Adding inverter: ")); - MessageOutput.print(config.Inverter[i].Serial, HEX); - MessageOutput.print(F(" - ")); - MessageOutput.print(config.Inverter[i].Name); - auto inv = Hoymiles.addInverter( - config.Inverter[i].Name, - config.Inverter[i].Serial); + for (uint8_t i = 0; i < INV_MAX_COUNT; i++) { + if (config.Inverter[i].Serial > 0) { + MessageOutput.print(F(" Adding inverter: ")); + MessageOutput.print(config.Inverter[i].Serial, HEX); + MessageOutput.print(F(" - ")); + MessageOutput.print(config.Inverter[i].Name); + auto inv = Hoymiles.addInverter( + config.Inverter[i].Name, + config.Inverter[i].Serial); - if (inv != nullptr) { - for (uint8_t c = 0; c < INV_MAX_CHAN_COUNT; c++) { - inv->Statistics()->setChannelMaxPower(c, config.Inverter[i].channel[c].MaxChannelPower); + if (inv != nullptr) { + for (uint8_t c = 0; c < INV_MAX_CHAN_COUNT; c++) { + inv->Statistics()->setChannelMaxPower(c, config.Inverter[i].channel[c].MaxChannelPower); + } } + MessageOutput.println(F(" done")); } - MessageOutput.println(F(" done")); } + MessageOutput.println(F("done")); + } else { + MessageOutput.println(F("Invalid pin config")); } - MessageOutput.println(F("done")); } void loop()