Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 30 additions & 0 deletions include/Configuration.h
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,33 @@ struct POWERMETER_UDP_VICTRON_CONFIG_T {
};
using PowerMeterUdpVictronConfig = struct POWERMETER_UDP_VICTRON_CONFIG_T;

struct POWERMETER_MODBUS_REGISTER_CONFIG_T {
uint16_t Address;
float ScalingFactor;

enum RegisterDataType { INT16 = 0, UINT16 = 1, INT32 = 2, UINT32 = 3, FLOAT = 4 };
RegisterDataType DataType;
};

struct POWERMETER_MODBUS_TCP_CONFIG_T {
uint32_t PollingInterval;
uint8_t IpAddress[4];
uint16_t Port;
uint8_t DeviceId;

POWERMETER_MODBUS_REGISTER_CONFIG_T PowerRegister;
POWERMETER_MODBUS_REGISTER_CONFIG_T PowerL1Register;
POWERMETER_MODBUS_REGISTER_CONFIG_T PowerL2Register;
POWERMETER_MODBUS_REGISTER_CONFIG_T PowerL3Register;
POWERMETER_MODBUS_REGISTER_CONFIG_T VoltageL1Register;
POWERMETER_MODBUS_REGISTER_CONFIG_T VoltageL2Register;
POWERMETER_MODBUS_REGISTER_CONFIG_T VoltageL3Register;
POWERMETER_MODBUS_REGISTER_CONFIG_T ImportRegister;
POWERMETER_MODBUS_REGISTER_CONFIG_T ExportRegister;
};
using PowerMeterModbusTcpRegisterConfig = struct POWERMETER_MODBUS_REGISTER_CONFIG_T;
using PowerMeterModbusTcpConfig = struct POWERMETER_MODBUS_TCP_CONFIG_T;

