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
- Polling :
should_poll: trueest obligatoire, mais aucune spec ni la doc publique ne le dit. Le template publiepoll_frequency: 300(en secondes, sansshould_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. min/maxobligatoires sur toutes les fonctionnalités,textcompris. La découverte les accepte absents, puis « Ajouter » échoue en 422. La spec les qualifie même d’« optionnels ». 4 intégrations touchées.- 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.
- Store : un rejet de l’indexeur ne prévient personne. La raison n’est que dans un
rejected.jsondont la doc ne donne pas l’URL. 7 intégrations touchées, dont ecojoko, restée introuvable pour son testeur pendant 2 h. - É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 === trueet quepoll_frequencyest défini. should_pollvautfalsepar défaut. Rien ne le déduit depoll_frequency, et la page Découverte poste l’appareil tel quel.- Résultat : un appareil publié avec
poll_frequencyseul 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:12etcommand-routing.md:5décrivent le poll « pour un appareil avec unepoll_frequency». Aucun fichier dedocs/specs/external-integrations/ne mentionneshould_poll. - La doc publique (
/docs/dev/external-integrations/) n’a qu’une ligne suronPoll. - Le README du SDK sur
mastera 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:19contientpoll_frequency: 300, // seconds, repris parweatherStation.js:45etplug.js:64, sansshould_poll.- Le cœur refuse ce lot en
400 devices[0].poll_frequency: invalid poll frequency. Le faux Gladys du SDK surmasterle 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_frequencyetshould_pollne repassent jamais sur un appareil déjà créé. Ils ne font pas partie de la signature destructure_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 = truedès qu’unepoll_frequencyvalide est publiée. À défaut, refuser en 400 unepoll_frequencysansshould_poll. - Cœur : inclure
should_polletpoll_frequencydans la signature destructure_changed. - Template :
should_poll: true, une valeur deDEVICE_POLL_FREQUENCIESen ms, et un exemple de minuteur interne pour les cadences lentes. - Doc : citer
should_polldans 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 niveauerror, pasdebug.
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_pollvautfalsepar défautserver/lib/external-integration/externalIntegration.setDiscoveredDevices.js:55-56: refuse unepoll_frequencyhors liste, ne contrôle passhould_pollfront/src/routes/integration/all/external-integration/discover-page/index.js:136-151: poste l’objet publié tel quelexternalIntegration.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_featuredéclaremin,max,read_onlyethas_feedbacken 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:40parle 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/0par défaut à la publication, comme le fait Zigbee2MQTT, soit il refuse en 400 dèsPOST /discovered_device. Dans les deux cas, plus d’échec tardif. - Rendre
min/maxobligatoires dansindex.d.tset 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, etDEVICE_FEATURE_UNITS_BY_CATEGORYn’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
sensorinexistante, puisspeed-sensor/integerà remplacer pardatarate/rate, puishas_feedbackmanquant. Trois versions. - Android TV :
button/clicktraité comme un capteur, donc des boutons d’application non cliquables (1.0.5 → 1.1.0). Il a fallu passer à untext/select. - IPP :
level-sensoravec un type générique n’a pas d’icône. - Astronomie :
light-sensor/binaryn’a aucun libellé, donc « Appareil (undefined) » en Découverte. Remplacé parinput/binary. - ecojoko : la puissance publiée en
energy-sensor/poweravecmin: 0faisait sortir l’aiguille de la jauge en surplus solaire. Il fallaitgrid-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.
- UniFi : catégorie
- Petit bug au passage :
front/src/utils/consts.jsdéclare deux fois la cléLIGHT_SENSORdansDeviceFeatureCategoriesIcon(l. 189 et 271). La seconde écrase la première, si bien quelight-sensor/integern’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_onlyattendu, 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_URLpointe 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 ;
placeholdernon multilingue (Speedtest) ;- cover de plus de 150 Ko (Astronomie) ;
- types
text/passwordà renommer enstring/secret, etdisplay_ifrefusé (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.jsondans 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-checkdans la CI du template. Le commentaire deci.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 /staterépond200 { 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_idinconnus, 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,maxetstepde 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_optionsni le polling.supported_optionsetparamssont 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.runet 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
languagedans 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/getLatestVersioncomparent 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:23etcontracts/management-api.md:13affirment pourtant que le catalogue est « filtré pargladys_version». Je propose de tester la compatibilité dansisUpdateAvailable.
Énergie.
- Seul un
energy-sensor/indexcumulé déclenche la consommation 30 min et le coût. Unenergy-production-sensor/indexne dérive rien : la fonctionnalitéthirty-minutes-productionn’est créée par personne. host-api-endpoints.md:63cite 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
requestWidgetRefreshsuronDeviceCreated: à documenter, ou mieux, à invalider côté cœur.
- Bouton dont la clé d’action est déjà prise : il est jeté avec un
warncô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
statussansvalue: 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é
enne donne aucun toast. MAX_WIDGET_MESSAGE_LENGTHexiste dans le SDK, mais rien ne l’utilise.
- Un objet multilingue sans clé
card-list: ladateremplace 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).validateWidgetContentne tourne qu’en mode debug. Je propose de le lancer à chaqueonWidgetGetavec unwarncôté intégration, pour que le développeur voie ce que le cœur va retirer.
8. Template et doc publique
-
.gitignoreet.prettierignore: la règledata/, prévue pour le volume/data, exclut aussisrc/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
numberdécimaux : c’est corrigé surmaster(#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.
- Sur la 5.1.4, l’exemple latitude/longitude du template (
-
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.
- elle annonce le SDK
-
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_frequencysansshould_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 }sansshould_poll, avec une fonctionnalitélevel-sensor/decimalsans bornes, y répond{ success: true }.
Déjà corrigé ou déjà demandé : je ne le redemande pas
- Bouton
primaryen mode sombre (#3153 → #3162),secretetdefaultdans les actions (#3154 → #3163, #3155 → #3164), listes de l’aspirateur etsupported_options(#3156 → #3171), champsnumberdécimaux (#3167). Tout est surmaster, 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,placeholdermultilingue. - Limite de 200 appareils par découverte, relevée en août.
Merci d’avoir lu ou de le faire faire par Claude
!