feat: génération complète des fiches appareils depuis PORTING_STATUS + JSON
Some checks failed
Build & Deploy docs / build-deploy (push) Failing after 13m43s

Le générateur crée désormais entièrement les fiches inexistantes (titre,
badges, section générée BEGIN/END) et les index.md de catégorie manquants.
Les fiches rédigées manuellement (eastron, abb-b2x, abb-terra, keba,
waveshare) sont préservées — seul le contenu entre marqueurs est réinjecté.

- gen_device_reference.py : ensure_device_pages() + ensure_category_indexes()
  créent les .md manquants dans le format exact de process() → idempotent
- render_matrix() : inclut tous les canaux (nightly sans JSON = ligne stub)
- generate_summary() : intègre check mode + inclut toutes les entrées nav
- process()/repl() : gère __index_<cat>__ et nightly sans JSON → stub
- load_plugins/load_meta : logging explicite pour JSON vides ou non parsables
- Supprimer stubs manuels hvac/{daikinairco,sgready,simpleheatpump,index}.md
  et onduleurs/{fronius,index}.md → remplacés par fichiers générés
- 9 appareils dans la matrice et dans le SUMMARY.md, mkdocs build --strict OK

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Patrick Schurig 2026-06-03 13:05:23 +02:00
parent 5ca9b91b17
commit e6d9225a4a
8 changed files with 411 additions and 52 deletions

View File

@ -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 | <span class="badge stable">STABLE</span> | <span class="badge consumer">CONSUMER</span> |
| Compteur ABB B2x | Modbus RTU | <span class="badge testing">TESTING</span> | <span class="badge consumer">CONSUMER</span> |
| Eastron SDM | Modbus RTU | <span class="badge stable">STABLE</span> | |
| Compteur ABB B2x | Modbus RTU | <span class="badge testing">TESTING</span> | |
## Bornes de recharge
| Marque / Modèle | Protocole | Canal | Stabilité |
|---|---|---|---|
| Borne ABB Terra AC | Modbus TCP | <span class="badge testing">TESTING</span> | <span class="badge consumer">CONSUMER</span> |
| Keba | Modbus TCP | <span class="badge nightly">NIGHTLY</span> | <span class="badge consumer">CONSUMER</span> |
| Borne ABB Terra AC | Modbus TCP | <span class="badge testing">TESTING</span> | |
| Keba | Modbus TCP | <span class="badge nightly">NIGHTLY</span> | |
## SmartDevices
| Marque / Modèle | Protocole | Canal | Stabilité |
|---|---|---|---|
| Waveshare relais | Modbus RTU | <span class="badge testing">TESTING</span> | <span class="badge consumer">CONSUMER</span> |
| Waveshare relais | Modbus RTU | <span class="badge testing">TESTING</span> | |
## HVAC
| Marque / Modèle | Protocole | Canal | Stabilité |
|---|---|---|---|
| Daikin | — | <span class="badge nightly">NIGHTLY</span> | <span class="badge consumer">CONSUMER</span> |
| SG-Ready | — | <span class="badge nightly">NIGHTLY</span> | <span class="badge consumer">CONSUMER</span> |
| SimpleHeatpump | — | <span class="badge nightly">NIGHTLY</span> | <span class="badge consumer">CONSUMER</span> |
| Daikin | — | <span class="badge nightly">NIGHTLY</span> | |
| SG-Ready | — | <span class="badge nightly">NIGHTLY</span> | |
| SimpleHeatpump | — | <span class="badge nightly">NIGHTLY</span> | |
## Onduleurs / PV
| Marque / Modèle | Protocole | Canal | Stabilité |
|---|---|---|---|
| Fronius | Modbus TCP | <span class="badge nightly">NIGHTLY</span> | <span class="badge consumer">CONSUMER</span> |
| Fronius | Modbus TCP | <span class="badge nightly">NIGHTLY</span> | |
<!-- END GENERATED -->

View File

@ -1,3 +1,35 @@
# daikinairco
# Daikin
Documentation en cours d'intégration.
<span class="badge nightly">NIGHTLY</span>
<!-- BEGIN GENERATED: integrationplugindaikinairco.json -->
**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 | — |
<!-- END GENERATED -->

View File

@ -1,3 +1,10 @@
# index
# HVAC
Documentation en cours d'intégration.
<!-- BEGIN GENERATED: __index_hvac__ -->
| Appareil | Protocole | Canal | Stabilité |
| --- | --- | --- | --- |
| [Daikin](daikinairco.md) | — | <span class="badge nightly">NIGHTLY</span> | — |
| [SG-Ready](sgready.md) | — | <span class="badge nightly">NIGHTLY</span> | — |
| [SimpleHeatpump](simpleheatpump.md) | — | <span class="badge nightly">NIGHTLY</span> | — |
<!-- END GENERATED -->

View File

@ -1,3 +1,29 @@
# sgready
# SG-Ready
Documentation en cours d'intégration.
<span class="badge nightly">NIGHTLY</span>
<!-- BEGIN GENERATED: integrationpluginsgready.json -->
**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 | — |
<!-- END GENERATED -->

View File

@ -1,3 +1,26 @@
# simpleheatpump
# SimpleHeatpump
Documentation en cours d'intégration.
<span class="badge nightly">NIGHTLY</span>
<!-- BEGIN GENERATED: integrationpluginsimpleheatpump.json -->
**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 | — |
<!-- END GENERATED -->

View File

@ -1,3 +1,91 @@
# fronius
# Fronius
Documentation en cours d'intégration.
<span class="badge nightly">NIGHTLY</span>
<!-- BEGIN GENERATED: integrationpluginfronius.json -->
**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 | — |
<!-- END GENERATED -->

View File

@ -1,3 +1,8 @@
# index
# Onduleurs / PV
Documentation en cours d'intégration.
<!-- BEGIN GENERATED: __index_onduleur__ -->
| Appareil | Protocole | Canal | Stabilité |
| --- | --- | --- | --- |
| [Fronius](fronius.md) | Modbus TCP | <span class="badge nightly">NIGHTLY</span> | — |
<!-- END GENERATED -->

View File

@ -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.
<!-- BEGIN GENERATED: integrationplugineastron.json -->
... contenu régénéré ...
<!-- END GENERATED -->
Marqueur spécial pour la matrice de compatibilité :
<!-- BEGIN GENERATED: __matrix__ -->
Marqueurs gérés :
<!-- BEGIN GENERATED: integrationplugin<plugin>.json --> fiche appareil
<!-- BEGIN GENERATED: __matrix__ --> matrice compatibilité
<!-- BEGIN GENERATED: __index_<cat>__ --> 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<plugin>.json."""
"""Charge meta.json depuis le même dossier que integrationplugin<plugin>.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"<!-- BEGIN GENERATED: {fname} -->\n{gen}\n<!-- END GENERATED -->"
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"<!-- BEGIN GENERATED: __index_{cat}__ -->\n{gen}\n<!-- END GENERATED -->"
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_<cat>__" → 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__":