First version
This commit is contained in:
parent
217fddf405
commit
c7c1506e42
@ -75,6 +75,9 @@ struct CONFIG_T {
|
|||||||
bool Mqtt_Tls;
|
bool Mqtt_Tls;
|
||||||
char Mqtt_RootCaCert[MQTT_MAX_ROOT_CA_CERT_STRLEN +1];
|
char Mqtt_RootCaCert[MQTT_MAX_ROOT_CA_CERT_STRLEN +1];
|
||||||
|
|
||||||
|
bool Vedirect_Enabled;
|
||||||
|
bool Vedirect_UpdatesOnly;
|
||||||
|
|
||||||
char Mqtt_Hostname[MQTT_MAX_HOSTNAME_STRLEN + 1];
|
char Mqtt_Hostname[MQTT_MAX_HOSTNAME_STRLEN + 1];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
27
include/MqttVedirectPublishing.h
Normal file
27
include/MqttVedirectPublishing.h
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "VeDirectFrameHandler.h"
|
||||||
|
#include "Configuration.h"
|
||||||
|
#include <Arduino.h>
|
||||||
|
#include <map>
|
||||||
|
|
||||||
|
#ifndef VICTRON_PIN_RX
|
||||||
|
#define VICTRON_PIN_RX 22
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#ifndef VICTRON_PIN_TX
|
||||||
|
#define VICTRON_PIN_TX 21
|
||||||
|
#endif
|
||||||
|
|
||||||
|
class MqttVedirectPublishingClass {
|
||||||
|
public:
|
||||||
|
void init();
|
||||||
|
void loop();
|
||||||
|
private:
|
||||||
|
std::map<String, String> _kv_map;
|
||||||
|
VeDirectFrameHandler _myve;
|
||||||
|
uint32_t _lastPublish;
|
||||||
|
};
|
||||||
|
|
||||||
|
extern MqttVedirectPublishingClass MqttVedirectPublishing;
|
||||||
@ -12,6 +12,7 @@
|
|||||||
#include "WebApi_sysstatus.h"
|
#include "WebApi_sysstatus.h"
|
||||||
#include "WebApi_webapp.h"
|
#include "WebApi_webapp.h"
|
||||||
#include "WebApi_ws_live.h"
|
#include "WebApi_ws_live.h"
|
||||||
|
#include "WebApi_vedirect.h"
|
||||||
#include <ESPAsyncWebServer.h>
|
#include <ESPAsyncWebServer.h>
|
||||||
|
|
||||||
class WebApiClass {
|
class WebApiClass {
|
||||||
@ -35,6 +36,7 @@ private:
|
|||||||
WebApiSysstatusClass _webApiSysstatus;
|
WebApiSysstatusClass _webApiSysstatus;
|
||||||
WebApiWebappClass _webApiWebapp;
|
WebApiWebappClass _webApiWebapp;
|
||||||
WebApiWsLiveClass _webApiWsLive;
|
WebApiWsLiveClass _webApiWsLive;
|
||||||
|
WebApiVedirectClass _webApiVedirect;
|
||||||
};
|
};
|
||||||
|
|
||||||
extern WebApiClass WebApi;
|
extern WebApiClass WebApi;
|
||||||
17
include/WebApi_vedirect.h
Normal file
17
include/WebApi_vedirect.h
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <ESPAsyncWebServer.h>
|
||||||
|
|
||||||
|
class WebApiVedirectClass {
|
||||||
|
public:
|
||||||
|
void init(AsyncWebServer* server);
|
||||||
|
void loop();
|
||||||
|
|
||||||
|
private:
|
||||||
|
void onVedirectStatus(AsyncWebServerRequest* request);
|
||||||
|
void onVedirectAdminGet(AsyncWebServerRequest* request);
|
||||||
|
void onVedirectAdminPost(AsyncWebServerRequest* request);
|
||||||
|
|
||||||
|
AsyncWebServer* _server;
|
||||||
|
};
|
||||||
@ -75,3 +75,6 @@
|
|||||||
#define MQTT_HASS_RETAIN true
|
#define MQTT_HASS_RETAIN true
|
||||||
#define MQTT_HASS_TOPIC "homeassistant/"
|
#define MQTT_HASS_TOPIC "homeassistant/"
|
||||||
#define MQTT_HASS_INDIVIDUALPANELS false
|
#define MQTT_HASS_INDIVIDUALPANELS false
|
||||||
|
|
||||||
|
#define VEDIRECT_ENABLED false
|
||||||
|
#define VEDIRECT_UPDATESONLY true
|
||||||
BIN
lib/.DS_Store
vendored
Normal file
BIN
lib/.DS_Store
vendored
Normal file
Binary file not shown.
BIN
lib/Hoymiles/.DS_Store
vendored
Normal file
BIN
lib/Hoymiles/.DS_Store
vendored
Normal file
Binary file not shown.
210
lib/VeDirectFrameHandler/VeDirectFrameHandler.cpp
Normal file
210
lib/VeDirectFrameHandler/VeDirectFrameHandler.cpp
Normal file
@ -0,0 +1,210 @@
|
|||||||
|
/* framehandler.cpp
|
||||||
|
*
|
||||||
|
* Arduino library to read from Victron devices using VE.Direct protocol.
|
||||||
|
* Derived from Victron framehandler reference implementation.
|
||||||
|
*
|
||||||
|
* The MIT License
|
||||||
|
*
|
||||||
|
* Copyright (c) 2019 Victron Energy BV
|
||||||
|
* Portions Copyright (C) 2020 Chris Terwilliger
|
||||||
|
* https://github.com/cterwilliger/VeDirectFrameHandler
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
* of this software and associated documentation files (the "Software"), to deal
|
||||||
|
* in the Software without restriction, including without limitation the rights
|
||||||
|
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
* copies of the Software, and to permit persons to whom the Software is
|
||||||
|
* furnished to do so, subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in
|
||||||
|
* all copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||||
|
* THE SOFTWARE.
|
||||||
|
*
|
||||||
|
* 2020.05.05 - 0.2 - initial release
|
||||||
|
* 2020.06.21 - 0.2 - add MIT license, no code changes
|
||||||
|
* 2020.08.20 - 0.3 - corrected #include reference
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include <Arduino.h>
|
||||||
|
#include "VeDirectFrameHandler.h"
|
||||||
|
|
||||||
|
char MODULE[] = "VE.Frame"; // Victron seems to use this to find out where logging messages were generated
|
||||||
|
|
||||||
|
// The name of the record that contains the checksum.
|
||||||
|
static constexpr char checksumTagName[] = "CHECKSUM";
|
||||||
|
|
||||||
|
VeDirectFrameHandler::VeDirectFrameHandler() :
|
||||||
|
//mStop(false), // don't know what Victron uses this for, not using
|
||||||
|
veName(),
|
||||||
|
veValue(),
|
||||||
|
frameIndex(0),
|
||||||
|
veEnd(0),
|
||||||
|
mState(IDLE),
|
||||||
|
mChecksum(0),
|
||||||
|
mTextPointer(0),
|
||||||
|
tempName(),
|
||||||
|
tempValue()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* rxData
|
||||||
|
* This function is called by the application which passes a byte of serial data
|
||||||
|
* It is unchanged from Victron's example code
|
||||||
|
*/
|
||||||
|
void VeDirectFrameHandler::rxData(uint8_t inbyte)
|
||||||
|
{
|
||||||
|
//if (mStop) return;
|
||||||
|
if ( (inbyte == ':') && (mState != CHECKSUM) ) {
|
||||||
|
mState = RECORD_HEX;
|
||||||
|
}
|
||||||
|
if (mState != RECORD_HEX) {
|
||||||
|
mChecksum += inbyte;
|
||||||
|
}
|
||||||
|
inbyte = toupper(inbyte);
|
||||||
|
|
||||||
|
switch(mState) {
|
||||||
|
case IDLE:
|
||||||
|
/* wait for \n of the start of an record */
|
||||||
|
switch(inbyte) {
|
||||||
|
case '\n':
|
||||||
|
mState = RECORD_BEGIN;
|
||||||
|
break;
|
||||||
|
case '\r': /* Skip */
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case RECORD_BEGIN:
|
||||||
|
mTextPointer = mName;
|
||||||
|
*mTextPointer++ = inbyte;
|
||||||
|
mState = RECORD_NAME;
|
||||||
|
break;
|
||||||
|
case RECORD_NAME:
|
||||||
|
// The record name is being received, terminated by a \t
|
||||||
|
switch(inbyte) {
|
||||||
|
case '\t':
|
||||||
|
// the Checksum record indicates a EOR
|
||||||
|
if ( mTextPointer < (mName + sizeof(mName)) ) {
|
||||||
|
*mTextPointer = 0; /* Zero terminate */
|
||||||
|
if (strcmp(mName, checksumTagName) == 0) {
|
||||||
|
mState = CHECKSUM;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
mTextPointer = mValue; /* Reset value pointer */
|
||||||
|
mState = RECORD_VALUE;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
// add byte to name, but do no overflow
|
||||||
|
if ( mTextPointer < (mName + sizeof(mName)) )
|
||||||
|
*mTextPointer++ = inbyte;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case RECORD_VALUE:
|
||||||
|
// The record value is being received. The \r indicates a new record.
|
||||||
|
switch(inbyte) {
|
||||||
|
case '\n':
|
||||||
|
// forward record, only if it could be stored completely
|
||||||
|
if ( mTextPointer < (mValue + sizeof(mValue)) ) {
|
||||||
|
*mTextPointer = 0; // make zero ended
|
||||||
|
textRxEvent(mName, mValue);
|
||||||
|
}
|
||||||
|
mState = RECORD_BEGIN;
|
||||||
|
break;
|
||||||
|
case '\r': /* Skip */
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
// add byte to value, but do no overflow
|
||||||
|
if ( mTextPointer < (mValue + sizeof(mValue)) )
|
||||||
|
*mTextPointer++ = inbyte;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case CHECKSUM:
|
||||||
|
{
|
||||||
|
bool valid = mChecksum == 0;
|
||||||
|
if (!valid)
|
||||||
|
logE(MODULE,"[CHECKSUM] Invalid frame");
|
||||||
|
mChecksum = 0;
|
||||||
|
mState = IDLE;
|
||||||
|
frameEndEvent(valid);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case RECORD_HEX:
|
||||||
|
if (hexRxEvent(inbyte)) {
|
||||||
|
mChecksum = 0;
|
||||||
|
mState = IDLE;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* textRxEvent
|
||||||
|
* This function is called every time a new name/value is successfully parsed. It writes the values to the temporary buffer.
|
||||||
|
*/
|
||||||
|
void VeDirectFrameHandler::textRxEvent(char * mName, char * mValue) {
|
||||||
|
strcpy(tempName[frameIndex], mName); // copy name to temporary buffer
|
||||||
|
strcpy(tempValue[frameIndex], mValue); // copy value to temporary buffer
|
||||||
|
frameIndex++;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* frameEndEvent
|
||||||
|
* This function is called at the end of the received frame. If the checksum is valid, the temp buffer is read line by line.
|
||||||
|
* If the name exists in the public buffer, the new value is copied to the public buffer. If not, a new name/value entry
|
||||||
|
* is created in the public buffer.
|
||||||
|
*/
|
||||||
|
void VeDirectFrameHandler::frameEndEvent(bool valid) {
|
||||||
|
if ( valid ) {
|
||||||
|
for ( int i = 0; i < frameIndex; i++ ) { // read each name already in the temp buffer
|
||||||
|
bool nameExists = false;
|
||||||
|
for ( int j = 0; j <= veEnd; j++ ) { // compare to existing names in the public buffer
|
||||||
|
if ( strcmp(tempName[i], veName[j]) == 0 ) {
|
||||||
|
strcpy(veValue[j], tempValue[i]); // overwrite tempValue in the public buffer
|
||||||
|
nameExists = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ( !nameExists ) {
|
||||||
|
strcpy(veName[veEnd], tempName[i]); // write new Name to public buffer
|
||||||
|
strcpy(veValue[veEnd], tempValue[i]); // write new Value to public buffer
|
||||||
|
veEnd++; // increment end of public buffer
|
||||||
|
if ( veEnd >= buffLen ) { // stop any buffer overrun
|
||||||
|
veEnd = buffLen - 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
frameIndex = 0; // reset frame
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* logE
|
||||||
|
* This function included for continuity and possible future use.
|
||||||
|
*/
|
||||||
|
void VeDirectFrameHandler::logE(const char * module, const char * error) {
|
||||||
|
Serial.print("MODULE: ");
|
||||||
|
Serial.println(module);
|
||||||
|
Serial.print("ERROR: ");
|
||||||
|
Serial.println(error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* hexRxEvent
|
||||||
|
* This function included for continuity and possible future use.
|
||||||
|
*/
|
||||||
|
bool VeDirectFrameHandler::hexRxEvent(uint8_t inbyte) {
|
||||||
|
return true; // stubbed out for future
|
||||||
|
}
|
||||||
63
lib/VeDirectFrameHandler/VeDirectFrameHandler.h
Normal file
63
lib/VeDirectFrameHandler/VeDirectFrameHandler.h
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
/* frameHandler.h
|
||||||
|
*
|
||||||
|
* Arduino library to read from Victron devices using VE.Direct protocol.
|
||||||
|
* Derived from Victron framehandler reference implementation.
|
||||||
|
*
|
||||||
|
* 2020.05.05 - 0.2 - initial release
|
||||||
|
* 2021.02.23 - 0.3 - change frameLen to 22 per VE.Direct Protocol version 3.30
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
#ifndef FRAMEHANDLER_H_
|
||||||
|
#define FRAMEHANDLER_H_
|
||||||
|
|
||||||
|
#include <Arduino.h>
|
||||||
|
|
||||||
|
const byte frameLen = 22; // VE.Direct Protocol: max frame size is 18
|
||||||
|
const byte nameLen = 9; // VE.Direct Protocol: max name size is 9 including /0
|
||||||
|
const byte valueLen = 33; // VE.Direct Protocol: max value size is 33 including /0
|
||||||
|
const byte buffLen = 40; // Maximum number of lines possible from the device. Current protocol shows this to be the BMV700 at 33 lines.
|
||||||
|
|
||||||
|
|
||||||
|
class VeDirectFrameHandler {
|
||||||
|
|
||||||
|
public:
|
||||||
|
VeDirectFrameHandler();
|
||||||
|
void rxData(uint8_t inbyte); // byte of serial data to be passed by the application
|
||||||
|
|
||||||
|
char veName[buffLen][nameLen] = { }; // public buffer for received names
|
||||||
|
char veValue[buffLen][valueLen] = { }; // public buffer for received values
|
||||||
|
|
||||||
|
int frameIndex; // which line of the frame are we on
|
||||||
|
int veEnd; // current size (end) of the public buffer
|
||||||
|
|
||||||
|
private:
|
||||||
|
//bool mStop; // not sure what Victron uses this for, not using
|
||||||
|
|
||||||
|
enum States { // state machine
|
||||||
|
IDLE,
|
||||||
|
RECORD_BEGIN,
|
||||||
|
RECORD_NAME,
|
||||||
|
RECORD_VALUE,
|
||||||
|
CHECKSUM,
|
||||||
|
RECORD_HEX
|
||||||
|
};
|
||||||
|
|
||||||
|
int mState; // current state
|
||||||
|
|
||||||
|
uint8_t mChecksum; // checksum value
|
||||||
|
|
||||||
|
char * mTextPointer; // pointer to the private buffer we're writing to, name or value
|
||||||
|
|
||||||
|
char mName[9]; // buffer for the field name
|
||||||
|
char mValue[33]; // buffer for the field value
|
||||||
|
char tempName[frameLen][nameLen]; // private buffer for received names
|
||||||
|
char tempValue[frameLen][valueLen]; // private buffer for received values
|
||||||
|
|
||||||
|
void textRxEvent(char *, char *);
|
||||||
|
void frameEndEvent(bool);
|
||||||
|
void logE(const char *, const char *);
|
||||||
|
bool hexRxEvent(uint8_t);
|
||||||
|
};
|
||||||
|
|
||||||
|
#endif // FRAMEHANDLER_H_
|
||||||
@ -34,13 +34,11 @@ monitor_filters = time, colorize, log2file, esp32_exception_decoder
|
|||||||
monitor_speed = 115200
|
monitor_speed = 115200
|
||||||
upload_protocol = esptool
|
upload_protocol = esptool
|
||||||
|
|
||||||
|
|
||||||
[env:generic]
|
[env:generic]
|
||||||
board = esp32dev
|
board = esp32dev
|
||||||
monitor_port = COM4
|
monitor_port = COM4
|
||||||
upload_port = COM4
|
upload_port = COM4
|
||||||
|
|
||||||
|
|
||||||
[env:olimex_esp32_poe]
|
[env:olimex_esp32_poe]
|
||||||
; https://www.olimex.com/Products/IoT/ESP32/ESP32-POE/open-source-hardware
|
; https://www.olimex.com/Products/IoT/ESP32/ESP32-POE/open-source-hardware
|
||||||
|
|
||||||
@ -57,9 +55,7 @@ build_flags = ${env.build_flags}
|
|||||||
monitor_port = COM3
|
monitor_port = COM3
|
||||||
upload_port = COM3
|
upload_port = COM3
|
||||||
|
|
||||||
|
|
||||||
[env:d1 mini esp32]
|
[env:d1 mini esp32]
|
||||||
board = wemos_d1_mini32
|
|
||||||
build_flags =
|
build_flags =
|
||||||
${env.build_flags}
|
${env.build_flags}
|
||||||
-DHOYMILES_PIN_MISO=19
|
-DHOYMILES_PIN_MISO=19
|
||||||
@ -68,6 +64,8 @@ build_flags =
|
|||||||
-DHOYMILES_PIN_IRQ=16
|
-DHOYMILES_PIN_IRQ=16
|
||||||
-DHOYMILES_PIN_CE=17
|
-DHOYMILES_PIN_CE=17
|
||||||
-DHOYMILES_PIN_CS=5
|
-DHOYMILES_PIN_CS=5
|
||||||
|
-DVICTRON_PIN_TX=21
|
||||||
|
-DVICTRON_PIN_RX=22
|
||||||
|
board = wemos_d1_mini32
|
||||||
monitor_port = /dev/cu.usbserial-01E68DD0
|
monitor_port = /dev/cu.usbserial-01E68DD0
|
||||||
upload_port = /dev/cu.usbserial-01E68DD0
|
upload_port = /dev/cu.usbserial-01E68DD0
|
||||||
|
|||||||
@ -56,6 +56,9 @@ void ConfigurationClass::init()
|
|||||||
config.Mqtt_Hass_Retain = MQTT_HASS_RETAIN;
|
config.Mqtt_Hass_Retain = MQTT_HASS_RETAIN;
|
||||||
strlcpy(config.Mqtt_Hass_Topic, MQTT_TOPIC, sizeof(config.Mqtt_Hass_Topic));
|
strlcpy(config.Mqtt_Hass_Topic, MQTT_TOPIC, sizeof(config.Mqtt_Hass_Topic));
|
||||||
config.Mqtt_Hass_IndividualPanels = MQTT_HASS_INDIVIDUALPANELS;
|
config.Mqtt_Hass_IndividualPanels = MQTT_HASS_INDIVIDUALPANELS;
|
||||||
|
|
||||||
|
config.Vedirect_Enabled = VEDIRECT_ENABLED;
|
||||||
|
config.Vedirect_UpdatesOnly = VEDIRECT_UPDATESONLY;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool ConfigurationClass::write()
|
bool ConfigurationClass::write()
|
||||||
@ -145,6 +148,8 @@ void ConfigurationClass::migrate()
|
|||||||
if (config.Cfg_Version < 0x00011300) {
|
if (config.Cfg_Version < 0x00011300) {
|
||||||
config.Mqtt_Tls = MQTT_TLS;
|
config.Mqtt_Tls = MQTT_TLS;
|
||||||
strlcpy(config.Mqtt_RootCaCert, MQTT_ROOT_CA_CERT, sizeof(config.Mqtt_RootCaCert));
|
strlcpy(config.Mqtt_RootCaCert, MQTT_ROOT_CA_CERT, sizeof(config.Mqtt_RootCaCert));
|
||||||
|
config.Vedirect_Enabled = VEDIRECT_ENABLED;
|
||||||
|
config.Vedirect_UpdatesOnly = VEDIRECT_UPDATESONLY;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (config.Cfg_Version < 0x00011400) {
|
if (config.Cfg_Version < 0x00011400) {
|
||||||
|
|||||||
74
src/MqttVedirectPublishing.cpp
Normal file
74
src/MqttVedirectPublishing.cpp
Normal file
@ -0,0 +1,74 @@
|
|||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
/*
|
||||||
|
* Copyright (C) 2022 Helge Erbe and others
|
||||||
|
*/
|
||||||
|
#include "MqttVedirectPublishing.h"
|
||||||
|
#include "VeDirectFrameHandler.h"
|
||||||
|
#include "MqttSettings.h"
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
MqttVedirectPublishingClass MqttVedirectPublishing;
|
||||||
|
|
||||||
|
void MqttVedirectPublishingClass::init()
|
||||||
|
{
|
||||||
|
Serial2.begin(19200, SERIAL_8N1, VICTRON_PIN_RX, VICTRON_PIN_TX);
|
||||||
|
Serial2.flush();
|
||||||
|
}
|
||||||
|
|
||||||
|
void MqttVedirectPublishingClass::loop()
|
||||||
|
{
|
||||||
|
CONFIG_T& config = Configuration.get();
|
||||||
|
|
||||||
|
if (!MqttSettings.getConnected() && !config.Vedirect_Enabled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (millis() - _lastPublish > (config.Mqtt_PublishInterval * 1000)) {
|
||||||
|
String key;
|
||||||
|
String value;
|
||||||
|
bool bChanged;
|
||||||
|
|
||||||
|
while ( Serial2.available() ) {
|
||||||
|
_myve.rxData(Serial2.read());
|
||||||
|
}
|
||||||
|
yield();
|
||||||
|
|
||||||
|
String topic = "";
|
||||||
|
for ( int i = 0; i < _myve.veEnd; i++ ) {
|
||||||
|
key = _myve.veName[i];
|
||||||
|
value = _myve.veValue[i];
|
||||||
|
|
||||||
|
// just for debug
|
||||||
|
Serial.print(key.c_str());
|
||||||
|
Serial.print("= ");
|
||||||
|
Serial.println(value.c_str());
|
||||||
|
|
||||||
|
// Add new key, value pairs to map and update changed values.
|
||||||
|
// Mark changed values
|
||||||
|
auto a = _kv_map.find(key);
|
||||||
|
if (a != _kv_map.end()) {
|
||||||
|
if (_kv_map[key] == value) {
|
||||||
|
bChanged = false;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
_kv_map[key] = value;
|
||||||
|
bChanged = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
_kv_map.insert(std::make_pair(key, value));
|
||||||
|
bChanged = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// publish only changed key, values pairs
|
||||||
|
if (!config.Vedirect_UpdatesOnly || (bChanged && config.Vedirect_UpdatesOnly)) {
|
||||||
|
topic = "victron/";
|
||||||
|
topic.concat(key);
|
||||||
|
topic.replace("#",""); // # is a no go in mqtt topic
|
||||||
|
MqttSettings.publish(topic.c_str(), value.c_str());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_lastPublish = millis();
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -30,6 +30,7 @@ void WebApiClass::init()
|
|||||||
_webApiSysstatus.init(&_server);
|
_webApiSysstatus.init(&_server);
|
||||||
_webApiWebapp.init(&_server);
|
_webApiWebapp.init(&_server);
|
||||||
_webApiWsLive.init(&_server);
|
_webApiWsLive.init(&_server);
|
||||||
|
_webApiVedirect.init(&_server);
|
||||||
|
|
||||||
_server.begin();
|
_server.begin();
|
||||||
}
|
}
|
||||||
@ -47,6 +48,7 @@ void WebApiClass::loop()
|
|||||||
_webApiSysstatus.loop();
|
_webApiSysstatus.loop();
|
||||||
_webApiWebapp.loop();
|
_webApiWebapp.loop();
|
||||||
_webApiWsLive.loop();
|
_webApiWsLive.loop();
|
||||||
|
_webApiVedirect.loop();
|
||||||
}
|
}
|
||||||
|
|
||||||
WebApiClass WebApi;
|
WebApiClass WebApi;
|
||||||
103
src/WebApi_vedirect.cpp
Normal file
103
src/WebApi_vedirect.cpp
Normal file
@ -0,0 +1,103 @@
|
|||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
/*
|
||||||
|
* Copyright (C) 2022 Thomas Basler and others
|
||||||
|
*/
|
||||||
|
#include "WebApi_vedirect.h"
|
||||||
|
#include "ArduinoJson.h"
|
||||||
|
#include "AsyncJson.h"
|
||||||
|
#include "Configuration.h"
|
||||||
|
#include "helper.h"
|
||||||
|
|
||||||
|
void WebApiVedirectClass::init(AsyncWebServer* server)
|
||||||
|
{
|
||||||
|
using namespace std::placeholders;
|
||||||
|
|
||||||
|
_server = server;
|
||||||
|
|
||||||
|
_server->on("/api/vedirect/status", HTTP_GET, std::bind(&WebApiVedirectClass::onVedirectStatus, this, _1));
|
||||||
|
_server->on("/api/vedirect/config", HTTP_GET, std::bind(&WebApiVedirectClass::onVedirectAdminGet, this, _1));
|
||||||
|
_server->on("/api/vedirect/config", HTTP_POST, std::bind(&WebApiVedirectClass::onVedirectAdminPost, this, _1));
|
||||||
|
}
|
||||||
|
|
||||||
|
void WebApiVedirectClass::loop()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
void WebApiVedirectClass::onVedirectStatus(AsyncWebServerRequest* request)
|
||||||
|
{
|
||||||
|
AsyncJsonResponse* response = new AsyncJsonResponse();
|
||||||
|
JsonObject root = response->getRoot();
|
||||||
|
CONFIG_T& config = Configuration.get();
|
||||||
|
|
||||||
|
root[F("vedirect_enabled")] = config.Vedirect_Enabled;
|
||||||
|
root[F("vedirect_updatesonly")] = config.Vedirect_UpdatesOnly;
|
||||||
|
|
||||||
|
response->setLength();
|
||||||
|
request->send(response);
|
||||||
|
}
|
||||||
|
|
||||||
|
void WebApiVedirectClass::onVedirectAdminGet(AsyncWebServerRequest* request)
|
||||||
|
{
|
||||||
|
AsyncJsonResponse* response = new AsyncJsonResponse();
|
||||||
|
JsonObject root = response->getRoot();
|
||||||
|
CONFIG_T& config = Configuration.get();
|
||||||
|
|
||||||
|
root[F("vedirect_enabled")] = config.Vedirect_Enabled;
|
||||||
|
root[F("vedirect_updatesonly")] = config.Vedirect_UpdatesOnly;
|
||||||
|
|
||||||
|
response->setLength();
|
||||||
|
request->send(response);
|
||||||
|
}
|
||||||
|
|
||||||
|
void WebApiVedirectClass::onVedirectAdminPost(AsyncWebServerRequest* request)
|
||||||
|
{
|
||||||
|
AsyncJsonResponse* response = new AsyncJsonResponse();
|
||||||
|
JsonObject retMsg = response->getRoot();
|
||||||
|
retMsg[F("type")] = F("warning");
|
||||||
|
|
||||||
|
if (!request->hasParam("data", true)) {
|
||||||
|
retMsg[F("message")] = F("No values found!");
|
||||||
|
response->setLength();
|
||||||
|
request->send(response);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
String json = request->getParam("data", true)->value();
|
||||||
|
|
||||||
|
if (json.length() > 1024) {
|
||||||
|
retMsg[F("message")] = F("Data too large!");
|
||||||
|
response->setLength();
|
||||||
|
request->send(response);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
DynamicJsonDocument root(1024);
|
||||||
|
DeserializationError error = deserializeJson(root, json);
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
retMsg[F("message")] = F("Failed to parse data!");
|
||||||
|
response->setLength();
|
||||||
|
request->send(response);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!(root.containsKey("vedirect_enabled"))) {
|
||||||
|
retMsg[F("message")] = F("Values are missing!");
|
||||||
|
response->setLength();
|
||||||
|
request->send(response);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
CONFIG_T& config = Configuration.get();
|
||||||
|
config.Vedirect_Enabled = root[F("vedirect_enabled")].as<bool>();
|
||||||
|
config.Vedirect_UpdatesOnly = root[F("vedirect_updatesonly")].as<bool>();
|
||||||
|
Configuration.write();
|
||||||
|
|
||||||
|
retMsg[F("type")] = F("success");
|
||||||
|
retMsg[F("message")] = F("Settings saved!");
|
||||||
|
|
||||||
|
response->setLength();
|
||||||
|
request->send(response);
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
@ -6,6 +6,7 @@
|
|||||||
#include "Hoymiles.h"
|
#include "Hoymiles.h"
|
||||||
#include "MqttHassPublishing.h"
|
#include "MqttHassPublishing.h"
|
||||||
#include "MqttPublishing.h"
|
#include "MqttPublishing.h"
|
||||||
|
#include "MqttVedirectPublishing.h"
|
||||||
#include "MqttSettings.h"
|
#include "MqttSettings.h"
|
||||||
#include "NtpSettings.h"
|
#include "NtpSettings.h"
|
||||||
#include "WebApi.h"
|
#include "WebApi.h"
|
||||||
@ -68,6 +69,7 @@ void setup()
|
|||||||
Serial.print(F("Initialize MqTT... "));
|
Serial.print(F("Initialize MqTT... "));
|
||||||
MqttSettings.init();
|
MqttSettings.init();
|
||||||
MqttPublishing.init();
|
MqttPublishing.init();
|
||||||
|
MqttVedirectPublishing.init();
|
||||||
MqttHassPublishing.init();
|
MqttHassPublishing.init();
|
||||||
Serial.println(F("done"));
|
Serial.println(F("done"));
|
||||||
|
|
||||||
@ -106,6 +108,8 @@ void loop()
|
|||||||
yield();
|
yield();
|
||||||
MqttPublishing.loop();
|
MqttPublishing.loop();
|
||||||
yield();
|
yield();
|
||||||
|
MqttVedirectPublishing.loop();
|
||||||
|
yield();
|
||||||
MqttHassPublishing.loop();
|
MqttHassPublishing.loop();
|
||||||
yield();
|
yield();
|
||||||
WebApi.loop();
|
WebApi.loop();
|
||||||
|
|||||||
@ -32,6 +32,9 @@
|
|||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<router-link @click="onClick" class="dropdown-item" to="/settings/dtu">DTU Settings</router-link>
|
<router-link @click="onClick" class="dropdown-item" to="/settings/dtu">DTU Settings</router-link>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<router-link @click="onClick" class="dropdown-item" to="/settings/vedirect">Ve.direct Settings</router-link>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<hr class="dropdown-divider" />
|
<hr class="dropdown-divider" />
|
||||||
@ -59,6 +62,9 @@
|
|||||||
<li>
|
<li>
|
||||||
<router-link @click="onClick" class="dropdown-item" to="/info/mqtt">MqTT</router-link>
|
<router-link @click="onClick" class="dropdown-item" to="/info/mqtt">MqTT</router-link>
|
||||||
</li>
|
</li>
|
||||||
|
<li>
|
||||||
|
<router-link @click="onClick" class="dropdown-item" to="/info/vedirect">Ve.direct</router-link>
|
||||||
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</li>
|
</li>
|
||||||
<li class="nav-item">
|
<li class="nav-item">
|
||||||
|
|||||||
107
webapp/src/components/VedirectAdminView.vue
Normal file
107
webapp/src/components/VedirectAdminView.vue
Normal file
@ -0,0 +1,107 @@
|
|||||||
|
<template>
|
||||||
|
<div class="container-xxl" role="main">
|
||||||
|
<div class="page-header">
|
||||||
|
<h1>Ve.direct Settings</h1>
|
||||||
|
</div>
|
||||||
|
<BootstrapAlert v-model="showAlert" dismissible :variant="alertType">
|
||||||
|
{{ alertMessage }}
|
||||||
|
</BootstrapAlert>
|
||||||
|
|
||||||
|
<div class="text-center" v-if="dataLoading">
|
||||||
|
<div class="spinner-border" role="status">
|
||||||
|
<span class="visually-hidden">Loading...</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<template v-if="!dataLoading">
|
||||||
|
<form @submit="saveVedirectConfig">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header text-white bg-primary">Ve.direct Configuration</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="row mb-3">
|
||||||
|
<label class="col-sm-4 form-check-label" for="inputVedirect">Enable Ve.direct</label>
|
||||||
|
<div class="col-sm-8">
|
||||||
|
<div class="form-check form-switch">
|
||||||
|
<input class="form-check-input" type="checkbox" id="inputVedirect"
|
||||||
|
v-model="vedirectConfigList.vedirect_enabled" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row mb-3" v-show="vedirectConfigList.vedirect_enabled">
|
||||||
|
<label class="col-sm-2 form-check-label" for="inputTls">Send only updates</label>
|
||||||
|
<div class="col-sm-10">
|
||||||
|
<div class="form-check form-switch">
|
||||||
|
<input class="form-check-input" type="checkbox" id="inputTls"
|
||||||
|
v-model="vedirectConfigList.vedirect_updatesonly" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button type="submit" class="btn btn-primary mb-3">Save</button>
|
||||||
|
</form>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { defineComponent } from 'vue';
|
||||||
|
import BootstrapAlert from "@/components/partials/BootstrapAlert.vue";
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
components: {
|
||||||
|
BootstrapAlert,
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
dataLoading: true,
|
||||||
|
vedirectConfigList: {
|
||||||
|
vedirect_enabled: false,
|
||||||
|
vedirect_updatesonly: true,
|
||||||
|
},
|
||||||
|
alertMessage: "",
|
||||||
|
alertType: "info",
|
||||||
|
showAlert: false,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
created() {
|
||||||
|
this.getVedirectConfig();
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
getVedirectConfig() {
|
||||||
|
this.dataLoading = true;
|
||||||
|
fetch("/api/vedirect/config")
|
||||||
|
.then((response) => response.json())
|
||||||
|
.then((data) => {
|
||||||
|
this.vedirectConfigList = data;
|
||||||
|
this.dataLoading = false;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
saveVedirectConfig(e: Event) {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append("data", JSON.stringify(this.vedirectConfigList));
|
||||||
|
|
||||||
|
fetch("/api/vedirect/config", {
|
||||||
|
method: "POST",
|
||||||
|
body: formData,
|
||||||
|
})
|
||||||
|
.then(function (response) {
|
||||||
|
if (response.status != 200) {
|
||||||
|
throw response.status;
|
||||||
|
} else {
|
||||||
|
return response.json();
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.then(
|
||||||
|
(response) => {
|
||||||
|
this.alertMessage = response.message;
|
||||||
|
this.alertType = response.type;
|
||||||
|
this.showAlert = true;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
||||||
77
webapp/src/components/VedirectInfoView.vue
Normal file
77
webapp/src/components/VedirectInfoView.vue
Normal file
@ -0,0 +1,77 @@
|
|||||||
|
<template>
|
||||||
|
<div class="container-xxl" role="main">
|
||||||
|
<div class="page-header">
|
||||||
|
<h1>Ve.direct Info</h1>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="text-center" v-if="dataLoading">
|
||||||
|
<div class="spinner-border" role="status">
|
||||||
|
<span class="visually-hidden">Loading...</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<template v-if="!dataLoading">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header text-white bg-primary">Configuration Summary</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table class="table table-hover table-condensed">
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<th>Status</th>
|
||||||
|
<td class="badge" :class="{
|
||||||
|
'bg-danger': !vedirectDataList.vedirect_enabled,
|
||||||
|
'bg-success': vedirectDataList.vedirect_enabled,
|
||||||
|
}">
|
||||||
|
<span v-if="vedirectDataList.vedirect_enabled">enabled</span>
|
||||||
|
<span v-else>disabled</span>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr v-show="vedirectDataList.vedirect_enabled">
|
||||||
|
<th>Updates Only</th>
|
||||||
|
<td class="badge" :class="{
|
||||||
|
'bg-danger': !vedirectDataList.vedirect_updatesonly,
|
||||||
|
'bg-success': vedirectDataList.vedirect_updatesonly,
|
||||||
|
}">
|
||||||
|
<span v-if="vedirectDataList.vedirect_updatesonly">enabled</span>
|
||||||
|
<span v-else>disabled</span>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { defineComponent } from 'vue';
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
dataLoading: true,
|
||||||
|
vedirectDataList: {
|
||||||
|
vedirect_enabled: false,
|
||||||
|
vedirect_updatesonly: true,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
created() {
|
||||||
|
this.getVedirectInfo();
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
getVedirectInfo() {
|
||||||
|
this.dataLoading = true;
|
||||||
|
fetch("/api/vedirect/status")
|
||||||
|
.then((response) => response.json())
|
||||||
|
.then((data) => {
|
||||||
|
this.vedirectDataList = data;
|
||||||
|
this.dataLoading = false;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
||||||
@ -11,6 +11,8 @@ import MqttInfoView from '@/components/MqttInfoView.vue'
|
|||||||
import InverterAdminView from '@/components/InverterAdminView.vue'
|
import InverterAdminView from '@/components/InverterAdminView.vue'
|
||||||
import DtuAdminView from '@/components/DtuAdminView.vue'
|
import DtuAdminView from '@/components/DtuAdminView.vue'
|
||||||
import FirmwareUpgradeView from '@/components/FirmwareUpgradeView.vue'
|
import FirmwareUpgradeView from '@/components/FirmwareUpgradeView.vue'
|
||||||
|
import VedirectAdminView from '@/components/VedirectAdminView.vue'
|
||||||
|
import VedirectInfoView from '@/components/VedirectInfoView.vue'
|
||||||
|
|
||||||
const routes: Array<RouteRecordRaw> = [
|
const routes: Array<RouteRecordRaw> = [
|
||||||
{
|
{
|
||||||
@ -43,6 +45,11 @@ const routes: Array<RouteRecordRaw> = [
|
|||||||
name: 'MqTT',
|
name: 'MqTT',
|
||||||
component: MqttInfoView
|
component: MqttInfoView
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: '/info/vedirect',
|
||||||
|
name: 'Ve.direct',
|
||||||
|
component: VedirectInfoView
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: '/settings/network',
|
path: '/settings/network',
|
||||||
name: 'Network Settings',
|
name: 'Network Settings',
|
||||||
@ -53,6 +60,11 @@ const routes: Array<RouteRecordRaw> = [
|
|||||||
name: 'NTP Settings',
|
name: 'NTP Settings',
|
||||||
component: NtpAdminView
|
component: NtpAdminView
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: '/settings/vedirect',
|
||||||
|
name: 'Ve.direct Settings',
|
||||||
|
component: VedirectAdminView
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: '/settings/mqtt',
|
path: '/settings/mqtt',
|
||||||
name: 'MqTT Settings',
|
name: 'MqTT Settings',
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user