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
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:
parent
5ca9b91b17
commit
e6d9225a4a
@ -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 -->
|
||||
|
||||
@ -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 -->
|
||||
|
||||
@ -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 -->
|
||||
|
||||
@ -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 -->
|
||||
|
||||
@ -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 -->
|
||||
|
||||
@ -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 -->
|
||||
|
||||
@ -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 -->
|
||||
|
||||
@ -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__":
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user