fix: detect actual EV phase instead of hardcoding phase A
- evcharger.cpp: phases() now calls meteredPhases() instead of returning PhaseNone - smartchargingmanager: add chargerPhaseKey() with 3-level fallback 1. meteredPhases() when charger is active 2. effectivePhases from last known state 3. fallback 'A' + warning (previous behavior) - Remove 4 FIXME comments on lines 394, 477, 517
This commit is contained in:
parent
a679e76286
commit
d76e7e61d5
184
CLAUDE.md
184
CLAUDE.md
@ -1,84 +1,133 @@
|
||||
# Agent Plugin — `powersync-energy-plugin-etm`
|
||||
# Agent Plugin — `powersync-energy-plugin-etm` (GPL3)
|
||||
> Lire aussi le `CLAUDE.md` du dossier parent avant de commencer.
|
||||
|
||||
---
|
||||
|
||||
## Mon rôle
|
||||
Je suis le **cœur du HEMS ETM-PowerSync**. Je contiens toute la logique
|
||||
d'optimisation énergétique : gestion des consommateurs (EV, ECS, PAC),
|
||||
tarification, météo, et décision d'activation par surplus solaire.
|
||||
Je suis le **plugin nymea Community** du HEMS ETM-PowerSync.
|
||||
Je contiens toute la logique GPL3 : recharge EV sur surplus, tarif HP/HC,
|
||||
protection surcharge, et le pont vers `powersync-optimizer` pour les tiers payants.
|
||||
|
||||
Je suis un **code propriétaire ETM** — pas open-source, pas de publication upstream.
|
||||
**Licence : GPL3** — tout mon code est open-source assumé.
|
||||
Origine : fork de nymea-energy-plugin-nymea (source nymea/Chargebyte, GPL3).
|
||||
Pas de remote upstream Git public — mises à jour via portage manuel depuis
|
||||
`etm-nymea/nymea-energy-plugin-nymea`.
|
||||
|
||||
---
|
||||
|
||||
## Règle fondamentale
|
||||
|
||||
```
|
||||
Ce repo = fonctionnalités Community UNIQUEMENT.
|
||||
Zéro logique Auto / Predict AI dans ce repo.
|
||||
Ces features passent exclusivement par PowerSyncClient → optimizer.
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Ce que je FOURNIS
|
||||
|
||||
### API JSON-RPC `EnergyPlugin.*`
|
||||
| Méthode | Rôle |
|
||||
|---|---|
|
||||
| `GetChargingInfos(evChargerId)` | Config recharge EV (mode, deadline, SOC cible) |
|
||||
| `SetChargingInfo(chargingInfo)` | Mettre à jour la config d'une borne EV |
|
||||
| `GetChargingSchedules(evChargerId)` | Planning calculé par l'OptimizationEngine |
|
||||
| `GetAvailableSpotMarketProviders()` | Liste des providers tarifs disponibles |
|
||||
| `SetSpotMarketConfiguration(enabled, providerId)` | Activer/choisir un provider |
|
||||
| `GetSpotMarketScoreEntries(date)` | Cotations horaires aWATTar |
|
||||
| `SetPhasePowerLimit(Uint)` | Protection surcharge réseau (A/phase) |
|
||||
| `SetAcquisitionTolerance(Double)` | Seuil surplus déclenchant la charge |
|
||||
| `SetBatteryLevelConsideration(Double)` | Facteur batterie dans le calcul surplus |
|
||||
| Méthode | Rôle | Tier |
|
||||
|---|---|---|
|
||||
| `GetChargingInfos(evChargerId)` | Config recharge EV | Community |
|
||||
| `SetChargingInfo(chargingInfo)` | Mettre à jour config borne EV | Community |
|
||||
| `GetChargingSchedules(evChargerId)` | Planning calculé | Community |
|
||||
| `GetAvailableSpotMarketProviders()` | Liste providers tarifs | Community |
|
||||
| `SetSpotMarketConfiguration(enabled, providerId)` | Activer provider | Community |
|
||||
| `GetSpotMarketScoreEntries(date)` | Cotations horaires | Community |
|
||||
| `SetPhasePowerLimit(Uint)` | Protection surcharge | Community |
|
||||
| `SetAcquisitionTolerance(Double)` | Seuil surplus | Community |
|
||||
| `SetBatteryLevelConsideration(Double)` | Facteur batterie | Community |
|
||||
|
||||
### Notifications push
|
||||
`ChargingInfoAdded/Removed/Changed`, `ChargingSchedulesChanged`,
|
||||
`SpotMarketConfigurationChanged`, `SpotMarketScoreEntriesChanged`,
|
||||
`PhasePowerLimitChanged`
|
||||
|
||||
### `.so` produit
|
||||
```
|
||||
libnymea_energypluginnymea.so ← nom identique à l'upstream (drop-in replacement)
|
||||
install : /usr/lib/nymea/energy/
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Ce que je CONSOMME
|
||||
|
||||
### Interfaces nymea (détection par interface, jamais par ThingClassId)
|
||||
### Interfaces nymea (par interface, jamais par ThingClassId)
|
||||
| Interface | États lus | Actions envoyées |
|
||||
|---|---|---|
|
||||
| `evcharger` | `chargingEnabled`, `maxChargingCurrent`, `pluggedIn`, `charging`, phases | `setChargingEnabled`, `setMaxChargingCurrent` |
|
||||
| `electricvehicle` | `batteryLevel`, `maxChargingCurrent`, `capacity` | — |
|
||||
| `rootmeter` / `energymeter` | `currentPowerPhaseA/B/C`, `currentPhaseA/B/C` | — |
|
||||
| `energystorage` | `currentPower`, `batteryLevel` | — |
|
||||
| `thermostat` | `temperature`, `mode` | `setMode`, `setTargetTemperature` |
|
||||
|
||||
### Depuis `nymea-experience-plugin-energy`
|
||||
- `EnergyManager*` injecté via `EnergyPlugin::init()`
|
||||
- Signal `PowerBalanceEntryAdded` → déclenche le cycle d'optimisation (~1 min)
|
||||
### Signal déclencheur
|
||||
`PowerBalanceEntryAdded` depuis `nymea-experience-plugin-energy` → cycle ~1 min.
|
||||
|
||||
### Service propriétaire (optionnel)
|
||||
`PowerSyncClient` → Unix socket `/run/powersync/optimizer.sock`
|
||||
Si absent → mode Community local, aucune erreur.
|
||||
|
||||
---
|
||||
|
||||
## Architecture interne cible
|
||||
## Architecture interne
|
||||
|
||||
```
|
||||
powersync-energy-plugin-etm
|
||||
powersync-energy-plugin-etm/
|
||||
│
|
||||
├── TierManager ← lit /etc/powersync/tier.conf
|
||||
│ └── active/désactive les modules selon le tier
|
||||
├── [code upstream nymea/Chargebyte] ← ne pas modifier directement
|
||||
│ ├── SmartChargingManager.* ← à corriger (bugs phase EV)
|
||||
│ ├── SpotMarketManager.* ← aWATTar AT/DE ✅
|
||||
│ └── NymeaEnergyJsonHandler.* ← API JSON-RPC
|
||||
│
|
||||
├── OptimizationEngine ← chef d'orchestre (à créer)
|
||||
│ ├── calcule le surplus PV disponible
|
||||
│ ├── consulte TariffManager (tarif actuel)
|
||||
│ ├── consulte WeatherManager (météo J+1 si tier Auto+)
|
||||
│ └── distribue la puissance selon la priorité :
|
||||
│ 1. ECS (priorité haute — chaleur)
|
||||
│ 2. PAC (selon température extérieure)
|
||||
│ 3. EV (selon deadline connue)
|
||||
│
|
||||
├── ConsumerManager
|
||||
│ ├── EvConsumer ← refactor SmartChargingManager existant
|
||||
│ ├── EcsConsumer ← à créer
|
||||
│ └── HeatPumpConsumer ← à créer
|
||||
│
|
||||
├── TariffManager
|
||||
│ ├── StaticHcHpProvider ← à créer (Community)
|
||||
│ └── aWATTarProvider ← ✅ existe (AT + DE)
|
||||
│
|
||||
└── WeatherManager
|
||||
└── OpenMeteoProvider ← à créer (Auto uniquement)
|
||||
└── etm/ ← tout notre code ETM ici
|
||||
├── PowerSyncClient.* ← pont vers optimizer (Unix socket)
|
||||
├── tariff/
|
||||
│ └── StaticHcHpProvider.* ← HP/HC statique (Community)
|
||||
└── [futures extensions Community]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## PowerSyncClient — le pont vers l'optimizer
|
||||
|
||||
```cpp
|
||||
class PowerSyncClient : public QObject {
|
||||
Q_OBJECT
|
||||
public:
|
||||
// Vérifie si powersync-optimizer tourne
|
||||
bool isAvailable() const;
|
||||
|
||||
// Demande une décision d'optimisation (Auto/Predict AI)
|
||||
OptimizationResult requestOptimization(const SurplusData &data);
|
||||
|
||||
// Récupère la météo J+1 (Auto)
|
||||
WeatherForecast getWeatherForecast();
|
||||
|
||||
// Récupère le tarif dynamique courant (Predict AI)
|
||||
TariffData getDynamicTariff();
|
||||
|
||||
signals:
|
||||
void availabilityChanged(bool available);
|
||||
void optimizationResultReceived(OptimizationResult result);
|
||||
};
|
||||
```
|
||||
|
||||
**Comportement du cycle principal :**
|
||||
```cpp
|
||||
void SmartChargingManager::runCycle() {
|
||||
if (m_powerSyncClient->isAvailable()) {
|
||||
// Auto / Predict AI — délègue à l'optimizer
|
||||
auto result = m_powerSyncClient->requestOptimization(buildSurplusData());
|
||||
applyOptimizationResult(result);
|
||||
} else {
|
||||
// Community — logique GPL3 locale
|
||||
planSurplusCharging(); // EV sur surplus
|
||||
planSpotMarketCharging(); // EV sur aWATTar
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
@ -86,36 +135,39 @@ powersync-energy-plugin-etm
|
||||
## État actuel du code
|
||||
|
||||
### ✅ Fonctionnel
|
||||
- `SmartChargingManager` : recharge EV sur surplus solaire (mode Eco)
|
||||
- `SpotMarketManager` : planification aWATTar AT/DE avec cache 24h
|
||||
- `NymeaEnergyJsonHandler` : API JSON-RPC `EnergyPlugin.*` complète
|
||||
- SmartChargingManager : recharge EV surplus (mode Eco) + aWATTar AT/DE
|
||||
- Overload protection triphasée
|
||||
- Détection appareils par interface (zero UUID hardcodé)
|
||||
- API JSON-RPC `EnergyPlugin.*` complète
|
||||
- Détection appareils par interface (zéro UUID hardcodé)
|
||||
|
||||
### ❌ À corriger en priorité
|
||||
| Fichier | Problème | Priorité |
|
||||
|---|---|---|
|
||||
| `evcharger.cpp:171`, `smartchargingmanager.cpp:394,477,517` | Assume toujours phase A — faux pour EV monophasé sur B/C | 🔴 Haute |
|
||||
| `smartchargingmanager.cpp:59` | Migration `endTime → endDateTime` + récurrence hebdo non terminée | 🟠 Moyenne |
|
||||
| `smartchargingmanager.cpp:884` | Planification limitée à 24h | 🟠 Moyenne |
|
||||
| `smartchargingmanager.cpp:1835` | Actions EV non séquentielles, pas de retry | 🟠 Moyenne |
|
||||
| `EnergyPluginNymea::init()` | Pas de guard si `EnergyManager*` est null | 🟠 Moyenne |
|
||||
| `evcharger.cpp:171`, `smartchargingmanager.cpp:394,477,517` | Assume toujours phase A — faux pour EV sur phase B/C | 🔴 |
|
||||
| `EnergyPluginNymea::init()` | Pas de guard si `EnergyManager*` null | 🟠 |
|
||||
| `smartchargingmanager.cpp:59` | Récurrence hebdo non terminée | 🟠 |
|
||||
| `smartchargingmanager.cpp:884` | Planification limitée à 24h | 🟠 |
|
||||
| `smartchargingmanager.cpp:1835` | Actions EV non séquentielles, pas de retry | 🟠 |
|
||||
|
||||
### ❌ À créer
|
||||
- `StaticHcHpProvider` (TariffManager — Community)
|
||||
- `EcsConsumer` (chauffe-eau / ECS sur surplus)
|
||||
- `HeatPumpConsumer` (PAC sur surplus)
|
||||
- `OptimizationEngine` (orchestrateur multi-consommateurs)
|
||||
- `WeatherManager` + `OpenMeteoProvider` (Auto)
|
||||
- `TierManager` + lecture `/etc/powersync/tier.conf`
|
||||
### ❌ À créer (code ETM dans `etm/`)
|
||||
- `PowerSyncClient` (pont Unix socket vers optimizer)
|
||||
- `StaticHcHpProvider` (tarif HP/HC statique — Community)
|
||||
|
||||
---
|
||||
|
||||
## Règles de modification
|
||||
|
||||
- Tout changement de signature `EnergyPlugin.*` → mettre à jour `INTERFACE.md`
|
||||
- Tout nouveau StateType ou ActionType → notifier l'Agent App
|
||||
- Ne jamais modifier `nymea-experience-plugin-energy` depuis ce repo
|
||||
- Tester sur un système Community avant d'activer des features Auto/Predict AI
|
||||
- Tout code ETM va dans `etm/` — jamais dans le code upstream
|
||||
- Modifier le code upstream uniquement pour corriger des bugs (FIXME existants)
|
||||
- Tout changement d'API `EnergyPlugin.*` → mettre à jour `INTERFACE.md`
|
||||
- Ne jamais ajouter de logique Auto/Predict AI dans ce repo
|
||||
- Build : `qmake energyplugin.pro && make -j$(nproc)`
|
||||
- Install : `/usr/lib/nymea/energy/libnymea_energypluginnymea.so`
|
||||
|
||||
---
|
||||
|
||||
## Portage des mises à jour nymea/Chargebyte
|
||||
|
||||
Quand une nouvelle version est disponible dans `etm-nymea/nymea-energy-plugin-nymea` :
|
||||
1. `diff -r etm-nymea/nymea-energy-plugin-nymea/ powersync-energy-plugin-etm/`
|
||||
2. Porter manuellement les corrections hors dossier `etm/`
|
||||
3. Ne jamais écraser `etm/`
|
||||
|
||||
@ -168,13 +168,13 @@ Electricity::Phases EvCharger::phases() const
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Can we figure out from the root meter on which phase we're attached?
|
||||
// One idea would be to sign up on root meter changes. When chargingEnabled is set or unset,
|
||||
// memorize root meter values and compare the next (or some more) cycles if a phase changed
|
||||
// by a similar value as we'd expect
|
||||
|
||||
// Until we have this detection, we must ask the user how many phases will be used while charging
|
||||
// For single-phase chargers, detect the actual connected phase from per-phase metering
|
||||
Electricity::Phases metered = meteredPhases();
|
||||
if (metered != Electricity::PhaseNone) {
|
||||
return metered;
|
||||
}
|
||||
|
||||
// Phase unknown — charger has no per-phase metering or is not currently charging
|
||||
return Electricity::PhaseNone;
|
||||
}
|
||||
|
||||
|
||||
@ -391,9 +391,9 @@ void SmartChargingManager::verifyOverloadProtection(const QDateTime ¤tDate
|
||||
|
||||
if (evCharger->phaseCount() == 1) {
|
||||
|
||||
// FIXME: get the actual phase, not assume it is phase A in single phase charging
|
||||
if (requiredThrottlePower.value("A") > 0) {
|
||||
int throttleAmpere = qCeil(requiredThrottlePower.value("A") / 230);
|
||||
const QString phase = chargerPhaseKey(evCharger);
|
||||
if (requiredThrottlePower.value(phase) > 0) {
|
||||
int throttleAmpere = qCeil(requiredThrottlePower.value(phase) / 230);
|
||||
int desiredFallbackAmpere = evCharger->maxChargingCurrent() - throttleAmpere;
|
||||
|
||||
if (desiredFallbackAmpere < static_cast<int>(m_processInfos[evCharger].minimalChargingCurrent)) {
|
||||
@ -474,8 +474,7 @@ void SmartChargingManager::verifyOverloadProtectionRecovery(const QDateTime &cur
|
||||
bool restoreCharger = false;
|
||||
|
||||
if (evCharger->phaseCount() == 1) {
|
||||
// FIXME: get the actual phase, not assume it is phase A in single phase charging
|
||||
if (availablePhasePower.value("A") >= requiredRestorePhasePower) {
|
||||
if (availablePhasePower.value(chargerPhaseKey(evCharger)) >= requiredRestorePhasePower) {
|
||||
qCDebug(dcNymeaEnergy()) << "Overload protection: Enought power available to restore the original configuration"
|
||||
<< manualMaxChargingCurrent(evCharger->id()) << "[A]";
|
||||
restoreCharger = true;
|
||||
@ -514,8 +513,7 @@ void SmartChargingManager::verifyOverloadProtectionRecovery(const QDateTime &cur
|
||||
bool restoreChargerPower = false;
|
||||
|
||||
if (evCharger->phaseCount() == 1) {
|
||||
// FIXME: get the actual phase, not assume it is phase A in single phase charging
|
||||
if (availablePhasePower.value("A") >= requiredRestorePhasePower) {
|
||||
if (availablePhasePower.value(chargerPhaseKey(evCharger)) >= requiredRestorePhasePower) {
|
||||
qCDebug(dcNymeaEnergy()) << "Overload protection: Enought power available to start charging using the minimal charging current of"
|
||||
<< m_processInfos.value(evCharger).minimalChargingCurrent << "[A]";
|
||||
restoreChargerPower = true;
|
||||
@ -1770,6 +1768,28 @@ Electricity::Phases SmartChargingManager::getAscendingPhasesForCount(uint phaseC
|
||||
return phases;
|
||||
}
|
||||
|
||||
QString SmartChargingManager::chargerPhaseKey(EvCharger *evCharger) const
|
||||
{
|
||||
// Use live metering first — reliable when charger is actively charging
|
||||
Electricity::Phases metered = evCharger->meteredPhases();
|
||||
if (metered.testFlag(Electricity::PhaseA)) return QStringLiteral("A");
|
||||
if (metered.testFlag(Electricity::PhaseB)) return QStringLiteral("B");
|
||||
if (metered.testFlag(Electricity::PhaseC)) return QStringLiteral("C");
|
||||
|
||||
// Charger may be off — fall back to last known phase stored by prepareInformation()
|
||||
if (m_processInfos.contains(evCharger)) {
|
||||
Electricity::Phases known = m_processInfos.value(evCharger).effectivePhases;
|
||||
if (known.testFlag(Electricity::PhaseA)) return QStringLiteral("A");
|
||||
if (known.testFlag(Electricity::PhaseB)) return QStringLiteral("B");
|
||||
if (known.testFlag(Electricity::PhaseC)) return QStringLiteral("C");
|
||||
}
|
||||
|
||||
// Phase unknown — conservative fallback (unchanged previous behavior)
|
||||
qCWarning(dcNymeaEnergy()) << "Cannot determine connected phase for"
|
||||
<< evCharger->name() << "— defaulting to phase A";
|
||||
return QStringLiteral("A");
|
||||
}
|
||||
|
||||
uint SmartChargingManager::getBestPhaseCount(EvCharger *evCharger, double surplusAmpere)
|
||||
{
|
||||
uint desiredPhaseCount = 1;
|
||||
|
||||
@ -123,6 +123,7 @@ private:
|
||||
|
||||
Electricity::Phases getAscendingPhasesForCount(uint phaseCount);
|
||||
uint getBestPhaseCount(EvCharger *evCharger, double surplusAmpere);
|
||||
QString chargerPhaseKey(EvCharger *evCharger) const;
|
||||
|
||||
EnergyManager *m_energyManager = nullptr;
|
||||
ThingManager *m_thingManager = nullptr;
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user