powersync-energy-plugin-etm/energyplugin/adapters/loadadapterregistry.cpp

409 lines
15 KiB
C++

// SPDX-License-Identifier: GPL-3.0-or-later
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
*
* Copyright (C) 2013 - 2024, nymea GmbH
* Copyright (C) 2024 - 2025, chargebyte austria GmbH
*
* This file is part of nymea-energy-plugin-nymea.
*
* nymea-energy-plugin-nymea.s free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* nymea-energy-plugin-nymea.s distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with nymea-energy-plugin-nymea. If not, see <https://www.gnu.org/licenses/>.
*
* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
#include "loadadapterregistry.h"
#include "adaptersettings.h"
#include "adapters/relayadapter.h"
#include "adapters/sgreadyadapter.h"
#include "adapters/evchargeradapter.h"
#include "adapters/batteryadapter.h"
#include <QLoggingCategory>
Q_DECLARE_LOGGING_CATEGORY(dcNymeaEnergy)
LoadAdapterRegistry::LoadAdapterRegistry(ThingManager *thingManager, QObject *parent)
: QObject(parent),
m_thingManager(thingManager)
{
// Initialise empty entries for every role
for (LoadRole role : allLoadRoles())
m_entries.insert(role, RoleEntry());
if (!thingManager)
return;
connect(thingManager, &ThingManager::thingAdded, this, &LoadAdapterRegistry::onThingAdded);
}
// ---------------------------------------------------------------------------
// Static helpers
// ---------------------------------------------------------------------------
QString LoadAdapterRegistry::detectAdapterType(LoadRole role, const QStringList &interfaces)
{
switch (role) {
case LoadRole::EVCharger:
if (interfaces.contains(QLatin1String("evcharger")))
return QStringLiteral("evcharger");
break;
case LoadRole::Battery:
if (interfaces.contains(QLatin1String("energystorage")))
return QStringLiteral("battery");
break;
case LoadRole::DHW:
if (interfaces.contains(QLatin1String("relay")) ||
interfaces.contains(QLatin1String("smartmeterconsumer")))
return QStringLiteral("relay");
break;
case LoadRole::HeatPump:
if (interfaces.contains(QLatin1String("heating")))
return QStringLiteral("sgready");
if (interfaces.contains(QLatin1String("relay")))
return QStringLiteral("relay");
break;
case LoadRole::SolarMeter:
case LoadRole::GridMeter:
if (interfaces.contains(QLatin1String("energymeter")))
return QStringLiteral("readonly");
break;
}
return QString();
}
// ---------------------------------------------------------------------------
// Persistence
// ---------------------------------------------------------------------------
void LoadAdapterRegistry::loadFromSettings(AdapterSettings *settings)
{
if (!settings || !m_thingManager)
return;
for (const QVariantMap &assignment : settings->assignments()) {
const QString roleStr = assignment.value(QStringLiteral("role")).toString();
const ThingId thingId = ThingId(assignment.value(QStringLiteral("thingId")).toString());
const bool enabled = assignment.value(QStringLiteral("enabled"), true).toBool();
QVariantMap params = assignment;
LoadRole role = loadRoleFromString(roleStr);
// Try to find the Thing
Thing *thing = m_thingManager->findConfiguredThing(thingId);
if (!thing) {
qCWarning(dcNymeaEnergy()) << "LoadAdapterRegistry: saved thing not found:"
<< thingId << "for role" << roleStr;
RoleEntry &entry = m_entries[role];
entry.rawParams = params;
entry.enabled = enabled;
entry.lastError = QStringLiteral("Thing not found: %1").arg(thingId.toString());
emit setupStatusChanged(setupStatus());
continue;
}
const QString result = assignThing(role, thingId, params);
if (result.startsWith(QLatin1String("error:"))) {
qCWarning(dcNymeaEnergy()) << "LoadAdapterRegistry: failed to restore assignment:"
<< result;
m_entries[role].lastError = result;
} else {
m_entries[role].enabled = enabled;
}
}
}
// ---------------------------------------------------------------------------
// Public API
// ---------------------------------------------------------------------------
QList<ThingInfo> LoadAdapterRegistry::compatibleThings(LoadRole role) const
{
QList<ThingInfo> result;
if (!m_thingManager)
return result;
const QStringList required = loadRoleCompatibleInterfaces().value(role);
foreach (Thing *thing, m_thingManager->configuredThings()) {
const QStringList ifaces = thing->thingClass().interfaces();
bool compatible = false;
foreach (const QString &iface, required) {
if (ifaces.contains(iface)) { compatible = true; break; }
}
if (!compatible)
continue;
ThingInfo info;
info.thingId = thing->id();
info.displayName = thing->name();
info.pluginName = thing->pluginId().toString();
info.interfaces = ifaces;
result.append(info);
}
return result;
}
QString LoadAdapterRegistry::assignThing(LoadRole role,
const ThingId &thingId,
const QVariantMap &params)
{
if (!m_thingManager) {
return QStringLiteral("error: no ThingManager");
}
Thing *thing = m_thingManager->findConfiguredThing(thingId);
if (!thing) {
return QStringLiteral("error: Thing not found: %1").arg(thingId.toString());
}
const QStringList ifaces = thing->thingClass().interfaces();
const QString adapterType = detectAdapterType(role, ifaces);
if (adapterType.isEmpty()) {
return QStringLiteral("error: No compatible adapter for role %1 and interfaces %2")
.arg(loadRoleToString(role))
.arg(ifaces.join(QLatin1String(", ")));
}
// Destroy previous adapter for this role
RoleEntry &entry = m_entries[role];
if (entry.adapter) {
entry.adapter->deleteLater();
entry.adapter = nullptr;
}
// Create new adapter
ILoadAdapter *adapter = createAdapter(role, thingId, adapterType, params);
if (!adapter) {
entry.lastError = QStringLiteral("Failed to create adapter of type: %1").arg(adapterType);
return QStringLiteral("error: ") + entry.lastError;
}
// Forward reachability changes → setupStatusChanged
connect(adapter, &ILoadAdapter::reachabilityChanged, this, [this](bool) {
emit setupStatusChanged(setupStatus());
});
entry.adapter = adapter;
entry.rawParams = params;
entry.rawParams.insert(QStringLiteral("role"), loadRoleToString(role));
entry.rawParams.insert(QStringLiteral("thingId"), thingId.toString());
entry.rawParams.insert(QStringLiteral("adapterType"), adapterType);
entry.lastError.clear();
qCDebug(dcNymeaEnergy()) << "LoadAdapterRegistry: assigned"
<< loadRoleToString(role) << "" << thing->name()
<< "(adapter:" << adapterType << ")";
emit roleAssigned(role, thingId);
emit setupStatusChanged(setupStatus());
return adapterType;
}
void LoadAdapterRegistry::unassignRole(LoadRole role)
{
RoleEntry &entry = m_entries[role];
if (entry.adapter) {
entry.adapter->deleteLater();
entry.adapter = nullptr;
}
entry.rawParams.clear();
entry.lastError.clear();
emit roleUnassigned(role);
emit setupStatusChanged(setupStatus());
}
ILoadAdapter *LoadAdapterRegistry::adapterForRole(LoadRole role) const
{
return m_entries.value(role).adapter;
}
bool LoadAdapterRegistry::isRoleAssigned(LoadRole role) const
{
return m_entries.value(role).adapter != nullptr;
}
bool LoadAdapterRegistry::isRoleEnabled(LoadRole role) const
{
return m_entries.value(role).enabled;
}
void LoadAdapterRegistry::setRoleEnabled(LoadRole role, bool enabled)
{
RoleEntry &entry = m_entries[role];
if (entry.enabled == enabled)
return;
entry.enabled = enabled;
emit roleEnabledChanged(role, enabled);
emit setupStatusChanged(setupStatus());
}
void LoadAdapterRegistry::testConnection(LoadRole role)
{
ILoadAdapter *adapter = m_entries.value(role).adapter;
if (!adapter) {
emit connectionTestResult(role, false, QStringLiteral("Role not assigned"));
return;
}
connect(adapter, &ILoadAdapter::testResult, this, [this, role](bool ok, const QString &msg) {
emit connectionTestResult(role, ok, msg);
}, Qt::SingleShotConnection);
adapter->testConnection();
}
SetupStatus LoadAdapterRegistry::setupStatus() const
{
SetupStatus status;
status.configuredCount = 0;
status.errorCount = 0;
status.allEnabledRolesOk = true;
for (LoadRole role : allLoadRoles()) {
RoleStatus rs = buildRoleStatus(role);
status.roles.append(rs);
if (rs.assigned)
status.configuredCount++;
if (!rs.lastError.isEmpty())
status.errorCount++;
if (rs.enabled && rs.assigned && !rs.reachable)
status.allEnabledRolesOk = false;
if (rs.enabled && !rs.assigned)
status.allEnabledRolesOk = false;
}
return status;
}
QList<QVariantMap> LoadAdapterRegistry::rawAssignments() const
{
QList<QVariantMap> result;
for (auto it = m_entries.constBegin(); it != m_entries.constEnd(); ++it) {
const RoleEntry &e = it.value();
if (e.adapter || !e.rawParams.isEmpty()) {
QVariantMap m = e.rawParams;
m.insert(QStringLiteral("enabled"), e.enabled);
result.append(m);
}
}
return result;
}
// ---------------------------------------------------------------------------
// Private helpers
// ---------------------------------------------------------------------------
void LoadAdapterRegistry::onThingAdded(Thing *thing)
{
if (!thing)
return;
const QStringList ifaces = thing->thingClass().interfaces();
const QMap<LoadRole, QStringList> compatMap = loadRoleCompatibleInterfaces();
for (auto it = compatMap.constBegin(); it != compatMap.constEnd(); ++it) {
LoadRole role = it.key();
if (isRoleAssigned(role))
continue; // already assigned, no need to advertise
bool compatible = false;
foreach (const QString &iface, it.value()) {
if (ifaces.contains(iface)) { compatible = true; break; }
}
if (!compatible)
continue;
ThingInfo info;
info.thingId = thing->id();
info.displayName = thing->name();
info.pluginName = thing->pluginId().toString();
info.interfaces = ifaces;
qCDebug(dcNymeaEnergy()) << "LoadAdapterRegistry: new Thing" << thing->name()
<< "compatible with role" << loadRoleToString(role);
emit thingBecameCompatible(role, info);
}
}
ILoadAdapter *LoadAdapterRegistry::createAdapter(LoadRole role,
const ThingId &thingId,
const QString &adapterType,
const QVariantMap &params)
{
if (adapterType == QLatin1String("relay")) {
double nominalW = params.value(QStringLiteral("nominalPowerW"), 2000.0).toDouble();
return new RelayAdapter(m_thingManager, thingId, role, nominalW, this);
}
if (adapterType == QLatin1String("sgready")) {
ThingId relay1 = ThingId(params.value(QStringLiteral("relay1ThingId")).toString());
ThingId relay2 = ThingId(params.value(QStringLiteral("relay2ThingId")).toString());
double normalW = params.value(QStringLiteral("normalPowerW"), 1500.0).toDouble();
return new SgReadyAdapter(m_thingManager, relay1, relay2, normalW, this);
}
if (adapterType == QLatin1String("evcharger")) {
int phases = params.value(QStringLiteral("phases"), 1).toInt();
int minA = params.value(QStringLiteral("minA"), 6).toInt();
int maxA = params.value(QStringLiteral("maxA"), 32).toInt();
return new EvChargerAdapter(m_thingManager, thingId, phases, minA, maxA, this);
}
if (adapterType == QLatin1String("battery")) {
double cap = params.value(QStringLiteral("capacityKwh"), 10.0).toDouble();
double maxCharge = params.value(QStringLiteral("maxChargeW"), 5000.0).toDouble();
double maxDisch = params.value(QStringLiteral("maxDischargeW"),5000.0).toDouble();
return new BatteryAdapter(m_thingManager, thingId, cap, maxCharge, maxDisch, this);
}
if (adapterType == QLatin1String("readonly")) {
// Read-only meters: no adapter needed — just store the ThingId
qCDebug(dcNymeaEnergy()) << "LoadAdapterRegistry: read-only role" << loadRoleToString(role)
<< "— no adapter created (meter only)";
return nullptr;
}
qCWarning(dcNymeaEnergy()) << "LoadAdapterRegistry: unknown adapterType:" << adapterType;
return nullptr;
}
RoleStatus LoadAdapterRegistry::buildRoleStatus(LoadRole role) const
{
RoleStatus rs;
rs.role = role;
const RoleEntry &entry = m_entries.value(role);
rs.enabled = entry.enabled;
rs.lastError = entry.lastError;
if (entry.adapter) {
rs.assigned = true;
rs.assignedThingId = entry.adapter->thingId();
rs.adapterType = entry.adapter->adapterId();
rs.reachable = entry.adapter->isReachable();
// Try to get display name from ThingManager
if (m_thingManager) {
Thing *t = m_thingManager->findConfiguredThing(rs.assignedThingId);
if (t)
rs.assignedThingName = t->name();
}
} else if (!entry.rawParams.isEmpty()) {
// Assignment failed (Thing not found at startup)
rs.assigned = true;
rs.assignedThingId = ThingId(entry.rawParams.value(QStringLiteral("thingId")).toString());
rs.adapterType = entry.rawParams.value(QStringLiteral("adapterType")).toString();
rs.reachable = false;
}
return rs;
}