Retours sur développement des intégrations externes

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é.

    • Sur la 5.1.4, l’exemple latitude/longitude du template (48.8566) reste impossible à saisir. Astronomie a perdu une version là-dessus.
    • Le schéma du manifeste refuse toujours step : on ne peut pas déclarer une résolution.
  • Doc publique :

    • elle annonce le SDK 0.12.0, alors que npm est à 0.14.0 ;
    • elle décrit l’ancien flux de release, sans CHANGELOG ni release GitHub ;
    • elle ne dit pas que « Voir le changelog de cette version » ouvre la release GitHub du tag ;
    • elle ne parle pas encore du faux Gladys.
    • Le template fait maintenant tout ça (#20, merci) : il ne manque que la page.
  • 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 :

    • poll_frequency sans should_poll ;
    • l’absence de min/max ;
    • les couples catégorie/type ;
    • le quota de 300 états par minute ;
    • la longueur des toasts.

    Aujourd’hui, { poll_frequency: 60000 } sans should_poll, avec une fonctionnalité level-sensor/decimal sans bornes, y répond { success: true }.

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 :wink: !

3 « J'aime »