Rückmeldungen zur Entwicklung externer Integrationen

Erfahrungsbericht: 11 externe Integrationen und was uns Hin- und Herarbeit gekostet hat (Dokumentation, SDK, Vorlage, Kern)

Hallo zusammen, hallo @pierre-gilles,

Seit August habe ich 11 externe Integrationen veröffentlicht: UniFi, Android TV Remote, IPP, SolarEdge, Plex, Subsonic, Speedtest, ecojoko, Astronomie, Jellyfin & Emby und Dreame. Das System für externe Integrationen ist wirklich angenehm zu verwenden, danke!

Wenn man sich die Historie der Repositories und der Forenbeiträge ansieht, stammt ein großer Teil der „korrigierenden“ Versionen nicht von Fehlern in meinem Code. Sie kamen von Dingen, die Gladys erwartet, ohne dass es irgendwo geschrieben steht, oder die die Dokumentation anders beschreibt. Meistens war der Fehler stumm: ein Gerät, das akzeptiert, aber nie abgefragt wird, eine Widget-Komponente, die verschwindet, eine Integration, die im Store fehlt. Ich habe alles hier zusammengefasst, nach Kosten sortiert, mit Vorschlägen für Dokumentation, SDK oder Kern.

Alles wurde heute auf master überprüft (Kern 03d4696d, SDK 500db06, Vorlage 4ea0c7b, Store a052400), mit Datei und Zeile in den ausgeblendeten Blöcken. Ich habe das, was bereits behoben oder bereits angefordert wurde, aussortiert und liste es am Ende auf. Ich denke dabei insbesondere an die Tickets von @prohand (SDK #36, Vorlage #19) und das Thema Dreame (#3153 bis #3156).

Kurz gesagt: die 5 Punkte, die die meisten Versionen verhindert hätten

  1. Polling: should_poll: true ist obligatorisch, aber keine Spezifikation oder die öffentliche Dokumentation erwähnt dies. Die Vorlage veröffentlicht poll_frequency: 300 (in Sekunden, ohne should_poll), und ihre eigene Entdeckungsseite wird daher mit 400 abgelehnt. Kosten für mich: etwa 11 veröffentlichte Versionen, bei 5 Integrationen.
  2. min / max obligatorisch für alle Funktionen, einschließlich text. Die Entdeckung akzeptiert sie als fehlend, dann scheitert „Hinzufügen“ mit 422. Die Spezifikation bezeichnet sie sogar als „optional“. 4 Integrationen betroffen.
  3. Kategorie/Typ-Paare: Sie werden in zwei separaten Listen validiert, daher wird jedes Paar akzeptiert. In der Praxis hat ein Paar keine Bezeichnung, kein Icon, oder es wird als Sensor statt als Befehl behandelt. 5 Integrationen betroffen.
  4. Store: Eine Ablehnung durch den Indexer benachrichtigt niemanden. Der Grund steht nur in einer rejected.json, deren URL die Dokumentation nicht angibt. 7 Integrationen betroffen, darunter ecojoko, die für ihren Tester 2 Stunden lang nicht auffindbar war.
  5. Zustände, die veröffentlicht werden, bevor das Gerät hinzugefügt wird: Der Kern antwortet mit 200, wirft sie dann aber weg. Eine Integration, die Duplikate entfernt, wie die Dokumentation es empfiehlt, sendet sie nie wieder, daher „keine aktuellen Werte“ direkt nach dem Hinzufügen. 3 Integrationen betroffen.

1. Polling: should_poll, Einheiten und Taktraten

Was Gladys wirklich erwartet:

  • Der Planer fragt ein Gerät nur ab, wenn should_poll === true und poll_frequency definiert ist.
  • should_poll ist standardmäßig false. Nichts wird aus poll_frequency abgeleitet, und die Entdeckungsseite sendet das Gerät so, wie es ist.
  • Ergebnis: Ein Gerät, das nur mit poll_frequency veröffentlicht wird, wird akzeptiert, aber nie abgefragt. Es gibt keinen Fehler oder Log, nur „keine aktuellen Werte“.

Was die Dokumentation sagt:

  • Die Spezifikationen host-api-endpoints.md:40, websocket-protocol.md:12 und command-routing.md:5 beschreiben das Polling „für ein Gerät mit einer poll_frequency“. Keine Datei in docs/specs/external-integrations/ erwähnt should_poll.
  • Die öffentliche Dokumentation (/docs/dev/external-integrations/) hat nur eine Zeile zu onPoll.
  • Das README des SDK auf master hat heute eine Sektion „Polling devices“ erhalten (danke @prohand), aber sie ist noch nicht auf npm (0.14.0) veröffentlicht.

Die Vorlage zeigt das Beispiel, das nicht funktioniert:

  • src/config.js:19 enthält poll_frequency: 300, // Sekunden, übernommen von weatherStation.js:45 und plug.js:64, ohne should_poll.
  • Der Kern lehnt diese Charge mit 400 devices[0].poll_frequency: invalid poll frequency ab. Der falsche Gladys des SDK auf master reproduziert dies.
  • Die Entdeckungsseite einer Integration, die aus der Vorlage erstellt wurde, ist daher von Anfang an leer. Dieser Punkt von SDK #36 wurde auf SDK-Seite korrigiert, nicht in der Vorlage.

Weitere Fallstricke desselben Themas:

  • Geschlossene Liste von 1 bis 60 s. Nichts ist für eine langsame Taktrate (5 min, 15 min, 1 h) vorgesehen, die jedoch der häufige Fall einer Cloud-API mit Kontingent ist: SolarEdge, ecojoko, Speedtest. Jede Integration implementiert daher ihren eigenen Timer im Container. Wenn dies das gewünschte Verhalten ist, verdient es einen Satz in der Dokumentation.
  • Ein einzelnes ungültiges Feld führt zur Ablehnung der gesamten Charge, und das SDK protokolliert einen Handler-Fehler nur auf Debug-Ebene. Bei SolarEdge 1.0.1 war die Entdeckung leer, ohne eine einzige Spur auf der Standard-Log-Ebene.
  • poll_frequency und should_poll werden nie für ein bereits erstelltes Gerät aktualisiert. Sie sind kein Teil der Signatur von structure_changed, daher repariert das Ändern des Pollings in einer neuen Version nicht die bestehenden Geräte (IPP, Subsonic). Und die Geräte-Seite einer externen Integration bietet nur den Namen und den Raum: Der Benutzer kann es nicht selbst korrigieren.
  • Frontend-Detail: Der generische Frequenzselektor (UpdateDeviceForm.jsx:57-77) bietet keine 15 s an, obwohl der Planer sie unterstützt.

Vorschläge:

  • Kern (am einfachsten): Für eine externe Integration should_poll = true annehmen, sobald eine gültige poll_frequency veröffentlicht wird. Andernfalls eine poll_frequency ohne should_poll mit 400 ablehnen.
  • Kern: should_poll und poll_frequency in die Signatur von structure_changed aufnehmen.
  • Vorlage: should_poll: true, ein Wert von DEVICE_POLL_FREQUENCIES in ms und ein Beispiel für einen internen Timer für langsame Taktraten.
  • Dokumentation: should_poll in den drei Spezifikationen und auf der Website erwähnen und offen sagen „bei mehr als 60 s, Timer im Container“.
  • SDK: Fehler der Handler (onScanRequest…) auf Ebene error protokollieren, nicht debug.
Beweise (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 ist standardmäßig false
  • server/lib/external-integration/externalIntegration.setDiscoveredDevices.js:55-56: lehnt eine poll_frequency außerhalb der Liste ab, prüft nicht should_poll
  • front/src/routes/integration/all/external-integration/discover-page/index.js:136-151: sendet das veröffentlichte Objekt so, wie es ist
  • externalIntegration.getDiscoveredDevices.js:274-283: die Signatur enthält nur Funktionsfelder
  • Historie: UniFi 1.2.8 → 1.3.2 (5 Versionen an einem Tag), SolarEdge 1.0.1 → 1.0.3, Subsonic 1.0.2 → 1.0.3, Speedtest 1.0.1 → 1.0.2 und IPP und Plex vor der Veröffentlichung

2. min und max obligatorisch, auch für text

Die Feststellung:

  • t_device_feature erklärt min, max, read_only und has_feedback als NOT NULL ohne Standardwert.
  • Die Entdeckung überprüft keines der vier. Der Fehler tritt erst beim Klicken auf „Zu Gladys hinzufügen“ auf: eine 422. Seit #2733 benennt sie zumindest die fehlerhafte Funktion, was hilft.
  • Die Spezifikation sagt das Gegenteil: host-api-endpoints.md:40 spricht von „every other optional column (unit, min, max)“.
  • Die Typisierungen des SDK sagen auch min?: number; max?: number.

Das ist bei IPP, Plex, Subsonic und Astronomie passiert. Bei IPP wurden die Grenzen entfernt, weil sie „für Text unnötig“ waren, dann am selben Tag nach der 422 wieder hinzugefügt.

Vorschläge:

  • Entweder setzt der Kern standardmäßig 0/0 bei der Veröffentlichung, wie es Zigbee2MQTT tut, oder er lehnt mit 400 bei POST /discovered_device ab. In beiden Fällen gibt es keine späten Fehler.
  • min/max in index.d.ts verpflichtend machen und den Satz der Spezifikation korrigieren.

3. Kategorie/Typ-Paare: jedes Paar wird akzeptiert

Die Feststellung:

  • Die Entdeckung testet die Kategorie in einer Liste und den Typ in einer anderen (setDiscoveredDevices.js:69-74). Die Einheit wird ohne Berücksichtigung der Kategorie kontrolliert, und DEVICE_FEATURE_UNITS_BY_CATEGORY wird nirgendwo auf der Serverseite verwendet.
  • Die Dokumentation verweist auf die Konstanten, aber generische Typen (decimal, integer, binary…) werden mit jeder Kategorie akzeptiert.
  • Man entdeckt also die Bedeutung eines Paares in der Realität. Wofür wir bezahlt haben:
    • UniFi: Kategorie sensor existiert nicht, dann speed-sensor/integer durch datarate/rate zu ersetzen, dann fehlt has_feedback. Drei Versionen.
    • Android TV: button/click wird als Sensor behandelt, daher nicht klickbare App-Buttons (1.0.5 → 1.1.0). Es musste zu einem text/select gewechselt werden.
    • IPP: level-sensor mit einem generischen Typ hat kein Icon.
    • Astronomie: light-sensor/binary hat keine Bezeichnung, daher „Gerät (undefined)“ in der Entdeckung. Ersetzt durch input/binary.
    • ecojoko: Die Leistung, die in energy-sensor/power mit min: 0 veröffentlicht wird, lässt die Nadel des Messgeräts bei Solarüberschuss aus der Skala. Es sollte grid-sensor/power sein, signiert, mit symmetrischen Grenzen. Weder die Bedeutung „Verbrauch“ des einen noch die Bedeutung „signierter Austausch“ des anderen sind geschrieben.
  • Kleiner Bug nebenbei: front/src/utils/consts.js erklärt den Schlüssel LIGHT_SENSOR zweimal in DeviceFeatureCategoriesIcon (Z. 189 und 271). Der zweite überschreibt den ersten, sodass light-sensor/integer kein Icon mehr hat.

Vorschläge:

  • Das Paar (und die Einheit pro Kategorie) in setDiscoveredDevices validieren.
  • Aus dem SDK eine Tabelle „Kategorie → zulässige Typen“ exportieren, mit für jeden: Sensor oder Steuerung, erwartetes read_only, Vorzeichen und typische Grenzen.
  • All das von der falschen Gladys des SDK überprüfen lassen.

4. Store: Stille Ablehnungen

Die Feststellung:

  • Der Store-Validator ist wertvoll, aber ein Repository, das den Indexer nicht besteht, erhält keine Rückmeldung.
  • Der Grund steht nur in rejected.json, dessen URL weder das Store-README noch die Website angeben (https://integration-store-storage.gladysassistant.com/rejected.json). DEFAULT_STORE_BASE_URL zeigt sogar noch auf GitHub Pages.
  • Wofür wir so bezahlt haben:
    • Beschreibung von mehr als 100 Zeichen: ecojoko war 2 Stunden lang nicht im Store, und Pat fand sie nicht;
    • placeholder nicht mehrsprachig (Speedtest);
    • Cover von mehr als 150 KB (Astronomie);
    • Typen text/password in string/secret umbenennen und display_if abgelehnt (UniFi);
    • eine angekündigte Version ohne Bild: Die Integration verschwindet aus dem Katalog (IPP), oder das Update wird nicht angeboten und der Tester muss deinstallieren und neu installieren (Android TV 1.1.0).
  • Cadence: Die Dokumentation kündigt einen „stündlichen“ Durchlauf an (cron 13 * * * *). In der Praxis führt GitHub nur 3 bis 6 geplante Durchläufe pro Tag seit Ende September aus, z. B. heute um 08:56 Uhr und 16:08 Uhr. Besser wäre es, „im Laufe des Tages“ anzukündigen.

Vorschläge:

  • Die tatsächliche URL von rejected.json in der Dokumentation angeben.
  • Den Entwickler benachrichtigen: ein Commit-Status oder ein automatisch geöffnetes Issue in seinem Repository.
  • npx github:GladysAssistant/integration-store --skip-image-check in der CI des Templates ausführen. Der Kommentar von ci.yml:15-17, der das Gegenteil behauptet, ist mit --skip-image-check veraltet.

5. Veröffentlichte Zustände vor der Geräteergänzung: 200, dann verworfen

Die Feststellung:

  • POST /state antwortet 200 { success: true } für eine Funktion, die noch nicht existiert.
  • Der Kern sortiert sie dann mit einem einfachen logger.info aus (device.newStateEvent.js:16-21), und das Kontingent von 300 Zuständen pro Minute wird trotzdem verbraucht.
  • Die Dokumentation empfiehlt jedoch zu Recht, nur Änderungen zu veröffentlichen. Die Integration hat also „bereits“ den Wert gesendet, und das frisch hinzugefügte Gerät bleibt auf „kein aktueller Wert“, bis zur nächsten Änderung (Plex, IPP, Subsonic).
  • Die Umgehung, den Cache zu leeren und alles in onDeviceCreated neu zu veröffentlichen, steht nirgendwo geschrieben.

Vorschläge:

  • Das in der Dokumentation und im Template (onDeviceCreated → neu veröffentlichen) erwähnen.
  • Besser: In der Antwort die Liste der unbekannten external_id zurückgeben, damit das SDK sie in Wartestellung halten kann.

6. Stilles Verhalten, das zu dokumentieren oder zu protokollieren ist

„Aktualisieren“ im Entdeckungstab („structure_changed“).

  • Es vergleicht nur external_id, category, type, unit, min, max und step jedes Merkmals sowie die Hinzufügung oder Entfernung eines Merkmals.
  • Es wird nicht durch Namen, read_only, has_feedback, params, supported_options oder das Polling ausgelöst. supported_options und params werden jedoch bei jeder Veröffentlichung stillschweigend neu synchronisiert.
  • Die Spezifikation (host-api-endpoints.md:52) sagt nur „features added/modified“. Eine explizite Liste hätte zwei Löschungen und Neuerstellungen eines Geräts beim Tester Dreame verhindert.
  • Zu bestätigen auf deiner Seite (ich habe es im Code gelesen, ohne es in der Realität gesehen zu haben): „Aktualisieren“ scheint den vom Benutzer gewählten Gerätenamen mit dem veröffentlichten Namen zu überschreiben (device.create.js:140-143). Der Raum selbst wird beibehalten. Das wäre im Widerspruch zur Spezifikation, die besagt, dass Name und Raum dem Benutzer gehören.

Name eines einzigen Merkmals seines Typs.

  • Das Dashboard zeigt die generische Bezeichnung des Typs anstelle des veröffentlichten Namens an, außer bei MQTT (DISPLAY_FEATURE_NAME_FOR_THOSE_SERVICES = { mqtt: true }). Ich hatte das im Dreame-Thema gefragt, ohne Antwort.
  • Gleicher Effekt im Entdeckungsscreen: Vier UniFi-PoE-Ports wurden als vier „Schalter“ angezeigt, die nicht zu unterscheiden waren.
  • Eine externe Integration wählt ihre Namen: Ich schlage vor, sie zu dieser Regel hinzuzufügen.

Sprache des Benutzers.

  • setValue, poll, scene.action.run und die Konfigurationsaktionen erhalten keine Sprache. Nur die Widgets und das Wetter erhalten sie.
  • Jede Integration, die Text erzeugt, fügt daher ein Feld language zu ihrer Konfiguration hinzu (IPP, Astronomie, Jellyfin, Dreame).
  • Ich schlage entweder vor, language in diesen Payloads zu übertragen, oder die Grenze zu dokumentieren.

Kontingent von 300 Zuständen pro Minute.

  • Die Dokumentation erwähnt nur den 429. Es wäre wert, drei Dinge zu präzisieren:
    • es ist der gesamte Batch, der abgelehnt wird;
    • ein abgelehnter Batch mit 400 verbraucht trotzdem das Kontingent;
    • jeder akzeptierte Zustand bewertet die Szenen neu.

gladys_version und Updates.

  • Ein alter Kern lehnt jedes unbekannte Manifestfeld ab. Die Deklaration eines Widgets erzwingt daher gladys_version >= 5.1.0, und der Index behält nur das letzte Manifest: Ältere Kerne erhalten keine Updates mehr.
  • Das ist dokumentiert und verständlich. Andererseits, auf einem alten Kern:
    • isUpdateAvailable / getLatestVersion vergleichen nur die Nummern, daher leuchtet das Badge „Update verfügbar“ auf;
    • beim Klicken wird das Manifest mit einer einfachen warn (update.js:33-49) abgelehnt und der Container identisch neu erstellt;
    • das Badge bleibt leuchtend, ohne Nachricht für den Benutzer.
  • Die Spezifikationen core/store.md:23 und contracts/management-api.md:13 behaupten jedoch, dass der Katalog „nach gladys_version gefiltert“ wird. Ich schlage vor, die Kompatibilität in isUpdateAvailable zu testen.

Energie.

  • Nur ein energy-sensor/index kumuliert löst den Verbrauch von 30 Minuten und die Kosten aus. Ein energy-production-sensor/index leitet nichts ab: Das Merkmal thirty-minutes-production wird von niemandem erstellt.
  • host-api-endpoints.md:63 zitiert jedoch die Produktionsregler, was das Gegenteil vermuten lässt. ecojoko veröffentlicht einen Produktionsindex für Solarproduzenten, und er dient nur dem historischen Verlauf.

7. Widgets (5.1): Was ohne Vorwarnung verschwindet

Die Fähigkeit ist ausgezeichnet, und der Validator des SDK fängt bereits viele Dinge ab. Es bleiben jedoch einige Lücken:

  • Das Budget von 8 Komponenten wird vor der Auflösung der Referenzen angewendet (getWidgetContent.js:62-69).
    • Eine Kachel, die mit einem noch nicht hinzugefügten Gerät verknüpft ist, belegt einen Platz und wird dann entfernt. Sie könnte eine gültige Komponente verdrängt haben.
    • Das verkürzte Ergebnis bleibt im Cache bis zum TTL (bis zu 1 h), da das Hinzufügen des Geräts den Cache nicht ungültig macht.
    • Man sollte also daran denken, requestWidgetRefresh bei onDeviceCreated aufzurufen: zu dokumentieren oder besser, auf der Core-Seite zu invalidieren.
  • Schaltfläche mit bereits belegter Aktionsschlüssel : Sie wird mit einer warn-Meldung auf der Serverseite verworfen, und die Integration weiß nichts davon. Auf Dreame wurde nur ein Shortcut von drei angezeigt (0.3.0 → 0.4.0). Die Spezifikation sagt „einzigartig“, aber sie sagt nicht, dass das Duplikat entfernt wird.
  • status-Zeile ohne value : Sie wird ohne Logs verworfen.
  • Aktions-Toast : Er wird auf 200 Zeichen durch slice gekürzt, ohne Auslassungspunkte.
    • Ein mehrsprachiges Objekt ohne en-Schlüssel gibt keinen Toast.
    • MAX_WIDGET_MESSAGE_LENGTH existiert im SDK, aber nichts verwendet es.
  • card-list : Das date ersetzt den Untertitel, anstatt hinzugefügt zu werden. Die Spezifikation sagt „subtitle or date“; es sollte präzisiert werden „das Datum hat Vorrang“ (Jellyfin, Plex).
  • validateWidgetContent läuft nur im Debug-Modus. Ich schlage vor, es bei jedem onWidgetGet mit einer warn-Meldung auf der Integrationsseite auszuführen, damit der Entwickler sieht, was das Core entfernen wird.

8. Vorlage und öffentliche Dokumentation

  • .gitignore und .prettierignore : Die Regel data/, die für das Volume /data vorgesehen ist, schließt auch src/data/ aus. Der Ordner fehlt dann im durch die CI erstellten Image (Astronomie, vor der 1.0.0). Er muss mit /data/ verankert werden.

  • Dezimale number-Felder : Dies ist auf master korrigiert (#3167, step="any"), aber noch nicht veröffentlicht.

    • Auf der 5.1.4 bleibt das Beispiel für Breite/Länge der Vorlage (48.8566) unmöglich einzugeben. Astronomie hat eine Version darüber verloren.
    • Das Schema des Manifests lehnt step immer noch ab: Man kann keine Auflösung deklarieren.
  • Öffentliche Dokumentation :

    • Sie kündigt das SDK 0.12.0 an, während npm bei 0.14.0 ist;
    • Sie beschreibt den alten Release-Workflow, ohne CHANGELOG oder GitHub-Release;
    • Sie sagt nicht, dass „Changelog dieser Version anzeigen“ das GitHub-Release des Tags öffnet;
    • Sie spricht noch nicht über den falschen Gladys.
    • Die Vorlage macht jetzt alles das (#20, danke): Es fehlt nur die Seite.
  • SDK 0.15 veröffentlichen : Der falsche Gladys und die Dokumentation des Pollings warten auf master. Dieser falsche Gladys könnte das Sicherheitsnetz werden, wenn er auch prüft:

    • poll_frequency ohne should_poll;
    • Das Fehlen von min/max;
    • Die Paare Kategorie/Typ;
    • Das Kontingent von 300 Zuständen pro Minute;
    • Die Länge der Toasts.

    Heute, { poll_frequency: 60000 } ohne should_poll, mit einer Funktionalität level-sensor/decimal ohne Grenzen, gibt es { success: true }.

Bereits korrigiert oder bereits angefordert: Ich frage nicht noch einmal danach

  • Primärer Schaltfläche im dunklen Modus (#3153 → #3162), secret und default in den Aktionen (#3154 → #3163, #3155 → #3164), Listen des Staubsaugers und supported_options (#3156 → #3171), dezimale number-Felder (#3167). Alles ist auf master, nichts ist noch in einer veröffentlichten Version.
  • SDK #36 und Vorlage #19 von @prohand: Einheit von poll_frequency, falscher Gladys, Release, das Prettier kaputt gemacht hat, GitHub-Release und Changelog, Grenze von 100 Zeichen, mehrsprachiger placeholder.
  • Grenze von 200 Geräten pro Entdeckung, erhöht im August.

Danke, dass du es gelesen hast oder es von Claude lesen lässt :wink: !

3 „Gefällt mir“