Add AmperageBudgetManager, InfluxDB, ModbusMirror; expose version

This commit is contained in:
babayaga 2026-01-18 11:46:39 +01:00
parent 2d9815b199
commit 02faa08df1
7 changed files with 1865 additions and 1 deletions

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,202 @@
#ifndef AMPERAGE_BUDGET_MANAGER_H
#define AMPERAGE_BUDGET_MANAGER_H
#include <Component.h>
#include <ArduinoLog.h>
#include <vector>
#include <algorithm>
#include <enums.h>
#include "components/OmronE5.h"
#include "modbus/NetworkComponent.h"
#include "NetworkValue.h"
#include <ArduinoJson.h>
#include <LittleFS.h>
#include "config-modbus.h"
#include "config.h"
// Forward declaration
class OmronE5;
// Default values
#define MIN_HEATING_DURATION_S 3 // 3 seconds
#define MAX_HEATING_DURATION_S 5 // 5 seconds
#define DEFAULT_MAX_SIMULTANEOUS_HEATING 2 // Default number of devices that can heat simultaneously
#define DEFAULT_WINDOW_OFFSET 1 // Default window offset
#define DEFAULT_START_INDEX 0 // Default start index for cycling
#define DEFAULT_END_INDEX (MAX_MANAGED_DEVICES - 1) // Default end index for cycling
#define DEFAULT_MAX_HEATING_OSCILLATING_S 15 // 60 seconds
#define STOP_ALL_DEVICES_WAIT_MS 50 // Wait time after stopping all devices
// Modbus write boundaries
#define MB_MAX_TIME_MIN_S 1 // Minimum max time: 1s
#define MB_MAX_TIME_MAX_S (120 * 60) // Maximum max time: 2 hours
#define MB_MIN_TIME_MIN_S 1 // Minimum min time: 1s
#define MB_MIN_TIME_MAX_S 60 // Maximum min time: 60s
#define AMP_BUDGET_MB_COUNT 12 // m_enabled + 11 custom values
enum E_AMPERAGE_MODE
{
E_AM_CYCLE_ALL, // Cycle through all devices
E_AM_CYCLE_SP, // Cycle through partitions (REG_OFFSET_MAX_SIM), advance when SP - DEADBAND is reached
E_AM_CYCLE_SP_ANY, // Heat any N devices that require it
E_AM_CYCLE_SP_MOST_URGENT // Heat most urgent N devices, within window
};
enum E_OP_FLAGS : uint16_t
{
E_SQ_NONE = 0,
E_SQ_RUNNING_PROFILES_ONLY = 1 << 0,
E_SQ_VERBOSE = 1 << 1,
E_SQ_USER = 1 << 2,
};
class AmperageBudgetManager : public NetworkComponent<AMP_BUDGET_MB_COUNT>
{
public:
typedef bool (*CanUsePIDCallback)(Component *owner, OmronE5 *device);
typedef void (*OnWarmupCompleteCallback)(Component *owner);
enum E_MB_OFFSETS
{
REG_OFFSET_INFO = E_NVC_USER,
REG_OFFSET_MIN_TIME,
REG_OFFSET_MAX_TIME,
REG_OFFSET_MAX_SIM,
REG_OFFSET_OFFSET,
REG_OFFSET_START_INDEX,
REG_OFFSET_END_INDEX,
REG_OFFSET_MODE,
REG_OFFSET_MAX_TIME_OSCILLATING,
REG_OFFSET_POST_HEATUP_MODE,
REG_OFFSET_OP_FLAGS
};
AmperageBudgetManager(Component *owner, uint16_t baseAddress = MB_ADDR_AMPERAGE_BUDGET_BASE);
virtual ~AmperageBudgetManager() = default;
void setCanUseCallback(CanUsePIDCallback cb)
{
_canUseCallback = cb;
}
void setOnWarmupCompleteCallback(OnWarmupCompleteCallback cb)
{
_onWarmupCompleteCallback = cb;
}
bool addManagedDevice(OmronE5 *device);
virtual short setup() override;
virtual short loop() override;
virtual short info(short val0, short val1) override;
virtual short reset() override;
virtual void onCycleStart(const std::vector<OmronE5 *> &activeDevices);
virtual void onCycleEnd(const std::vector<OmronE5 *> &activeDevices);
virtual void onHeatupComplete();
// Max simultaneous heating control
uint8_t getMaxSimultaneousHeating() const { return m_maxSimultaneousHeating.getValue(); }
void setMaxSimultaneousHeating(uint8_t value)
{
if (value > 0 && value <= MAX_MANAGED_DEVICES)
{
m_maxSimultaneousHeating.update(value);
}
}
// Modbus interface
virtual short mb_tcp_write(MB_Registers *reg, short value) override;
virtual short mb_tcp_read(MB_Registers *reg) override;
virtual short serial_register(Bridge *bridge) override;
// Persistence methods
void toJson(JsonDocument &doc) const;
bool fromJson(const JsonObject &json);
bool load(const char *path);
bool save(const char *path) const;
void print() const;
protected:
virtual void notifyStateChange() override; // Override to handle device stopping
private:
CanUsePIDCallback _canUseCallback;
OnWarmupCompleteCallback _onWarmupCompleteCallback;
static const uint8_t MAX_MANAGED_DEVICES = NUM_OMRON_DEVICES;
OmronE5 *_devices[MAX_MANAGED_DEVICES];
uint8_t _numDevices;
uint8_t _currentIndex; // Current device index in the round-robin
// Configurable parameters
NetworkValue<uint16_t> m_minHeatingDurationS;
NetworkValue<uint16_t> m_maxHeatingDurationS;
NetworkValue<uint16_t> m_maxHeatingDurationOscillatingS;
NetworkValue<uint8_t> m_maxSimultaneousHeating;
NetworkValue<uint8_t> m_windowOffset;
NetworkValue<E_AMPERAGE_MODE> m_mode;
NetworkValue<E_AMPERAGE_MODE> m_postHeatupMode;
NetworkValue<uint8_t> m_startIndex;
NetworkValue<uint8_t> m_endIndex;
NetworkValue<uint16_t> m_opFlags;
E_AMPERAGE_MODE _initialMode;
uint32_t _deviceStartTimes[MAX_MANAGED_DEVICES];
bool _deviceHeating[MAX_MANAGED_DEVICES];
bool _deviceInHeatup[MAX_MANAGED_DEVICES];
bool _heatupPhaseComplete;
String _name;
millis_t _lastLoopTime;
millis_t _lastDebugLogTime;
uint32_t _lastStopTimeLog[MAX_MANAGED_DEVICES];
std::vector<OmronE5 *> _activeDevices;
bool _checkHeatup(OmronE5 *device);
void _stopAllDevices();
int16_t _stoppingIndex;
uint32_t _lastStopTimestamp;
void _stopDevice(uint8_t deviceIndex, const char *reason);
void _checkAllDevicesForHeatupCompletion();
void _loopCycleAll();
void _loopCycleSp();
void _loopCycleSpAny();
void _loopCycleSpMostUrgent();
// Validation methods
bool _validateMaxTime(short value) const
{
return value >= MB_MAX_TIME_MIN_S && value <= MB_MAX_TIME_MAX_S;
}
bool _validateMinTime(short value) const
{
return value >= MB_MIN_TIME_MIN_S && value <= MB_MIN_TIME_MAX_S;
}
bool _validateMaxTimeOscillating(short value) const
{
return value >= 1 && value <= 3600; // 1s to 1hr
}
bool _validateMaxSim(short value) const
{
return value >= 1 && value <= MAX_MANAGED_DEVICES;
}
bool _validateOffset(short value) const
{
return value >= 1 && value <= MAX_MANAGED_DEVICES;
}
bool _validateStartIndex(short value) const
{
return value >= 0 && value < MAX_MANAGED_DEVICES && value <= m_endIndex.getValue();
}
bool _validateEndIndex(short value) const
{
return value >= 0 && value < MAX_MANAGED_DEVICES && value >= m_startIndex.getValue();
}
};
#endif

