# Retours sur développement des intégrations externes

**URL:** <https://community.gladysassistant.com/t/retours-sur-developpement-des-integrations-externes/11005>\
**Category:** Développement\
**Tags:** integration-externe\
**Created:** [Octobre 9, 2026, 8:10 UTC](https://community.gladysassistant.com/t/retours-sur-developpement-des-integrations-externes/11005 "2026-10-09T20:10:54Z")\
**Posts on this page:** 1\
**Page:** 1

<div class="post-metadata">

**Author:** ![guim31](https://gladys-assistant-community.b-cdn.net/user_avatar/community.gladysassistant.com/guim31/32/4840_2.png) [@guim31](https://community.gladysassistant.com/u/guim31)\
**Post date:** [Octobre 9, 2026, 8:10 UTC](https://community.gladysassistant.com/t/retours-sur-developpement-des-integrations-externes/11005/1 "2026-10-09T20:10:55Z")

</div>

# Retour d’expérience : 11 intégrations externes, et ce qui nous a coûté des allers-retours (doc, SDK, template, cœur)

Salut à tous, salut @pierre-gilles,

Depuis août, j’ai publié 11 intégrations externes : UniFi, Android TV Remote, IPP, SolarEdge, Plex, Subsonic, Speedtest, ecojoko, Astronomie, Jellyfin & Emby et Dreame. Le système d’intégrations externes est vraiment agréable à utiliser, merci !

En reprenant l’historique des dépôts et des fils du forum, une bonne partie des versions « correctives » ne venait pas de bugs de mon code. Elles venaient de **choses que Gladys attend sans que ce soit écrit nulle part** , ou que la doc dit autrement. Le plus souvent, l’erreur était silencieuse : un appareil accepté mais jamais interrogé, un composant de widget qui disparaît, une intégration absente du store. Je regroupe tout ici, classé par coût, avec pour chaque point ce que je propose côté doc, SDK ou cœur.

Tout a été **revérifié aujourd’hui sur `master`** (cœur `03d4696d`, SDK `500db06`, template `4ea0c7b`, store `a052400`), avec fichier et ligne dans les blocs repliés. J’ai écarté ce qui est déjà corrigé ou déjà demandé, et je le liste à la fin. Je pense notamment aux tickets de @prohand (SDK #36, template #19) et au sujet Dreame (#3153 à #3156).

## En bref : les 5 points qui auraient évité le plus de versions

1. **Polling** : `should_poll: true` est obligatoire, mais aucune spec ni la doc publique ne le dit. Le template publie `poll_frequency: 300` (en secondes, sans `should_poll`), et sa propre page Découverte est donc refusée en 400. Coût chez moi : environ 11 versions publiées, sur 5 intégrations.
2. **`min` / `max` obligatoires** sur toutes les fonctionnalités, `text` compris. La découverte les accepte absents, puis « Ajouter » échoue en 422. La spec les qualifie même d’« optionnels ». 4 intégrations touchées.
3. **Couples catégorie/type** : ils sont validés en deux listes séparées, donc n’importe quel couple passe. On découvre en réel qu’un couple n’a pas de libellé, pas d’icône, ou qu’il est traité comme un capteur au lieu d’une commande. 5 intégrations touchées.
4. **Store** : un rejet de l’indexeur ne prévient personne. La raison n’est que dans un `rejected.json` dont la doc ne donne pas l’URL. 7 intégrations touchées, dont ecojoko, restée introuvable pour son testeur pendant 2 h.
5. **États publiés avant que l’appareil soit ajouté** : le cœur répond `200`, puis les jette. Une intégration qui déduplique, comme la doc le recommande, ne les renvoie jamais, d’où « pas de valeur récente » juste après l’ajout. 3 intégrations touchées.

* * *

## 1. Polling : `should_poll`, unités et cadences

**Ce que Gladys attend vraiment :**

- Le planificateur n’interroge un appareil que si `should_poll === true` **et** que `poll_frequency` est défini.
- `should_poll` vaut `false` par défaut. Rien ne le déduit de `poll_frequency`, et la page Découverte poste l’appareil tel quel.
- Résultat : un appareil publié avec `poll_frequency` seul est accepté, mais **jamais interrogé**. Il n’y a ni erreur ni log, seulement « pas de valeur récente ».

**Ce que dit la doc :**

- Les specs `host-api-endpoints.md:40`, `websocket-protocol.md:12` et `command-routing.md:5` décrivent le poll « pour un appareil avec une `poll_frequency` ». Aucun fichier de `docs/specs/external-integrations/` ne mentionne `should_poll`.
- La doc publique (`/docs/dev/external-integrations/`) n’a qu’une ligne sur `onPoll`.
- Le README du SDK sur `master` a gagné aujourd’hui une section « Polling devices » (merci @prohand), mais elle n’est pas encore publiée sur npm (0.14.0).

**Le template montre l’exemple qui ne marche pas :**

- `src/config.js:19` contient `poll_frequency: 300, // seconds`, repris par `weatherStation.js:45` et `plug.js:64`, sans `should_poll`.
- Le cœur refuse ce lot en `400 devices[0].poll_frequency: invalid poll frequency`. Le faux Gladys du SDK sur `master` le reproduit.
- **La page Découverte d’une intégration créée depuis le template est donc vide d’origine.** Ce point de SDK #36 a été corrigé côté SDK, pas dans le template.

**Autres pièges du même sujet :**

- **Liste fermée de 1 à 60 s.** Rien n’est prévu pour une cadence lente (5 min, 15 min, 1 h), qui est pourtant le cas courant d’une API cloud avec quota : SolarEdge, ecojoko, Speedtest. Chaque intégration réimplémente donc son minuteur dans le conteneur. Si c’est le comportement voulu, il mérite une phrase dans la doc.
- **Un seul champ invalide fait rejeter tout le lot** , et le SDK ne journalise une erreur de handler qu’au niveau debug. Sur SolarEdge 1.0.1, la Découverte était vide sans aucune trace au niveau de log par défaut.
- **`poll_frequency` et `should_poll` ne repassent jamais sur un appareil déjà créé.** Ils ne font pas partie de la signature de `structure_changed`, donc corriger le polling dans une nouvelle version ne répare pas les appareils existants (IPP, Subsonic). Et la page Appareils d’une intégration externe ne propose que le nom et la pièce : l’utilisateur ne peut pas le corriger lui-même.
- **Détail du front** : le sélecteur de fréquence générique (`UpdateDeviceForm.jsx:57-77`) ne propose pas 15 s, alors que le planificateur la gère.

**Propositions :**

- **Cœur (le plus simple)** : pour une intégration externe, considérer `should_poll = true` dès qu’une `poll_frequency` valide est publiée. À défaut, refuser en 400 une `poll_frequency` sans `should_poll`.
- **Cœur** : inclure `should_poll` et `poll_frequency` dans la signature de `structure_changed`.
- **Template** : `should_poll: true`, une valeur de `DEVICE_POLL_FREQUENCIES` en ms, et un exemple de minuteur interne pour les cadences lentes.
- **Doc** : citer `should_poll` dans les trois specs et sur le site, et dire franchement « au-delà de 60 s, minuteur dans le conteneur ».
- **SDK** : journaliser les erreurs des handlers (`onScanRequest`…) au niveau `error`, pas `debug`.

> **Preuves (master 03d4696d)**
>
> - `server/lib/device/device.add.js:37` : `if (device.should_poll === true && device.poll_frequency)`
> - `server/models/device.js:46-50` : `should_poll` vaut `false` par défaut
> - `server/lib/external-integration/externalIntegration.setDiscoveredDevices.js:55-56` : refuse une `poll_frequency` hors liste, ne contrôle pas `should_poll`
> - `front/src/routes/integration/all/external-integration/discover-page/index.js:136-151` : poste l’objet publié tel quel
> - `externalIntegration.getDiscoveredDevices.js:274-283` : la signature ne contient que des champs de fonctionnalité
> - Historique : UniFi 1.2.8 → 1.3.2 (5 versions en une journée), SolarEdge 1.0.1 → 1.0.3, Subsonic 1.0.2 → 1.0.3, Speedtest 1.0.1 → 1.0.2, et IPP et Plex avant publication

## 2. `min` et `max` obligatoires, même pour `text`

**Le constat :**

- `t_device_feature` déclare `min`, `max`, `read_only` et `has_feedback` en NOT NULL sans valeur par défaut.
- La découverte ne contrôle aucun des quatre. L’erreur n’arrive qu’au clic sur « Ajouter à Gladys » : une 422. Depuis #2733 elle nomme au moins la fonctionnalité fautive, ce qui aide.
- La spec dit l’inverse : `host-api-endpoints.md:40` parle de « every other optional column (`unit`, `min`, `max`) ».
- Les typages du SDK disent aussi `min?: number; max?: number`.

C’est arrivé sur IPP, Plex, Subsonic et Astronomie. Sur IPP, les bornes ont été retirées parce que « inutiles pour du texte », puis remises le jour même après la 422.

**Propositions :**

- Soit le cœur met `0`/`0` par défaut à la publication, comme le fait Zigbee2MQTT, soit il refuse en 400 dès `POST /discovered_device`. Dans les deux cas, plus d’échec tardif.
- Rendre `min`/`max` obligatoires dans `index.d.ts` et corriger la phrase de la spec.

## 3. Couples catégorie/type : n’importe quel couple passe

**Le constat :**

- La découverte teste la catégorie dans une liste et le type dans une autre (`setDiscoveredDevices.js:69-74`). L’unité est contrôlée sans regarder la catégorie, et `DEVICE_FEATURE_UNITS_BY_CATEGORY` n’est utilisée nulle part côté serveur.
- La doc renvoie aux constantes, mais les types génériques (`decimal`, `integer`, `binary`…) sont acceptés avec n’importe quelle catégorie.
- On découvre donc le sens d’un couple en réel. Ce qu’on a payé :
  - **UniFi** : catégorie `sensor` inexistante, puis `speed-sensor/integer` à remplacer par `datarate/rate`, puis `has_feedback` manquant. Trois versions.
  - **Android TV** : `button/click` traité comme un capteur, donc des boutons d’application non cliquables (1.0.5 → 1.1.0). Il a fallu passer à un `text/select`.
  - **IPP** : `level-sensor` avec un type générique n’a pas d’icône.
  - **Astronomie** : `light-sensor/binary` n’a aucun libellé, donc « Appareil (undefined) » en Découverte. Remplacé par `input/binary`.
  - **ecojoko** : la puissance publiée en `energy-sensor/power` avec `min: 0` faisait sortir l’aiguille de la jauge en surplus solaire. Il fallait `grid-sensor/power`, signé, avec des bornes symétriques. Ni le sens « consommation » de l’un ni le sens « échange signé » de l’autre ne sont écrits.

- **Petit bug au passage** : `front/src/utils/consts.js` déclare deux fois la clé `LIGHT_SENSOR` dans `DeviceFeatureCategoriesIcon` (l. 189 et 271). La seconde écrase la première, si bien que `light-sensor/integer` n’a plus d’icône.

**Propositions :**

- Valider le **couple** (et l’unité par catégorie) dans `setDiscoveredDevices`.
- Exporter depuis le SDK une table « catégorie → types autorisés », avec pour chacun : capteur ou commande, `read_only` attendu, signe et bornes typiques.
- Faire vérifier tout cela par le faux Gladys du SDK.

## 4. Store : des rejets silencieux

**Le constat :**

- Le validateur du store est précieux, mais un dépôt qui ne passe pas l’indexeur **ne reçoit aucun retour**.
- La raison n’est que dans `rejected.json`, dont ni le README du store ni le site ne donnent l’URL réelle (`https://integration-store-storage.gladysassistant.com/rejected.json`). `DEFAULT_STORE_BASE_URL` pointe même encore vers GitHub Pages.
- Ce qu’on a payé ainsi :
  - description de plus de 100 caractères : ecojoko absente du store pendant 2 h, et Pat ne la trouvait pas ;
  - `placeholder` non multilingue (Speedtest) ;
  - cover de plus de 150 Ko (Astronomie) ;
  - types `text`/`password` à renommer en `string`/`secret`, et `display_if` refusé (UniFi) ;
  - une version annoncée sans image : l’intégration sort du catalogue (IPP), ou la mise à jour n’est pas proposée et le testeur doit désinstaller puis réinstaller (Android TV 1.1.0).

- **Cadence** : la doc annonce un passage « toutes les heures » (cron `13 * * * *`). En pratique, GitHub n’exécute que 3 à 6 passages planifiés par jour depuis fin septembre, par exemple 08:56 puis 16:08 aujourd’hui. Mieux vaut annoncer « dans la journée ».

**Propositions :**

- Donner l’URL réelle de `rejected.json` dans la doc.
- Prévenir le développeur : un statut de commit, ou une issue ouverte automatiquement sur son dépôt.
- Lancer `npx github:GladysAssistant/integration-store --skip-image-check` dans la CI du template. Le commentaire de `ci.yml:15-17`, qui dit le contraire, est devenu obsolète avec `--skip-image-check`.

## 5. États publiés avant l’ajout de l’appareil : `200`, puis jetés

**Le constat :**

- `POST /state` répond `200 { success: true }` pour une fonctionnalité qui n’existe pas encore.
- Le cœur l’écarte ensuite avec un simple `logger.info` (`device.newStateEvent.js:16-21`), et le quota de 300 états par minute est quand même consommé.
- Or la doc recommande, à juste titre, de ne publier que les changements. L’intégration a donc « déjà envoyé » la valeur, et l’appareil fraîchement ajouté reste sur « pas de valeur récente » jusqu’au prochain changement (Plex, IPP, Subsonic).
- Le contournement, vider le cache et tout republier dans `onDeviceCreated`, n’est écrit nulle part.

**Propositions :**

- Le dire dans la doc et dans le template (`onDeviceCreated` → republier).
- Mieux : renvoyer dans la réponse la liste des `external_id` inconnus, pour que le SDK puisse les garder en attente.

## 6. Comportements silencieux à documenter, ou à journaliser

**« Mettre à jour » dans l’onglet Découverte (`structure_changed`).**

- Il compare seulement `external_id`, `category`, `type`, `unit`, `min`, `max` et `step` de chaque fonctionnalité, plus l’ajout ou le retrait d’une fonctionnalité.
- Il ne se déclenche pas sur les noms, `read_only`, `has_feedback`, `params`, `supported_options` ni le polling. `supported_options` et `params` sont toutefois resynchronisés en silence à chaque publication.
- La spec (`host-api-endpoints.md:52`) dit seulement « features added/modified ». Une liste explicite aurait évité deux suppressions et recréations d’appareil chez le testeur Dreame.
- **À confirmer de ton côté** (je l’ai lu dans le code, sans l’avoir vu en réel) : « Mettre à jour » semble réécrire le **nom de l’appareil choisi par l’utilisateur** avec le nom publié (`device.create.js:140-143`). La pièce, elle, est préservée. Ce serait contraire à la spec, qui dit que le nom et la pièce appartiennent à l’utilisateur.

**Nom d’une fonctionnalité seule de son type.**

- Le tableau de bord affiche le libellé générique du type au lieu du nom publié, sauf pour MQTT (`DISPLAY_FEATURE_NAME_FOR_THOSE_SERVICES = { mqtt: true }`). Je l’avais demandé dans le sujet Dreame, sans réponse.
- Même effet dans l’écran Découverte : quatre ports PoE UniFi s’affichaient comme quatre « Commutateur » indiscernables.
- Une intégration externe choisit ses noms : je propose de l’ajouter à cette règle.

**Langue de l’utilisateur.**

- `setValue`, `poll`, `scene.action.run` et les actions de configuration ne reçoivent pas la langue. Seuls les widgets et la météo la reçoivent.
- Chaque intégration qui produit du texte ajoute donc un champ `language` à sa configuration (IPP, Astronomie, Jellyfin, Dreame).
- Je propose soit de transmettre `language` dans ces payloads, soit de documenter la limite.

**Quota de 300 états par minute.**

- La doc ne mentionne que le `429`. Il vaudrait la peine de préciser trois choses :
  - c’est le **lot entier** qui est refusé ;
  - un lot refusé en 400 consomme quand même le quota ;
  - chaque état accepté réévalue les scènes.

**`gladys_version` et mises à jour.**

- Un cœur ancien refuse tout champ de manifeste inconnu. Déclarer un widget force donc `gladys_version >= 5.1.0`, et l’index ne garde que le dernier manifeste : les cœurs plus anciens n’ont plus aucune mise à jour.
- C’est documenté et compréhensible. En revanche, **sur un vieux cœur** :
  - `isUpdateAvailable` / `getLatestVersion` comparent seulement les numéros, donc le badge « Mise à jour disponible » s’allume ;
  - au clic, le manifeste est écarté avec un simple `warn` (`update.js:33-49`) et le conteneur est recréé à l’identique ;
  - le badge reste allumé, sans message pour l’utilisateur.

- Les specs `core/store.md:23` et `contracts/management-api.md:13` affirment pourtant que le catalogue est « filtré par `gladys_version` ». Je propose de tester la compatibilité dans `isUpdateAvailable`.

**Énergie.**

- Seul un `energy-sensor/index` cumulé déclenche la consommation 30 min et le coût. Un `energy-production-sensor/index` ne dérive rien : la fonctionnalité `thirty-minutes-production` n’est créée par personne.
- `host-api-endpoints.md:63` cite pourtant les curseurs de production, ce qui laisse croire le contraire. ecojoko publie un index de production pour les producteurs solaires, et il ne sert qu’à l’historique.

## 7. Widgets (5.1) : ce qui disparaît sans prévenir

La capacité est excellente, et le validateur du SDK attrape déjà beaucoup de choses. Restent quelques trous :

- **Le budget de 8 composants est appliqué avant la résolution des références** (`getWidgetContent.js:62-69`).
  - Une tuile liée à un appareil pas encore ajouté occupe une place, puis est retirée. Elle a pu évincer un composant valide.
  - Le résultat amputé reste en cache jusqu’au TTL (jusqu’à 1 h), car l’ajout de l’appareil n’invalide pas le cache.
  - Il faut donc penser à appeler `requestWidgetRefresh` sur `onDeviceCreated` : à documenter, ou mieux, à invalider côté cœur.

- **Bouton dont la clé d’action est déjà prise** : il est jeté avec un `warn` côté serveur, et l’intégration n’en sait rien. Sur Dreame, un seul raccourci s’affichait sur trois (0.3.0 → 0.4.0). La spec dit « unique », mais ne dit pas que le doublon est supprimé.
- **Ligne `status` sans `value`** : elle est jetée sans aucun log.
- **Toast d’action** : il est tronqué à 200 caractères par `slice`, sans ellipse.
  - Un objet multilingue sans clé `en` ne donne aucun toast.
  - `MAX_WIDGET_MESSAGE_LENGTH` existe dans le SDK, mais rien ne l’utilise.

- **`card-list`** : la `date` remplace le sous-titre, au lieu de s’y ajouter. La spec dit « subtitle or date » ; il faudrait préciser « la date l’emporte » (Jellyfin, Plex).
- **`validateWidgetContent` ne tourne qu’en mode debug.** Je propose de le lancer à chaque `onWidgetGet` avec un `warn` côté intégration, pour que le développeur voie ce que le cœur va retirer.

## 8. Template et doc publique

- **`.gitignore` et `.prettierignore`** : la règle `data/`, prévue pour le volume `/data`, exclut aussi `src/data/`. Le dossier manque alors dans l’image construite par la CI (Astronomie, avant la 1.0.0). Il faut l’ancrer en `/data/`.

- **Champs `number` décimaux** : c’est corrigé sur `master` (#3167, `step="any"`), mais pas encore publié.

- **Doc publique** :

- **Publier le SDK 0.15** : le faux Gladys et la doc du polling attendent sur `master`. Ce faux Gladys pourrait devenir **le** filet de sécurité s’il vérifiait aussi :

## Déjà corrigé ou déjà demandé : je ne le redemande pas

- Bouton `primary` en mode sombre (#3153 → #3162), `secret` et `default` dans les actions (#3154 → #3163, #3155 → #3164), listes de l’aspirateur et `supported_options` (#3156 → #3171), champs `number` décimaux (#3167). Tout est sur `master`, rien n’est encore dans une version publiée.
- SDK #36 et template #19 de @prohand : unité de `poll_frequency`, faux Gladys, release qui cassait Prettier, release GitHub et changelog, limite de 100 caractères, `placeholder` multilingue.
- Limite de 200 appareils par découverte, relevée en août.

Merci d’avoir lu ou de le faire faire par Claude 😉 !
