// SPDX-License-Identifier: GPL-3.0-or-later // Copyright (C) 2025 - 2026, Patrick Schurig / ETM PowerSync #pragma once #include #include #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); /*! * \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; };