Le libellé part tel quel dans chaque GetPlan. Rien ne le bornait : une saisie de plus de 1 Mio suffisait à faire dépasser la limite de ligne du protocole. La borne arrête la saisie en amont ; le refus d'émission du client (commit précédent) reste le filet. - LoadConfigStore::validateLabels(), appelée en tête de setConfigs() : un libellé de plus de 256 points de code Unicode → refus en bloc, rien n'est modifié ; SetLoadConfig rend EnergyErrorInvalidParameter et journalise le motif en français, qui nomme la charge par son id et donne la longueur, jamais le libellé. Placée AVANT isValid() parce que les motifs suivants citent le libellé. - Même partage que les rangs (LM-1209-a-bis) : load() ne l'applique pas. Un libellé plus long déjà persisté est chargé tel quel, ni tronqué ni écarté, et signalé ; la première écriture devra le raccourcir. - Test : testLabelIsBoundedOnWriteNeverOnLoad (256 accepté, 257 refusé sans modification en mémoire ni sur disque, configuration héritée conservée entière). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
145 lines
7.8 KiB
C++
145 lines
7.8 KiB
C++
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
// Copyright (C) 2025 - 2026, Patrick Schurig / ETM PowerSync
|
|
#pragma once
|
|
|
|
#include <QObject>
|
|
#include <QString>
|
|
#include "../types/loadconfig.h"
|
|
|
|
/*!
|
|
* \brief Store persistant des configs de charge pilotée (contrat etmvariableload §4).
|
|
*
|
|
* Master store unique : \c setConfigs() valide, **persiste** (\c /var/lib/nymea/\c
|
|
* energy-load-configuration.json, à côté de \c energy-manager-configuration.json) et émet
|
|
* \c changed() — l'arbitre reconstruit ses adaptateurs sur ce signal, le handler notifie l'app.
|
|
*
|
|
* Format fichier : \c {"version":1,"loads":[ ... ]}, chaque entrée étant un \c LoadConfig §4.
|
|
* Le tableau \c loads[] est **mot pour mot** le \c LoadDescriptor §4 (jonction inter-repos
|
|
* avec l'app).
|
|
*
|
|
* \invariant Écriture atomique (fichier temporaire + \c rename) — jamais de fichier mi-écrit.
|
|
* \invariant \c setConfigs() rejette en bloc (rien persisté) si une seule entrée est invalide.
|
|
*/
|
|
class LoadConfigStore : public QObject
|
|
{
|
|
Q_OBJECT
|
|
public:
|
|
/*!
|
|
* \brief Construit le store et charge le fichier s'il existe.
|
|
* \param parent Propriétaire Qt.
|
|
* \note Un fichier absent, illisible ou au JSON invalide n'est pas une erreur :
|
|
* le store démarre à vide. Une ENTRÉE invalide est ignorée individuellement
|
|
* (tolérance ascendante) — on ne perd pas les autres pour une seule.
|
|
*/
|
|
explicit LoadConfigStore(QObject *parent = nullptr);
|
|
|
|
/*! \brief Configs actuellement en mémoire (chargées au démarrage ou via setConfigs).
|
|
* \return Copie de l'ensemble courant ; toutes ont passé \c LoadConfig::isValid(). */
|
|
LoadConfigs configs() const { return m_configs; }
|
|
|
|
/*!
|
|
* \brief Remplace l'ensemble des configs : valide tout → persiste → \c emit changed().
|
|
* \param configs Nouvel ensemble (remplace l'existant — l'app envoie la liste complète).
|
|
* \param[out] error Message FR si rejet (aucune écriture dans ce cas).
|
|
* \return true si validé + persisté ; false si invalide ou échec d'écriture.
|
|
*/
|
|
bool setConfigs(const LoadConfigs &configs, QString *error = nullptr);
|
|
|
|
/*! \brief Cohérence de l'ENSEMBLE, ce qu'aucune charge ne peut vérifier seule.
|
|
*
|
|
* ECS-110 s'arrête au bord d'une charge : \c LoadConfig::isValid() ne voit pas les
|
|
* autres. Deux exigences ne vivent donc que ici — identifiants uniques, et un Thing
|
|
* revendiqué par au plus UNE charge active. Le second cas est le même défaut que la
|
|
* garde de l'arbitre contre la PAC codée en dur, mais entre deux charges de
|
|
* configuration : deux commandes contradictoires sur un organe, et sa puissance
|
|
* comptée deux fois dans le budget (règle absolue 1).
|
|
*
|
|
* \note Seules les charges \c enabled sont confrontées. Une charge désactivée ne
|
|
* construit aucun adaptateur et ne commande rien ; l'interdire empêcherait de
|
|
* préparer une configuration de remplacement. La bascule \c enabled repasse par
|
|
* \c setConfigs(), donc par cette vérification.
|
|
*
|
|
* \param configs Ensemble complet à confronter (les charges désactivées sont ignorées).
|
|
* \param[out] error Motif de refus, en français, exploitable tel quel.
|
|
* \return true si l'ensemble est cohérent ; false au PREMIER conflit rencontré.
|
|
*/
|
|
static bool validateSet(const LoadConfigs &configs, QString *error = nullptr);
|
|
|
|
/*!
|
|
* \brief Contrôle de BONNE FORME, appliqué aux ÉCRITURES et à elles seules. §12 / LM-1209-a.
|
|
*
|
|
* \param configs Ensemble candidat. \param error Motif du refus, si refus.
|
|
* \return Faux si deux charges portent le même rang.
|
|
*
|
|
* \note **Distincte de \c validateSet(), et la distinction est de sûreté, pas de propreté**
|
|
* — \rule{LM-1209-a-bis} : une vérification qui ÉCARTE et une qui REFUSE ne peuvent pas
|
|
* être la même fonction.
|
|
* \c validateSet() nomme ce qui rend un ensemble DANGEREUX — deux charges sur un même
|
|
* organe — et \c load() s'en sert pour ÉCARTER l'entrée fautive. Celle-ci nomme ce qui
|
|
* rend un ensemble MAL FORMÉ, et le seul geste correct y est de REFUSER l'écriture
|
|
* entière. Les fusionner ferait écarter au démarrage les entrées d'une configuration
|
|
* héritée à rangs doublés : des charges disparaîtraient de la configuration ET de
|
|
* l'arbitrage, sans \c applySafeState — une règle de contrat convertie en perte de
|
|
* configuration silencieuse.
|
|
* \note N'est donc JAMAIS appelée par \c load(). Un doublon hérité est **signalé
|
|
* bruyamment et CONSERVÉ** ; la première écriture cliente le répare.
|
|
*/
|
|
static bool validateRanks(const LoadConfigs &configs, QString *error = nullptr);
|
|
|
|
//! Plus long libellé de charge accepté à l'écriture, en caractères (points de code Unicode).
|
|
static constexpr int MaxLabelChars = 256;
|
|
|
|
/*!
|
|
* \brief Borne la longueur des libellés, à l'ÉCRITURE seulement (protocole optimiseur 1.2, Q7).
|
|
*
|
|
* Le libellé part tel quel dans chaque \c GetPlan (§5.3). Sans borne, une saisie démesurée
|
|
* ferait dépasser la limite de 1 Mio par ligne du protocole, et l'optimiseur ne recevrait plus
|
|
* rien. Le client d'optimiseur refuse d'émettre une telle ligne (\c request-too-long) : cette
|
|
* borne-ci arrête la saisie avant qu'elle n'y arrive.
|
|
*
|
|
* \param configs Ensemble candidat. \param[out] error Motif du refus, en français ; il nomme
|
|
* la charge par son identifiant et donne la longueur, jamais le libellé.
|
|
* \return Faux si un libellé compte plus de \c MaxLabelChars caractères.
|
|
*
|
|
* \note Même famille que \c validateRanks() (\rule{LM-1209-a-bis}) : un refus d'écriture, jamais
|
|
* un écart au chargement. \c load() ne l'appelle pas — un libellé plus long déjà persisté est
|
|
* **chargé tel quel, signalé, jamais tronqué** ; la première écriture devra le raccourcir.
|
|
*/
|
|
static bool validateLabels(const LoadConfigs &configs, QString *error = nullptr);
|
|
|
|
/*!
|
|
* \brief Réconcilie \c rankOrigin d'une écriture CLIENTE avec l'état persisté. LM-1209-c.
|
|
*
|
|
* \param incoming Ensemble reçu du client. \param error Motif du refus, si refus.
|
|
* \param[out] out Ensemble à persister, \c rankOrigin réconcilié.
|
|
* \return Faux si le client tente de FABRIQUER un \c "auto".
|
|
*
|
|
* Trois règles, et la deuxième est celle qu'on se serait trompé à écrire :
|
|
* 1. Rang CHANGÉ pour cet id → la marque est **effacée** : quelqu'un a classé la charge.
|
|
* 2. Rang INCHANGÉ → la marque est **conservée telle quelle**, même si le client l'omet.
|
|
* `SetLoadConfig` remplace en bloc : réordonner une AUTRE charge renvoie forcément
|
|
* l'entrée de celle-ci, et l'effacer à cette occasion perdrait une marque encore
|
|
* vraie — la charge n'a toujours été classée par personne. **L'effacement porte sur le
|
|
* RANG, jamais sur la réception d'une écriture.**
|
|
* 3. \c "auto" reçu là où le persisté ne l'a pas → **refus**. Un client ne fabrique pas
|
|
* une affirmation sur un geste du moteur. L'aller-retour verbatim d'une entrée qui la
|
|
* porte déjà reste licite, sans quoi la neutralité Get→Set serait rompue.
|
|
*
|
|
* \note Un client lève la marque SANS déplacer la charge en envoyant \c "user" — c'est la
|
|
* seule voie, et elle est explicite. L'omission ne lève rien (règle 2).
|
|
*/
|
|
bool reconcileRankOrigin(const LoadConfigs &incoming, LoadConfigs *out,
|
|
QString *error = nullptr) const;
|
|
|
|
signals:
|
|
/*! \brief Émis après une persistance réussie de \c setConfigs(). */
|
|
void changed();
|
|
|
|
private:
|
|
QString filePath() const;
|
|
bool load();
|
|
bool save(QString *error) const;
|
|
|
|
LoadConfigs m_configs;
|
|
};
|