409 lines
15 KiB
C++
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 ¶ms)
|
|
{
|
|
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 ¶ms)
|
|
{
|
|
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;
|
|
}
|