diff --git a/docs/appareils/compatibilite.md b/docs/appareils/compatibilite.md
index 75eed93..49905f0 100644
--- a/docs/appareils/compatibilite.md
+++ b/docs/appareils/compatibilite.md
@@ -14,34 +14,34 @@ Liste de référence des équipements supportés par ETM PowerSync. Générée d
| Marque / Modèle | Protocole | Canal | Stabilité |
|---|---|---|---|
-| Eastron SDM | Modbus RTU | STABLE | CONSUMER |
-| Compteur ABB B2x | Modbus RTU | TESTING | CONSUMER |
+| Eastron SDM | Modbus RTU | STABLE | — |
+| Compteur ABB B2x | Modbus RTU | TESTING | — |
## Bornes de recharge
| Marque / Modèle | Protocole | Canal | Stabilité |
|---|---|---|---|
-| Borne ABB Terra AC | Modbus TCP | TESTING | CONSUMER |
-| Keba | Modbus TCP | NIGHTLY | CONSUMER |
+| Borne ABB Terra AC | Modbus TCP | TESTING | — |
+| Keba | Modbus TCP | NIGHTLY | — |
## SmartDevices
| Marque / Modèle | Protocole | Canal | Stabilité |
|---|---|---|---|
-| Waveshare relais | Modbus RTU | TESTING | CONSUMER |
+| Waveshare relais | Modbus RTU | TESTING | — |
## HVAC
| Marque / Modèle | Protocole | Canal | Stabilité |
|---|---|---|---|
-| Daikin | — | NIGHTLY | CONSUMER |
-| SG-Ready | — | NIGHTLY | CONSUMER |
-| SimpleHeatpump | — | NIGHTLY | CONSUMER |
+| Daikin | — | NIGHTLY | — |
+| SG-Ready | — | NIGHTLY | — |
+| SimpleHeatpump | — | NIGHTLY | — |
## Onduleurs / PV
| Marque / Modèle | Protocole | Canal | Stabilité |
|---|---|---|---|
-| Fronius | Modbus TCP | NIGHTLY | CONSUMER |
+| Fronius | Modbus TCP | NIGHTLY | — |
diff --git a/docs/appareils/hvac/daikinairco.md b/docs/appareils/hvac/daikinairco.md
index 3e951ad..dfcf4e4 100644
--- a/docs/appareils/hvac/daikinairco.md
+++ b/docs/appareils/hvac/daikinairco.md
@@ -1,3 +1,35 @@
-# daikinairco
+# Daikin
-Documentation en cours d'intégration.
+NIGHTLY
+
+
+**Fabricant :** Daikin
+**Plugin :** `Daikinairco`
+
+#### Modèles pris en charge
+| Modèle | Rôle | Transport | Ajout | Grandeurs |
+| --- | --- | --- | --- | --- |
+| **Daikin Airco** | power, thermostat, temperaturesensor | — | Découverte automatique | 9 |
+
+#### Détail par modèle
+??? abstract "Daikin Airco — `airco`"
+ _Réglages :_
+ | Clé | Libellé | Type | Plage | Défaut | Lecture seule |
+ | --- | --- | --- | --- | --- | --- |
+ | `deviceId` | Device ID | QString | — | — | oui |
+ | `deviceName` | Device name | QString | — | — | oui |
+
+ _Grandeurs mesurées :_
+ | Clé | Grandeur | Type | Unité |
+ | --- | --- | --- | --- |
+ | `connected` | Connected | bool | — |
+ | `power` | Power | bool | — |
+ | `targetTemperature` | Target temperature | double | DegreeCelsius |
+ | `temperature` | Home temperature | double | DegreeCelsius |
+ | `outsideTemperature` | Outside temperature | double | DegreeCelsius |
+ | `humidity` | Humidity | double | Percentage |
+ | `Mode` | Mode | QString | — |
+ | `heatingOn` | Heating On | bool | — |
+ | `coolingOn` | Cooling On | bool | — |
+
+
diff --git a/docs/appareils/hvac/index.md b/docs/appareils/hvac/index.md
index 193e3b3..5b19f0d 100644
--- a/docs/appareils/hvac/index.md
+++ b/docs/appareils/hvac/index.md
@@ -1,3 +1,10 @@
-# index
+# HVAC
-Documentation en cours d'intégration.
+
+| Appareil | Protocole | Canal | Stabilité |
+| --- | --- | --- | --- |
+| [Daikin](daikinairco.md) | — | NIGHTLY | — |
+| [SG-Ready](sgready.md) | — | NIGHTLY | — |
+| [SimpleHeatpump](simpleheatpump.md) | — | NIGHTLY | — |
+
+
diff --git a/docs/appareils/hvac/sgready.md b/docs/appareils/hvac/sgready.md
index 2d5934e..d0c4aae 100644
--- a/docs/appareils/hvac/sgready.md
+++ b/docs/appareils/hvac/sgready.md
@@ -1,3 +1,29 @@
-# sgready
+# SG-Ready
-Documentation en cours d'intégration.
+NIGHTLY
+
+
+**Fabricant :** nymea
+**Plugin :** `SgReady`
+
+#### Modèles pris en charge
+| Modèle | Rôle | Transport | Ajout | Grandeurs |
+| --- | --- | --- | --- | --- |
+| **SG Ready interface** | smartgridheatpump | — | Ajout manuel | 3 |
+
+#### Détail par modèle
+??? abstract "SG Ready interface — `sgReadyInterface`"
+ _Réglages :_
+ | Clé | Libellé | Type | Plage | Défaut | Lecture seule |
+ | --- | --- | --- | --- | --- | --- |
+ | `gpioNumber1` | GPIO number 1 | int | — | `-1` | non |
+ | `gpioNumber2` | GPIO number 2 | int | — | `-1` | non |
+
+ _Grandeurs mesurées :_
+ | Clé | Grandeur | Type | Unité |
+ | --- | --- | --- | --- |
+ | `sgReadyMode` | Smart grid mode | QString | — |
+ | `gpio1State` | Relay 1 enabled | bool | — |
+ | `gpio2State` | Relay 2 enabled | bool | — |
+
+
diff --git a/docs/appareils/hvac/simpleheatpump.md b/docs/appareils/hvac/simpleheatpump.md
index ee47d01..c86faa0 100644
--- a/docs/appareils/hvac/simpleheatpump.md
+++ b/docs/appareils/hvac/simpleheatpump.md
@@ -1,3 +1,26 @@
-# simpleheatpump
+# SimpleHeatpump
-Documentation en cours d'intégration.
+NIGHTLY
+
+
+**Fabricant :** nymea
+**Plugin :** `SimpleHeatPump`
+
+#### Modèles pris en charge
+| Modèle | Rôle | Transport | Ajout | Grandeurs |
+| --- | --- | --- | --- | --- |
+| **Simple heat pump interface** | simpleheatpump | — | Ajout manuel | 1 |
+
+#### Détail par modèle
+??? abstract "Simple heat pump interface — `simpleHeatPumpInterface`"
+ _Réglages :_
+ | Clé | Libellé | Type | Plage | Défaut | Lecture seule |
+ | --- | --- | --- | --- | --- | --- |
+ | `gpioNumber` | GPIO number | int | — | `-1` | non |
+
+ _Grandeurs mesurées :_
+ | Clé | Grandeur | Type | Unité |
+ | --- | --- | --- | --- |
+ | `power` | Heat pump enabled | bool | — |
+
+
diff --git a/docs/appareils/onduleurs/fronius.md b/docs/appareils/onduleurs/fronius.md
index 49141a5..7d7dc1b 100644
--- a/docs/appareils/onduleurs/fronius.md
+++ b/docs/appareils/onduleurs/fronius.md
@@ -1,3 +1,91 @@
-# fronius
+# Fronius
-Documentation en cours d'intégration.
+NIGHTLY
+
+
+**Fabricant :** Fronius
+**Plugin :** `fronius`
+
+#### Modèles pris en charge
+| Modèle | Rôle | Transport | Ajout | Grandeurs |
+| --- | --- | --- | --- | --- |
+| **Fronius Solar** | gateway | Modbus TCP | Découverte automatique / Ajout manuel | 2 |
+| **Fronius solar inverter** | solarinverter | — | Automatique | 5 |
+| **Fronius smart meter** | Compteur d'énergie | — | Automatique | 14 |
+| **Fronius solar storage** | energystorage | — | Automatique | 7 |
+
+#### Détail par modèle
+??? abstract "Fronius Solar — `connection`"
+ _Réglages :_
+ | Clé | Libellé | Type | Plage | Défaut | Lecture seule |
+ | --- | --- | --- | --- | --- | --- |
+ | `address` | Host address | QString | — | — | non |
+ | `hostName` | Host name | QString | — | — | non |
+ | `macAddress` | Mac address | QString | — | `00:00:00:00:00:00` | oui |
+
+ _Grandeurs mesurées :_
+ | Clé | Grandeur | Type | Unité |
+ | --- | --- | --- | --- |
+ | `connected` | Reachable | bool | — |
+ | `version` | Version | QString | — |
+
+??? abstract "Fronius solar inverter — `inverter`"
+ _Réglages :_
+ | Clé | Libellé | Type | Plage | Défaut | Lecture seule |
+ | --- | --- | --- | --- | --- | --- |
+ | `id` | Device ID | QString | — | — | oui |
+ | `serialNumber` | Serial number | QString | — | — | oui |
+
+ _Grandeurs mesurées :_
+ | Clé | Grandeur | Type | Unité |
+ | --- | --- | --- | --- |
+ | `connected` | Reachable | bool | — |
+ | `currentPower` | Current power | double | Watt |
+ | `energyDay` | Energy produced today | double | KiloWattHour |
+ | `energyYear` | Energy produced year | int | KiloWattHour |
+ | `totalEnergyProduced` | Total produced energy | double | KiloWattHour |
+
+??? abstract "Fronius smart meter — `meter`"
+ _Réglages :_
+ | Clé | Libellé | Type | Plage | Défaut | Lecture seule |
+ | --- | --- | --- | --- | --- | --- |
+ | `id` | Device ID | QString | — | — | oui |
+ | `serialNumber` | Serial number | QString | — | — | oui |
+
+ _Grandeurs mesurées :_
+ | Clé | Grandeur | Type | Unité |
+ | --- | --- | --- | --- |
+ | `connected` | Reachable | bool | — |
+ | `currentPower` | Current power usage | double | Watt |
+ | `voltagePhaseA` | Voltage phase A | double | Volt |
+ | `voltagePhaseB` | Voltage phase B | double | Volt |
+ | `voltagePhaseC` | Voltage phase C | double | Volt |
+ | `currentPhaseA` | Current phase A | double | Ampere |
+ | `currentPhaseB` | Current phase B | double | Ampere |
+ | `currentPhaseC` | Current phase C | double | Ampere |
+ | `totalEnergyProduced` | Energy produced | double | KiloWattHour |
+ | `totalEnergyConsumed` | Energy Consumed | double | KiloWattHour |
+ | `currentPowerPhaseA` | Current power phase A | double | Watt |
+ | `currentPowerPhaseB` | Current power phase B | double | Watt |
+ | `currentPowerPhaseC` | Current power phase C | double | Watt |
+ | `frequency` | Frequency | double | Hertz |
+
+??? abstract "Fronius solar storage — `storage`"
+ _Réglages :_
+ | Clé | Libellé | Type | Plage | Défaut | Lecture seule |
+ | --- | --- | --- | --- | --- | --- |
+ | `id` | Device ID | QString | — | — | oui |
+ | `serialNumber` | Serial number | QString | — | — | oui |
+
+ _Grandeurs mesurées :_
+ | Clé | Grandeur | Type | Unité |
+ | --- | --- | --- | --- |
+ | `connected` | Reachable | bool | — |
+ | `chargingState` | Charging state | QString | — |
+ | `currentPower` | Current power | double | Watt |
+ | `capacity` | Capacity | double | KiloWattHour |
+ | `batteryLevel` | Battery level | int | Percentage |
+ | `cellTemperature` | Cell temperature | double | DegreeCelsius |
+ | `batteryCritical` | Battery level critical | bool | — |
+
+
diff --git a/docs/appareils/onduleurs/index.md b/docs/appareils/onduleurs/index.md
index 193e3b3..3819fb5 100644
--- a/docs/appareils/onduleurs/index.md
+++ b/docs/appareils/onduleurs/index.md
@@ -1,3 +1,8 @@
-# index
+# Onduleurs / PV
-Documentation en cours d'intégration.
+
+| Appareil | Protocole | Canal | Stabilité |
+| --- | --- | --- | --- |
+| [Fronius](fronius.md) | Modbus TCP | NIGHTLY | — |
+
+
diff --git a/scripts/gen_device_reference.py b/scripts/gen_device_reference.py
index 22c1e78..ecf2572 100644
--- a/scripts/gen_device_reference.py
+++ b/scripts/gen_device_reference.py
@@ -4,14 +4,14 @@ gen_device_reference.py — génère la matrice de compatibilité, les sections
référence des appareils et le fichier de nav literate-nav (SUMMARY.md) depuis
PORTING_STATUS.yaml + integrationplugin*.json + meta.json.
-Principe docs-as-code : ne remplace que le contenu entre marqueurs existants.
+Deux modes de traitement des fiches :
+ - Injection : remplace le contenu entre marqueurs dans les fiches existantes.
+ - Création : crée entièrement les fiches et index inexistants.
-
- ... contenu régénéré ...
-
-
-Marqueur spécial pour la matrice de compatibilité :
-
+Marqueurs gérés :
+ fiche appareil
+ matrice compatibilité
+ index de catégorie
Usage :
python3 scripts/gen_device_reference.py --src .plugins-src --docs docs --lang fr
@@ -37,7 +37,7 @@ INTERFACE_ROLE = {
"heatpump": "Pompe à chaleur",
"smartmeter": "Compteur intelligent",
}
-TECH_INTERFACES = {"connectable", "networkdevice"}
+TECH_INTERFACES = {"connectable", "networkdevice", "wirelessconnectable"}
CREATE_METHOD = {
"discovery": "Découverte automatique",
@@ -95,21 +95,44 @@ def load_porting_status(root: Path) -> list:
def load_plugins(src: Path) -> dict:
- """Charge tous les integrationplugin*.json trouvés (récursivement)."""
+ """Charge tous les integrationplugin*.json trouvés récursivement.
+
+ Logue explicitement chaque fichier vide ou non parsable (ne plante pas).
+ """
plugins: dict = {}
for f in sorted(src.rglob("integrationplugin*.json")):
- if f.name not in plugins:
- plugins[f.name] = json.loads(f.read_text(encoding="utf-8"))
+ if f.name in plugins:
+ continue
+ raw = f.read_text(encoding="utf-8").strip()
+ if not raw:
+ print(f" AVERTISSEMENT : {f} est vide — ignoré", file=sys.stderr)
+ continue
+ try:
+ plugins[f.name] = json.loads(raw)
+ except json.JSONDecodeError as exc:
+ print(f" AVERTISSEMENT : {f} non parsable ({exc}) — ignoré", file=sys.stderr)
return plugins
def load_meta(src: Path, plugin: str) -> dict:
- """Charge meta.json depuis le même dossier que integrationplugin.json."""
+ """Charge meta.json depuis le même dossier que integrationplugin.json.
+
+ Logue explicitement si le fichier est vide ou non parsable.
+ """
for f in sorted(src.rglob(f"integrationplugin{plugin}.json")):
meta_path = f.parent / "meta.json"
- if meta_path.exists():
- return json.loads(meta_path.read_text(encoding="utf-8"))
- return {}
+ if not meta_path.exists():
+ return {}
+ raw = meta_path.read_text(encoding="utf-8").strip()
+ if not raw:
+ print(f" AVERTISSEMENT : {meta_path} est vide — meta ignoré", file=sys.stderr)
+ return {}
+ try:
+ return json.loads(raw)
+ except json.JSONDecodeError as exc:
+ print(f" AVERTISSEMENT : {meta_path} non parsable ({exc}) — meta ignoré",
+ file=sys.stderr)
+ return {}
return {}
@@ -238,6 +261,40 @@ def render_plugin(plugin: dict) -> str:
return "\n".join(out).rstrip() + "\n"
+def render_nightly_stub(entry: dict, meta: dict) -> str:
+ """Contenu généré pour une entrée nightly dont le JSON est absent."""
+ name = entry_name(entry, meta)
+ return (
+ f"_Support de **{name}** en cours d'intégration — canal nightly._\n"
+ "\n"
+ "_La documentation technique sera disponible dès que le plugin sera "
+ "disponible sur le dépôt stable._\n"
+ )
+
+
+def render_category_index(cat: str, entries: list, plugins: dict, src: Path) -> str:
+ """Tableau listant les appareils d'une catégorie, pour l'index.md auto-généré."""
+ folder = CATEGORY_FOLDER.get(cat, cat)
+ cat_entries = [e for e in entries if e.get("category") == cat and e.get("plugin")]
+ if not cat_entries:
+ return "_Aucun appareil dans cette catégorie._\n"
+
+ rows = []
+ for e in cat_entries:
+ fname = f"integrationplugin{e['plugin']}.json"
+ plugin_data = plugins.get(fname)
+ meta = load_meta(src, e["plugin"])
+ name = entry_name(e, meta)
+ slug = e.get("slug") or e["plugin"]
+ protocol = resolve_protocol(plugin_data) if plugin_data else "—"
+ channel_badge = CHANNEL_BADGES.get(e["channel"], e["channel"])
+ stability = meta.get("stability", "")
+ stability_badge = STABILITY_BADGES.get(stability, "—") if stability else "—"
+ rows.append([f"[{name}]({slug}.md)", protocol, channel_badge, stability_badge])
+
+ return md_table(["Appareil", "Protocole", "Canal", "Stabilité"], rows) + "\n"
+
+
def render_matrix(entries: list, plugins: dict, src: Path) -> str:
by_cat: dict = {}
for e in entries:
@@ -253,9 +310,9 @@ def render_matrix(entries: list, plugins: dict, src: Path) -> str:
for e in by_cat[cat]:
fname = f"integrationplugin{e['plugin']}.json" if e.get("plugin") else None
plugin_data = plugins.get(fname) if fname else None
- # nightly sans JSON → absent de la matrice et du nav
if e["channel"] == "nightly" and (not fname or fname not in plugins):
- continue
+ print(f" INFO matrice : {e.get('plugin', '?')} nightly sans JSON "
+ f"→ ligne stub dans la matrice", file=sys.stderr)
meta = load_meta(src, e["plugin"]) if e.get("plugin") else {}
name = entry_name(e, meta)
protocol = resolve_protocol(plugin_data)
@@ -276,8 +333,9 @@ def render_matrix(entries: list, plugins: dict, src: Path) -> str:
# ── SUMMARY.md pour mkdocs-literate-nav ──────────────────────────────────────
-def generate_summary(entries: list, docs: Path, src: Path, plugins: dict) -> None:
- """Écrit docs/appareils/SUMMARY.md (nav literate-nav)."""
+def generate_summary(entries: list, docs: Path, src: Path, plugins: dict,
+ check: bool) -> int:
+ """Écrit docs/appareils/SUMMARY.md. Retourne 1 en mode --check si périmé."""
by_cat: dict = {}
for e in entries:
by_cat.setdefault(e.get("category", "autre"), []).append(e)
@@ -294,10 +352,6 @@ def generate_summary(entries: list, docs: Path, src: Path, plugins: dict) -> Non
for e in by_cat[cat]:
if not e.get("plugin"):
continue
- fname = f"integrationplugin{e['plugin']}.json"
- # nightly sans JSON → pas de fiche, pas d'entrée nav
- if e["channel"] == "nightly" and fname not in plugins:
- continue
meta = load_meta(src, e["plugin"])
name = entry_name(e, meta)
slug = e.get("slug") or e["plugin"]
@@ -306,9 +360,18 @@ def generate_summary(entries: list, docs: Path, src: Path, plugins: dict) -> Non
lines.append(f"* [{label}]({folder}/index.md)")
lines.extend(cat_entries)
+ new_content = "\n".join(lines) + "\n"
summary_path = docs / "appareils" / "SUMMARY.md"
- summary_path.write_text("\n".join(lines) + "\n", encoding="utf-8")
+
+ if check:
+ if not summary_path.exists() or summary_path.read_text(encoding="utf-8") != new_content:
+ print(f" SUMMARY.md périmé : {summary_path.relative_to(docs.parent)}")
+ return 1
+ return 0
+
+ summary_path.write_text(new_content, encoding="utf-8")
print(f"SUMMARY.md → {summary_path.relative_to(docs.parent)}")
+ return 0
# ── Validation ────────────────────────────────────────────────────────────────
@@ -321,13 +384,11 @@ def validate_entries(entries: list, plugins: dict, src: Path) -> None:
fname = f"integrationplugin{e['plugin']}.json"
if e["channel"] != "nightly":
- # JSON obligatoire pour stable / testing
if fname not in plugins:
errors.append(
f" BLOQUANT : {fname} introuvable dans --src "
f"(channel={e['channel']}, plugin={e['plugin']})"
)
- # Libellé obligatoire
meta = load_meta(src, e["plugin"])
if not e.get("name") and not meta.get("title"):
errors.append(
@@ -339,7 +400,107 @@ def validate_entries(entries: list, plugins: dict, src: Path) -> None:
sys.exit("ERREURS BLOQUANTES :\n" + "\n".join(errors))
-# ── Traitement des fichiers MD ────────────────────────────────────────────────
+# ── Construction d'une page complète ─────────────────────────────────────────
+
+def build_device_page(e: dict, plugin_data: dict | None, meta: dict) -> str:
+ """Construit le contenu complet d'une fiche appareil générée."""
+ title = entry_name(e, meta)
+ plugin = e["plugin"]
+ fname = f"integrationplugin{plugin}.json"
+
+ channel_badge = CHANNEL_BADGES.get(e["channel"], e["channel"])
+ stability = meta.get("stability", "")
+ stability_badge = STABILITY_BADGES.get(stability, "") if stability else ""
+ badges_line = channel_badge + (" " + stability_badge if stability_badge else "")
+ tagline = meta.get("tagline", "")
+
+ if plugin_data:
+ gen = render_plugin(plugin_data)
+ else:
+ print(f" INFO : {fname} absent (nightly) → page stub pour {title}",
+ file=sys.stderr)
+ gen = render_nightly_stub(e, meta)
+
+ # Bloc BEGIN/END au format exact produit par process() :
+ # f"{m.group(1)}\n{gen}\n{m.group(4)}" avec gen qui se termine par \n
+ block = f"\n{gen}\n"
+
+ header_parts = [f"# {title}", "", badges_line]
+ if tagline:
+ header_parts += ["", tagline]
+ header_parts.append("") # ligne vide avant le bloc BEGIN
+ header = "\n".join(header_parts)
+
+ return header + "\n" + block + "\n"
+
+
+def build_category_index_page(cat: str, entries: list, plugins: dict, src: Path) -> str:
+ """Construit le contenu complet d'un index.md de catégorie."""
+ label = CATEGORY_LABELS.get(cat, cat.capitalize())
+ gen = render_category_index(cat, entries, plugins, src)
+ # Même format que process() : f"{m.group(1)}\n{gen}\n{m.group(4)}"
+ block = f"\n{gen}\n"
+ return f"# {label}\n\n{block}\n"
+
+
+# ── Création des fichiers manquants ───────────────────────────────────────────
+
+def ensure_device_pages(entries: list, plugins: dict, src: Path,
+ docs: Path, check: bool) -> int:
+ """Crée les fiches appareils inexistantes. Retourne 1 si manquant en mode --check."""
+ rc = 0
+ for e in entries:
+ if not e.get("plugin"):
+ continue
+ slug = e.get("slug") or e["plugin"]
+ folder = CATEGORY_FOLDER.get(e.get("category", ""), e.get("category", "autre"))
+ md_path = docs / "appareils" / folder / f"{slug}.md"
+
+ if md_path.exists():
+ continue
+
+ fname = f"integrationplugin{e['plugin']}.json"
+ plugin_data = plugins.get(fname)
+ meta = load_meta(src, e["plugin"])
+ content = build_device_page(e, plugin_data, meta)
+
+ if check:
+ print(f" MANQUANT : {md_path.relative_to(docs.parent)}")
+ rc = 1
+ else:
+ md_path.parent.mkdir(parents=True, exist_ok=True)
+ md_path.write_text(content, encoding="utf-8")
+ print(f" CRÉÉ : {md_path.relative_to(docs.parent)}")
+
+ return rc
+
+
+def ensure_category_indexes(entries: list, plugins: dict, src: Path,
+ docs: Path, check: bool) -> int:
+ """Crée les index.md de catégorie inexistants. Retourne 1 si manquant en --check."""
+ cats_present = {e.get("category") for e in entries if e.get("category")}
+ rc = 0
+ for cat in cats_present:
+ folder = CATEGORY_FOLDER.get(cat, cat)
+ index_path = docs / "appareils" / folder / "index.md"
+
+ if index_path.exists():
+ continue
+
+ content = build_category_index_page(cat, entries, plugins, src)
+
+ if check:
+ print(f" MANQUANT : {index_path.relative_to(docs.parent)}")
+ rc = 1
+ else:
+ index_path.parent.mkdir(parents=True, exist_ok=True)
+ index_path.write_text(content, encoding="utf-8")
+ print(f" CRÉÉ : {index_path.relative_to(docs.parent)}")
+
+ return rc
+
+
+# ── Traitement des fichiers MD (injection dans les marqueurs) ─────────────────
def process(docs: Path, entries: list, plugins: dict, src: Path, check: bool) -> int:
entry_by_plugin = {e["plugin"]: e for e in entries if e.get("plugin")}
@@ -354,11 +515,23 @@ def process(docs: Path, entries: list, plugins: dict, src: Path, check: bool) ->
key = m.group("key")
if key == "__matrix__":
gen = render_matrix(entries, plugins, src)
+ elif key.startswith("__index_"):
+ # key = "__index___" → strip prefix "__index_" et suffix "__"
+ cat = key.removeprefix("__index_").removesuffix("__")
+ gen = render_category_index(cat, entries, plugins, src)
elif key in plugins:
gen = render_plugin(plugins[key])
else:
- print(f" ! {md.name}: clé '{key}' absente de --src", file=sys.stderr)
- return m.group(0)
+ # Clé plugin absente : nightly sans JSON → stub ; sinon avertissement
+ plugin_name = key.removeprefix("integrationplugin").removesuffix(".json")
+ entry = entry_by_plugin.get(plugin_name)
+ if entry and entry.get("channel") == "nightly":
+ print(f" INFO : {key} absent (nightly) → stub dans {md.name}",
+ file=sys.stderr)
+ gen = render_nightly_stub(entry, load_meta(src, plugin_name))
+ else:
+ print(f" ! {md.name}: clé '{key}' absente de --src", file=sys.stderr)
+ return m.group(0)
return f"{m.group(1)}\n{gen}\n{m.group(4)}"
new = MARKER_RE.sub(repl, text)
@@ -401,8 +574,13 @@ def main() -> int:
print(f"{len(plugins)} plugin(s) chargé(s) : {', '.join(plugins)}")
validate_entries(entries, plugins, src)
- generate_summary(entries, a.docs, src, plugins)
- return process(a.docs, entries, plugins, src, a.check)
+
+ rc_pages = ensure_device_pages(entries, plugins, src, a.docs, a.check)
+ rc_indexes = ensure_category_indexes(entries, plugins, src, a.docs, a.check)
+ rc_summary = generate_summary(entries, a.docs, src, plugins, a.check)
+ rc_process = process(a.docs, entries, plugins, src, a.check)
+
+ return max(rc_pages, rc_indexes, rc_summary, rc_process)
if __name__ == "__main__":