// 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 . * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ #include "loadadapterregistry.h" #include "adaptersettings.h" #include "adapters/relayadapter.h" #include "adapters/sgreadyadapter.h" #include "adapters/evchargeradapter.h" #include "adapters/batteryadapter.h" #include 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 LoadAdapterRegistry::compatibleThings(LoadRole role) const { QList 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 LoadAdapterRegistry::rawAssignments() const { QList 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 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; }