View File

@ -0,0 +1,90 @@
#include "InfluxDB.h"
#include "config-extensions.h"
#ifdef ENABLE_INFLUXDB
InfluxDB::InfluxDB(Component *owner, short _id)
: Component("InfluxDB", _id, Component::COMPONENT_DEFAULT, owner)
{
setFlag(OBJECT_RUN_FLAGS::E_OF_BROKER);
}
void InfluxDB::_connect()
{
_lastConnectionAttempt = millis();
Log.infoln("InfluxDB: connecting...");
if (_client.validateConnection())
{
Log.infoln("InfluxDB: connected!");
_isConnected = true;
_reconnectInterval = 1000;
Log.infoln("InfluxDB: connected to %s", INFLUXDB_URL);
}
else
{
Log.warningln("InfluxDB: connection failed: %s", _client.getLastErrorMessage().c_str());
_isConnected = false;
// Exponential backoff
_reconnectInterval *= 2;
if (_reconnectInterval > _maxReconnectInterval)
{
_reconnectInterval = _maxReconnectInterval;
}
Log.warningln("InfluxDB: next reconnect attempt in %lu ms", _reconnectInterval);
}
}
short InfluxDB::setup()
{
Component::setup();
Log.verboseln(F("InfluxDB::setup - ID %d"), id);
_client.setConnectionParams(INFLUXDB_URL, INFLUXDB_ORG, INFLUXDB_BUCKET, INFLUXDB_TOKEN);
_client.setInsecure(true);
_connect();
return E_OK;
}
short InfluxDB::loop()
{
if (!_isConnected)
{
if (millis() - _lastConnectionAttempt > _reconnectInterval)
{
_connect();
}
}
return E_OK;
}
short InfluxDB::info(short flags, short val)
{
Log.verboseln("InfluxDB::info - ID: %d, Connected: %s", id, _isConnected ? "true" : "false");
return E_OK;
}
short InfluxDB::debug()
{
return info(0, 0);
}
short InfluxDB::write(Point &influxDbPoint)
{
if (!_isConnected)
{
Log.warningln(F("InfluxDB: not connected, cannot write point."));
return 1; // Not connected error
}
if (!_client.writePoint(influxDbPoint))
{
Log.warningln(F("InfluxDB: failed to write point: %s"), _client.getLastErrorMessage().c_str());
if (_client.isBufferFull())
{
Log.warningln(F("InfluxDB: buffer is full. Flushing..."));
_client.flushBuffer();
}
return 2; // Write error
}
return E_OK;
}
#endif

