Added Device Profiles

Allows the pin assignment during runtime.
Pin settings will be read from a json file called "pin_mapping.json"
This commit is contained in:
Thomas Basler 2023-01-16 21:09:24 +01:00
parent 587de2e3be
commit 5f699f4927
11 changed files with 293 additions and 25 deletions

View File

@ -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 {

33
include/PinMapping.h Normal file
View File

@ -0,0 +1,33 @@
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include <Arduino.h>
#include <stdint.h>
#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;

View File

@ -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;

16
include/WebApi_device.h Normal file
View File

@ -0,0 +1,16 @@
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include <ESPAsyncWebServer.h>
class WebApiDeviceClass {
public:
void init(AsyncWebServer* server);
void loop();
private:
void onDeviceAdminGet(AsyncWebServerRequest* request);
void onDeviceAdminPost(AsyncWebServerRequest* request);
AsyncWebServer* _server;
};

View File

@ -81,4 +81,7 @@ enum WebApiError {
PowerBase = 11000,
PowerSerialZero,
PowerInvalidInverter,
HardwareBase = 12000,
HardwarePinMappingLength,
};

View File

@ -77,4 +77,6 @@
#define MQTT_HASS_EXPIRE true
#define MQTT_HASS_RETAIN true
#define MQTT_HASS_TOPIC "homeassistant/"
#define MQTT_HASS_INDIVIDUALPANELS false
#define MQTT_HASS_INDIVIDUALPANELS false
#define DEV_PINMAPPING ""

View File

@ -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<JsonObject>();

72
src/PinMapping.cpp Normal file
View File

@ -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 <ArduinoJson.h>
#include <LittleFS.h>
#include <string.h>
#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;
}

View File

@ -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();

113
src/WebApi_device.cpp Normal file
View File

@ -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 <AsyncJson.h>
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<String>().length() == 0 || root[F("dev_pinmapping")].as<String>().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<String>().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();
}

View File

@ -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()