struct POWERLIMITER_INVERTER_CONFIG_T {
uint64_t Serial;
bool IsGoverned;
Expand Down Expand Up @@ -428,6 +455,7 @@ struct CONFIG_T {
PowerMeterHttpJsonConfig HttpJson;
PowerMeterHttpSmlConfig HttpSml;
PowerMeterUdpVictronConfig UdpVictron;
PowerMeterModbusTcpConfig ModbusTcp;
} PowerMeter;

PowerLimiterConfig PowerLimiter;
Expand Down Expand Up @@ -483,6 +511,7 @@ class ConfigurationClass {
static void serializePowerMeterHttpJsonConfig(PowerMeterHttpJsonConfig const& source, JsonObject& target);
static void serializePowerMeterHttpSmlConfig(PowerMeterHttpSmlConfig const& source, JsonObject& target);
static void serializePowerMeterUdpVictronConfig(PowerMeterUdpVictronConfig const& source, JsonObject& target);
static void serializePowerMeterModbusTcpConfig(PowerMeterModbusTcpConfig const& source, JsonObject& target);
static void serializeBatteryConfig(BatteryConfig const& source, JsonObject& target);
static void serializeBatteryZendureConfig(BatteryZendureConfig const& source, JsonObject& target);
static void serializeBatteryMqttConfig(BatteryMqttConfig const& source, JsonObject& target);
Expand All @@ -500,6 +529,7 @@ class ConfigurationClass {
static void deserializePowerMeterHttpJsonConfig(JsonObject const& source, PowerMeterHttpJsonConfig& target);
static void deserializePowerMeterHttpSmlConfig(JsonObject const& source, PowerMeterHttpSmlConfig& target);
static void deserializePowerMeterUdpVictronConfig(JsonObject const& source, PowerMeterUdpVictronConfig& target);
static void deserializePowerMeterModbusTcpConfig(JsonObject const& source, PowerMeterModbusTcpConfig& target);
static void deserializeBatteryConfig(JsonObject const& source, BatteryConfig& target);
static void deserializeBatteryZendureConfig(JsonObject const& source, BatteryZendureConfig& target);
static void deserializeBatteryMqttConfig(JsonObject const& source, BatteryMqttConfig& target);
Expand Down
3 changes: 2 additions & 1 deletion include/powermeter/Provider.h
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,8 @@ class Provider {
SERIAL_SML = 4,
SMAHM2 = 5,
HTTP_SML = 6,
MODBUS_UDP_VICTRON = 7
MODBUS_UDP_VICTRON = 7,
MODBUS_TCP = 8
};

// returns true if the provider is ready for use, false otherwise
Expand Down
45 changes: 45 additions & 0 deletions include/powermeter/modbus/tcp/Provider.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once

#include <atomic>
#include <mutex>
#include <condition_variable>
#include <WiFiClient.h>
#include <Configuration.h>
#include <powermeter/Provider.h>

namespace PowerMeters::Modbus::Tcp {

class Provider : public ::PowerMeters::Provider {
public:
explicit Provider(PowerMeterModbusTcpConfig const& cfg);
~Provider();

bool init() final;
void loop() final;
bool isDataValid() const final;

private:
static void pollingLoopHelper(void* context);
bool readModbusRegister(const PowerMeterModbusTcpRegisterConfig& regConfig, float& value);
bool connectToDevice();
void pollingLoop();
bool sendModbusRequest(uint8_t deviceId, uint8_t functionCode, uint16_t startRegister, uint16_t numRegisters);
bool receiveModbusResponse(uint16_t* values, uint16_t numRegisters);

PowerMeterModbusTcpConfig const _cfg;

uint32_t _lastPoll = 0;
std::atomic<bool> _taskDone;

std::unique_ptr<WiFiClient> _upWiFiClient = nullptr;

TaskHandle_t _taskHandle = nullptr;
bool _stopPolling;
mutable std::mutex _pollingMutex;
std::condition_variable _cv;

uint16_t _transactionId = 0;
};

} // namespace PowerMeters::Modbus::Tcp
65 changes: 65 additions & 0 deletions src/Configuration.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,32 @@ void ConfigurationClass::serializePowerMeterUdpVictronConfig(PowerMeterUdpVictro
target["ip_address"] = IPAddress(source.IpAddress).toString();
}

void ConfigurationClass::serializePowerMeterModbusTcpConfig(PowerMeterModbusTcpConfig const& source, JsonObject& target)
{
target["polling_interval"] = source.PollingInterval;
target["ip_address"] = IPAddress(source.IpAddress).toString();
target["port"] = source.Port;
target["device_id"] = source.DeviceId;

// Power registers
auto serializeRegisterConfig = [](JsonObject& parent, const char* name, const PowerMeterModbusTcpRegisterConfig& reg) {
JsonObject regObj = parent[name].to<JsonObject>();
regObj["address"] = reg.Address;
regObj["scaling_factor"] = reg.ScalingFactor;
regObj["data_type"] = static_cast<uint8_t>(reg.DataType);
};

serializeRegisterConfig(target, "power_register", source.PowerRegister);
serializeRegisterConfig(target, "power_l1_register", source.PowerL1Register);
serializeRegisterConfig(target, "power_l2_register", source.PowerL2Register);
serializeRegisterConfig(target, "power_l3_register", source.PowerL3Register);
serializeRegisterConfig(target, "voltage_l1_register", source.VoltageL1Register);
serializeRegisterConfig(target, "voltage_l2_register", source.VoltageL2Register);
serializeRegisterConfig(target, "voltage_l3_register", source.VoltageL3Register);
serializeRegisterConfig(target, "import_register", source.ImportRegister);
serializeRegisterConfig(target, "export_register", source.ExportRegister);
}

void ConfigurationClass::serializeBatteryConfig(BatteryConfig const& source, JsonObject& target)
{
target["enabled"] = config.Battery.Enabled;
Expand Down Expand Up @@ -423,6 +449,9 @@ bool ConfigurationClass::write()
JsonObject powermeter_udp_victron = powermeter["udp_victron"].to<JsonObject>();
serializePowerMeterUdpVictronConfig(config.PowerMeter.UdpVictron, powermeter_udp_victron);

JsonObject powermeter_modbus_tcp = powermeter["modbus_tcp"].to<JsonObject>();
serializePowerMeterModbusTcpConfig(config.PowerMeter.ModbusTcp, powermeter_modbus_tcp);

JsonObject powerlimiter = doc["powerlimiter"].to<JsonObject>();
serializePowerLimiterConfig(config.PowerLimiter, powerlimiter);

Expand Down Expand Up @@ -550,6 +579,40 @@ void ConfigurationClass::deserializePowerMeterUdpVictronConfig(JsonObject const&
target.IpAddress[3] = ip[3];
}

void ConfigurationClass::deserializePowerMeterModbusTcpConfig(JsonObject const& source, PowerMeterModbusTcpConfig& target)
{
target.PollingInterval = source["polling_interval"] | POWERMETER_POLLING_INTERVAL;
target.Port = source["port"] | 502; // Default Modbus TCP port
target.DeviceId = source["device_id"] | 1;

IPAddress ip;
ip.fromString(source["ip_address"] | "");
target.IpAddress[0] = ip[0];
target.IpAddress[1] = ip[1];
target.IpAddress[2] = ip[2];
target.IpAddress[3] = ip[3];

// Deserialize register configurations
auto deserializeRegisterConfig = [](JsonObject const& parent, const char* name, PowerMeterModbusTcpRegisterConfig& reg) {
JsonObject regObj = parent[name];
reg.Address = regObj["address"] | 0;
reg.ScalingFactor = regObj["scaling_factor"] | 1.0f;
reg.DataType = static_cast<PowerMeterModbusTcpRegisterConfig::RegisterDataType>(
regObj["data_type"] | static_cast<uint8_t>(PowerMeterModbusTcpRegisterConfig::RegisterDataType::UINT16)
);
};

deserializeRegisterConfig(source, "power_register", target.PowerRegister);
deserializeRegisterConfig(source, "power_l1_register", target.PowerL1Register);
deserializeRegisterConfig(source, "power_l2_register", target.PowerL2Register);
deserializeRegisterConfig(source, "power_l3_register", target.PowerL3Register);
deserializeRegisterConfig(source, "voltage_l1_register", target.VoltageL1Register);
deserializeRegisterConfig(source, "voltage_l2_register", target.VoltageL2Register);
deserializeRegisterConfig(source, "voltage_l3_register", target.VoltageL3Register);
deserializeRegisterConfig(source, "import_register", target.ImportRegister);
deserializeRegisterConfig(source, "export_register", target.ExportRegister);
}

void ConfigurationClass::deserializeBatteryConfig(JsonObject const& source, BatteryConfig& target)
{
target.Enabled = source["enabled"] | BATTERY_ENABLED;
Expand Down Expand Up @@ -879,6 +942,8 @@ bool ConfigurationClass::read()

deserializePowerMeterUdpVictronConfig(powermeter["udp_victron"], config.PowerMeter.UdpVictron);

deserializePowerMeterModbusTcpConfig(powermeter["modbus_tcp"], config.PowerMeter.ModbusTcp);

deserializePowerLimiterConfig(doc["powerlimiter"], config.PowerLimiter);

JsonObject battery = doc["battery"];
Expand Down
87 changes: 87 additions & 0 deletions src/WebApi_powermeter.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,9 @@ void WebApiPowerMeterClass::onStatus(AsyncWebServerRequest* request)
auto udpVictron = root["udp_victron"].to<JsonObject>();
Configuration.serializePowerMeterUdpVictronConfig(config.PowerMeter.UdpVictron, udpVictron);

auto modbusTcp = root["modbus_tcp"].to<JsonObject>();
Configuration.serializePowerMeterModbusTcpConfig(config.PowerMeter.ModbusTcp, modbusTcp);

WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__);
}

Expand Down Expand Up @@ -170,6 +173,87 @@ void WebApiPowerMeterClass::onAdminPost(AsyncWebServerRequest* request)
}
}

if (static_cast<::PowerMeters::Provider::Type>(root["source"].as<uint8_t>()) == ::PowerMeters::Provider::Type::MODBUS_TCP) {
JsonObject modbusTcp = root["modbus_tcp"];
if (!modbusTcp["ip_address"].is<String>()
|| modbusTcp["ip_address"].as<String>().length() == 0) {
retMsg["message"] = "IP address must not be empty!";
response->setLength();
request->send(response);
return;
}

if (!modbusTcp["port"].is<uint32_t>()
|| modbusTcp["port"].as<uint32_t>() <= 0 || modbusTcp["port"].as<uint32_t>() > 65535) {
retMsg["message"] = "Port must be between 1 and 65535!";
response->setLength();
request->send(response);
return;
}

if (!modbusTcp["device_id"].is<uint32_t>()
|| modbusTcp["device_id"].as<uint32_t>() < 1 || modbusTcp["device_id"].as<uint32_t>() > 247) {
retMsg["message"] = "Device ID must be between 1 and 247!";
response->setLength();
request->send(response);
return;
}

if (!modbusTcp["polling_interval"].is<uint32_t>()
|| modbusTcp["polling_interval"].as<uint32_t>() <= 0) {
retMsg["message"] = "Polling interval must be greater than 0 seconds!";
response->setLength();
request->send(response);
return;
}

// Validate register configurations
auto validateRegisterConfig = [&](JsonObject registerObj, const String& name) -> bool {
if (registerObj.isNull()) return true; // Optional

if (registerObj.containsKey("address")) {
uint32_t address = registerObj["address"].as<uint32_t>();
if (address > 9999) {
retMsg["message"] = name + " register address must be between 0 and 9999!";
return false;
}
}

if (registerObj.containsKey("scaling_factor")) {
float scaling = registerObj["scaling_factor"].as<float>();
if (scaling <= 0.0f) {
retMsg["message"] = name + " scaling factor must be greater than 0!";
return false;
}
}

if (registerObj.containsKey("data_type")) {
uint8_t dataType = registerObj["data_type"].as<uint8_t>();
if (dataType > 4) { // 0-4 are valid datatype values (INT16, UINT16, INT32, UINT32, FLOAT)
retMsg["message"] = name + " data type must be between 0 and 4!";
return false;
}
}

return true;
};

const char* registerNames[] = {
"power_register", "power_l1_register", "power_l2_register", "power_l3_register",
"voltage_l1_register", "voltage_l2_register", "voltage_l3_register",
"current_l1_register", "current_l2_register", "current_l3_register",
"import_register", "export_register"
};

for (const char* regName : registerNames) {
if (!validateRegisterConfig(modbusTcp[regName], regName)) {
response->setLength();
request->send(response);
return;
}
}
}

{
auto guard = Configuration.getWriteGuard();
auto& config = guard.getConfig();
Expand All @@ -190,6 +274,9 @@ void WebApiPowerMeterClass::onAdminPost(AsyncWebServerRequest* request)

Configuration.deserializePowerMeterUdpVictronConfig(root["udp_victron"].as<JsonObject>(),
config.PowerMeter.UdpVictron);

Configuration.deserializePowerMeterModbusTcpConfig(root["modbus_tcp"].as<JsonObject>(),
config.PowerMeter.ModbusTcp);
}

WebApi.writeConfig(retMsg);
Expand Down
4 changes: 4 additions & 0 deletions src/powermeter/Controller.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
#include <powermeter/sml/serial/Provider.h>
#include <powermeter/smahm/udp/Provider.h>
#include <powermeter/modbus/udp/victron/Provider.h>
#include <powermeter/modbus/tcp/Provider.h>

PowerMeters::Controller PowerMeter;

Expand Down Expand Up @@ -60,6 +61,9 @@ void Controller::updateSettings()
case Provider::Type::MODBUS_UDP_VICTRON:
_upProvider = std::make_unique<::PowerMeters::Modbus::Udp::Victron::Provider>(pmcfg.UdpVictron);
break;
case Provider::Type::MODBUS_TCP:
_upProvider = std::make_unique<::PowerMeters::Modbus::Tcp::Provider>(pmcfg.ModbusTcp);
break;
}

if (!_upProvider->init()) {
Expand Down
Loading
Loading