feat(ratios): NymeaEnergy.GetEnergyRatios + EnergyRatiosChanged (voie c)
Expose autoconsommation/autonomie comme source canonique de l'energymanager,
pour remplacer le seam interim app-side (EnergyRatiosInterim.compute).
Voie (c) : méthode + notification sur le handler NymeaEnergy de NOTRE plugin
(déjà forké/buildé), calculées depuis EnergyManager::totalX() — interface
abstraite stable. PAS de champs sur Energy.PowerBalance → aucun fork du coeur
nymea (nymea-experience-plugin-energy) à rebaser perpétuellement.
- EnergyRatiosCalculator (GPL pur mesure) : Δ de cumuls, baseline minuit local,
reseed (1er appel / nouveau jour / non-monotone Δ<0), den<=0 -> n/a, clamp
[0,100]. Porté 1:1 du seam interim app.
- NymeaEnergy.GetEnergyRatios -> {o:selfConsumptionRate, o:autonomyRate} Double,
champ OMIS si n/a (jamais null).
- NymeaEnergy.EnergyRatiosChanged : meme forme, branchee sur powerBalanceChanged(),
emise uniquement quand une valeur (ou sa disponibilite) change.
- testEnergyRatiosAlignment : vecteurs joues 1:1 contre l'interim (seed, normal,
clamp-bas, den<=0->n/a, non-monotone, nouveau jour local).
- docs/INTERFACE_energyratios.md : contrat pour l'agent app (RPC a consommer).
Build prod 0/0. Suite simulation 25/0 (testLoadConfigRpc traverse le handler
modifie -> pas de regression).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
ad385ca8fc
commit
444b13dbf8
112
RECAP_phase2_ratios.md
Normal file
112
RECAP_phase2_ratios.md
Normal file
@ -0,0 +1,112 @@
|
|||||||
|
# RECAP — Phase 2 ratios (autoconsommation / autonomie)
|
||||||
|
|
||||||
|
> Point de reprise. Dernière session : 2026-06-30. Branche : `feature/beta-rulebased`.
|
||||||
|
> Commit feature : **`b61c0ab`** (local, **non poussé**, **non mergé**).
|
||||||
|
|
||||||
|
## 1. Objectif (rappel)
|
||||||
|
|
||||||
|
Exposer **autoconsommation** + **autonomie** comme **source canonique de
|
||||||
|
l'energymanager**, pour remplacer le seam interim app-side
|
||||||
|
`EnergyRatiosInterim.compute()` (`etm-powersync-app/lib/services/energy_ratios.dart`).
|
||||||
|
|
||||||
|
## 2. Décision d'architecture — voie (c) (actée)
|
||||||
|
|
||||||
|
Comparé **(a)** champs sur `Energy.PowerBalance` vs **(c)** méthode/notif sur le
|
||||||
|
handler `NymeaEnergy` de NOTRE plugin. **Retenu : (c).**
|
||||||
|
|
||||||
|
- (a) aurait imposé de **forker `nymea-experience-plugin-energy`** (plugin **cœur**),
|
||||||
|
passage mirror→build + rebase 1.14.2→1.15.2 + **rebase perpétuel** à chaque bump
|
||||||
|
nymea. Coût d'infra que Patrick fuit.
|
||||||
|
- (c) vit dans `powersync-energy-plugin-nymea` (déjà forké/buildé), calcule depuis
|
||||||
|
l'**interface abstraite stable** `EnergyManager` (`totalProduction/Return/
|
||||||
|
Consumption/Acquisition` + signal `powerBalanceChanged()`). **Valeurs identiques**
|
||||||
|
à (a) ; seul le canal diffère. **Zéro fork du cœur.**
|
||||||
|
- Coût app : un appel `GetEnergyRatios` + un abonnement `EnergyRatiosChanged` (vs
|
||||||
|
lire 2 champs `PowerBalance`). Borné, ponctuel, dans un repo qu'on contrôle.
|
||||||
|
- **Feature request upstream** nymea (champs sur `PowerBalance`) à ouvrir **séparément**
|
||||||
|
— PAS besoin de forker pour la demander. Si acceptée → migration app triviale.
|
||||||
|
|
||||||
|
## 3. Ce qui est FAIT (commit b61c0ab, 10 fichiers)
|
||||||
|
|
||||||
|
| Fichier | Rôle |
|
||||||
|
|---|---|
|
||||||
|
| `energyplugin/etm/ratios/energyratioscalculator.{h,cpp}` | **Calc GPL pur mesure**, porté 1:1 de l'interim |
|
||||||
|
| `energyplugin/etm/etm.pri` | Sources |
|
||||||
|
| `energyplugin/nymeaenergyjsonhandler.{h,cpp}` | Méthode + notif + ctor `EnergyManager*` |
|
||||||
|
| `energyplugin/energypluginnymea.cpp` | Câblage `energyManager()` |
|
||||||
|
| `tests/auto/simulation/experience/energyexperienceenergymock.cpp` | Câblage mock |
|
||||||
|
| `tests/auto/simulation/simulation.{h,cpp}` | `testEnergyRatiosAlignment` |
|
||||||
|
| `docs/INTERFACE_energyratios.md` | **Contrat pour l'agent app** |
|
||||||
|
|
||||||
|
### Contrat exposé (namespace `NymeaEnergy`)
|
||||||
|
- `GetEnergyRatios` → `{o:selfConsumptionRate, o:autonomyRate}` Double (%).
|
||||||
|
**Champ OMIS si n/a** (dénominateur ≤ 0). **Jamais `null`.**
|
||||||
|
- `EnergyRatiosChanged` → mêmes champs ; branchée sur `powerBalanceChanged()` ;
|
||||||
|
émise **seulement si une valeur (ou sa disponibilité) change**.
|
||||||
|
|
||||||
|
### Sémantique (1:1 interim)
|
||||||
|
Δ de cumuls sur la **journée locale** (minuit local). Reseed si 1er appel /
|
||||||
|
nouveau jour / **non-monotone** (Δ<0 : restart nymead / re-add / rollover).
|
||||||
|
`den≤0 → n/a`. Clamp `[0,100]`. Unité %.
|
||||||
|
```
|
||||||
|
autonomie = (Δ totalConsumption − Δ totalAcquisition) / Δ totalConsumption
|
||||||
|
autoconsommation = (Δ totalProduction − Δ totalReturn) / Δ totalProduction
|
||||||
|
```
|
||||||
|
|
||||||
|
### Validation (session)
|
||||||
|
- Build **prod 0/0**, `energyratioscalculator.o` linké dans le `.so`.
|
||||||
|
- `testEnergyRatiosAlignment` **PASS** (seed, normal, clamp-bas, den≤0→n/a,
|
||||||
|
non-monotone, nouveau jour).
|
||||||
|
- Suite simulation **25/0** (`testLoadConfigRpc` traverse le handler → pas de
|
||||||
|
régression).
|
||||||
|
|
||||||
|
## 4. RESTE À FAIRE (reprise)
|
||||||
|
|
||||||
|
1. **Merge `landing-silo`** — Patrick le fait lui-même, **quand validé**.
|
||||||
|
(Workflow : `git push origin feature/beta-rulebased:landing-silo`, NE PAS toucher
|
||||||
|
master/stable.) Le commit ratios n'est PAS encore poussé.
|
||||||
|
2. **`.deb`** : pas rebuildé. hems tourne `1.15.2+etm2` (rév.3 validée live). Quand
|
||||||
|
on voudra livrer les ratios sur hems → cross-build arm64 + bump version + publier
|
||||||
|
APT (voir DEPLOY.md). À décider au moment voulu.
|
||||||
|
3. **Côté repo app** (`etm-powersync-app`, séparé, Patrick) :
|
||||||
|
- Mirrorer `docs/INTERFACE_energyratios.md`.
|
||||||
|
- Swap : `GetEnergyRatios` au boot + abonnement `EnergyRatiosChanged` ;
|
||||||
|
**supprimer** `EnergyRatiosInterim.compute()` + état baseline ; mapper
|
||||||
|
champ-absent → `null`.
|
||||||
|
4. **Feature request upstream nymea** (champs `PowerBalance`) — à ouvrir, séparé.
|
||||||
|
|
||||||
|
### Dette/notes non bloquantes (héritées)
|
||||||
|
- Worktrees git cassés `landing-silo`/`experimental-silo` (`git worktree prune/repair`).
|
||||||
|
- `docs/TEST_TERRAIN.md` stale (dit `[EcsRelayAdapter]` → devrait être `[RelayRouter]`).
|
||||||
|
- Mirror `docs/INTERFACE_etmvariableload.md` rév.3 vers le repo app (manuel).
|
||||||
|
|
||||||
|
## 5. Build & test (commandes vérifiées en sandbox amd64)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# --- Build prod (0/0 attendu) ---
|
||||||
|
BD=/tmp/build-prod; rm -rf $BD; mkdir -p $BD; cd $BD
|
||||||
|
qmake6 <repo>/etm-powersync-energy-plugin-etm.pro && make -j$(nproc)
|
||||||
|
|
||||||
|
# --- Build suite simulation ---
|
||||||
|
BD=/tmp/build-sim; rm -rf $BD; mkdir -p $BD; cd $BD
|
||||||
|
qmake6 CONFIG+=build_tests <repo>/tests/auto/simulation/simulation.pro && make -j$(nproc)
|
||||||
|
|
||||||
|
# --- PIÈGE : rebuild du .so mock si stale (sinon les tests ETM échouent au setup
|
||||||
|
# de création de thing : !ecsId.isNull(), relayA && relayB, "valid ThingId") ---
|
||||||
|
cd <repo>/tests/mocks/plugins/energymocks
|
||||||
|
qmake6 energymocks.pro && make -j$(nproc) # régénère aussi plugininfo.h si besoin
|
||||||
|
|
||||||
|
# --- Run (NYMEA_PLUGINS_PATH = energymocks + plugins système nymea) ---
|
||||||
|
cd /tmp/build-sim
|
||||||
|
export NYMEA_PLUGINS_PATH="/usr/lib/x86_64-linux-gnu/nymea/plugins:<repo>/tests/mocks/plugins/energymocks"
|
||||||
|
rm -rf /tmp/nymea-test
|
||||||
|
./nymea-energy-simulation # suite complète → 25/0
|
||||||
|
./nymea-energy-simulation testEnergyRatiosAlignment # ratios seul → 3/3
|
||||||
|
```
|
||||||
|
|
||||||
|
## 6. Garde-fous projet (ne pas oublier)
|
||||||
|
- **Frontière GPL** : ratios = pur mesure (Δ compteurs). Aucun optim/forecast/
|
||||||
|
pondération dans le GPL — ça reste dans l'optimiseur propriétaire.
|
||||||
|
- **Remotes** : `origin` = travail ; `etm-public` = miroir, **jamais** push direct ;
|
||||||
|
`etm-pro` = reliquat, ne pas utiliser.
|
||||||
|
- **Jamais** merger vers master/stable depuis cette branche ; rév.3 + ratios = testing.
|
||||||
98
docs/INTERFACE_energyratios.md
Normal file
98
docs/INTERFACE_energyratios.md
Normal file
@ -0,0 +1,98 @@
|
|||||||
|
# Contrat — Ratios énergétiques canoniques (`NymeaEnergy.GetEnergyRatios`)
|
||||||
|
|
||||||
|
> **Statut :** Phase 2, voie (c). Source canonique côté energymanager.
|
||||||
|
> Remplace le seam interim app-side `EnergyRatiosInterim.compute()`.
|
||||||
|
> Exposé par `powersync-energy-plugin-nymea` (experience-plugin, namespace
|
||||||
|
> JSON-RPC `NymeaEnergy`) — **pas** sur `Energy.PowerBalance` (voir §5).
|
||||||
|
|
||||||
|
## 1. Pourquoi (c) et pas des champs sur `PowerBalance`
|
||||||
|
|
||||||
|
`PowerBalance` (namespace `Energy`) appartient à `nymea-experience-plugin-energy`,
|
||||||
|
un plugin **cœur** nymea. Y ajouter des champs imposerait de forker et rebaser ce
|
||||||
|
plugin à chaque montée de version nymea — coût d'infra perpétuel. La voie (c)
|
||||||
|
expose les ratios sur le handler `NymeaEnergy` de **notre** plugin (déjà forké,
|
||||||
|
déjà buildé), calculés à partir des **mêmes cumuls canoniques** de l'energymanager.
|
||||||
|
Valeurs strictement identiques à ce que (a) aurait produit ; seul le canal diffère.
|
||||||
|
|
||||||
|
## 2. Sémantique
|
||||||
|
|
||||||
|
Ratios dérivés par **Δ de cumuls** sur la **journée locale courante** (depuis
|
||||||
|
minuit local), pas par intégration du signal live → déterministe, pas de drift.
|
||||||
|
|
||||||
|
```
|
||||||
|
autonomie = (Δ totalConsumption − Δ totalAcquisition) / Δ totalConsumption
|
||||||
|
autoconsommation = (Δ totalProduction − Δ totalReturn) / Δ totalProduction
|
||||||
|
```
|
||||||
|
|
||||||
|
- Unité : **pourcent**, borné `[0, 100]`.
|
||||||
|
- `Δ X = X_courant − X_baseline`, baseline = cumuls au début de la période.
|
||||||
|
- **Reseed de la baseline** si : 1er calcul, **nouveau jour local**, OU compteur
|
||||||
|
**non monotone** (`Δ<0` : restart nymead / re-add thing / rollover).
|
||||||
|
- **Dénominateur ≤ 0** (nuit sans prod, etc.) → ratio **n/a** : le champ est
|
||||||
|
**OMIS** de la réponse. **Jamais `null`, jamais `NaN`.**
|
||||||
|
|
||||||
|
C'est le portage 1:1 de `EnergyRatiosInterim.compute()` — l'app peut supprimer
|
||||||
|
ce calcul et lire la source.
|
||||||
|
|
||||||
|
## 3. Méthode : `NymeaEnergy.GetEnergyRatios`
|
||||||
|
|
||||||
|
**Params :** aucun.
|
||||||
|
|
||||||
|
**Returns :**
|
||||||
|
|
||||||
|
| Champ | Type | Présence |
|
||||||
|
|---|---|---|
|
||||||
|
| `selfConsumptionRate` | `Double` (%) | **optionnel** — omis si n/a |
|
||||||
|
| `autonomyRate` | `Double` (%) | **optionnel** — omis si n/a |
|
||||||
|
|
||||||
|
Exemple (les deux disponibles) :
|
||||||
|
```json
|
||||||
|
{"id": 1, "method": "NymeaEnergy.GetEnergyRatios", "params": {}}
|
||||||
|
```
|
||||||
|
```json
|
||||||
|
{"id": 1, "status": "success",
|
||||||
|
"params": {"selfConsumptionRate": 66.6667, "autonomyRate": 50.0}}
|
||||||
|
```
|
||||||
|
|
||||||
|
Exemple nuit (autoconso n/a, le champ disparaît) :
|
||||||
|
```json
|
||||||
|
{"id": 1, "status": "success", "params": {"autonomyRate": 50.0}}
|
||||||
|
```
|
||||||
|
|
||||||
|
> ⚠️ Champ absent = **n/a**. Ne pas confondre avec 0 %. Mapper l'absence sur
|
||||||
|
> ton `double?` (Dart `null`).
|
||||||
|
|
||||||
|
## 4. Notification : `NymeaEnergy.EnergyRatiosChanged`
|
||||||
|
|
||||||
|
Mêmes champs que `GetEnergyRatios`. Émise sur `powerBalanceChanged()` de
|
||||||
|
l'energymanager, **uniquement quand une valeur (ou sa disponibilité) change**.
|
||||||
|
|
||||||
|
```json
|
||||||
|
{"notification": "NymeaEnergy.EnergyRatiosChanged",
|
||||||
|
"params": {"selfConsumptionRate": 80.0, "autonomyRate": 50.0}}
|
||||||
|
```
|
||||||
|
|
||||||
|
S'abonner via `JSONRPC.SetNotificationStatus` avec le namespace `NymeaEnergy`
|
||||||
|
(le même que `LoadConfigChanged`, `ChargingSchedulesChanged`, etc. — donc même
|
||||||
|
session, pas de nouvelle connexion).
|
||||||
|
|
||||||
|
## 5. Migration côté app
|
||||||
|
|
||||||
|
1. Au boot : `NymeaEnergy.GetEnergyRatios` pour l'état initial.
|
||||||
|
2. S'abonner à `NymeaEnergy.EnergyRatiosChanged` pour les MAJ.
|
||||||
|
3. **Supprimer** `EnergyRatiosInterim.compute()` et son état de baseline
|
||||||
|
(`_baseProduction`/`_baseReturn`/…/`_baseDay`) : la baseline/reseed/minuit-local
|
||||||
|
vivent désormais côté plugin.
|
||||||
|
4. Mapper champ-absent → `null` (n/a). Conserver `EnergyRatios{autoconsommation,
|
||||||
|
autonomie}` côté app ; seul le **producteur** change.
|
||||||
|
|
||||||
|
> Si nymea expose un jour `selfConsumptionRate`/`autonomyRate` directement sur
|
||||||
|
> `PowerBalance` (feature request upstream ouverte séparément), la migration
|
||||||
|
> app sera triviale : lire les champs `PowerBalance` au lieu d'appeler
|
||||||
|
> `GetEnergyRatios`. Jamais bloqué, jamais de fork du cœur à porter.
|
||||||
|
|
||||||
|
## 6. Frontière GPL
|
||||||
|
|
||||||
|
Pur mesure (Δ de compteurs cumulés). Aucune logique d'optimisation / prévision /
|
||||||
|
pondération — celle-ci reste dans l'optimiseur propriétaire. Implémenté dans
|
||||||
|
`energyplugin/etm/ratios/energyratioscalculator.{h,cpp}` (GPL-3.0-or-later).
|
||||||
@ -78,5 +78,5 @@ void EnergyPluginNymea::init()
|
|||||||
SmartChargingManager *chargingManager = new SmartChargingManager(energyManager(), thingManager(), spotMarketManager, configuration, this);
|
SmartChargingManager *chargingManager = new SmartChargingManager(energyManager(), thingManager(), spotMarketManager, configuration, this);
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
jsonRpcServer()->registerExperienceHandler(new NymeaEnergyJsonHandler(spotMarketManager, chargingManager, loadConfigStore, this), 0, 8);
|
jsonRpcServer()->registerExperienceHandler(new NymeaEnergyJsonHandler(spotMarketManager, chargingManager, loadConfigStore, energyManager(), this), 0, 8);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -12,11 +12,13 @@ HEADERS += \
|
|||||||
$$PWD/adapters/etmvariableloadadapter.h \
|
$$PWD/adapters/etmvariableloadadapter.h \
|
||||||
$$PWD/adapters/relayrouter.h \
|
$$PWD/adapters/relayrouter.h \
|
||||||
$$PWD/scheduler/rulebasedscheduler.h \
|
$$PWD/scheduler/rulebasedscheduler.h \
|
||||||
|
$$PWD/ratios/energyratioscalculator.h \
|
||||||
$$PWD/energyarbitrator.h \
|
$$PWD/energyarbitrator.h \
|
||||||
|
|
||||||
SOURCES += \
|
SOURCES += \
|
||||||
$$PWD/types/loadconfig.cpp \
|
$$PWD/types/loadconfig.cpp \
|
||||||
$$PWD/config/loadconfigstore.cpp \
|
$$PWD/config/loadconfigstore.cpp \
|
||||||
|
$$PWD/ratios/energyratioscalculator.cpp \
|
||||||
$$PWD/adapters/evadapter.cpp \
|
$$PWD/adapters/evadapter.cpp \
|
||||||
$$PWD/adapters/sgreadyadapter.cpp \
|
$$PWD/adapters/sgreadyadapter.cpp \
|
||||||
$$PWD/adapters/etmvariableloadadapter.cpp \
|
$$PWD/adapters/etmvariableloadadapter.cpp \
|
||||||
|
|||||||
89
energyplugin/etm/ratios/energyratioscalculator.cpp
Normal file
89
energyplugin/etm/ratios/energyratioscalculator.cpp
Normal file
@ -0,0 +1,89 @@
|
|||||||
|
// 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 "energyratioscalculator.h"
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
|
||||||
|
void EnergyRatiosCalculator::reset()
|
||||||
|
{
|
||||||
|
m_hasBaseline = false;
|
||||||
|
m_baseProduction = 0.0;
|
||||||
|
m_baseReturn = 0.0;
|
||||||
|
m_baseConsumption = 0.0;
|
||||||
|
m_baseAcquisition = 0.0;
|
||||||
|
m_baseDay = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
EnergyRatiosCalculator::Ratios EnergyRatiosCalculator::compute(double totalProduction,
|
||||||
|
double totalReturn,
|
||||||
|
double totalConsumption,
|
||||||
|
double totalAcquisition,
|
||||||
|
const QDateTime &now)
|
||||||
|
{
|
||||||
|
const qint64 day = localDay(now);
|
||||||
|
|
||||||
|
// Reseed si : 1er appel, nouveau jour local, OU compteur non monotone
|
||||||
|
// (Δ<0 → restart nymead / re-add thing / rollover). Jamais de ratio négatif.
|
||||||
|
const bool nonMonotone = m_hasBaseline &&
|
||||||
|
(totalProduction < m_baseProduction ||
|
||||||
|
totalReturn < m_baseReturn ||
|
||||||
|
totalConsumption < m_baseConsumption ||
|
||||||
|
totalAcquisition < m_baseAcquisition);
|
||||||
|
|
||||||
|
if (!m_hasBaseline || m_baseDay != day || nonMonotone) {
|
||||||
|
m_hasBaseline = true;
|
||||||
|
m_baseDay = day;
|
||||||
|
m_baseProduction = totalProduction;
|
||||||
|
m_baseReturn = totalReturn;
|
||||||
|
m_baseConsumption = totalConsumption;
|
||||||
|
m_baseAcquisition = totalAcquisition;
|
||||||
|
}
|
||||||
|
|
||||||
|
const double dProduction = totalProduction - m_baseProduction;
|
||||||
|
const double dReturn = totalReturn - m_baseReturn;
|
||||||
|
const double dConsumption = totalConsumption - m_baseConsumption;
|
||||||
|
const double dAcquisition = totalAcquisition - m_baseAcquisition;
|
||||||
|
|
||||||
|
Ratios result;
|
||||||
|
result.autoconsommationValid = ratio(dProduction - dReturn, dProduction, &result.autoconsommation);
|
||||||
|
result.autonomieValid = ratio(dConsumption - dAcquisition, dConsumption, &result.autonomie);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ratio en % borné [0, 100]. Dénominateur ≤ 0 (nuit sans prod, etc.) → n/a
|
||||||
|
// (retourne false) — jamais de NaN.
|
||||||
|
bool EnergyRatiosCalculator::ratio(double numerator, double denominator, double *outPercent)
|
||||||
|
{
|
||||||
|
if (denominator <= 0)
|
||||||
|
return false;
|
||||||
|
*outPercent = std::clamp(numerator / denominator * 100.0, 0.0, 100.0);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
qint64 EnergyRatiosCalculator::localDay(const QDateTime &now)
|
||||||
|
{
|
||||||
|
// Index de jour local stable (minuit local) — équivalent au _localDay interim.
|
||||||
|
return now.date().toJulianDay();
|
||||||
|
}
|
||||||
78
energyplugin/etm/ratios/energyratioscalculator.h
Normal file
78
energyplugin/etm/ratios/energyratioscalculator.h
Normal file
@ -0,0 +1,78 @@
|
|||||||
|
// 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 ENERGYRATIOSCALCULATOR_H
|
||||||
|
#define ENERGYRATIOSCALCULATOR_H
|
||||||
|
|
||||||
|
#include <QDateTime>
|
||||||
|
|
||||||
|
// [Phase 2] Ratios énergétiques canoniques (autoconsommation / autonomie), en %.
|
||||||
|
//
|
||||||
|
// Source canonique côté energymanager : remplace le seam interim app-side
|
||||||
|
// (EnergyRatiosInterim.compute). PUR MESURE (frontière GPL) : dérive par Δ de
|
||||||
|
// cumuls sur la période affichée (journée depuis minuit local), pas par
|
||||||
|
// intégration du signal live → déterministe, pas de drift de polling.
|
||||||
|
//
|
||||||
|
// autonomie = (Δ totalConsumption − Δ totalAcquisition) / Δ totalConsumption
|
||||||
|
// autoconsommation = (Δ totalProduction − Δ totalReturn) / Δ totalProduction
|
||||||
|
//
|
||||||
|
// Reseed de la baseline : 1er appel, nouveau jour local, OU compteur non
|
||||||
|
// monotone (Δ<0 : restart nymead / re-add thing / rollover). Dénominateur ≤ 0
|
||||||
|
// → n/a (valid=false), jamais de NaN. Porté 1:1 de l'interim app.
|
||||||
|
class EnergyRatiosCalculator
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
struct Ratios {
|
||||||
|
bool autoconsommationValid = false; // false = n/a (dénominateur nul)
|
||||||
|
double autoconsommation = 0.0; // % dans [0, 100]
|
||||||
|
bool autonomieValid = false; // false = n/a
|
||||||
|
double autonomie = 0.0; // % dans [0, 100]
|
||||||
|
};
|
||||||
|
|
||||||
|
EnergyRatiosCalculator() = default;
|
||||||
|
|
||||||
|
// Réinitialise la baseline (ex. switch d'installation).
|
||||||
|
void reset();
|
||||||
|
|
||||||
|
// Calcule depuis les cumuls courants (mêmes unités en entrée — elles
|
||||||
|
// s'annulent dans le ratio). `now` (heure LOCALE) sert à détecter le jour.
|
||||||
|
Ratios compute(double totalProduction,
|
||||||
|
double totalReturn,
|
||||||
|
double totalConsumption,
|
||||||
|
double totalAcquisition,
|
||||||
|
const QDateTime &now);
|
||||||
|
|
||||||
|
private:
|
||||||
|
static bool ratio(double numerator, double denominator, double *outPercent);
|
||||||
|
static qint64 localDay(const QDateTime &now);
|
||||||
|
|
||||||
|
bool m_hasBaseline = false;
|
||||||
|
double m_baseProduction = 0.0;
|
||||||
|
double m_baseReturn = 0.0;
|
||||||
|
double m_baseConsumption = 0.0;
|
||||||
|
double m_baseAcquisition = 0.0;
|
||||||
|
qint64 m_baseDay = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
#endif // ENERGYRATIOSCALCULATOR_H
|
||||||
@ -34,11 +34,12 @@
|
|||||||
#include <QLoggingCategory>
|
#include <QLoggingCategory>
|
||||||
Q_DECLARE_LOGGING_CATEGORY(dcNymeaEnergy)
|
Q_DECLARE_LOGGING_CATEGORY(dcNymeaEnergy)
|
||||||
|
|
||||||
NymeaEnergyJsonHandler::NymeaEnergyJsonHandler(SpotMarketManager *spotMarketManager, SmartChargingManager *smartChargingManager, LoadConfigStore *loadConfigStore, QObject *parent):
|
NymeaEnergyJsonHandler::NymeaEnergyJsonHandler(SpotMarketManager *spotMarketManager, SmartChargingManager *smartChargingManager, LoadConfigStore *loadConfigStore, EnergyManager *energyManager, QObject *parent):
|
||||||
JsonHandler{parent},
|
JsonHandler{parent},
|
||||||
m_spotMarketManager{spotMarketManager},
|
m_spotMarketManager{spotMarketManager},
|
||||||
m_smartChargingManager{smartChargingManager},
|
m_smartChargingManager{smartChargingManager},
|
||||||
m_loadConfigStore{loadConfigStore}
|
m_loadConfigStore{loadConfigStore},
|
||||||
|
m_energyManager{energyManager}
|
||||||
{
|
{
|
||||||
|
|
||||||
registerEnum<ChargingInfo::ChargingMode>();
|
registerEnum<ChargingInfo::ChargingMode>();
|
||||||
@ -182,6 +183,17 @@ NymeaEnergyJsonHandler::NymeaEnergyJsonHandler(SpotMarketManager *spotMarketMana
|
|||||||
returns.insert("energyError", enumRef<EnergyManager::EnergyError>());
|
returns.insert("energyError", enumRef<EnergyManager::EnergyError>());
|
||||||
registerMethod("SetLoadConfig", description, params, returns, Types::PermissionScopeControlThings);
|
registerMethod("SetLoadConfig", description, params, returns, Types::PermissionScopeControlThings);
|
||||||
|
|
||||||
|
// [Phase 2] Ratios canoniques autoconsommation / autonomie (% [0,100]). Champ omis
|
||||||
|
// (jamais null) quand n/a (dénominateur ≤ 0). Source canonique de l'energymanager —
|
||||||
|
// remplace le seam interim app-side.
|
||||||
|
params.clear(); returns.clear();
|
||||||
|
description = "Get the current energy ratios (self-consumption / autonomy) in percent, "
|
||||||
|
"derived from the energy manager's cumulative counters over the current local "
|
||||||
|
"day. A rate field is omitted (never null) when not available (denominator <= 0).";
|
||||||
|
returns.insert("o:selfConsumptionRate", enumValueName(Double));
|
||||||
|
returns.insert("o:autonomyRate", enumValueName(Double));
|
||||||
|
registerMethod("GetEnergyRatios", description, params, returns, Types::PermissionScopeControlThings);
|
||||||
|
|
||||||
|
|
||||||
// Notifications
|
// Notifications
|
||||||
params.clear();
|
params.clear();
|
||||||
@ -291,6 +303,19 @@ NymeaEnergyJsonHandler::NymeaEnergyJsonHandler(SpotMarketManager *spotMarketMana
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// [Phase 2] EnergyRatiosChanged : émise sur powerBalanceChanged() de l'energymanager,
|
||||||
|
// uniquement quand une valeur (ou sa disponibilité) change. Mêmes champs que GetEnergyRatios.
|
||||||
|
params.clear();
|
||||||
|
description = "Emitted whenever the energy ratios (self-consumption / autonomy) change. "
|
||||||
|
"A rate field is omitted (never null) when not available.";
|
||||||
|
params.insert("o:selfConsumptionRate", enumValueName(Double));
|
||||||
|
params.insert("o:autonomyRate", enumValueName(Double));
|
||||||
|
registerNotification("EnergyRatiosChanged", description, params);
|
||||||
|
if (m_energyManager) {
|
||||||
|
connect(m_energyManager, &EnergyManager::powerBalanceChanged,
|
||||||
|
this, &NymeaEnergyJsonHandler::onPowerBalanceChanged);
|
||||||
|
}
|
||||||
|
|
||||||
// Spot market manager
|
// Spot market manager
|
||||||
params.clear();
|
params.clear();
|
||||||
description = "Emitted whenever the spot market manager status changed.";
|
description = "Emitted whenever the spot market manager status changed.";
|
||||||
@ -527,6 +552,60 @@ JsonReply *NymeaEnergyJsonHandler::SetLoadConfig(const QVariantMap ¶ms)
|
|||||||
return createReply({{"energyError", enumValueName(EnergyManager::EnergyErrorNoError)}});
|
return createReply({{"energyError", enumValueName(EnergyManager::EnergyErrorNoError)}});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// [Phase 2] Construit le map de ratios courant depuis les cumuls de l'energymanager.
|
||||||
|
// Champ omis (jamais null) quand n/a. Met à jour l'état caché (m_lastRatios).
|
||||||
|
QVariantMap NymeaEnergyJsonHandler::currentEnergyRatios()
|
||||||
|
{
|
||||||
|
QVariantMap result;
|
||||||
|
if (!m_energyManager)
|
||||||
|
return result;
|
||||||
|
|
||||||
|
const EnergyRatiosCalculator::Ratios r = m_ratiosCalculator.compute(
|
||||||
|
m_energyManager->totalProduction(),
|
||||||
|
m_energyManager->totalReturn(),
|
||||||
|
m_energyManager->totalConsumption(),
|
||||||
|
m_energyManager->totalAcquisition(),
|
||||||
|
QDateTime::currentDateTime());
|
||||||
|
|
||||||
|
m_lastRatios = r;
|
||||||
|
m_lastRatiosValid = true;
|
||||||
|
|
||||||
|
if (r.autoconsommationValid)
|
||||||
|
result.insert("selfConsumptionRate", r.autoconsommation);
|
||||||
|
if (r.autonomieValid)
|
||||||
|
result.insert("autonomyRate", r.autonomie);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
JsonReply *NymeaEnergyJsonHandler::GetEnergyRatios(const QVariantMap ¶ms)
|
||||||
|
{
|
||||||
|
Q_UNUSED(params)
|
||||||
|
return createReply(currentEnergyRatios());
|
||||||
|
}
|
||||||
|
|
||||||
|
void NymeaEnergyJsonHandler::onPowerBalanceChanged()
|
||||||
|
{
|
||||||
|
const EnergyRatiosCalculator::Ratios prev = m_lastRatios;
|
||||||
|
const bool hadPrev = m_lastRatiosValid;
|
||||||
|
|
||||||
|
const QVariantMap ratios = currentEnergyRatios();
|
||||||
|
const EnergyRatiosCalculator::Ratios &now = m_lastRatios;
|
||||||
|
|
||||||
|
// N'émet que si une valeur (ou sa disponibilité) a réellement bougé.
|
||||||
|
auto changed = [](bool prevValid, double prevVal, bool nowValid, double nowVal) {
|
||||||
|
if (prevValid != nowValid)
|
||||||
|
return true;
|
||||||
|
return nowValid && qAbs(prevVal - nowVal) > 1e-6;
|
||||||
|
};
|
||||||
|
|
||||||
|
const bool moved = !hadPrev
|
||||||
|
|| changed(prev.autoconsommationValid, prev.autoconsommation, now.autoconsommationValid, now.autoconsommation)
|
||||||
|
|| changed(prev.autonomieValid, prev.autonomie, now.autonomieValid, now.autonomie);
|
||||||
|
|
||||||
|
if (moved)
|
||||||
|
emit EnergyRatiosChanged(ratios);
|
||||||
|
}
|
||||||
|
|
||||||
void NymeaEnergyJsonHandler::sendSpotMarketConfigurationChangedNotification()
|
void NymeaEnergyJsonHandler::sendSpotMarketConfigurationChangedNotification()
|
||||||
{
|
{
|
||||||
QVariantMap params;
|
QVariantMap params;
|
||||||
|
|||||||
@ -30,16 +30,18 @@
|
|||||||
#include <jsonrpc/jsonhandler.h>
|
#include <jsonrpc/jsonhandler.h>
|
||||||
|
|
||||||
#include "types/scoreentry.h"
|
#include "types/scoreentry.h"
|
||||||
|
#include "etm/ratios/energyratioscalculator.h"
|
||||||
|
|
||||||
class SmartChargingManager;
|
class SmartChargingManager;
|
||||||
class SpotMarketManager;
|
class SpotMarketManager;
|
||||||
class LoadConfigStore;
|
class LoadConfigStore;
|
||||||
|
class EnergyManager;
|
||||||
|
|
||||||
class NymeaEnergyJsonHandler : public JsonHandler
|
class NymeaEnergyJsonHandler : public JsonHandler
|
||||||
{
|
{
|
||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
public:
|
public:
|
||||||
explicit NymeaEnergyJsonHandler(SpotMarketManager *spotMarketManager, SmartChargingManager *smartChargingManager, LoadConfigStore *loadConfigStore = nullptr, QObject *parent = nullptr);
|
explicit NymeaEnergyJsonHandler(SpotMarketManager *spotMarketManager, SmartChargingManager *smartChargingManager, LoadConfigStore *loadConfigStore = nullptr, EnergyManager *energyManager = nullptr, QObject *parent = nullptr);
|
||||||
|
|
||||||
QString name() const override;
|
QString name() const override;
|
||||||
|
|
||||||
@ -68,6 +70,9 @@ public:
|
|||||||
Q_INVOKABLE JsonReply *GetLoadConfig(const QVariantMap ¶ms);
|
Q_INVOKABLE JsonReply *GetLoadConfig(const QVariantMap ¶ms);
|
||||||
Q_INVOKABLE JsonReply *SetLoadConfig(const QVariantMap ¶ms);
|
Q_INVOKABLE JsonReply *SetLoadConfig(const QVariantMap ¶ms);
|
||||||
|
|
||||||
|
// [Phase 2] Ratios canoniques (autoconsommation / autonomie), % [0,100].
|
||||||
|
Q_INVOKABLE JsonReply *GetEnergyRatios(const QVariantMap ¶ms);
|
||||||
|
|
||||||
signals:
|
signals:
|
||||||
void PhasePowerLimitChanged(const QVariantMap ¶ms);
|
void PhasePowerLimitChanged(const QVariantMap ¶ms);
|
||||||
void AcquisitionToleranceChanged(const QVariantMap ¶ms);
|
void AcquisitionToleranceChanged(const QVariantMap ¶ms);
|
||||||
@ -80,12 +85,22 @@ signals:
|
|||||||
void ChargingSchedulesChanged(const QVariantMap ¶ms);
|
void ChargingSchedulesChanged(const QVariantMap ¶ms);
|
||||||
void BatteryLevelConsiderationChanged(const QVariantMap ¶ms);
|
void BatteryLevelConsiderationChanged(const QVariantMap ¶ms);
|
||||||
void LoadConfigChanged(const QVariantMap ¶ms);
|
void LoadConfigChanged(const QVariantMap ¶ms);
|
||||||
|
void EnergyRatiosChanged(const QVariantMap ¶ms);
|
||||||
|
|
||||||
|
private slots:
|
||||||
|
void onPowerBalanceChanged();
|
||||||
|
|
||||||
private:
|
private:
|
||||||
SpotMarketManager *m_spotMarketManager;
|
SpotMarketManager *m_spotMarketManager;
|
||||||
SmartChargingManager *m_smartChargingManager = nullptr;
|
SmartChargingManager *m_smartChargingManager = nullptr;
|
||||||
LoadConfigStore *m_loadConfigStore = nullptr;
|
LoadConfigStore *m_loadConfigStore = nullptr;
|
||||||
|
EnergyManager *m_energyManager = nullptr;
|
||||||
|
|
||||||
|
EnergyRatiosCalculator m_ratiosCalculator;
|
||||||
|
EnergyRatiosCalculator::Ratios m_lastRatios;
|
||||||
|
bool m_lastRatiosValid = false; // un calcul a-t-il déjà été émis ?
|
||||||
|
|
||||||
|
QVariantMap currentEnergyRatios();
|
||||||
void sendSpotMarketConfigurationChangedNotification();
|
void sendSpotMarketConfigurationChangedNotification();
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|||||||
@ -100,7 +100,7 @@ void ExperiencePluginEnergyMock::init()
|
|||||||
#endif
|
#endif
|
||||||
// [ETM] END
|
// [ETM] END
|
||||||
|
|
||||||
m_nymeaEnergyJsonHandler = new NymeaEnergyJsonHandler(m_spotMarketManager, m_smartChargingManager, loadConfigStore, this);
|
m_nymeaEnergyJsonHandler = new NymeaEnergyJsonHandler(m_spotMarketManager, m_smartChargingManager, loadConfigStore, m_energyManager, this);
|
||||||
jsonRpcServer()->registerExperienceHandler(m_nymeaEnergyJsonHandler, 0, 2);
|
jsonRpcServer()->registerExperienceHandler(m_nymeaEnergyJsonHandler, 0, 2);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -42,6 +42,7 @@ using namespace nymeaserver;
|
|||||||
#include "../../../energyplugin/etm/adapters/relayrouter.h"
|
#include "../../../energyplugin/etm/adapters/relayrouter.h"
|
||||||
#include "../../../energyplugin/etm/types/loadconfig.h"
|
#include "../../../energyplugin/etm/types/loadconfig.h"
|
||||||
#include "../../../energyplugin/etm/config/loadconfigstore.h"
|
#include "../../../energyplugin/etm/config/loadconfigstore.h"
|
||||||
|
#include "../../../energyplugin/etm/ratios/energyratioscalculator.h"
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
#include <QDir>
|
#include <QDir>
|
||||||
@ -756,6 +757,59 @@ void Simulation::testEcsRelayTopologies()
|
|||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void Simulation::testEnergyRatiosAlignment()
|
||||||
|
{
|
||||||
|
#ifndef ETM_ARBITRATOR
|
||||||
|
QSKIP("ETM_ARBITRATOR désactivé — ratios canoniques non compilés.");
|
||||||
|
#else
|
||||||
|
// Vecteurs joués 1:1 contre la sémantique du seam interim app
|
||||||
|
// (EnergyRatiosInterim.compute) : seed, normal, clamp-bas, den≤0→n/a,
|
||||||
|
// non-monotone (Δ<0)→reseed, nouveau jour local→reseed.
|
||||||
|
EnergyRatiosCalculator calc;
|
||||||
|
|
||||||
|
auto naAuto = [](const EnergyRatiosCalculator::Ratios &r) { QVERIFY(!r.autoconsommationValid); };
|
||||||
|
auto naAuton = [](const EnergyRatiosCalculator::Ratios &r) { QVERIFY(!r.autonomieValid); };
|
||||||
|
auto eqAuto = [](const EnergyRatiosCalculator::Ratios &r, double v) {
|
||||||
|
QVERIFY(r.autoconsommationValid); QVERIFY(qAbs(r.autoconsommation - v) < 1e-3);
|
||||||
|
};
|
||||||
|
auto eqAuton = [](const EnergyRatiosCalculator::Ratios &r, double v) {
|
||||||
|
QVERIFY(r.autonomieValid); QVERIFY(qAbs(r.autonomie - v) < 1e-3);
|
||||||
|
};
|
||||||
|
|
||||||
|
const QDateTime d29_00(QDate(2026, 6, 29), QTime(0, 0));
|
||||||
|
const QDateTime d29_06(QDate(2026, 6, 29), QTime(6, 0));
|
||||||
|
const QDateTime d29_08(QDate(2026, 6, 29), QTime(8, 0));
|
||||||
|
const QDateTime d29_10(QDate(2026, 6, 29), QTime(10, 0));
|
||||||
|
const QDateTime d30_02(QDate(2026, 6, 30), QTime(2, 0));
|
||||||
|
const QDateTime d30_10(QDate(2026, 6, 30), QTime(10, 0));
|
||||||
|
|
||||||
|
// A — 1er appel : seed baseline → deltas nuls → den≤0 → n/a (les deux).
|
||||||
|
EnergyRatiosCalculator::Ratios a = calc.compute(1000, 200, 3000, 2200, d29_00);
|
||||||
|
naAuto(a); naAuton(a);
|
||||||
|
|
||||||
|
// B — même jour, croissance monotone : auto=(500-100)/500=80%, autonomie=(1000-500)/1000=50%.
|
||||||
|
EnergyRatiosCalculator::Ratios b = calc.compute(1500, 300, 4000, 2700, d29_06);
|
||||||
|
eqAuto(b, 80.0); eqAuton(b, 50.0);
|
||||||
|
|
||||||
|
// C — clamp bas : dReturn(700) > dProd(600) → num<0 → auto borné à 0.0 ;
|
||||||
|
// autonomie=(1200-900)/1200=25%.
|
||||||
|
EnergyRatiosCalculator::Ratios c = calc.compute(1600, 900, 4200, 3100, d29_08);
|
||||||
|
eqAuto(c, 0.0); eqAuton(c, 25.0);
|
||||||
|
|
||||||
|
// D — compteur non monotone (production 900 < base 1000) → reseed → n/a.
|
||||||
|
EnergyRatiosCalculator::Ratios d = calc.compute(900, 900, 4200, 3100, d29_10);
|
||||||
|
naAuto(d); naAuton(d);
|
||||||
|
|
||||||
|
// E — nouveau jour local → reseed → n/a (même si cumuls croissants).
|
||||||
|
EnergyRatiosCalculator::Ratios e = calc.compute(2000, 500, 6000, 4000, d30_02);
|
||||||
|
naAuto(e); naAuton(e);
|
||||||
|
|
||||||
|
// F — même jour (30) : auto=(600-200)/600≈66.667%, autonomie=(1000-500)/1000=50%.
|
||||||
|
EnergyRatiosCalculator::Ratios f = calc.compute(2600, 700, 7000, 4500, d30_10);
|
||||||
|
eqAuto(f, 66.6667); eqAuton(f, 50.0);
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
void Simulation::testLoadConfigRelayRouter()
|
void Simulation::testLoadConfigRelayRouter()
|
||||||
{
|
{
|
||||||
#ifndef ETM_ARBITRATOR
|
#ifndef ETM_ARBITRATOR
|
||||||
|
|||||||
@ -81,6 +81,10 @@ private slots:
|
|||||||
// transition non-cascadée, et DÉDUPLICATION des niveaux (relais identiques fusionnés).
|
// transition non-cascadée, et DÉDUPLICATION des niveaux (relais identiques fusionnés).
|
||||||
void testEcsRelayTopologies();
|
void testEcsRelayTopologies();
|
||||||
|
|
||||||
|
// [Phase 2] Ratios canoniques : EnergyRatiosCalculator aligné 1:1 sur le seam interim app
|
||||||
|
// (EnergyRatiosInterim.compute) — seed, normal, clamp, den≤0→n/a, non-monotone, nouveau jour.
|
||||||
|
void testEnergyRatiosAlignment();
|
||||||
|
|
||||||
void printStates(Thing *thing);
|
void printStates(Thing *thing);
|
||||||
void updateChargerMeter(Thing *thing);
|
void updateChargerMeter(Thing *thing);
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user