31
src/components/InfluxDB.h Normal file
View File

@ -0,0 +1,31 @@
#ifndef INFLUXDB_H
#define INFLUXDB_H
#include "config.h"
#include <Component.h>
#include <ArduinoLog.h>
#ifdef ENABLE_INFLUXDB
#include <InfluxDbClient.h>
class InfluxDB : public Component
{
private:
InfluxDBClient _client;
bool _isConnected = false;
unsigned long _lastConnectionAttempt = 0;
unsigned long _reconnectInterval = 1000; // Initial interval 1s
static const unsigned long _maxReconnectInterval = 25000; // Max 25s
void _connect();
public:
InfluxDB(Component *owner, short _id);
short setup() override;
short info(short flags = 0, short val = 0) override;
short debug() override;
short loop() override;
short write(Point &influxDbPoint);
};
#endif
#endif // ENABLE_INFLUXDB

View File

@ -0,0 +1,160 @@
#include "components/ModbusMirror.h"
#include "PHApp.h"
#include <ArduinoLog.h>
#include <modbus/ModbusTypes.h>
#ifdef ENABLE_MODBUS_MIRROR
static ModbusMirror* instance = nullptr;
void ModbusMirror::onData(ModbusMessage response, uint32_t token) {
if (instance && !instance->_isConnected) {
instance->_isConnected = true;
instance->m_status.update(MBM_STATUS_CONNECTED);
LS_INFO("ModbusMirror: Connected to server %s:%d.", instance->_serverIP.toString().c_str(), instance->_serverPort);
}
}
void ModbusMirror::onError(Error error, uint32_t token) {
ModbusError me(error);
LS_ERROR("ModbusMirror::onError: %02X - %s, token: %08X", (int)me, (const char *)me, token);
if (instance) {
instance->_isConnected = false;
if (me.operator int() == (int)MB_Error::Timeout) {
instance->m_status.update(MBM_STATUS_TIMEOUT);
} else {
instance->m_status.update(MBM_STATUS_DISCONNECTED);
}
}
}
ModbusMirror::ModbusMirror(PHApp* owner, uint16_t id) :
NetworkComponent(MB_ADDR_MB_MIRROR_START, "ModbusMirror", id, Component::COMPONENT_DEFAULT, owner),
_serverIP(),
_serverPort(MODBUS_MIRROR_SERVER_PORT),
_mbClient(nullptr),
_isConnected(false),
_lastConnectionAttempt(0),
_initialConnectionAttempt(0),
m_command(this, 0, "Command"),
m_server_id(this, 1, "ServerID"),
m_status(this, 2, "Status")
{
instance = this;
setNetCapability(OBJECT_NET_CAPS::E_NCAPS_MODBUS);
IPAddress ip(MODBUS_MIRROR_SERVER_IP);
_serverIP = ip;
_mbClient = new ModbusClientTCPasync(_serverIP, _serverPort);
_mbClient->onDataHandler(&ModbusMirror::onData);
_mbClient->onErrorHandler(&ModbusMirror::onError);
_mbClient->setTimeout(10000);
_mbClient->setIdleTimeout(60000);
m_server_id.update(1);
m_status.update(MBM_STATUS_DISCONNECTED);
}
ModbusMirror::~ModbusMirror() {
delete _mbClient;
instance = nullptr;
}
short ModbusMirror::setup() {
NetworkComponent::setup();
_initialConnectionAttempt = 0;
const uint16_t baseAddr = mb_tcp_base_address();
m_command.initModbus(baseAddr + MB_OFS_COMMAND, 1, this->id, this->slaveId, FN_WRITE_HOLD_REGISTER, m_command.name.c_str(), this->name.c_str());
m_server_id.initModbus(baseAddr + MB_OFS_SERVER_ID, 1, this->id, this->slaveId, FN_WRITE_HOLD_REGISTER, m_server_id.name.c_str(), this->name.c_str());
m_status.initModbus(baseAddr + MB_OFS_STATUS, 1, this->id, this->slaveId, FN_READ_HOLD_REGISTER, m_status.name.c_str(), this->name.c_str());
registerBlock(m_command.getRegisterInfo());
registerBlock(m_server_id.getRegisterInfo());
registerBlock(m_status.getRegisterInfo());
return E_OK;
}
short ModbusMirror::loop() {
Component::loop();
if (!_isConnected) {
unsigned long now = millis();
if (_initialConnectionAttempt == 0) {
_initialConnectionAttempt = now;
}
if (now - _initialConnectionAttempt > MODBUS_MIRROR_MAX_RECONNECT_TIME_MS) {
L_ERROR("ModbusMirror: Max reconnect time exceeded. Giving up.");
m_status.update(MBM_STATUS_TIMEOUT);
disable();
return E_OK;
}
if (now - _lastConnectionAttempt > MODBUS_MIRROR_RECONNECT_INTERVAL_MS) {
_lastConnectionAttempt = now;
L_INFO("ModbusMirror: Attempting to connect to %s:%d...", _serverIP.toString().c_str(), _serverPort);
m_status.update(MBM_STATUS_CONNECTING);
uint32_t token = millis();
Error err = _mbClient->addRequest(token, m_server_id.getValue(), READ_HOLD_REGISTER, 0, 1);
if (err != SUCCESS) {
ModbusError e(err);
L_ERROR("ModbusMirror: Error creating request: %02X - %s", (int)e, (const char *)e);
m_status.update(MBM_STATUS_DISCONNECTED);
}
}
}
return E_OK;
}
ModbusClientTCPasync* ModbusMirror::getClient() {
return _mbClient;
}
short ModbusMirror::mb_tcp_write(MB_Registers *reg, short networkValue)
{
short result = NetworkComponent::mb_tcp_write(reg, networkValue);
if (result != E_NOT_IMPLEMENTED) {
return result;
}
uint16_t address = reg->startAddress;
if (address == (_baseAddress + MB_OFS_COMMAND))
{
m_command.update(networkValue);
// Handle command
return E_OK;
}
if (address == (_baseAddress + MB_OFS_SERVER_ID))
{
m_server_id.update(networkValue);
return E_OK;
}
return E_INVALID_PARAMETER;
}
short ModbusMirror::mb_tcp_read(MB_Registers *reg)
{
short result = NetworkComponent::mb_tcp_read(reg);
if (result != E_NOT_IMPLEMENTED) {
return result;
}
uint16_t address = reg->startAddress;
if (address == (_baseAddress + MB_OFS_COMMAND))
{
return m_command.getValue();
}
if (address == (_baseAddress + MB_OFS_SERVER_ID))
{
return m_server_id.getValue();
}
if (address == (_baseAddress + MB_OFS_STATUS))
{
return m_status.getValue();
}
return 0;
}
#endif // ENABLE_MODBUS_MIRROR

