From e6d9225a4aa7ce9ee336887a3810d8456724c75a Mon Sep 17 00:00:00 2001 From: Patrick Schurig Date: Wed, 3 Jun 2026 13:05:23 +0200 Subject: [PATCH] =?UTF-8?q?feat:=20g=C3=A9n=C3=A9ration=20compl=C3=A8te=20?= =?UTF-8?q?des=20fiches=20appareils=20depuis=20PORTING=5FSTATUS=20+=20JSON?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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___ 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 --- docs/appareils/compatibilite.md | 18 +- docs/appareils/hvac/daikinairco.md | 36 +++- docs/appareils/hvac/index.md | 11 +- docs/appareils/hvac/sgready.md | 30 +++- docs/appareils/hvac/simpleheatpump.md | 27 ++- docs/appareils/onduleurs/fronius.md | 92 +++++++++- docs/appareils/onduleurs/index.md | 9 +- scripts/gen_device_reference.py | 240 ++++++++++++++++++++++---- 8 files changed, 411 insertions(+), 52 deletions(-) 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__":