feat: Installer Setup — LoadAdapterRegistry + adapter layer (v12)
Adds a thin adapter layer that maps installer-visible roles (EVCharger, DHW, HeatPump, Battery, SolarMeter, GridMeter) to nymea Things and applies Scheduler slot decisions to real hardware. New files: - energyplugin/types/loadrole.h — LoadRole enum + compat interface map - energyplugin/adapters/iloadapter.h — abstract ILoadAdapter interface - energyplugin/adapters/relayadapter.h/.cpp — binary on/off relay (DHW/HP) - energyplugin/adapters/sgreadyadapter.h/.cpp — SG-Ready 2-relay HP (SG1–SG4) - energyplugin/adapters/evchargeradapter.h/.cpp — EV charger W→A adapter - energyplugin/adapters/batteryadapter.h/.cpp — battery charge/discharge/standby - energyplugin/adapters/loadadapterregistry.h/.cpp — role→Thing→adapter registry - energyplugin/adapters/adaptersettings.h/.cpp — persists to adapters.conf (INI) Modified: - SchedulerManager: optional LoadAdapterRegistry param; applyCurrentSlot() now calls adapter->applyPower() for DHW/HP/Battery roles - NymeaEnergyJsonHandler: 6 new v12 methods (GetSetupStatus, GetCompatibleThings, AssignThingToRole, UnassignRole, SetRoleEnabled, TestRoleConnection) + 3 push notifications - energypluginnymea.cpp: wires AdapterSettings + LoadAdapterRegistry, bumps JSON-RPC version to 12 - energyplugin.pri: adds 7 headers + 6 sources - testscheduler: 4 new tests (detectAdapterType, AdapterSettings round-trip, null-registry no-crash, LoadRole serialization) — 15/15 pass - doc.md: section 12 Installer Setup Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
69246618ee
commit
67836b7234
170
doc.md
170
doc.md
@ -1524,3 +1524,173 @@ NymeaSettings::settingsPath() + "/scheduler.conf" [section: manualSlots]
|
||||
---
|
||||
|
||||
*Section 11 ajoutée le 2026-02-24 — ManualStrategy Community Tier*
|
||||
|
||||
---
|
||||
|
||||
## Section 12 — Installer Setup (LoadAdapterRegistry, v12)
|
||||
|
||||
### 12.1 Vue d'ensemble
|
||||
|
||||
L'**Installer Setup** permet à un installateur de mapper des rôles énergétiques
|
||||
(`EVCharger`, `DHW`, `HeatPump`, `Battery`, `SolarMeter`, `GridMeter`) à des Things nymea concrètes.
|
||||
Le `LoadAdapterRegistry` crée ensuite l'adaptateur approprié (relay, SG-Ready, evcharger, battery)
|
||||
et transmet les consignes du Scheduler au matériel réel.
|
||||
|
||||
**Flux en 3 étapes :**
|
||||
1. Activer le rôle → `SetRoleEnabled(role, true)`
|
||||
2. Choisir la Thing → `AssignThingToRole(role, thingId)`
|
||||
3. Tester la connexion → `TestRoleConnection(role)` → notification `ConnectionTestResult`
|
||||
|
||||
### 12.2 Tableau de compatibilité (rôle → interfaces requises)
|
||||
|
||||
| Rôle | Interfaces nymea requises |
|
||||
|---|---|
|
||||
| `EVCharger` | `evcharger` |
|
||||
| `DHW` | `relay` ou `smartmeterconsumer` |
|
||||
| `HeatPump` | `heating` (SG-Ready) ou `relay` |
|
||||
| `Battery` | `energystorage` |
|
||||
| `SolarMeter` | `energymeter` |
|
||||
| `GridMeter` | `energymeter` |
|
||||
|
||||
### 12.3 Détection automatique du type d'adaptateur
|
||||
|
||||
| Rôle | Interface détectée | Adaptateur créé |
|
||||
|---|---|---|
|
||||
| `EVCharger` | `evcharger` | `EvChargerAdapter` |
|
||||
| `DHW` | `relay` | `RelayAdapter` |
|
||||
| `HeatPump` | `heating` | `SgReadyAdapter` (2 relais) |
|
||||
| `HeatPump` | `relay` | `RelayAdapter` |
|
||||
| `Battery` | `energystorage` | `BatteryAdapter` |
|
||||
| `SolarMeter` | `energymeter` | read-only (pas d'adaptateur) |
|
||||
| `GridMeter` | `energymeter` | read-only (pas d'adaptateur) |
|
||||
|
||||
### 12.4 Référence API JSON-RPC v12
|
||||
|
||||
#### `GetSetupStatus` → `{ setupStatus: object }`
|
||||
|
||||
Retourne l'état de tous les rôles.
|
||||
|
||||
```json
|
||||
{
|
||||
"method": "NymeaEnergy.GetSetupStatus",
|
||||
"params": {}
|
||||
}
|
||||
// Réponse:
|
||||
{
|
||||
"setupStatus": {
|
||||
"roles": [
|
||||
{
|
||||
"role": "DHW",
|
||||
"enabled": true,
|
||||
"assigned": true,
|
||||
"assignedThingId": "…uuid…",
|
||||
"assignedThingName": "Ballon ECS",
|
||||
"reachable": true,
|
||||
"adapterType": "relay",
|
||||
"lastError": ""
|
||||
}
|
||||
],
|
||||
"allEnabledRolesOk": true,
|
||||
"configuredCount": 2,
|
||||
"errorCount": 0
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### `GetCompatibleThings` → `{ role, things: [] }`
|
||||
|
||||
```json
|
||||
{ "method": "NymeaEnergy.GetCompatibleThings", "params": { "role": "DHW" } }
|
||||
```
|
||||
|
||||
#### `AssignThingToRole` → `{ energyError, adapterType, detectedParams }`
|
||||
|
||||
```json
|
||||
{
|
||||
"method": "NymeaEnergy.AssignThingToRole",
|
||||
"params": {
|
||||
"role": "HeatPump",
|
||||
"thingId": "…relay1-uuid…",
|
||||
"params": { "relay1ThingId": "…", "relay2ThingId": "…", "normalPowerW": 1500 }
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### `UnassignRole` → `{ energyError }`
|
||||
```json
|
||||
{ "method": "NymeaEnergy.UnassignRole", "params": { "role": "Battery" } }
|
||||
```
|
||||
|
||||
#### `SetRoleEnabled` → `{ energyError }`
|
||||
```json
|
||||
{ "method": "NymeaEnergy.SetRoleEnabled", "params": { "role": "DHW", "enabled": true } }
|
||||
```
|
||||
|
||||
#### `TestRoleConnection` → `{ energyError }` + notification `ConnectionTestResult`
|
||||
```json
|
||||
{ "method": "NymeaEnergy.TestRoleConnection", "params": { "role": "DHW" } }
|
||||
// Notification:
|
||||
{ "notification": "NymeaEnergy.ConnectionTestResult",
|
||||
"params": { "role": "DHW", "success": true, "message": "Connection test OK" } }
|
||||
```
|
||||
|
||||
### 12.5 Notifications push
|
||||
|
||||
| Notification | Payload | Déclencheur |
|
||||
|---|---|---|
|
||||
| `SetupStatusChanged` | `{ setupStatus }` | Assignment, enable, reachability |
|
||||
| `ConnectionTestResult` | `{ role, success, message }` | Fin de `TestRoleConnection` |
|
||||
| `ThingBecameCompatible` | `{ role, thing }` | Nouvelle Thing ajoutée dans nymea |
|
||||
|
||||
### 12.6 Gestion des erreurs — Thing disparue
|
||||
|
||||
Si une Thing assignée disparaît au redémarrage du daemon :
|
||||
- L'entrée dans `adapters.conf` est conservée
|
||||
- `RoleStatus.reachable = false`, `lastError = "Thing not found: <uuid>"`
|
||||
- `SetupStatus.allEnabledRolesOk = false`
|
||||
- Notification `SetupStatusChanged` émise avec l'erreur
|
||||
- Dès que la Thing réapparaît (`ThingManager::thingAdded`), notification `ThingBecameCompatible`
|
||||
|
||||
### 12.7 Exemple complet — EV charger hebdomadaire via Installer Setup
|
||||
|
||||
```json
|
||||
// 1. Activer le rôle
|
||||
{ "method": "NymeaEnergy.SetRoleEnabled",
|
||||
"params": { "role": "EVCharger", "enabled": true } }
|
||||
|
||||
// 2. Lister les Things compatibles
|
||||
{ "method": "NymeaEnergy.GetCompatibleThings",
|
||||
"params": { "role": "EVCharger" } }
|
||||
|
||||
// 3. Assigner la Thing choisie
|
||||
{ "method": "NymeaEnergy.AssignThingToRole",
|
||||
"params": { "role": "EVCharger", "thingId": "…uuid…", "params": { "phases": 1 } } }
|
||||
|
||||
// 4. Tester
|
||||
{ "method": "NymeaEnergy.TestRoleConnection",
|
||||
"params": { "role": "EVCharger" } }
|
||||
// → ConnectionTestResult { "success": true, "message": "EV charger connected" }
|
||||
|
||||
// 5. Configurer un créneau manuel EV
|
||||
{ "method": "NymeaEnergy.SetManualSlot",
|
||||
"params": {
|
||||
"start": "2026-02-23T22:00:00.000Z",
|
||||
"end": "2026-02-24T06:00:00.000Z",
|
||||
"label": "Recharge hebdo VE",
|
||||
"repeating": true,
|
||||
"allocations": { "ev": 2000 }
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 12.8 Persistance
|
||||
|
||||
Les assignements sont sauvegardés dans :
|
||||
```
|
||||
NymeaSettings::settingsPath() + "/adapters.conf"
|
||||
```
|
||||
Format INI, identique à `scheduler.conf`. Chargés automatiquement au démarrage du daemon.
|
||||
|
||||
---
|
||||
|
||||
*Section 12 ajoutée le 2026-02-24 — Installer Setup LoadAdapterRegistry v12*
|
||||
|
||||
126
energyplugin/adapters/adaptersettings.cpp
Normal file
126
energyplugin/adapters/adaptersettings.cpp
Normal file
@ -0,0 +1,126 @@
|
||||
// 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 "adaptersettings.h"
|
||||
|
||||
#include <nymeasettings.h>
|
||||
|
||||
#include <QSettings>
|
||||
#include <QLoggingCategory>
|
||||
|
||||
Q_DECLARE_LOGGING_CATEGORY(dcNymeaEnergy)
|
||||
|
||||
AdapterSettings::AdapterSettings(QObject *parent)
|
||||
: QObject(parent)
|
||||
{
|
||||
load();
|
||||
}
|
||||
|
||||
QString AdapterSettings::settingsFilePath() const
|
||||
{
|
||||
// Allow test override via environment variable
|
||||
const QString envPath = qEnvironmentVariable("NYMEA_ADAPTER_SETTINGS");
|
||||
if (!envPath.isEmpty())
|
||||
return envPath;
|
||||
return NymeaSettings::settingsPath() + QStringLiteral("/adapters.conf");
|
||||
}
|
||||
|
||||
QList<QVariantMap> AdapterSettings::assignments() const
|
||||
{
|
||||
return m_assignments;
|
||||
}
|
||||
|
||||
void AdapterSettings::save(const QList<QVariantMap> &assignments)
|
||||
{
|
||||
m_assignments = assignments;
|
||||
|
||||
QSettings s(settingsFilePath(), QSettings::IniFormat);
|
||||
s.remove(QStringLiteral("roles")); // clear previous
|
||||
|
||||
s.beginWriteArray(QStringLiteral("roles"), m_assignments.size());
|
||||
for (int i = 0; i < m_assignments.size(); ++i) {
|
||||
s.setArrayIndex(i);
|
||||
const QVariantMap &m = m_assignments.at(i);
|
||||
for (auto it = m.constBegin(); it != m.constEnd(); ++it) {
|
||||
s.setValue(it.key(), it.value());
|
||||
}
|
||||
}
|
||||
s.endArray();
|
||||
|
||||
qCDebug(dcNymeaEnergy()) << "AdapterSettings: saved" << m_assignments.size()
|
||||
<< "role assignment(s) to" << settingsFilePath();
|
||||
}
|
||||
|
||||
void AdapterSettings::setAssignment(LoadRole role, const QVariantMap ¶ms)
|
||||
{
|
||||
const QString roleStr = loadRoleToString(role);
|
||||
for (int i = 0; i < m_assignments.size(); ++i) {
|
||||
if (m_assignments.at(i).value(QStringLiteral("role")).toString() == roleStr) {
|
||||
QVariantMap merged = m_assignments.at(i);
|
||||
for (auto it = params.constBegin(); it != params.constEnd(); ++it)
|
||||
merged.insert(it.key(), it.value());
|
||||
merged.insert(QStringLiteral("role"), roleStr);
|
||||
m_assignments[i] = merged;
|
||||
save(m_assignments);
|
||||
return;
|
||||
}
|
||||
}
|
||||
// New assignment
|
||||
QVariantMap m = params;
|
||||
m.insert(QStringLiteral("role"), roleStr);
|
||||
m_assignments.append(m);
|
||||
save(m_assignments);
|
||||
}
|
||||
|
||||
void AdapterSettings::removeAssignment(LoadRole role)
|
||||
{
|
||||
const QString roleStr = loadRoleToString(role);
|
||||
for (int i = 0; i < m_assignments.size(); ++i) {
|
||||
if (m_assignments.at(i).value(QStringLiteral("role")).toString() == roleStr) {
|
||||
m_assignments.removeAt(i);
|
||||
save(m_assignments);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void AdapterSettings::load()
|
||||
{
|
||||
m_assignments.clear();
|
||||
QSettings s(settingsFilePath(), QSettings::IniFormat);
|
||||
|
||||
int count = s.beginReadArray(QStringLiteral("roles"));
|
||||
for (int i = 0; i < count; ++i) {
|
||||
s.setArrayIndex(i);
|
||||
QVariantMap m;
|
||||
foreach (const QString &key, s.childKeys())
|
||||
m.insert(key, s.value(key));
|
||||
if (!m.isEmpty())
|
||||
m_assignments.append(m);
|
||||
}
|
||||
s.endArray();
|
||||
|
||||
qCDebug(dcNymeaEnergy()) << "AdapterSettings: loaded" << m_assignments.size()
|
||||
<< "role assignment(s) from" << settingsFilePath();
|
||||
}
|
||||
84
energyplugin/adapters/adaptersettings.h
Normal file
84
energyplugin/adapters/adaptersettings.h
Normal file
@ -0,0 +1,84 @@
|
||||
// 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/>.
|
||||
*
|
||||
* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
|
||||
|
||||
#ifndef ADAPTERSETTINGS_H
|
||||
#define ADAPTERSETTINGS_H
|
||||
|
||||
#include <QObject>
|
||||
#include <QList>
|
||||
#include <QVariantMap>
|
||||
|
||||
#include "types/loadrole.h"
|
||||
|
||||
// Persists LoadAdapterRegistry role assignments to:
|
||||
// NymeaSettings::settingsPath() + "/adapters.conf" (QSettings INI)
|
||||
//
|
||||
// Section layout:
|
||||
// [roles]
|
||||
// size = N
|
||||
//
|
||||
// [roles/1]
|
||||
// role = DHW
|
||||
// enabled = true
|
||||
// thingId = <uuid>
|
||||
// adapterType = relay
|
||||
// nominalPowerW = 2000
|
||||
//
|
||||
// [roles/2]
|
||||
// role = HeatPump
|
||||
// enabled = true
|
||||
// thingId = <uuid>
|
||||
// adapterType = sgready
|
||||
// relay1ThingId = <uuid>
|
||||
// relay2ThingId = <uuid>
|
||||
// normalPowerW = 1500
|
||||
class AdapterSettings : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit AdapterSettings(QObject *parent = nullptr);
|
||||
|
||||
// Return all saved role assignments (one QVariantMap per role).
|
||||
// Each map contains at least: "role", "enabled", "thingId", "adapterType".
|
||||
QList<QVariantMap> assignments() const;
|
||||
|
||||
// Persist the given set of raw assignment maps (from LoadAdapterRegistry::rawAssignments()).
|
||||
void save(const QList<QVariantMap> &assignments);
|
||||
|
||||
// Override a single role's assignment (loads first, then upserts).
|
||||
void setAssignment(LoadRole role, const QVariantMap ¶ms);
|
||||
|
||||
// Remove a single role's assignment by role name.
|
||||
void removeAssignment(LoadRole role);
|
||||
|
||||
// Path of the settings file (for testing: override via NYMEA_ADAPTER_SETTINGS env var).
|
||||
QString settingsFilePath() const;
|
||||
|
||||
private:
|
||||
void load();
|
||||
|
||||
QList<QVariantMap> m_assignments;
|
||||
};
|
||||
|
||||
#endif // ADAPTERSETTINGS_H
|
||||
160
energyplugin/adapters/batteryadapter.cpp
Normal file
160
energyplugin/adapters/batteryadapter.cpp
Normal file
@ -0,0 +1,160 @@
|
||||
// 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 "batteryadapter.h"
|
||||
|
||||
#include "types/action.h"
|
||||
#include "integrations/thingmanager.h"
|
||||
#include "integrations/thingactioninfo.h"
|
||||
|
||||
#include <QLoggingCategory>
|
||||
|
||||
Q_DECLARE_LOGGING_CATEGORY(dcNymeaEnergy)
|
||||
|
||||
BatteryAdapter::BatteryAdapter(ThingManager *thingManager,
|
||||
const ThingId &thingId,
|
||||
double capacityKwh,
|
||||
double maxChargeW,
|
||||
double maxDischargeW,
|
||||
QObject *parent)
|
||||
: ILoadAdapter(parent),
|
||||
m_thingManager(thingManager),
|
||||
m_capacityKwh(capacityKwh),
|
||||
m_maxChargeW(maxChargeW),
|
||||
m_maxDischargeW(maxDischargeW)
|
||||
{
|
||||
m_thing = thingManager->findConfiguredThing(thingId);
|
||||
if (!m_thing)
|
||||
qCWarning(dcNymeaEnergy()) << "BatteryAdapter: thing not found:" << thingId;
|
||||
}
|
||||
|
||||
QString BatteryAdapter::adapterId() const
|
||||
{
|
||||
return QStringLiteral("battery");
|
||||
}
|
||||
|
||||
LoadRole BatteryAdapter::role() const
|
||||
{
|
||||
return LoadRole::Battery;
|
||||
}
|
||||
|
||||
ThingId BatteryAdapter::thingId() const
|
||||
{
|
||||
return m_thing ? m_thing->id() : ThingId();
|
||||
}
|
||||
|
||||
void BatteryAdapter::applyPower(double targetPowerW)
|
||||
{
|
||||
if (!m_thing) {
|
||||
qCWarning(dcNymeaEnergy()) << "BatteryAdapter::applyPower: no thing";
|
||||
emit powerApplied(0, false);
|
||||
return;
|
||||
}
|
||||
|
||||
QString actionName;
|
||||
double powerArg = 0;
|
||||
|
||||
if (targetPowerW > 0) {
|
||||
actionName = QStringLiteral("setChargingPower");
|
||||
powerArg = qMin(targetPowerW, m_maxChargeW);
|
||||
} else if (targetPowerW < 0) {
|
||||
actionName = QStringLiteral("setDischargingPower");
|
||||
powerArg = qMin(-targetPowerW, m_maxDischargeW);
|
||||
} else {
|
||||
actionName = QStringLiteral("setStandby");
|
||||
}
|
||||
|
||||
ActionType actionType;
|
||||
foreach (const ActionType &at, m_thing->thingClass().actionTypes()) {
|
||||
if (at.name() == actionName) { actionType = at; break; }
|
||||
}
|
||||
if (actionType.id().isNull()) {
|
||||
qCWarning(dcNymeaEnergy()) << "BatteryAdapter: action" << actionName
|
||||
<< "not found on" << m_thing->name();
|
||||
emit powerApplied(0, false);
|
||||
return;
|
||||
}
|
||||
|
||||
Action action(actionType.id(), m_thing->id(), Action::TriggeredByRule);
|
||||
|
||||
// For setStandby there are no params; for charge/discharge add the power param
|
||||
if (targetPowerW != 0) {
|
||||
ParamTypeId powerParamId;
|
||||
foreach (const ParamType &pt, actionType.paramTypes()) {
|
||||
if (pt.name() == QLatin1String("power")) { powerParamId = pt.id(); break; }
|
||||
}
|
||||
if (!powerParamId.isNull())
|
||||
action.setParams(ParamList() << Param(powerParamId, powerArg));
|
||||
}
|
||||
|
||||
ThingActionInfo *info = m_thingManager->executeAction(action);
|
||||
connect(info, &ThingActionInfo::finished, this, [this, targetPowerW, info]() {
|
||||
bool ok = (info->status() == Thing::ThingErrorNoError);
|
||||
emit powerApplied(targetPowerW, ok);
|
||||
if (!ok)
|
||||
qCWarning(dcNymeaEnergy()) << "BatteryAdapter: action failed on" << m_thing->name();
|
||||
});
|
||||
}
|
||||
|
||||
void BatteryAdapter::testConnection()
|
||||
{
|
||||
if (!m_thing) {
|
||||
emit testResult(false, QStringLiteral("Thing not found"));
|
||||
return;
|
||||
}
|
||||
|
||||
// Try reading batteryLevel or currentPower state
|
||||
foreach (const StateType &st, m_thing->thingClass().stateTypes()) {
|
||||
if (st.name() == QLatin1String("batteryLevel") ||
|
||||
st.name() == QLatin1String("currentPower")) {
|
||||
emit testResult(true, QStringLiteral("Battery state readable: %1 = %2")
|
||||
.arg(st.name())
|
||||
.arg(m_thing->stateValue(st.id()).toDouble()));
|
||||
return;
|
||||
}
|
||||
}
|
||||
emit testResult(true, QStringLiteral("Battery reachable"));
|
||||
}
|
||||
|
||||
double BatteryAdapter::currentPowerW() const
|
||||
{
|
||||
if (!m_thing)
|
||||
return 0;
|
||||
foreach (const StateType &st, m_thing->thingClass().stateTypes()) {
|
||||
if (st.name() == QLatin1String("currentPower"))
|
||||
return m_thing->stateValue(st.id()).toDouble();
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
bool BatteryAdapter::isReachable() const
|
||||
{
|
||||
if (!m_thing)
|
||||
return false;
|
||||
foreach (const StateType &st, m_thing->thingClass().stateTypes()) {
|
||||
if (st.name() == QLatin1String("connected"))
|
||||
return m_thing->stateValue(st.id()).toBool();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
65
energyplugin/adapters/batteryadapter.h
Normal file
65
energyplugin/adapters/batteryadapter.h
Normal file
@ -0,0 +1,65 @@
|
||||
// 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/>.
|
||||
*
|
||||
* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
|
||||
|
||||
#ifndef BATTERYADAPTER_H
|
||||
#define BATTERYADAPTER_H
|
||||
|
||||
#include "adapters/iloadapter.h"
|
||||
|
||||
#include <integrations/thingmanager.h>
|
||||
|
||||
// Battery adapter for bidirectional energy storage.
|
||||
//
|
||||
// Power convention:
|
||||
// applyPower(W > 0) → charge at W watts
|
||||
// applyPower(W < 0) → discharge at |W| watts
|
||||
// applyPower(0) → standby
|
||||
class BatteryAdapter : public ILoadAdapter
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit BatteryAdapter(ThingManager *thingManager,
|
||||
const ThingId &thingId,
|
||||
double capacityKwh = 10.0,
|
||||
double maxChargeW = 5000.0,
|
||||
double maxDischargeW = 5000.0,
|
||||
QObject *parent = nullptr);
|
||||
|
||||
QString adapterId() const override;
|
||||
LoadRole role() const override;
|
||||
ThingId thingId() const override;
|
||||
void applyPower(double targetPowerW) override;
|
||||
void testConnection() override;
|
||||
double currentPowerW() const override;
|
||||
bool isReachable() const override;
|
||||
|
||||
private:
|
||||
ThingManager *m_thingManager = nullptr;
|
||||
Thing *m_thing = nullptr;
|
||||
double m_capacityKwh;
|
||||
double m_maxChargeW;
|
||||
double m_maxDischargeW;
|
||||
};
|
||||
|
||||
#endif // BATTERYADAPTER_H
|
||||
168
energyplugin/adapters/evchargeradapter.cpp
Normal file
168
energyplugin/adapters/evchargeradapter.cpp
Normal file
@ -0,0 +1,168 @@
|
||||
// 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 "evchargeradapter.h"
|
||||
|
||||
#include "types/action.h"
|
||||
#include "integrations/thingmanager.h"
|
||||
#include "integrations/thingactioninfo.h"
|
||||
|
||||
#include <QLoggingCategory>
|
||||
#include <QtMath>
|
||||
|
||||
Q_DECLARE_LOGGING_CATEGORY(dcNymeaEnergy)
|
||||
|
||||
EvChargerAdapter::EvChargerAdapter(ThingManager *thingManager,
|
||||
const ThingId &thingId,
|
||||
int phases,
|
||||
int minA,
|
||||
int maxA,
|
||||
QObject *parent)
|
||||
: ILoadAdapter(parent),
|
||||
m_thingManager(thingManager),
|
||||
m_phases(phases),
|
||||
m_minA(minA),
|
||||
m_maxA(maxA)
|
||||
{
|
||||
m_thing = thingManager->findConfiguredThing(thingId);
|
||||
if (!m_thing)
|
||||
qCWarning(dcNymeaEnergy()) << "EvChargerAdapter: thing not found:" << thingId;
|
||||
}
|
||||
|
||||
QString EvChargerAdapter::adapterId() const
|
||||
{
|
||||
return QStringLiteral("evcharger");
|
||||
}
|
||||
|
||||
LoadRole EvChargerAdapter::role() const
|
||||
{
|
||||
return LoadRole::EVCharger;
|
||||
}
|
||||
|
||||
ThingId EvChargerAdapter::thingId() const
|
||||
{
|
||||
return m_thing ? m_thing->id() : ThingId();
|
||||
}
|
||||
|
||||
void EvChargerAdapter::applyPower(double targetPowerW)
|
||||
{
|
||||
if (!m_thing) {
|
||||
qCWarning(dcNymeaEnergy()) << "EvChargerAdapter::applyPower: no thing";
|
||||
emit powerApplied(0, false);
|
||||
return;
|
||||
}
|
||||
|
||||
if (targetPowerW <= 0) {
|
||||
// Disable charging
|
||||
ActionType powerActionType;
|
||||
foreach (const ActionType &at, m_thing->thingClass().actionTypes()) {
|
||||
if (at.name() == QLatin1String("power")) { powerActionType = at; break; }
|
||||
}
|
||||
if (powerActionType.id().isNull()) {
|
||||
qCWarning(dcNymeaEnergy()) << "EvChargerAdapter: no 'power' action on" << m_thing->name();
|
||||
emit powerApplied(0, false);
|
||||
return;
|
||||
}
|
||||
ParamTypeId powerParamId;
|
||||
foreach (const ParamType &pt, powerActionType.paramTypes()) {
|
||||
if (pt.name() == QLatin1String("power")) { powerParamId = pt.id(); break; }
|
||||
}
|
||||
Action action(powerActionType.id(), m_thing->id(), Action::TriggeredByRule);
|
||||
action.setParams(ParamList() << Param(powerParamId, false));
|
||||
ThingActionInfo *info = m_thingManager->executeAction(action);
|
||||
connect(info, &ThingActionInfo::finished, this, [this, info]() {
|
||||
emit powerApplied(0, info->status() == Thing::ThingErrorNoError);
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Convert W → A, clamped to [minA, maxA]
|
||||
double amps = targetPowerW / (m_phases * 230.0);
|
||||
int clampedA = static_cast<int>(qBound(static_cast<double>(m_minA), amps, static_cast<double>(m_maxA)));
|
||||
|
||||
// Find "maxChargingCurrent" action type (evcharger interface)
|
||||
ActionType currentActionType;
|
||||
foreach (const ActionType &at, m_thing->thingClass().actionTypes()) {
|
||||
if (at.name() == QLatin1String("maxChargingCurrent")) { currentActionType = at; break; }
|
||||
}
|
||||
if (currentActionType.id().isNull()) {
|
||||
qCWarning(dcNymeaEnergy()) << "EvChargerAdapter: no 'maxChargingCurrent' action on" << m_thing->name();
|
||||
emit powerApplied(0, false);
|
||||
return;
|
||||
}
|
||||
ParamTypeId currentParamId;
|
||||
foreach (const ParamType &pt, currentActionType.paramTypes()) {
|
||||
if (pt.name() == QLatin1String("maxChargingCurrent")) { currentParamId = pt.id(); break; }
|
||||
}
|
||||
Action action(currentActionType.id(), m_thing->id(), Action::TriggeredByRule);
|
||||
action.setParams(ParamList() << Param(currentParamId, clampedA));
|
||||
ThingActionInfo *info = m_thingManager->executeAction(action);
|
||||
double appliedW = clampedA * m_phases * 230.0;
|
||||
connect(info, &ThingActionInfo::finished, this, [this, appliedW, info]() {
|
||||
emit powerApplied(appliedW, info->status() == Thing::ThingErrorNoError);
|
||||
});
|
||||
}
|
||||
|
||||
void EvChargerAdapter::testConnection()
|
||||
{
|
||||
if (!m_thing) {
|
||||
emit testResult(false, QStringLiteral("Thing not found"));
|
||||
return;
|
||||
}
|
||||
|
||||
// Check "connected" state
|
||||
foreach (const StateType &st, m_thing->thingClass().stateTypes()) {
|
||||
if (st.name() == QLatin1String("connected")) {
|
||||
bool connected = m_thing->stateValue(st.id()).toBool();
|
||||
emit testResult(connected,
|
||||
connected ? QStringLiteral("EV charger connected")
|
||||
: QStringLiteral("EV charger not connected"));
|
||||
return;
|
||||
}
|
||||
}
|
||||
// No "connected" state — assume reachable
|
||||
emit testResult(true, QStringLiteral("EV charger reachable"));
|
||||
}
|
||||
|
||||
double EvChargerAdapter::currentPowerW() const
|
||||
{
|
||||
if (!m_thing)
|
||||
return 0;
|
||||
foreach (const StateType &st, m_thing->thingClass().stateTypes()) {
|
||||
if (st.name() == QLatin1String("currentPower"))
|
||||
return m_thing->stateValue(st.id()).toDouble();
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
bool EvChargerAdapter::isReachable() const
|
||||
{
|
||||
if (!m_thing)
|
||||
return false;
|
||||
foreach (const StateType &st, m_thing->thingClass().stateTypes()) {
|
||||
if (st.name() == QLatin1String("connected"))
|
||||
return m_thing->stateValue(st.id()).toBool();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
66
energyplugin/adapters/evchargeradapter.h
Normal file
66
energyplugin/adapters/evchargeradapter.h
Normal file
@ -0,0 +1,66 @@
|
||||
// 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/>.
|
||||
*
|
||||
* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
|
||||
|
||||
#ifndef EVCHARGERADAPTER_H
|
||||
#define EVCHARGERADAPTER_H
|
||||
|
||||
#include "adapters/iloadapter.h"
|
||||
|
||||
#include <integrations/thingmanager.h>
|
||||
|
||||
// EV charger adapter — translates a power setpoint into a charging current.
|
||||
//
|
||||
// Converts W → A using: amps = targetPowerW / (phases * 230)
|
||||
// Clamps to [minA, maxA] as supported by the charger.
|
||||
//
|
||||
// Note: Coexists with SmartChargingManager which uses its own EvCharger wrappers.
|
||||
// EVChargerAdapter is only exercised when the Scheduler explicitly allocates EV power.
|
||||
class EvChargerAdapter : public ILoadAdapter
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit EvChargerAdapter(ThingManager *thingManager,
|
||||
const ThingId &thingId,
|
||||
int phases = 1,
|
||||
int minA = 6,
|
||||
int maxA = 32,
|
||||
QObject *parent = nullptr);
|
||||
|
||||
QString adapterId() const override;
|
||||
LoadRole role() const override;
|
||||
ThingId thingId() const override;
|
||||
void applyPower(double targetPowerW) override;
|
||||
void testConnection() override;
|
||||
double currentPowerW() const override;
|
||||
bool isReachable() const override;
|
||||
|
||||
private:
|
||||
ThingManager *m_thingManager = nullptr;
|
||||
Thing *m_thing = nullptr;
|
||||
int m_phases;
|
||||
int m_minA;
|
||||
int m_maxA;
|
||||
};
|
||||
|
||||
#endif // EVCHARGERADAPTER_H
|
||||
86
energyplugin/adapters/iloadapter.h
Normal file
86
energyplugin/adapters/iloadapter.h
Normal file
@ -0,0 +1,86 @@
|
||||
// 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/>.
|
||||
*
|
||||
* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
|
||||
|
||||
#ifndef ILOADAPTER_H
|
||||
#define ILOADAPTER_H
|
||||
|
||||
#include <QObject>
|
||||
#include <QString>
|
||||
|
||||
#include <integrations/thing.h>
|
||||
|
||||
#include "types/loadrole.h"
|
||||
|
||||
// Abstract interface for all load adapters.
|
||||
//
|
||||
// Each adapter wraps a nymea Thing and knows how to:
|
||||
// - Apply a target power setpoint (applyPower)
|
||||
// - Test the physical connection (testConnection)
|
||||
// - Report current power draw and reachability
|
||||
//
|
||||
// Adapters are created by LoadAdapterRegistry based on the detected adapter type
|
||||
// for a given role + Thing combination.
|
||||
class ILoadAdapter : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit ILoadAdapter(QObject *parent = nullptr) : QObject(parent) {}
|
||||
virtual ~ILoadAdapter() = default;
|
||||
|
||||
// Unique adapter type identifier (e.g. "relay", "sgready", "evcharger", "battery")
|
||||
virtual QString adapterId() const = 0;
|
||||
|
||||
// Role this adapter serves
|
||||
virtual LoadRole role() const = 0;
|
||||
|
||||
// Underlying nymea ThingId
|
||||
virtual ThingId thingId() const = 0;
|
||||
|
||||
// Apply a target power setpoint in watts.
|
||||
// > 0 → charge/heat at this power
|
||||
// = 0 → stop / standby
|
||||
// < 0 → discharge (batteries only)
|
||||
virtual void applyPower(double targetPowerW) = 0;
|
||||
|
||||
// Initiate a connection test. Result is delivered via testResult() signal.
|
||||
virtual void testConnection() = 0;
|
||||
|
||||
// Current reported power draw in watts (from Thing state, may be 0 if not metered).
|
||||
virtual double currentPowerW() const = 0;
|
||||
|
||||
// Whether the underlying Thing is currently reachable.
|
||||
virtual bool isReachable() const = 0;
|
||||
|
||||
signals:
|
||||
// Emitted after testConnection() completes.
|
||||
void testResult(bool success, const QString &message);
|
||||
|
||||
// Emitted after applyPower() completes.
|
||||
void powerApplied(double powerW, bool success);
|
||||
|
||||
// Emitted when the reachability of the underlying Thing changes.
|
||||
void reachabilityChanged(bool reachable);
|
||||
};
|
||||
|
||||
#endif // ILOADAPTER_H
|
||||
408
energyplugin/adapters/loadadapterregistry.cpp
Normal file
408
energyplugin/adapters/loadadapterregistry.cpp
Normal file
@ -0,0 +1,408 @@
|
||||
// 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;
|
||||
}
|
||||
151
energyplugin/adapters/loadadapterregistry.h
Normal file
151
energyplugin/adapters/loadadapterregistry.h
Normal file
@ -0,0 +1,151 @@
|
||||
// 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/>.
|
||||
*
|
||||
* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
|
||||
|
||||
#ifndef LOADADAPTERREGISTRY_H
|
||||
#define LOADADAPTERREGISTRY_H
|
||||
|
||||
#include <QObject>
|
||||
#include <QHash>
|
||||
#include <QList>
|
||||
#include <QVariantMap>
|
||||
#include <QString>
|
||||
|
||||
#include <integrations/thingmanager.h>
|
||||
|
||||
#include "types/loadrole.h"
|
||||
#include "adapters/iloadapter.h"
|
||||
|
||||
class AdapterSettings;
|
||||
|
||||
// Summarises a nymea Thing that is compatible with a given role.
|
||||
struct ThingInfo {
|
||||
ThingId thingId;
|
||||
QString displayName;
|
||||
QString pluginName;
|
||||
QStringList interfaces;
|
||||
};
|
||||
|
||||
// Per-role status, serialisable to JSON-RPC.
|
||||
struct RoleStatus {
|
||||
LoadRole role;
|
||||
bool enabled = false;
|
||||
bool assigned = false;
|
||||
ThingId assignedThingId;
|
||||
QString assignedThingName;
|
||||
bool reachable = false;
|
||||
QString adapterType;
|
||||
QString lastError;
|
||||
};
|
||||
|
||||
// Aggregated setup status across all roles.
|
||||
struct SetupStatus {
|
||||
QList<RoleStatus> roles;
|
||||
bool allEnabledRolesOk = false;
|
||||
int configuredCount = 0;
|
||||
int errorCount = 0;
|
||||
};
|
||||
|
||||
// LoadAdapterRegistry maps installer-visible roles to nymea Things and
|
||||
// creates the appropriate ILoadAdapter for each assignment.
|
||||
//
|
||||
// Usage:
|
||||
// LoadAdapterRegistry *reg = new LoadAdapterRegistry(thingManager, this);
|
||||
// reg->loadFromSettings(adapterSettings);
|
||||
// reg->assignThing(LoadRole::DHW, dhwThingId, {});
|
||||
// reg->adapterForRole(LoadRole::DHW)->applyPower(2000);
|
||||
class LoadAdapterRegistry : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit LoadAdapterRegistry(ThingManager *thingManager, QObject *parent = nullptr);
|
||||
|
||||
// Load saved assignments from AdapterSettings.
|
||||
// Call once after construction; safe to call with nullptr (no-op).
|
||||
void loadFromSettings(AdapterSettings *settings);
|
||||
|
||||
// List all configured Things compatible with the given role.
|
||||
// Filters by the required interfaces for the role.
|
||||
QList<ThingInfo> compatibleThings(LoadRole role) const;
|
||||
|
||||
// Assign a Thing to a role.
|
||||
// 'params' is adapter-specific:
|
||||
// relay: { nominalPowerW: double }
|
||||
// sgready: { relay1ThingId: uuid, relay2ThingId: uuid, normalPowerW: double }
|
||||
// evcharger:{ phases: int, minA: int, maxA: int }
|
||||
// battery: { capacityKwh: double, maxChargeW: double, maxDischargeW: double }
|
||||
// Returns the detected adapter type, or an error string prefixed with "error:".
|
||||
QString assignThing(LoadRole role, const ThingId &thingId, const QVariantMap ¶ms);
|
||||
|
||||
// Unassign a role; destroys the underlying adapter.
|
||||
void unassignRole(LoadRole role);
|
||||
|
||||
// Return the adapter for a role, or nullptr if not assigned.
|
||||
ILoadAdapter *adapterForRole(LoadRole role) const;
|
||||
|
||||
bool isRoleAssigned(LoadRole role) const;
|
||||
bool isRoleEnabled(LoadRole role) const;
|
||||
void setRoleEnabled(LoadRole role, bool enabled);
|
||||
|
||||
// Initiate a connection test for the role's adapter.
|
||||
// Result is delivered via connectionTestResult() signal.
|
||||
void testConnection(LoadRole role);
|
||||
|
||||
// Current setup status across all roles.
|
||||
SetupStatus setupStatus() const;
|
||||
|
||||
// Raw assignment maps for persistence (one map per assigned role).
|
||||
QList<QVariantMap> rawAssignments() const;
|
||||
|
||||
// Static: determine adapter type from role + Thing interfaces (no ThingManager needed).
|
||||
// Returns "relay", "sgready", "evcharger", "battery", or "readonly".
|
||||
static QString detectAdapterType(LoadRole role, const QStringList &interfaces);
|
||||
|
||||
signals:
|
||||
void roleAssigned(LoadRole role, ThingId thingId);
|
||||
void roleUnassigned(LoadRole role);
|
||||
void roleEnabledChanged(LoadRole role, bool enabled);
|
||||
void connectionTestResult(LoadRole role, bool success, const QString &message);
|
||||
void setupStatusChanged(const SetupStatus &status);
|
||||
void thingBecameCompatible(LoadRole role, const ThingInfo &info);
|
||||
|
||||
private:
|
||||
struct RoleEntry {
|
||||
bool enabled = false;
|
||||
ILoadAdapter *adapter = nullptr;
|
||||
QVariantMap rawParams;
|
||||
QString lastError;
|
||||
};
|
||||
|
||||
void onThingAdded(Thing *thing);
|
||||
ILoadAdapter *createAdapter(LoadRole role,
|
||||
const ThingId &thingId,
|
||||
const QString &adapterType,
|
||||
const QVariantMap ¶ms);
|
||||
RoleStatus buildRoleStatus(LoadRole role) const;
|
||||
|
||||
ThingManager *m_thingManager;
|
||||
QHash<LoadRole, RoleEntry> m_entries;
|
||||
};
|
||||
|
||||
#endif // LOADADAPTERREGISTRY_H
|
||||
151
energyplugin/adapters/relayadapter.cpp
Normal file
151
energyplugin/adapters/relayadapter.cpp
Normal file
@ -0,0 +1,151 @@
|
||||
// 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 "relayadapter.h"
|
||||
|
||||
#include "types/action.h"
|
||||
#include "integrations/thingmanager.h"
|
||||
#include "integrations/thingactioninfo.h"
|
||||
|
||||
#include <QLoggingCategory>
|
||||
#include <QTimer>
|
||||
|
||||
Q_DECLARE_LOGGING_CATEGORY(dcNymeaEnergy)
|
||||
|
||||
RelayAdapter::RelayAdapter(ThingManager *thingManager,
|
||||
const ThingId &thingId,
|
||||
LoadRole role,
|
||||
double nominalPowerW,
|
||||
QObject *parent)
|
||||
: ILoadAdapter(parent),
|
||||
m_thingManager(thingManager),
|
||||
m_role(role),
|
||||
m_nominalPowerW(nominalPowerW)
|
||||
{
|
||||
m_thing = thingManager->findConfiguredThing(thingId);
|
||||
if (!m_thing)
|
||||
qCWarning(dcNymeaEnergy()) << "RelayAdapter: thing not found:" << thingId;
|
||||
}
|
||||
|
||||
QString RelayAdapter::adapterId() const
|
||||
{
|
||||
return QStringLiteral("relay");
|
||||
}
|
||||
|
||||
LoadRole RelayAdapter::role() const
|
||||
{
|
||||
return m_role;
|
||||
}
|
||||
|
||||
ThingId RelayAdapter::thingId() const
|
||||
{
|
||||
return m_thing ? m_thing->id() : ThingId();
|
||||
}
|
||||
|
||||
void RelayAdapter::applyPower(double targetPowerW)
|
||||
{
|
||||
if (!m_thing) {
|
||||
qCWarning(dcNymeaEnergy()) << "RelayAdapter::applyPower: no thing available";
|
||||
emit powerApplied(0, false);
|
||||
return;
|
||||
}
|
||||
setPower(targetPowerW > 0);
|
||||
}
|
||||
|
||||
void RelayAdapter::testConnection()
|
||||
{
|
||||
if (!m_thing) {
|
||||
emit testResult(false, QStringLiteral("Thing not found"));
|
||||
return;
|
||||
}
|
||||
|
||||
// Toggle on → wait 2s → toggle off → report success
|
||||
setPower(true);
|
||||
QTimer::singleShot(2000, this, [this]() {
|
||||
setPower(false);
|
||||
emit testResult(true, QStringLiteral("Connection test OK"));
|
||||
});
|
||||
}
|
||||
|
||||
double RelayAdapter::currentPowerW() const
|
||||
{
|
||||
return m_relayOn ? m_nominalPowerW : 0.0;
|
||||
}
|
||||
|
||||
bool RelayAdapter::isReachable() const
|
||||
{
|
||||
if (!m_thing)
|
||||
return false;
|
||||
|
||||
// If the ThingClass has a "connected" state, use it; otherwise assume reachable.
|
||||
foreach (const StateType &st, m_thing->thingClass().stateTypes()) {
|
||||
if (st.name() == QLatin1String("connected"))
|
||||
return m_thing->stateValue(st.id()).toBool();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void RelayAdapter::setPower(bool on)
|
||||
{
|
||||
if (!m_thing)
|
||||
return;
|
||||
|
||||
// Find the "power" action type (relay interface)
|
||||
ActionType powerActionType;
|
||||
foreach (const ActionType &at, m_thing->thingClass().actionTypes()) {
|
||||
if (at.name() == QLatin1String("power")) {
|
||||
powerActionType = at;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (powerActionType.id().isNull()) {
|
||||
qCWarning(dcNymeaEnergy()) << "RelayAdapter: no 'power' action type on thing"
|
||||
<< m_thing->name();
|
||||
emit powerApplied(on ? m_nominalPowerW : 0, false);
|
||||
return;
|
||||
}
|
||||
|
||||
// Find the "power" param type
|
||||
ParamTypeId powerParamTypeId;
|
||||
foreach (const ParamType &pt, powerActionType.paramTypes()) {
|
||||
if (pt.name() == QLatin1String("power")) {
|
||||
powerParamTypeId = pt.id();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
Action action(powerActionType.id(), m_thing->id(), Action::TriggeredByRule);
|
||||
action.setParams(ParamList() << Param(powerParamTypeId, on));
|
||||
|
||||
ThingActionInfo *info = m_thingManager->executeAction(action);
|
||||
connect(info, &ThingActionInfo::finished, this, [this, on, info]() {
|
||||
bool ok = (info->status() == Thing::ThingErrorNoError);
|
||||
if (ok)
|
||||
m_relayOn = on;
|
||||
emit powerApplied(on ? m_nominalPowerW : 0, ok);
|
||||
if (!ok)
|
||||
qCWarning(dcNymeaEnergy()) << "RelayAdapter: action failed on" << m_thing->name();
|
||||
});
|
||||
}
|
||||
64
energyplugin/adapters/relayadapter.h
Normal file
64
energyplugin/adapters/relayadapter.h
Normal file
@ -0,0 +1,64 @@
|
||||
// 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/>.
|
||||
*
|
||||
* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
|
||||
|
||||
#ifndef RELAYADAPTER_H
|
||||
#define RELAYADAPTER_H
|
||||
|
||||
#include "adapters/iloadapter.h"
|
||||
|
||||
#include <integrations/thingmanager.h>
|
||||
|
||||
// Relay-based adapter for DHW tanks and simple heat pumps.
|
||||
//
|
||||
// Maps the power setpoint to a binary on/off relay action.
|
||||
// A nominal power (watts) is used to report currentPowerW() when the relay is on.
|
||||
class RelayAdapter : public ILoadAdapter
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit RelayAdapter(ThingManager *thingManager,
|
||||
const ThingId &thingId,
|
||||
LoadRole role,
|
||||
double nominalPowerW = 2000.0,
|
||||
QObject *parent = nullptr);
|
||||
|
||||
QString adapterId() const override;
|
||||
LoadRole role() const override;
|
||||
ThingId thingId() const override;
|
||||
void applyPower(double targetPowerW) override;
|
||||
void testConnection() override;
|
||||
double currentPowerW() const override;
|
||||
bool isReachable() const override;
|
||||
|
||||
private:
|
||||
void setPower(bool on);
|
||||
|
||||
ThingManager *m_thingManager = nullptr;
|
||||
Thing *m_thing = nullptr;
|
||||
LoadRole m_role;
|
||||
double m_nominalPowerW;
|
||||
bool m_relayOn = false;
|
||||
};
|
||||
|
||||
#endif // RELAYADAPTER_H
|
||||
176
energyplugin/adapters/sgreadyadapter.cpp
Normal file
176
energyplugin/adapters/sgreadyadapter.cpp
Normal file
@ -0,0 +1,176 @@
|
||||
// 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 "sgreadyadapter.h"
|
||||
|
||||
#include "types/action.h"
|
||||
#include "integrations/thingmanager.h"
|
||||
#include "integrations/thingactioninfo.h"
|
||||
|
||||
#include <QLoggingCategory>
|
||||
#include <QTimer>
|
||||
|
||||
Q_DECLARE_LOGGING_CATEGORY(dcNymeaEnergy)
|
||||
|
||||
SgReadyAdapter::SgReadyAdapter(ThingManager *thingManager,
|
||||
const ThingId &relay1ThingId,
|
||||
const ThingId &relay2ThingId,
|
||||
double normalPowerW,
|
||||
QObject *parent)
|
||||
: ILoadAdapter(parent),
|
||||
m_thingManager(thingManager),
|
||||
m_normalPowerW(normalPowerW)
|
||||
{
|
||||
m_relay1 = thingManager->findConfiguredThing(relay1ThingId);
|
||||
m_relay2 = thingManager->findConfiguredThing(relay2ThingId);
|
||||
|
||||
if (!m_relay1)
|
||||
qCWarning(dcNymeaEnergy()) << "SgReadyAdapter: relay1 not found:" << relay1ThingId;
|
||||
if (!m_relay2)
|
||||
qCWarning(dcNymeaEnergy()) << "SgReadyAdapter: relay2 not found:" << relay2ThingId;
|
||||
}
|
||||
|
||||
QString SgReadyAdapter::adapterId() const
|
||||
{
|
||||
return QStringLiteral("sgready");
|
||||
}
|
||||
|
||||
LoadRole SgReadyAdapter::role() const
|
||||
{
|
||||
return LoadRole::HeatPump;
|
||||
}
|
||||
|
||||
ThingId SgReadyAdapter::thingId() const
|
||||
{
|
||||
return m_relay1 ? m_relay1->id() : ThingId();
|
||||
}
|
||||
|
||||
void SgReadyAdapter::applyPower(double targetPowerW)
|
||||
{
|
||||
SgMode mode;
|
||||
if (targetPowerW <= 0)
|
||||
mode = SgMode::SG1_Blocked;
|
||||
else if (targetPowerW < m_normalPowerW)
|
||||
mode = SgMode::SG2_Normal;
|
||||
else if (qFuzzyCompare(targetPowerW, m_normalPowerW))
|
||||
mode = SgMode::SG3_Surplus;
|
||||
else
|
||||
mode = SgMode::SG4_Boost;
|
||||
|
||||
applyMode(mode);
|
||||
}
|
||||
|
||||
void SgReadyAdapter::testConnection()
|
||||
{
|
||||
if (!m_relay1 || !m_relay2) {
|
||||
emit testResult(false, QStringLiteral("One or both relay Things not found"));
|
||||
return;
|
||||
}
|
||||
|
||||
// SG2 → wait 1s → SG3 → wait 1s → SG2 → emit success
|
||||
applyMode(SgMode::SG2_Normal);
|
||||
QTimer::singleShot(1000, this, [this]() {
|
||||
applyMode(SgMode::SG3_Surplus);
|
||||
QTimer::singleShot(1000, this, [this]() {
|
||||
applyMode(SgMode::SG2_Normal);
|
||||
emit testResult(true, QStringLiteral("SG-Ready connection test OK"));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
double SgReadyAdapter::currentPowerW() const
|
||||
{
|
||||
switch (m_currentMode) {
|
||||
case SgMode::SG1_Blocked: return 0;
|
||||
case SgMode::SG2_Normal: return m_normalPowerW;
|
||||
case SgMode::SG3_Surplus: return m_normalPowerW * 1.2;
|
||||
case SgMode::SG4_Boost: return m_normalPowerW * 1.5;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
bool SgReadyAdapter::isReachable() const
|
||||
{
|
||||
auto checkConnected = [](Thing *thing) -> bool {
|
||||
if (!thing) return false;
|
||||
foreach (const StateType &st, thing->thingClass().stateTypes()) {
|
||||
if (st.name() == QLatin1String("connected"))
|
||||
return thing->stateValue(st.id()).toBool();
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
return checkConnected(m_relay1) && checkConnected(m_relay2);
|
||||
}
|
||||
|
||||
void SgReadyAdapter::applyMode(SgMode mode)
|
||||
{
|
||||
// SG-Ready contact matrix:
|
||||
// SG1: r1=OFF r2=OFF (blocked)
|
||||
// SG2: r1=OFF r2=ON (normal)
|
||||
// SG3: r1=ON r2=OFF (surplus)
|
||||
// SG4: r1=ON r2=ON (boost)
|
||||
bool r1on = (mode == SgMode::SG3_Surplus || mode == SgMode::SG4_Boost);
|
||||
bool r2on = (mode == SgMode::SG2_Normal || mode == SgMode::SG4_Boost);
|
||||
|
||||
setRelayPower(m_relay1, r1on);
|
||||
setRelayPower(m_relay2, r2on);
|
||||
|
||||
m_currentMode = mode;
|
||||
|
||||
qCDebug(dcNymeaEnergy()) << "SgReadyAdapter: mode" << static_cast<int>(mode)
|
||||
<< "r1=" << r1on << "r2=" << r2on;
|
||||
|
||||
emit powerApplied(currentPowerW(), true);
|
||||
}
|
||||
|
||||
void SgReadyAdapter::setRelayPower(Thing *thing, bool on)
|
||||
{
|
||||
if (!thing)
|
||||
return;
|
||||
|
||||
ActionType powerActionType;
|
||||
foreach (const ActionType &at, thing->thingClass().actionTypes()) {
|
||||
if (at.name() == QLatin1String("power")) {
|
||||
powerActionType = at;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (powerActionType.id().isNull()) {
|
||||
qCWarning(dcNymeaEnergy()) << "SgReadyAdapter: no 'power' action on" << thing->name();
|
||||
return;
|
||||
}
|
||||
|
||||
ParamTypeId powerParamTypeId;
|
||||
foreach (const ParamType &pt, powerActionType.paramTypes()) {
|
||||
if (pt.name() == QLatin1String("power")) {
|
||||
powerParamTypeId = pt.id();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
Action action(powerActionType.id(), thing->id(), Action::TriggeredByRule);
|
||||
action.setParams(ParamList() << Param(powerParamTypeId, on));
|
||||
m_thingManager->executeAction(action);
|
||||
}
|
||||
75
energyplugin/adapters/sgreadyadapter.h
Normal file
75
energyplugin/adapters/sgreadyadapter.h
Normal file
@ -0,0 +1,75 @@
|
||||
// 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/>.
|
||||
*
|
||||
* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
|
||||
|
||||
#ifndef SGREADYADAPTER_H
|
||||
#define SGREADYADAPTER_H
|
||||
|
||||
#include "adapters/iloadapter.h"
|
||||
|
||||
#include <integrations/thingmanager.h>
|
||||
|
||||
// SG-Ready adapter for heat pumps controlled via two relay contacts.
|
||||
//
|
||||
// SG-Ready uses 2 binary relay contacts to select operating mode:
|
||||
// SG1 (blocked): relay1=OFF, relay2=OFF — forced off
|
||||
// SG2 (normal): relay1=OFF, relay2=ON — normal operation
|
||||
// SG3 (surplus): relay1=ON, relay2=OFF — raise setpoint (surplus mode)
|
||||
// SG4 (boost): relay1=ON, relay2=ON — forced max heat
|
||||
//
|
||||
// Power mapping:
|
||||
// applyPower(0) → SG1 (blocked)
|
||||
// applyPower(<normalPowerW) → SG2 (normal)
|
||||
// applyPower(normalPowerW) → SG3 (surplus)
|
||||
// applyPower(>normalPowerW) → SG4 (boost)
|
||||
class SgReadyAdapter : public ILoadAdapter
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit SgReadyAdapter(ThingManager *thingManager,
|
||||
const ThingId &relay1ThingId,
|
||||
const ThingId &relay2ThingId,
|
||||
double normalPowerW = 1500.0,
|
||||
QObject *parent = nullptr);
|
||||
|
||||
QString adapterId() const override;
|
||||
LoadRole role() const override;
|
||||
ThingId thingId() const override; // returns relay1 ThingId
|
||||
void applyPower(double targetPowerW) override;
|
||||
void testConnection() override;
|
||||
double currentPowerW() const override;
|
||||
bool isReachable() const override;
|
||||
|
||||
private:
|
||||
enum class SgMode { SG1_Blocked, SG2_Normal, SG3_Surplus, SG4_Boost };
|
||||
void applyMode(SgMode mode);
|
||||
void setRelayPower(Thing *thing, bool on);
|
||||
|
||||
ThingManager *m_thingManager = nullptr;
|
||||
Thing *m_relay1 = nullptr;
|
||||
Thing *m_relay2 = nullptr;
|
||||
double m_normalPowerW;
|
||||
SgMode m_currentMode = SgMode::SG2_Normal;
|
||||
};
|
||||
|
||||
#endif // SGREADYADAPTER_H
|
||||
@ -45,11 +45,19 @@ HEADERS += \
|
||||
$$PWD/types/chargingschedule.h \
|
||||
$$PWD/types/energytimeslot.h \
|
||||
$$PWD/types/flexibleload.h \
|
||||
$$PWD/types/loadrole.h \
|
||||
$$PWD/types/manualslotconfig.h \
|
||||
$$PWD/types/schedulerconfig.h \
|
||||
$$PWD/types/scoreentry.h \
|
||||
$$PWD/types/smartchargingstate.h \
|
||||
$$PWD/types/timeframe.h \
|
||||
$$PWD/adapters/iloadapter.h \
|
||||
$$PWD/adapters/relayadapter.h \
|
||||
$$PWD/adapters/sgreadyadapter.h \
|
||||
$$PWD/adapters/evchargeradapter.h \
|
||||
$$PWD/adapters/batteryadapter.h \
|
||||
$$PWD/adapters/loadadapterregistry.h \
|
||||
$$PWD/adapters/adaptersettings.h \
|
||||
|
||||
SOURCES += \
|
||||
$$PWD/energymanagerconfiguration.cpp \
|
||||
@ -77,3 +85,9 @@ SOURCES += \
|
||||
$$PWD/types/scoreentry.cpp \
|
||||
$$PWD/types/smartchargingstate.cpp \
|
||||
$$PWD/types/timeframe.cpp \
|
||||
$$PWD/adapters/relayadapter.cpp \
|
||||
$$PWD/adapters/sgreadyadapter.cpp \
|
||||
$$PWD/adapters/evchargeradapter.cpp \
|
||||
$$PWD/adapters/batteryadapter.cpp \
|
||||
$$PWD/adapters/loadadapterregistry.cpp \
|
||||
$$PWD/adapters/adaptersettings.cpp \
|
||||
|
||||
@ -29,6 +29,9 @@
|
||||
#include "energymanagerconfiguration.h"
|
||||
#include "spotmarket/spotmarketmanager.h"
|
||||
#include "schedulingstrategies/manualstrategy.h"
|
||||
#include "adapters/loadadapterregistry.h"
|
||||
#include "adapters/adaptersettings.h"
|
||||
#include "types/loadrole.h"
|
||||
|
||||
#include "plugininfo.h"
|
||||
|
||||
@ -47,12 +50,27 @@ void EnergyPluginNymea::init()
|
||||
SpotMarketManager *spotMarketManager = new SpotMarketManager(networkManager, this);
|
||||
SmartChargingManager *chargingManager = new SmartChargingManager(energyManager(), thingManager(), spotMarketManager, configuration, this);
|
||||
|
||||
SchedulerManager *schedulerManager = new SchedulerManager(spotMarketManager, energyManager(), thingManager(), this);
|
||||
// Adapter registry: maps installer roles to nymea Things
|
||||
AdapterSettings *adapterSettings = new AdapterSettings(this);
|
||||
LoadAdapterRegistry *adapterRegistry = new LoadAdapterRegistry(thingManager(), this);
|
||||
adapterRegistry->loadFromSettings(adapterSettings);
|
||||
|
||||
// Auto-save role assignments whenever they change
|
||||
auto saveAdapters = [adapterSettings, adapterRegistry]() {
|
||||
adapterSettings->save(adapterRegistry->rawAssignments());
|
||||
};
|
||||
connect(adapterRegistry, &LoadAdapterRegistry::roleAssigned, this, [saveAdapters](LoadRole, ThingId) { saveAdapters(); });
|
||||
connect(adapterRegistry, &LoadAdapterRegistry::roleUnassigned, this, [saveAdapters](LoadRole) { saveAdapters(); });
|
||||
connect(adapterRegistry, &LoadAdapterRegistry::roleEnabledChanged, this, [saveAdapters](LoadRole, bool) { saveAdapters(); });
|
||||
|
||||
SchedulerManager *schedulerManager = new SchedulerManager(
|
||||
spotMarketManager, energyManager(), thingManager(), adapterRegistry, this);
|
||||
|
||||
// Community-tier strategies (always available, no feature flag)
|
||||
schedulerManager->registerStrategy(new ManualStrategy(this));
|
||||
|
||||
jsonRpcServer()->registerExperienceHandler(
|
||||
new NymeaEnergyJsonHandler(spotMarketManager, chargingManager, schedulerManager, this),
|
||||
0, 11);
|
||||
new NymeaEnergyJsonHandler(spotMarketManager, chargingManager,
|
||||
schedulerManager, adapterRegistry, this),
|
||||
0, 12);
|
||||
}
|
||||
|
||||
@ -28,6 +28,8 @@
|
||||
#include "spotmarket/spotmarketmanager.h"
|
||||
#include "schedulermanager.h"
|
||||
#include "schedulingstrategies/manualstrategy.h"
|
||||
#include "adapters/loadadapterregistry.h"
|
||||
#include "types/loadrole.h"
|
||||
|
||||
#include <energymanager.h>
|
||||
|
||||
@ -37,11 +39,13 @@ Q_DECLARE_LOGGING_CATEGORY(dcNymeaEnergy)
|
||||
NymeaEnergyJsonHandler::NymeaEnergyJsonHandler(SpotMarketManager *spotMarketManager,
|
||||
SmartChargingManager *smartChargingManager,
|
||||
SchedulerManager *schedulerManager,
|
||||
LoadAdapterRegistry *adapterRegistry,
|
||||
QObject *parent):
|
||||
JsonHandler{parent},
|
||||
m_spotMarketManager{spotMarketManager},
|
||||
m_smartChargingManager{smartChargingManager},
|
||||
m_schedulerManager{schedulerManager}
|
||||
m_schedulerManager{schedulerManager},
|
||||
m_adapterRegistry{adapterRegistry}
|
||||
{
|
||||
|
||||
registerEnum<ChargingInfo::ChargingMode>();
|
||||
@ -448,6 +452,107 @@ NymeaEnergyJsonHandler::NymeaEnergyJsonHandler(SpotMarketManager *spotMarketMana
|
||||
params.insert("reason", enumValueName(String));
|
||||
registerNotification("ManualSlotActivated", description, params);
|
||||
}
|
||||
|
||||
// --- Installer Setup API (v12) ---
|
||||
if (m_adapterRegistry) {
|
||||
params.clear(); returns.clear();
|
||||
description = "Get the current installer setup status: which roles are assigned, "
|
||||
"enabled, reachable and what adapter type is used.";
|
||||
returns.insert("setupStatus", QVariantMap());
|
||||
registerMethod("GetSetupStatus", description, params, returns,
|
||||
Types::PermissionScopeControlThings);
|
||||
|
||||
params.clear(); returns.clear();
|
||||
description = "List all nymea Things that are compatible with the given installer role. "
|
||||
"Role must be one of: EVCharger, DHW, HeatPump, Battery, SolarMeter, GridMeter.";
|
||||
params.insert("role", enumValueName(String));
|
||||
returns.insert("role", enumValueName(String));
|
||||
returns.insert("things", QVariantList());
|
||||
registerMethod("GetCompatibleThings", description, params, returns,
|
||||
Types::PermissionScopeControlThings);
|
||||
|
||||
params.clear(); returns.clear();
|
||||
description = "Assign a nymea Thing to an installer role. "
|
||||
"The adapter type is auto-detected from the Thing's interfaces. "
|
||||
"Optional params: nominalPowerW (relay), normalPowerW+relay1/2ThingId (sgready), "
|
||||
"phases/minA/maxA (evcharger), capacityKwh/maxChargeW/maxDischargeW (battery).";
|
||||
params.insert("role", enumValueName(String));
|
||||
params.insert("thingId", enumValueName(Uuid));
|
||||
params.insert("o:params", QVariantMap());
|
||||
returns.insert("energyError", enumRef<EnergyManager::EnergyError>());
|
||||
returns.insert("adapterType", enumValueName(String));
|
||||
returns.insert("detectedParams", QVariantMap());
|
||||
registerMethod("AssignThingToRole", description, params, returns,
|
||||
Types::PermissionScopeControlThings);
|
||||
|
||||
params.clear(); returns.clear();
|
||||
description = "Unassign the currently assigned Thing from the given installer role.";
|
||||
params.insert("role", enumValueName(String));
|
||||
returns.insert("energyError", enumRef<EnergyManager::EnergyError>());
|
||||
registerMethod("UnassignRole", description, params, returns,
|
||||
Types::PermissionScopeControlThings);
|
||||
|
||||
params.clear(); returns.clear();
|
||||
description = "Enable or disable an installer role. "
|
||||
"Disabled roles are not acted upon by the Scheduler even if assigned.";
|
||||
params.insert("role", enumValueName(String));
|
||||
params.insert("enabled", enumValueName(Bool));
|
||||
returns.insert("energyError", enumRef<EnergyManager::EnergyError>());
|
||||
registerMethod("SetRoleEnabled", description, params, returns,
|
||||
Types::PermissionScopeControlThings);
|
||||
|
||||
params.clear(); returns.clear();
|
||||
description = "Initiate a connection test for the assigned Thing of the given role. "
|
||||
"Result is delivered via the ConnectionTestResult push notification.";
|
||||
params.insert("role", enumValueName(String));
|
||||
returns.insert("energyError", enumRef<EnergyManager::EnergyError>());
|
||||
registerMethod("TestRoleConnection", description, params, returns,
|
||||
Types::PermissionScopeControlThings);
|
||||
|
||||
// Push notifications
|
||||
params.clear();
|
||||
description = "Emitted when the installer setup status changes "
|
||||
"(assignment, enable/disable, reachability).";
|
||||
params.insert("setupStatus", QVariantMap());
|
||||
registerNotification("SetupStatusChanged", description, params);
|
||||
|
||||
params.clear();
|
||||
description = "Emitted with the result of a TestRoleConnection call.";
|
||||
params.insert("role", enumValueName(String));
|
||||
params.insert("success", enumValueName(Bool));
|
||||
params.insert("message", enumValueName(String));
|
||||
registerNotification("ConnectionTestResult", description, params);
|
||||
|
||||
params.clear();
|
||||
description = "Emitted when a newly added nymea Thing is compatible with an unassigned role. "
|
||||
"The UI can use this to prompt the installer.";
|
||||
params.insert("role", enumValueName(String));
|
||||
params.insert("thing", QVariantMap());
|
||||
registerNotification("ThingBecameCompatible", description, params);
|
||||
|
||||
// Wire adapter registry signals to notifications
|
||||
connect(m_adapterRegistry, &LoadAdapterRegistry::setupStatusChanged,
|
||||
this, [this](const SetupStatus &status) {
|
||||
emit SetupStatusChanged({{"setupStatus", setupStatusToVariant(status)}});
|
||||
});
|
||||
|
||||
connect(m_adapterRegistry, &LoadAdapterRegistry::connectionTestResult,
|
||||
this, [this](LoadRole role, bool success, const QString &message) {
|
||||
QVariantMap p;
|
||||
p.insert("role", loadRoleToString(role));
|
||||
p.insert("success", success);
|
||||
p.insert("message", message);
|
||||
emit ConnectionTestResult(p);
|
||||
});
|
||||
|
||||
connect(m_adapterRegistry, &LoadAdapterRegistry::thingBecameCompatible,
|
||||
this, [this](LoadRole role, const ThingInfo &info) {
|
||||
QVariantMap p;
|
||||
p.insert("role", loadRoleToString(role));
|
||||
p.insert("thing", thingInfoToVariant(info));
|
||||
emit ThingBecameCompatible(p);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
QString NymeaEnergyJsonHandler::name() const
|
||||
@ -895,3 +1000,132 @@ QVariantMap NymeaEnergyJsonHandler::buildTimelineSummary(const QList<EnergyTimeS
|
||||
summary.insert("totalGridExportKwh", totalExportKwh);
|
||||
return summary;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Installer Setup API implementations — NymeaEnergy v12
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
JsonReply *NymeaEnergyJsonHandler::GetSetupStatus(const QVariantMap ¶ms)
|
||||
{
|
||||
Q_UNUSED(params)
|
||||
if (!m_adapterRegistry)
|
||||
return createReply({{"setupStatus", QVariantMap()}});
|
||||
return createReply({{"setupStatus", setupStatusToVariant(m_adapterRegistry->setupStatus())}});
|
||||
}
|
||||
|
||||
JsonReply *NymeaEnergyJsonHandler::GetCompatibleThings(const QVariantMap ¶ms)
|
||||
{
|
||||
const QString roleStr = params.value("role").toString();
|
||||
if (roleStr.isEmpty() || !m_adapterRegistry)
|
||||
return createReply({{"energyError", enumValueName(EnergyManager::EnergyErrorMissingParameter)},
|
||||
{"role", roleStr}, {"things", QVariantList()}});
|
||||
|
||||
LoadRole role = loadRoleFromString(roleStr);
|
||||
QVariantList thingList;
|
||||
foreach (const ThingInfo &info, m_adapterRegistry->compatibleThings(role))
|
||||
thingList.append(thingInfoToVariant(info));
|
||||
|
||||
return createReply({{"role", roleStr}, {"things", thingList}});
|
||||
}
|
||||
|
||||
JsonReply *NymeaEnergyJsonHandler::AssignThingToRole(const QVariantMap ¶ms)
|
||||
{
|
||||
if (!m_adapterRegistry)
|
||||
return createReply({{"energyError", enumValueName(EnergyManager::EnergyErrorInvalidParameter)},
|
||||
{"adapterType", QString()}, {"detectedParams", QVariantMap()}});
|
||||
|
||||
const QString roleStr = params.value("role").toString();
|
||||
const ThingId thingId = ThingId(params.value("thingId").toUuid());
|
||||
const QVariantMap extra = params.value("params").toMap();
|
||||
|
||||
if (roleStr.isEmpty() || thingId.isNull())
|
||||
return createReply({{"energyError", enumValueName(EnergyManager::EnergyErrorMissingParameter)},
|
||||
{"adapterType", QString()}, {"detectedParams", QVariantMap()}});
|
||||
|
||||
LoadRole role = loadRoleFromString(roleStr);
|
||||
const QString result = m_adapterRegistry->assignThing(role, thingId, extra);
|
||||
|
||||
if (result.startsWith(QLatin1String("error:")))
|
||||
return createReply({{"energyError", enumValueName(EnergyManager::EnergyErrorInvalidParameter)},
|
||||
{"adapterType", QString()}, {"detectedParams", QVariantMap()}});
|
||||
|
||||
return createReply({{"energyError", enumValueName(EnergyManager::EnergyErrorNoError)},
|
||||
{"adapterType", result},
|
||||
{"detectedParams", extra}});
|
||||
}
|
||||
|
||||
JsonReply *NymeaEnergyJsonHandler::UnassignRole(const QVariantMap ¶ms)
|
||||
{
|
||||
const QString roleStr = params.value("role").toString();
|
||||
if (roleStr.isEmpty() || !m_adapterRegistry)
|
||||
return createReply({{"energyError", enumValueName(EnergyManager::EnergyErrorMissingParameter)}});
|
||||
|
||||
m_adapterRegistry->unassignRole(loadRoleFromString(roleStr));
|
||||
return createReply({{"energyError", enumValueName(EnergyManager::EnergyErrorNoError)}});
|
||||
}
|
||||
|
||||
JsonReply *NymeaEnergyJsonHandler::SetRoleEnabled(const QVariantMap ¶ms)
|
||||
{
|
||||
const QString roleStr = params.value("role").toString();
|
||||
if (roleStr.isEmpty() || !m_adapterRegistry)
|
||||
return createReply({{"energyError", enumValueName(EnergyManager::EnergyErrorMissingParameter)}});
|
||||
|
||||
bool enabled = params.value("enabled").toBool();
|
||||
m_adapterRegistry->setRoleEnabled(loadRoleFromString(roleStr), enabled);
|
||||
return createReply({{"energyError", enumValueName(EnergyManager::EnergyErrorNoError)}});
|
||||
}
|
||||
|
||||
JsonReply *NymeaEnergyJsonHandler::TestRoleConnection(const QVariantMap ¶ms)
|
||||
{
|
||||
const QString roleStr = params.value("role").toString();
|
||||
if (roleStr.isEmpty() || !m_adapterRegistry)
|
||||
return createReply({{"energyError", enumValueName(EnergyManager::EnergyErrorMissingParameter)}});
|
||||
|
||||
if (!m_adapterRegistry->isRoleAssigned(loadRoleFromString(roleStr)))
|
||||
return createReply({{"energyError", enumValueName(EnergyManager::EnergyErrorInvalidParameter)}});
|
||||
|
||||
m_adapterRegistry->testConnection(loadRoleFromString(roleStr));
|
||||
return createReply({{"energyError", enumValueName(EnergyManager::EnergyErrorNoError)}});
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Installer Setup serialization helpers
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
QVariantMap NymeaEnergyJsonHandler::setupStatusToVariant(const SetupStatus &status) const
|
||||
{
|
||||
QVariantList roleList;
|
||||
foreach (const RoleStatus &rs, status.roles)
|
||||
roleList.append(roleStatusToVariant(rs));
|
||||
|
||||
QVariantMap m;
|
||||
m.insert("roles", roleList);
|
||||
m.insert("allEnabledRolesOk", status.allEnabledRolesOk);
|
||||
m.insert("configuredCount", status.configuredCount);
|
||||
m.insert("errorCount", status.errorCount);
|
||||
return m;
|
||||
}
|
||||
|
||||
QVariantMap NymeaEnergyJsonHandler::roleStatusToVariant(const RoleStatus &rs) const
|
||||
{
|
||||
QVariantMap m;
|
||||
m.insert("role", loadRoleToString(rs.role));
|
||||
m.insert("enabled", rs.enabled);
|
||||
m.insert("assigned", rs.assigned);
|
||||
m.insert("assignedThingId", rs.assignedThingId.isNull() ? QVariant() : QVariant::fromValue(rs.assignedThingId));
|
||||
m.insert("assignedThingName", rs.assignedThingName);
|
||||
m.insert("reachable", rs.reachable);
|
||||
m.insert("adapterType", rs.adapterType);
|
||||
m.insert("lastError", rs.lastError);
|
||||
return m;
|
||||
}
|
||||
|
||||
QVariantMap NymeaEnergyJsonHandler::thingInfoToVariant(const ThingInfo &info) const
|
||||
{
|
||||
QVariantMap m;
|
||||
m.insert("thingId", info.thingId.isNull() ? QVariant() : QVariant::fromValue(info.thingId));
|
||||
m.insert("displayName", info.displayName);
|
||||
m.insert("pluginName", info.pluginName);
|
||||
m.insert("interfaces", info.interfaces);
|
||||
return m;
|
||||
}
|
||||
|
||||
@ -32,6 +32,7 @@
|
||||
#include "types/scoreentry.h"
|
||||
#include "types/energytimeslot.h"
|
||||
#include "types/manualslotconfig.h"
|
||||
#include "adapters/loadadapterregistry.h"
|
||||
|
||||
class SmartChargingManager;
|
||||
class SpotMarketManager;
|
||||
@ -44,6 +45,7 @@ public:
|
||||
explicit NymeaEnergyJsonHandler(SpotMarketManager *spotMarketManager,
|
||||
SmartChargingManager *smartChargingManager,
|
||||
SchedulerManager *schedulerManager,
|
||||
LoadAdapterRegistry *adapterRegistry = nullptr,
|
||||
QObject *parent = nullptr);
|
||||
|
||||
QString name() const override;
|
||||
@ -89,6 +91,14 @@ public:
|
||||
Q_INVOKABLE JsonReply *RemoveManualSlot(const QVariantMap ¶ms);
|
||||
Q_INVOKABLE JsonReply *ClearManualSlots(const QVariantMap ¶ms);
|
||||
|
||||
// --- Installer Setup API (NymeaEnergy v12) ---
|
||||
Q_INVOKABLE JsonReply *GetSetupStatus(const QVariantMap ¶ms);
|
||||
Q_INVOKABLE JsonReply *GetCompatibleThings(const QVariantMap ¶ms);
|
||||
Q_INVOKABLE JsonReply *AssignThingToRole(const QVariantMap ¶ms);
|
||||
Q_INVOKABLE JsonReply *UnassignRole(const QVariantMap ¶ms);
|
||||
Q_INVOKABLE JsonReply *SetRoleEnabled(const QVariantMap ¶ms);
|
||||
Q_INVOKABLE JsonReply *TestRoleConnection(const QVariantMap ¶ms);
|
||||
|
||||
signals:
|
||||
void PhasePowerLimitChanged(const QVariantMap ¶ms);
|
||||
void AcquisitionToleranceChanged(const QVariantMap ¶ms);
|
||||
@ -109,10 +119,16 @@ signals:
|
||||
// Manual slot push notification
|
||||
void ManualSlotActivated(const QVariantMap ¶ms);
|
||||
|
||||
// Installer setup push notifications (v12)
|
||||
void SetupStatusChanged(const QVariantMap ¶ms);
|
||||
void ConnectionTestResult(const QVariantMap ¶ms);
|
||||
void ThingBecameCompatible(const QVariantMap ¶ms);
|
||||
|
||||
private:
|
||||
SpotMarketManager *m_spotMarketManager;
|
||||
SmartChargingManager *m_smartChargingManager = nullptr;
|
||||
SchedulerManager *m_schedulerManager = nullptr;
|
||||
LoadAdapterRegistry *m_adapterRegistry = nullptr;
|
||||
|
||||
void sendSpotMarketConfigurationChangedNotification();
|
||||
QVariantMap buildTimelineSummary(const QList<EnergyTimeSlot> &timeline) const;
|
||||
@ -123,6 +139,11 @@ private:
|
||||
// Helper: access the registered ManualStrategy (null if not registered)
|
||||
class ManualStrategy *manualStrategy() const;
|
||||
|
||||
// Installer setup serialization helpers
|
||||
QVariantMap setupStatusToVariant(const SetupStatus &status) const;
|
||||
QVariantMap roleStatusToVariant(const RoleStatus &rs) const;
|
||||
QVariantMap thingInfoToVariant(const ThingInfo &info) const;
|
||||
|
||||
};
|
||||
|
||||
#endif // NYMEAENERGYJSONHANDLER_H
|
||||
|
||||
@ -28,19 +28,24 @@
|
||||
#include "schedulingstrategies/manualstrategy.h"
|
||||
#include "schedulersettings.h"
|
||||
#include "spotmarket/spotmarketmanager.h"
|
||||
#include "adapters/loadadapterregistry.h"
|
||||
#include "adapters/iloadapter.h"
|
||||
#include "types/loadrole.h"
|
||||
|
||||
#include <QLoggingCategory>
|
||||
Q_DECLARE_LOGGING_CATEGORY(dcNymeaEnergy)
|
||||
|
||||
SchedulerManager::SchedulerManager(
|
||||
SpotMarketManager *spotMarketManager,
|
||||
EnergyManager *energyManager,
|
||||
ThingManager *thingManager,
|
||||
QObject *parent)
|
||||
SpotMarketManager *spotMarketManager,
|
||||
EnergyManager *energyManager,
|
||||
ThingManager *thingManager,
|
||||
LoadAdapterRegistry *adapterRegistry,
|
||||
QObject *parent)
|
||||
: QObject(parent),
|
||||
m_spotMarketManager(spotMarketManager),
|
||||
m_energyManager(energyManager),
|
||||
m_thingManager(thingManager)
|
||||
m_thingManager(thingManager),
|
||||
m_adapterRegistry(adapterRegistry)
|
||||
{
|
||||
m_settings = new SchedulerSettings(this);
|
||||
|
||||
@ -393,15 +398,29 @@ QList<FlexibleLoad> SchedulerManager::collectLoads() const
|
||||
|
||||
void SchedulerManager::applyCurrentSlot(const EnergyTimeSlot &slot)
|
||||
{
|
||||
// In Phase 1, we log the decision. Actual hardware commands are issued by
|
||||
// each specialist manager (SmartChargingManager, etc.) which listens to
|
||||
// the timelineUpdated() signal and reads the current slot.
|
||||
qCDebug(dcNymeaEnergy()) << "SchedulerManager: applying slot:"
|
||||
<< slot.decisionReason
|
||||
<< "EV=" << slot.allocatedToEV << "W"
|
||||
<< "HP=" << slot.allocatedToHP << "W"
|
||||
<< "DHW=" << slot.allocatedToDHW << "W"
|
||||
<< "Bat=" << slot.allocatedToBattery << "W";
|
||||
|
||||
if (!m_adapterRegistry)
|
||||
return;
|
||||
|
||||
// Apply DHW allocation
|
||||
if (ILoadAdapter *a = m_adapterRegistry->adapterForRole(LoadRole::DHW))
|
||||
a->applyPower(slot.allocatedToDHW);
|
||||
|
||||
// Apply HeatPump allocation
|
||||
if (ILoadAdapter *a = m_adapterRegistry->adapterForRole(LoadRole::HeatPump))
|
||||
a->applyPower(slot.allocatedToHP);
|
||||
|
||||
// Apply Battery allocation (positive = charge, negative = discharge)
|
||||
if (ILoadAdapter *a = m_adapterRegistry->adapterForRole(LoadRole::Battery))
|
||||
a->applyPower(slot.allocatedToBattery);
|
||||
|
||||
// EVCharger is managed by SmartChargingManager — skip here to avoid conflicts
|
||||
}
|
||||
|
||||
ManualStrategy *SchedulerManager::findManualStrategy() const
|
||||
|
||||
@ -42,6 +42,7 @@
|
||||
#include <integrations/thingmanager.h>
|
||||
|
||||
class SpotMarketManager;
|
||||
class LoadAdapterRegistry;
|
||||
|
||||
// SchedulerManager orchestrates the full energy scheduling pipeline.
|
||||
//
|
||||
@ -62,6 +63,7 @@ public:
|
||||
SpotMarketManager *spotMarketManager,
|
||||
EnergyManager *energyManager,
|
||||
ThingManager *thingManager,
|
||||
LoadAdapterRegistry *adapterRegistry = nullptr,
|
||||
QObject *parent = nullptr
|
||||
);
|
||||
|
||||
@ -135,10 +137,11 @@ private:
|
||||
// Helper: return the registered ManualStrategy, or nullptr if not found
|
||||
class ManualStrategy *findManualStrategy() const;
|
||||
|
||||
SpotMarketManager *m_spotMarketManager = nullptr;
|
||||
EnergyManager *m_energyManager = nullptr;
|
||||
ThingManager *m_thingManager = nullptr;
|
||||
class SchedulerSettings *m_settings = nullptr;
|
||||
SpotMarketManager *m_spotMarketManager = nullptr;
|
||||
EnergyManager *m_energyManager = nullptr;
|
||||
ThingManager *m_thingManager = nullptr;
|
||||
LoadAdapterRegistry *m_adapterRegistry = nullptr;
|
||||
class SchedulerSettings *m_settings = nullptr;
|
||||
|
||||
ISchedulingStrategy *m_activeStrategy = nullptr;
|
||||
QList<ISchedulingStrategy *> m_strategies;
|
||||
|
||||
109
energyplugin/types/loadrole.h
Normal file
109
energyplugin/types/loadrole.h
Normal file
@ -0,0 +1,109 @@
|
||||
// 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/>.
|
||||
*
|
||||
* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
|
||||
|
||||
#ifndef LOADROLE_H
|
||||
#define LOADROLE_H
|
||||
|
||||
#include <QString>
|
||||
#include <QMap>
|
||||
#include <QStringList>
|
||||
|
||||
#include "types/flexibleload.h"
|
||||
|
||||
// Installer-visible roles that can be assigned to nymea Things.
|
||||
enum class LoadRole {
|
||||
EVCharger,
|
||||
DHW,
|
||||
HeatPump,
|
||||
Battery,
|
||||
SolarMeter,
|
||||
GridMeter
|
||||
};
|
||||
|
||||
inline QString loadRoleToString(LoadRole role)
|
||||
{
|
||||
switch (role) {
|
||||
case LoadRole::EVCharger: return QStringLiteral("EVCharger");
|
||||
case LoadRole::DHW: return QStringLiteral("DHW");
|
||||
case LoadRole::HeatPump: return QStringLiteral("HeatPump");
|
||||
case LoadRole::Battery: return QStringLiteral("Battery");
|
||||
case LoadRole::SolarMeter: return QStringLiteral("SolarMeter");
|
||||
case LoadRole::GridMeter: return QStringLiteral("GridMeter");
|
||||
}
|
||||
return QStringLiteral("Unknown");
|
||||
}
|
||||
|
||||
inline LoadRole loadRoleFromString(const QString &str)
|
||||
{
|
||||
if (str == QLatin1String("EVCharger")) return LoadRole::EVCharger;
|
||||
if (str == QLatin1String("DHW")) return LoadRole::DHW;
|
||||
if (str == QLatin1String("HeatPump")) return LoadRole::HeatPump;
|
||||
if (str == QLatin1String("Battery")) return LoadRole::Battery;
|
||||
if (str == QLatin1String("SolarMeter")) return LoadRole::SolarMeter;
|
||||
if (str == QLatin1String("GridMeter")) return LoadRole::GridMeter;
|
||||
return LoadRole::GridMeter; // fallback
|
||||
}
|
||||
|
||||
// Required nymea interfaces for each role.
|
||||
// A Thing must implement at least one of the listed interfaces to be eligible.
|
||||
inline QMap<LoadRole, QStringList> loadRoleCompatibleInterfaces()
|
||||
{
|
||||
QMap<LoadRole, QStringList> map;
|
||||
map.insert(LoadRole::EVCharger, QStringList() << QStringLiteral("evcharger"));
|
||||
map.insert(LoadRole::DHW, QStringList() << QStringLiteral("smartmeterconsumer")
|
||||
<< QStringLiteral("relay"));
|
||||
map.insert(LoadRole::HeatPump, QStringList() << QStringLiteral("heating")
|
||||
<< QStringLiteral("relay"));
|
||||
map.insert(LoadRole::Battery, QStringList() << QStringLiteral("energystorage"));
|
||||
map.insert(LoadRole::SolarMeter, QStringList() << QStringLiteral("energymeter"));
|
||||
map.insert(LoadRole::GridMeter, QStringList() << QStringLiteral("energymeter"));
|
||||
return map;
|
||||
}
|
||||
|
||||
// Convenience mapping from LoadSource (scheduling domain) to LoadRole (installer domain).
|
||||
inline LoadRole loadRoleFromLoadSource(LoadSource source)
|
||||
{
|
||||
switch (source) {
|
||||
case LoadSource::SmartCharging: return LoadRole::EVCharger;
|
||||
case LoadSource::HeatPump: return LoadRole::HeatPump;
|
||||
case LoadSource::DHW: return LoadRole::DHW;
|
||||
case LoadSource::Battery: return LoadRole::Battery;
|
||||
default: return LoadRole::GridMeter;
|
||||
}
|
||||
}
|
||||
|
||||
// All installer roles, in display order.
|
||||
inline QList<LoadRole> allLoadRoles()
|
||||
{
|
||||
return {
|
||||
LoadRole::EVCharger,
|
||||
LoadRole::DHW,
|
||||
LoadRole::HeatPump,
|
||||
LoadRole::Battery,
|
||||
LoadRole::SolarMeter,
|
||||
LoadRole::GridMeter
|
||||
};
|
||||
}
|
||||
|
||||
#endif // LOADROLE_H
|
||||
@ -24,6 +24,9 @@
|
||||
|
||||
#include "testscheduler.h"
|
||||
#include <QSignalSpy>
|
||||
#include <QDir>
|
||||
#include <QFile>
|
||||
#include <QCoreApplication>
|
||||
#include <algorithm>
|
||||
|
||||
// Typical winter day electricity prices (EUR/kWh), hours 00:00–23:00
|
||||
@ -551,6 +554,127 @@ void TestScheduler::testManualStrategy_persistence()
|
||||
"Restored slot must produce ManualSlot rule after round-trip");
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Test A — LoadAdapterRegistry::detectAdapterType (pure static, no ThingManager)
|
||||
// ---------------------------------------------------------------------------
|
||||
void TestScheduler::testDetectAdapterType()
|
||||
{
|
||||
QCOMPARE(LoadAdapterRegistry::detectAdapterType(LoadRole::EVCharger,
|
||||
QStringList() << QStringLiteral("evcharger")),
|
||||
QStringLiteral("evcharger"));
|
||||
|
||||
QCOMPARE(LoadAdapterRegistry::detectAdapterType(LoadRole::HeatPump,
|
||||
QStringList() << QStringLiteral("heating")),
|
||||
QStringLiteral("sgready"));
|
||||
|
||||
QCOMPARE(LoadAdapterRegistry::detectAdapterType(LoadRole::HeatPump,
|
||||
QStringList() << QStringLiteral("relay")),
|
||||
QStringLiteral("relay"));
|
||||
|
||||
QCOMPARE(LoadAdapterRegistry::detectAdapterType(LoadRole::DHW,
|
||||
QStringList() << QStringLiteral("relay")),
|
||||
QStringLiteral("relay"));
|
||||
|
||||
QCOMPARE(LoadAdapterRegistry::detectAdapterType(LoadRole::Battery,
|
||||
QStringList() << QStringLiteral("energystorage")),
|
||||
QStringLiteral("battery"));
|
||||
|
||||
QCOMPARE(LoadAdapterRegistry::detectAdapterType(LoadRole::SolarMeter,
|
||||
QStringList() << QStringLiteral("energymeter")),
|
||||
QStringLiteral("readonly"));
|
||||
|
||||
QCOMPARE(LoadAdapterRegistry::detectAdapterType(LoadRole::GridMeter,
|
||||
QStringList() << QStringLiteral("energymeter")),
|
||||
QStringLiteral("readonly"));
|
||||
|
||||
// Incompatible interface returns empty
|
||||
QVERIFY(LoadAdapterRegistry::detectAdapterType(LoadRole::Battery,
|
||||
QStringList() << QStringLiteral("relay")).isEmpty());
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Test B — AdapterSettings round-trip
|
||||
// ---------------------------------------------------------------------------
|
||||
void TestScheduler::testAdapterSettings_roundTrip()
|
||||
{
|
||||
// Use a temp file to avoid polluting any production settings
|
||||
const QString tmpPath = QDir::tempPath()
|
||||
+ QStringLiteral("/nymea_adapter_test_%1.conf")
|
||||
.arg(QCoreApplication::applicationPid());
|
||||
|
||||
// Override path via environment variable
|
||||
qputenv("NYMEA_ADAPTER_SETTINGS", tmpPath.toUtf8());
|
||||
|
||||
{
|
||||
AdapterSettings s;
|
||||
QVariantMap params;
|
||||
params.insert(QStringLiteral("thingId"), QStringLiteral("11111111-1111-1111-1111-111111111111"));
|
||||
params.insert(QStringLiteral("adapterType"), QStringLiteral("relay"));
|
||||
params.insert(QStringLiteral("nominalPowerW"), 2000.0);
|
||||
params.insert(QStringLiteral("enabled"), true);
|
||||
s.setAssignment(LoadRole::DHW, params);
|
||||
} // destructor flushes (already saved by setAssignment)
|
||||
|
||||
{
|
||||
AdapterSettings s2;
|
||||
const QList<QVariantMap> loaded = s2.assignments();
|
||||
QCOMPARE(loaded.size(), 1);
|
||||
QCOMPARE(loaded.first().value(QStringLiteral("role")).toString(),
|
||||
QStringLiteral("DHW"));
|
||||
QCOMPARE(loaded.first().value(QStringLiteral("adapterType")).toString(),
|
||||
QStringLiteral("relay"));
|
||||
QCOMPARE(loaded.first().value(QStringLiteral("nominalPowerW")).toDouble(), 2000.0);
|
||||
}
|
||||
|
||||
// Cleanup
|
||||
QFile::remove(tmpPath);
|
||||
qputenv("NYMEA_ADAPTER_SETTINGS", QByteArray());
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Test C — SchedulerManager with null registry does not crash
|
||||
// ---------------------------------------------------------------------------
|
||||
void TestScheduler::testSchedulerManager_nullRegistry_noCrash()
|
||||
{
|
||||
// SchedulerManager(nullptr registry) must not crash on construction or forceRecompute().
|
||||
// It will still compute a 24-slot timeline (stub forecast, default strategy).
|
||||
SchedulerManager mgr(nullptr, nullptr, nullptr, nullptr);
|
||||
// forceRecompute is called via singleShot in the constructor; give the event loop a tick
|
||||
QCoreApplication::processEvents();
|
||||
|
||||
// The manager builds a 24-slot forecast even with all null managers.
|
||||
// The registry being nullptr means applyCurrentSlot() is a no-op — verify no crash.
|
||||
QVERIFY2(mgr.planHealth() == QLatin1String("ok") ||
|
||||
mgr.planHealth() == QLatin1String("no_forecast") ||
|
||||
mgr.planHealth() == QLatin1String("degraded"),
|
||||
"planHealth() must return a valid string");
|
||||
|
||||
// forceRecompute() again — must not crash with null registry
|
||||
mgr.forceRecompute();
|
||||
QVERIFY(true); // reached here = no crash
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Test D — LoadRole string serialization round-trip
|
||||
// ---------------------------------------------------------------------------
|
||||
void TestScheduler::testLoadRole_stringRoundTrip()
|
||||
{
|
||||
const QList<LoadRole> roles = {
|
||||
LoadRole::EVCharger,
|
||||
LoadRole::DHW,
|
||||
LoadRole::HeatPump,
|
||||
LoadRole::Battery,
|
||||
LoadRole::SolarMeter,
|
||||
LoadRole::GridMeter
|
||||
};
|
||||
foreach (LoadRole role, roles) {
|
||||
const QString str = loadRoleToString(role);
|
||||
const LoadRole restored = loadRoleFromString(str);
|
||||
QVERIFY2(!str.isEmpty(), "loadRoleToString returned empty string");
|
||||
QCOMPARE(restored, role);
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Test runner
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
@ -32,11 +32,15 @@
|
||||
|
||||
#include "types/energytimeslot.h"
|
||||
#include "types/flexibleload.h"
|
||||
#include "types/loadrole.h"
|
||||
#include "types/schedulerconfig.h"
|
||||
#include "schedulingstrategies/rulebasedstrategy.h"
|
||||
#include "schedulingstrategies/aistrategy.h"
|
||||
#include "schedulingstrategies/manualstrategy.h"
|
||||
#include "types/manualslotconfig.h"
|
||||
#include "adapters/loadadapterregistry.h"
|
||||
#include "adapters/adaptersettings.h"
|
||||
#include "schedulermanager.h"
|
||||
|
||||
// Unit tests for the scheduling algorithm.
|
||||
// These tests operate on pure algorithm logic (RuleBasedStrategy, AIStrategy)
|
||||
@ -89,6 +93,18 @@ private slots:
|
||||
// Test 9: ManualStrategy — JSON round-trip (persistence simulation)
|
||||
void testManualStrategy_persistence();
|
||||
|
||||
// Test A: LoadAdapterRegistry::detectAdapterType — static method, no ThingManager needed
|
||||
void testDetectAdapterType();
|
||||
|
||||
// Test B: AdapterSettings — save/load round-trip
|
||||
void testAdapterSettings_roundTrip();
|
||||
|
||||
// Test C: SchedulerManager — null registry → applyCurrentSlot is a no-op (no crash)
|
||||
void testSchedulerManager_nullRegistry_noCrash();
|
||||
|
||||
// Test D: LoadRole — string serialization round-trip
|
||||
void testLoadRole_stringRoundTrip();
|
||||
|
||||
private:
|
||||
// Build a synthetic 24h forecast starting at a given base time
|
||||
QList<EnergyTimeSlot> buildWinterForecast(const QDateTime &start) const;
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user