View File

@ -0,0 +1,68 @@
#ifndef MODBUS_MIRROR_H
#define MODBUS_MIRROR_H
#include "config.h"
#ifdef ENABLE_MODBUS_MIRROR
#include "modbus/NetworkComponent.h"
#include <ModbusClientTCPasync.h>
#include "config-modbus.h"
#include "NetworkValue.h"
#define MODBUS_MIRROR_MB_COUNT 4 // command, server_id, status
class PHApp;
class ModbusMirror : public NetworkComponent<MODBUS_MIRROR_MB_COUNT> {
public:
enum E_MBM_CMD {
E_MBM_INFO, // (stub)
E_MBM_SYNC
};
enum E_MBM_Status {
MBM_STATUS_DISCONNECTED,
MBM_STATUS_CONNECTING,
MBM_STATUS_CONNECTED,
MBM_STATUS_TIMEOUT
};
enum E_MB_Offset {
MB_OFS_COMMAND = E_NVC_USER + 0,
MB_OFS_SERVER_ID = E_NVC_USER + 1,
MB_OFS_STATUS = E_NVC_USER + 2
};
ModbusMirror(PHApp* owner, uint16_t id);
~ModbusMirror() override;
short setup() override;
short loop() override;
short mb_tcp_write(MB_Registers *reg, short networkValue) override;
short mb_tcp_read(MB_Registers *reg) override;
ModbusClientTCPasync* getClient();
private:
friend void onData(ModbusMessage response, uint32_t token);
friend void onError(Error error, uint32_t token);
static void onData(ModbusMessage response, uint32_t token);
static void onError(Error error, uint32_t token);
IPAddress _serverIP;
uint16_t _serverPort;
ModbusClientTCPasync* _mbClient;
bool _isConnected;
unsigned long _lastConnectionAttempt;
unsigned long _initialConnectionAttempt;
NetworkValue<uint16_t> m_command;
NetworkValue<uint16_t> m_server_id;
NetworkValue<uint16_t> m_status;
};
#endif // ENABLE_MODBUS_MIRROR
#endif // MODBUS_MIRROR_H

View File

@ -1,4 +1,5 @@
#include "config.h" // Application configuration
#include "version.h"
// #define WS_MAX_QUEUED_MESSAGES 512
@ -684,7 +685,16 @@ void RESTServer::getSystemInfoHandler(AsyncWebServerRequest *request)
{
AsyncResponseStream *response = request->beginResponseStream("application/json");
JsonDocument doc;
doc["version"] = "3ce112f";
#ifdef VERSION
doc["version"] = VERSION;
#else
doc["version"] = "unknown";
#endif
#ifdef PACKAGE_URL
doc["url"] = PACKAGE_URL;
#else
doc["url"] = "unknown";
#endif
doc["board"] = BOARD_NAME;
doc["uptime"] = millis() / 1000;
doc["timestamp"] = millis();