// SPDX-License-Identifier: GPL-3.0-or-later // Copyright (C) 2025 - 2026, Patrick Schurig / ETM PowerSync #include "etmvariableloadadapter.h" #include "plugininfo.h" #include #include #include #include #include #include EtmVariableLoadAdapter::EtmVariableLoadAdapter(ThingManager *thingManager, const QString &id, const QString &label, const QList &powerLevels, int maxPowerW, int priority, const LoadNeeds &needs, QObject *parent) : QObject(parent) , m_thingManager(thingManager) , m_id(id) , m_label(label) , m_powerLevels(powerLevels) , m_maxPowerW(maxPowerW) , m_priority(priority) , m_needs(needs) { // Re-tri par sécurité : l'UI émet trié, l'energymanager re-trie (contrat §6 cas limites). std::sort(m_powerLevels.begin(), m_powerLevels.end()); // Contrat §2 : les paliers incluent 0. En *fixed* (liste non vide), 0 doit être présent. Q_ASSERT(m_powerLevels.isEmpty() || m_powerLevels.first() == 0); } LoadDescriptor EtmVariableLoadAdapter::descriptor() const { LoadDescriptor d; d.id = m_id; d.label = m_label; d.adapter = QStringLiteral("etmvariableload"); d.priority = m_priority; d.declared.powerLevels = m_powerLevels; d.declared.maxPowerW = m_maxPowerW; d.needs = m_needs; d.supportedKinds = { LoadAction::Setpoint }; return d; } LoadTelemetry EtmVariableLoadAdapter::telemetry() const { LoadTelemetry t; Thing *thing = m_thingManager->findConfiguredThing(ThingId(m_id)); t.available = (thing != nullptr); t.currentPowerW = readCurrentPowerW(); // juge runtime (contrat §4) t.lastActionAt = m_lastActionAt; return t; } LoadContext EtmVariableLoadAdapter::toLoadContext(const QDateTime &now) const { Q_UNUSED(now) // aucune fenêtre de verrou côté moteur : l'anti-rebond vit dans le thing. LoadContext ctx; ctx.id = m_id; ctx.adapter = QStringLiteral("etmvariableload"); ctx.label = m_label; ctx.priority = m_priority; ctx.declared = descriptor().declared; ctx.telemetry.currentPowerW = readCurrentPowerW(); return ctx; } LoadAction EtmVariableLoadAdapter::applyAction(const LoadAction &action, const QDateTime &now) { if (action.kind != LoadAction::Setpoint) return action; if (action.reason.isEmpty()) { qCWarning(dcNymeaEnergy()) << "[EtmVariableLoadAdapter]" << m_label << "— LoadAction sans reason rejetée."; return action; } // Second filet (invariant ILoadAdapter) : borner la consigne au plafond physique. // L'arrondi au powerLevels (mode *fixed*) est fait par le scheduler (contrat §4, T3). const double setpointW = qBound(0.0, action.powerW, static_cast(m_maxPowerW)); qCInfo(dcNymeaEnergy()) << "[EtmVariableLoadAdapter]" << m_label << "→ setpoint" << qRound(setpointW) << "W" << (action.force ? "(force)" : "") << "|" << action.reason; writeSetpoint(setpointW); m_currentSetpointW = setpointW; m_lastActionAt = now; LoadAction applied = action; applied.powerW = setpointW; applied.estimatedPowerW = setpointW; return applied; } // ---- privé --------------------------------------------------------------- double EtmVariableLoadAdapter::readCurrentPowerW() const { Thing *thing = m_thingManager->findConfiguredThing(ThingId(m_id)); if (!thing) return 0.0; if (thing->thingClass().stateTypes().findByName("currentPowerW").id().isNull()) return 0.0; return thing->stateValue("currentPowerW").toDouble(); } void EtmVariableLoadAdapter::writeSetpoint(double powerW) { Thing *thing = m_thingManager->findConfiguredThing(ThingId(m_id)); if (!thing) { qCWarning(dcNymeaEnergy()) << "[EtmVariableLoadAdapter]" << m_label << "— thing non trouvé:" << m_id; return; } StateType setpointStateType = thing->thingClass().stateTypes().findByName("powerSetpoint"); if (!setpointStateType.id().isNull()) { Action setpointAction(setpointStateType.id(), thing->id(), Action::TriggeredByRule); setpointAction.setParams(ParamList() << Param(setpointStateType.id(), powerW)); m_thingManager->executeAction(setpointAction); } else { thing->setStateValue("powerSetpoint", powerW); // repli mock } }