feat(ph): switch calibration to two-points (Gravity v2.0)#9
Merged
Conversation
Capture le travail de cadrage pour migrer packages/ph.yaml d'une calibration single-point pH 7 (offset additif) vers une calibration two-points pH 7+pH 4 qui recalcule slope ET intercept. Le plan intègre les findings de la doc-review du 2026-04-26 : verrou anti-race reset-vs-calibration, multi-capture spread check (couvre staleness I²C invisibles au NaN guard), gating de l'interval pendant la calib (évite la pollution de pool_ph), text_sensor de recovery résilient à la déconnexion HA.
Remplace l'unique offset additif `g_ph_offset` par un modèle slope+intercept explicite (`pH = g_ph_slope * V + g_ph_intercept`). Les valeurs d'usine (3.56, -1.889) reproduisent la formule pré-calibration — comportement inchangé tant qu'aucune calibration n'est lancée. Mise en place des globals dont U2 a besoin : - g_ph_slope, g_ph_intercept (restored, valeurs d'usine au premier boot) - g_v_ph7, g_v_ph4 (restored, dernières captures pour audit/SAV) - g_ph_calibration_in_progress (NON restored — repart à false au boot, le bouton reset reste utilisable même après reboot mid-séquence) - g_ph_last_result_code (restored, int — std::string n'est pas fiable en restore_value sur ESPHome ; le text_sensor mappe le code vers une chaîne via lambda) Surface diagnostic (R10, R11, R16) : - pH Slope, pH Intercept, V_pH7, V_pH4 (sensors numériques) - pH Calibration Last Result (text_sensor avec lookup par code) Le bloc `interval: 1min` qui latche `g_store_pool_ph` est conditionné sur `!id(g_ph_calibration_in_progress)` (R15) — la calibration U2 figera proprement `pool_ph` même si la pompe tournait depuis longtemps. L'OTA depuis l'ancien firmware invalide `g_ph_offset` ; les valeurs d'usine reproduisent la formule pré-calibration donc le pH affiché reste numériquement identique tant que l'utilisateur ne lance pas la nouvelle calibration two-points (livrée en U2). Refs : R1, R2, R3, R10, R11, R13, R15, R16 Plan : docs/plans/2026-04-25-001-feat-ph-two-point-calibration-plan.md
…otection
Ajoute la séquence interactive complète : un bouton "Démarrer calibration pH"
qui orchestre pH 7 → pH 4 (~6 min 20 s) avec garde-fous, et un bouton
"Reset calibration pH usine" gardé contre la race avec le script.
Script `_ph_calibration` (mode: single) :
- Verrou g_ph_calibration_in_progress posé à l'entrée, libéré sur les
4 chemins de sortie (3 abort handlers + 1 succès) — vérifié par grep
qui retourne exactement 4 occurrences de "= false"
- Multi-capture R14 : 3 échantillons espacés de 5 s en fin de fenêtre
de stabilisation 180 s, spread max 50 mV. Couvre les staleness I²C
invisibles au NaN guard (ex: bus figé qui retourne dernière valeur
valide, latence I²C qui produit lecture non-stabilisée)
- NaN guard R12 sur chaque sous-capture : sur NaN détecté, code d'erreur
4 (pH 7) ou 5 (pH 4), abort, aucun global slope/intercept écrit
- Garde-fous R4 + R5 calculés sur variables locales avant écriture :
V_pH7 > V_pH4 (sinon code 2 "bains inversés"), slope ∈ [2.5, 5.0]
(sinon code 3 "pente hors plage", message dynamique inclut la pente
rejetée calculée à partir des V capturés)
- Notifications HA wrappées dans api.connected: ; les messages dynamiques
(slope/intercept, V capturées, spread mV) embarqués via snprintf
Bouton "Reset calibration pH usine" (R13) :
- Vérifie g_ph_calibration_in_progress en premier ; si true, notif
"Reset ignoré, calibration en cours" sans écrire
- Sinon (branche else) : restaure slope=3.56, intercept=-1.889,
code 8 ("Reset usine"), met à jour 4 sondes diagnostic + realtime_ph,
notifie. Ne touche pas g_v_ph7 / g_v_ph4 (audit historique conservé)
Validation locale : tous les 8 presets (les 6 incluant ph.yaml + les 2
sans) passent `esphome config` après sed-rewrite vers !include.
Refs : R4, R5, R6, R7, R8, R9, R12, R13, R14, R16
Plan : docs/plans/2026-04-25-001-feat-ph-two-point-calibration-plan.md
Met à jour le README pour refléter le nouveau flux pH : - Tables boutons : "pH Calibration 7.00" remplacé par "Démarrer calibration pH" (séquence two-points guidée par notifs HA) et "Reset calibration pH usine" (refusé pendant qu'une calib tourne) - Table packages : description packages/ph.yaml mise à jour pour mentionner slope+intercept + 4 sondes diagnostic + text_sensor d'audit - Nouvelle section "## Calibration pH (procédure)" : matériel, préparation (couper la pompe), 6 étapes détaillées avec les notifications HA attendues, durée totale 6 min 20 s, table des codes de rejet (2/3/4-5/6-7), recovery via le text_sensor `pH Calibration Last Result` quand HA déconnecte mid-séquence, limitations (pas de compensation T°, exclusivement Gravity v2.0 non-inversé), fréquence recommandée saisonnière Pas de changement dashboard : la review feasibility a confirmé que homeassistant/dashboard/frangipool.yaml ne référence que sensor.frangipool_ph (préservé par l'OTA), pas l'ancien `pH Offset` ni le bouton single-point. Les 4 nouvelles sondes diagnostic + text_sensor apparaissent automatiquement dans HA mais ne sont pas pré-cartographiées dans le dashboard, par défaut "drop dashboard refs" du CLAUDE.md. Plan U1/U2/U3 marqués done. Plan : docs/plans/2026-04-25-001-feat-ph-two-point-calibration-plan.md
…, sample spacing, magic numbers)
Code review (8 reviewers) a remonté 4 vrais bugs et 2 P1 maintainability,
tous corrigés ici. Le code reste fonctionnellement équivalent au commit
précédent au niveau du happy path mais durcit les bords.
R-1 (P2 race adv-001) — Reset entre flag-flip et success notif
Le `g_ph_calibration_in_progress = false` était la 1ère action du chemin
succès, suivi de 5 actions (4 component.update + 1 notif). Si l'opérateur
pressait Reset entre la 1ère et la 5ème action, le code 8 ("Reset usine")
écrasait le code 1 ("OK") avant que le text_sensor ne refresh, mais la
notif success tirait quand même son message snprintf depuis g_ph_slope/
intercept qui venaient d'être ramenés à 3.56/-1.889. Résultat : notif
"Calibration pH OK pente=3.560 intercept=-1.889" simultanée avec
text_sensor "Reset usine". Fix : déplacer le flag clear en TOUTE DERNIÈRE
action sur les 4 chemins de sortie (succès + 3 abort handlers). Pendant
toute la séquence component.update + notif, le flag reste true et le
bouton Reset notifie "Calibration en cours, ignoré" comme prévu.
R-2 (P2 filter contamination adv-003 + REL-01 cross-confirmed)
Le sliding_window_moving_average (180s) sur realtime_ph reste saturé de
voltages bain-tampon ~3 min après la fin de la calibration. Quand le
flag clear arrive, le tick interval suivant latche `realtime_ph.state`
dans `g_store_pool_ph` — qui est encore une moyenne mix bain/pool.
Conséquence : pool_ph dans HA affiche ~4-5 pH pendant 1-3 min après
calibration, potentiellement déclenchant des automations HA qui lisent
pool_ph. Fix : nouveau global `g_ph_post_cal_cooldown` (int, non-restored,
init 0). Set à 3 ticks (3 min) à chaque sortie du script. Decrement à
chaque interval tick de 1 min. 4ème branche AND ajoutée au gating de
l'interval : `g_ph_post_cal_cooldown <= 0`. Le latch reste figé jusqu'à
ce que la fenêtre glissante se vide naturellement.
R-3 (P3 ADS1115 cache adv-002)
ADS1115 update_interval=10s → publie 1 valeur tous les 10s. Captures
multi-sample espacées de 5s lisaient potentiellement la même valeur
cachée 2 fois. Spread check (50 mV) opérait alors sur 2 effective
samples au lieu de 3. Fix : `delay: 5s` → `delay: 12s` (4 occurrences,
2 par bain). 12s > 10s garantit qu'au moins une nouvelle publication
ADS1115 a eu lieu entre captures. Total séquence passe de ~6m20s à
~6m48s — coût marginal pour 3 samples vraiment distincts.
R-4 (P3 zero-guard text_sensor case 3 — correctness + REL-02 + project-standards)
Sous partial NVS restore, g_ph_last_result_code=3 pourrait être restauré
avec g_v_ph7=g_v_ph4=0.0, faisant `3.0f / 0.0f` = +inf dans le snprintf
du text_sensor. Fix : guard `denom != 0.0f ? 3.0f / denom : 0.0f`. Pas
de crash, juste pente=0.00 affichée plutôt que +inf.
R-5 (P1 magic constants M-01)
Les littéraux 7.0f, 4.0f, 3.0f (= 7-4) et les bornes 2.5f / 5.0f étaient
disséminés dans 2 lambdas (validation + text_sensor case 3). Fix :
constantes nommées au top du lambda de validation : PH_BUF_HIGH=7,
PH_BUF_LOW=4, PH_RANGE=3, SLOPE_MIN=2.5, SLOPE_MAX=5. Slope et intercept
candidats dérivent maintenant des constantes. Si les buffers changent
un jour (improbable, vu Scope Boundaries), plus de drift entre les sites.
R-6 (P1 result code coupling M-02)
La table des codes 0-8 n'existait qu'en commentaire d'en-tête de fichier.
Le default du switch retournait silencieusement "" (corrigé par safe_auto
en "Jamais calibré"). Si un futur dev ajoutait un code dans le script
sans toucher au switch, aucun signal compile/runtime. Fix : table
autoritative dupliquée en commentaire C++ juste au-dessus du switch,
co-localisée avec le seul consumer.
Header comment mis à jour pour refléter R14 12s + R15 cooldown.
Validation locale : esphome config passe sur 4 presets représentatifs
(salt_full, salt_minimal, salt_booster_full, salt_wo_redox).
Cross-reviewer agreements : adv-003 + REL-01 (filter), correctness #1
+ REL-02 + project-standards PS-010 (case-3 zero division).
Plan : docs/plans/2026-04-25-001-feat-ph-two-point-calibration-plan.md
Run artifact : .context/compound-engineering/ce-code-review/20260426-210326-1cec8a02/
Toutes les implementation units (U1 + U2 + U3) sont landed sur la branche. Doc-review et code-review (8 reviewers) ont chacun appliqué leurs safe_auto fixes ; les real bugs identifiés (R-1 race, R-2 filter contamination, R-3 sample spacing, R-4 zero-guard) plus les P1 maintainability (R-5 named consts, R-6 result code coupling) ont été appliqués via le fixer pass dans le commit f362025.
gaetanars
added a commit
that referenced
this pull request
Apr 26, 2026
Capture le workaround découvert en implémentant `packages/ph.yaml` (PR #9, commit eb31048) : `restore_value: true` n'est pas fiable sur les globals ESPHome de type `std::string` (le mécanisme NVS travaille avec des slots de taille fixe). Pattern réutilisable : encoder l'état dans un global `int` (codes énumérés), exposer la chaîne dérivée via un `text_sensor` template avec un lambda switch/case + default sentinel. Le brainstorm/plan initial spécifiait un global string ; au moment de coder j'ai constaté la limitation et adopté ce workaround. Identifié comme "net-new institutional knowledge" par ce-learnings-researcher pendant le code-review (run 20260426-210326-1cec8a02). Catégorie : design-patterns/ (nouveau dossier — premier doc) Track : knowledge / design_pattern Référence : packages/ph.yaml `g_ph_last_result_code` + `text_sensor: pH Calibration Last Result`
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Le calcul du pH passe d'une calibration single-point pH 7 (offset additif sur la valeur d'usine, pente figée à 3.56 pH/V) à une calibration two-points pH 7 + pH 4 qui recalcule slope ET intercept. Le vieillissement de la sonde Gravity v2.0 (DFRobot SEN0161-V2) ne produira plus silencieusement un pH faux : un cycle de calibration en bains tampons de 6 min 48 s pose une nouvelle droite de Nernst et l'expose en diagnostics HA (slope, intercept, V_pH7, V_pH4, dernier résultat).
L'opérateur déclenche la séquence depuis HA via "Démarrer calibration pH" et est guidé par notifications. Les valeurs sont rejetées avec messages explicites en cas de bains inversés, sonde HS, capture instable ou capteur indisponible — le firmware préserve toujours la dernière calibration valide.
Comportement
pH OffsetpH Slope,pH Intercept,V_pH7,V_pH4,pH Calibration Last Resultpool_phcontinue de latcher pendant calibrationpool_phfigé pendant calib + 3 min après (purge de la fenêtre glissante du moving-average)Décisions techniques
Script
_ph_calibrationenmode: single. La séquence est un pipeline à état partiel (V_pH7 capturé avant V_pH4).restartannulerait avec un état partiellement consistant ;queued max_runs:1drop silencieusement les triggers.singleignore le re-press en cours.Verrou
g_ph_calibration_in_progress(R13).mode: singleempêche le re-press du démarrage mais ne protège pas contre des écritures concurrentes du bouton reset. Le flag bloque le reset pendant la séquence et est libéré comme dernière action de chaque chemin de sortie (succès + 3 abort handlers) pour fermer la race "reset entre flag-flip et notif success" identifiée par adversarial review.Multi-capture 3 échantillons espacés de 12 s, spread max 50 mV (R14). ADS1115
update_interval=10scache la dernière valeur entre publications ; un espacement de 5 s aurait collapsé 3 captures en 2 effective samples. 12 s > 10 s garantit qu'au moins une nouvelle publication ADS1115 a eu lieu entre captures.Cooldown post-calibration (R15 + R-2). Le filtre
sliding_window_moving_average(180 s) surrealtime_phreste saturé de bains tampons après la fin de la calibration. Sans cooldown, le tick interval suivant immédiatement la calibration latcherait un mix bain/pool danspool_ph. Un compteurg_ph_post_cal_cooldownset à 3 ticks (3 min) à chaque sortie, decrement à chaque interval, prolonge le gating jusqu'à ce que la fenêtre glissante se vide.text_sensoralimenté par un code int (R16). Le brainstorm initial spécifiait un globalg_ph_last_resultde typestd::string. L'implémentation a opté pourg_ph_last_result_code(int + lookup lambda dans letext_sensor) parce querestore_value: truen'est pas fiable sur lesstd::stringglobals en ESPHome (le mécanisme de préférences travaille mal avec les types de taille variable). Comportement utilisateur identique ; codes 0-8 documentés en header de fichier et co-localisés avec le switch.Validation
esphome configvalide sur les 8 presets (salt_full,salt_minimal,salt_wo_redox,salt_wo_ph,salt_booster_full,salt_booster_minimal,salt_booster_wo_redox,salt_booster_wo_ph) avec ESPHome 2026.4.1 local aprèssed-rewrite des URLsgithub://vers!include.g_ph_calibration_in_progressET set le cooldown : 4 occurrences de chaque (vérifié par grep dans le commit f362025).Tests hardware requis post-flash (pas de test suite en firmware-as-config) :
g_ph_slopeetg_ph_interceptinchangéspump_uptime_delay, vérifier quepool_phreste figé pendant la calib + 3 min aprèspH Calibration Last Resultreflète l'issue (R16)Post-Deploy Monitoring & Validation
esphome run salt_full.yaml(ou preset choisi), monitoreresphome logsg_ph_calibration_in_progress = false(éphémère, repart à false) etg_ph_last_result_coderestauré depuis NVS (0 si jamais calibré, sinon code de la dernière issue)pH Calibration Last Resultfinal =OK pente=X.XXX, intercept=Y.YYYtext_sensorreste surEchec:...ouRejet:...sans cause hardware identifiable, OUpool_phaberrant > 3 min après fin de calibration(3.56, -1.889)Known Residuals (accepted, not blocking)
g_ph_cap_a/b/créutilisés silencieusement entre pH 7 et pH 4 sans documentation expliciteg_ph_abortlit comme un flag subsystem-wide vs script-onlyg_ph_calibration_in_progressnon exposé enbinary_sensorg_ph_last_result_codeint non exposé en sensor numériquePipeline de génération
Produit via
/ce-brainstorm→/ce-plan→/ce-work→/ce-doc-review(3 reviewers, 5 findings appliqués) →/ce-code-review(8 reviewers, cross-reviewer agreement sur 2 P2, 6 fixes appliqués via le Residual Work Gate). Origine : docs/brainstorms/2026-04-25-001-ph-two-point-calibration-requirements.md. Plan : docs/plans/2026-04-25-001-feat-ph-two-point-calibration-plan.md.BREAKING CHANGE
L'OTA depuis la version précédente du firmware invalide l'ancien global
g_ph_offset. Les nouveaux globalsg_ph_slopeetg_ph_interceptdémarrent sur leursinitial_value(3.56,-1.889) — qui reproduisent la formule pré-calibrationpH = 3.56 × V − 1.889du firmware précédent.Tant que
g_ph_offsetétait à 0 (cas le plus courant pour une sonde non encore manuellement calibrée), le pH affiché reste numériquement identique post-OTA. Si une calibration single-point était en place (par ex.g_ph_offset = 0.15après réglage manuel), la nouvelle calibration two-points doit être exécutée pour retrouver une mesure correcte.