Feedback on external integration development

Experience Report: 11 External Integrations and What Caused Us Back-and-Forth (Doc, SDK, Template, Core)

Hello everyone, hello @pierre-gilles,

Since August, I have published 11 external integrations: UniFi, Android TV Remote, IPP, SolarEdge, Plex, Subsonic, Speedtest, ecojoko, Astronomy, Jellyfin & Emby, and Dreame. The external integrations system is really pleasant to use, thank you!

By reviewing the repository history and forum threads, a good part of the « corrective » versions did not come from bugs in my code. They came from things that Gladys expects without it being written anywhere, or that the documentation says otherwise. Most often, the error was silent: a device accepted but never queried, a widget component that disappears, an integration missing from the store. I group everything here, classified by cost, with for each point what I propose on the side of the documentation, SDK, or core.

Everything has been rechecked today on master (core 03d4696d, SDK 500db06, template 4ea0c7b, store a052400), with file and line in the collapsible blocks. I have excluded what is already fixed or already requested, and I list it at the end. I am thinking in particular of the tickets from @prohand (SDK #36, template #19) and the Dreame topic (#3153 to #3156).

In short: the 5 points that would have avoided the most versions

  1. Polling: should_poll: true is mandatory, but no spec nor the public documentation mentions it. The template publishes poll_frequency: 300 (in seconds, without should_poll), and its own Discovery page is therefore rejected with a 400. Cost for me: approximately 11 published versions, across 5 integrations.
  2. min / max required on all features, including text. Discovery accepts them as absent, then « Add » fails with a 422. The spec even qualifies them as « optional ». 4 integrations affected.
  3. Category/type pairs: They are validated in two separate lists, so any pair passes. In reality, a pair may have no label, no icon, or be treated as a sensor instead of a command. 5 integrations affected.
  4. Store: A rejection by the indexer notifies no one. The reason is only in a rejected.json whose documentation does not provide the URL. 7 integrations affected, including ecojoko, which remained unfindable for its tester for 2 hours.
  5. States published before the device is added: The core responds 200, then discards them. An integration that deduplicates, as the documentation recommends, never sends them back, hence « no recent value » right after addition. 3 integrations affected.

1. Polling: should_poll, Units, and Rates

What Gladys Really Expects:

  • The scheduler queries a device only if should_poll === true and poll_frequency is defined.
  • should_poll defaults to false. Nothing is inferred from poll_frequency, and the Discovery page posts the device as-is.
  • Result: A device published with only poll_frequency is accepted but never queried. There is no error or log, only « no recent value ».

What the Documentation Says:

  • The specs host-api-endpoints.md:40, websocket-protocol.md:12, and command-routing.md:5 describe polling « for a device with a poll_frequency ». No file in docs/specs/external-integrations/ mentions should_poll.
  • The public documentation (/docs/dev/external-integrations/) has only one line about onPoll.
  • The SDK README on master has gained today a « Polling devices » section (thanks @prohand), but it is not yet published on npm (0.14.0).

The Template Shows the Non-Working Example:

  • src/config.js:19 contains poll_frequency: 300, // seconds, reused by weatherStation.js:45 and plug.js:64, without should_poll.
  • The core rejects this batch with 400 devices[0].poll_frequency: invalid poll frequency. The fake Gladys in the SDK on master reproduces this.
  • The Discovery page of an integration created from the template is therefore empty by default. This point of SDK #36 has been fixed on the SDK side, not in the template.

Other Pitfalls of the Same Topic:

  • Closed list from 1 to 60 s. Nothing is provided for a slow rate (5 min, 15 min, 1 h), which is yet the common case of a cloud API with quota: SolarEdge, ecojoko, Speedtest. Each integration therefore reimplements its own timer in the container. If this is the desired behavior, it deserves a sentence in the documentation.
  • A single invalid field causes the entire batch to be rejected, and the SDK only logs a handler error at the debug level. On SolarEdge 1.0.1, Discovery was empty without any trace at the default log level.
  • poll_frequency and should_poll never reapply to an already created device. They are not part of the structure_changed signature, so fixing polling in a new version does not repair existing devices (IPP, Subsonic). And the Devices page of an external integration only offers the name and the room: the user cannot correct it themselves.
  • Front-end detail: The generic frequency selector (UpdateDeviceForm.jsx:57-77) does not offer 15 s, even though the scheduler handles it.

Proposals:

  • Core (the simplest): For an external integration, consider should_poll = true as soon as a valid poll_frequency is published. Failing that, reject with a 400 a poll_frequency without should_poll.
  • Core: Include should_poll and poll_frequency in the structure_changed signature.
  • Template: should_poll: true, a value of DEVICE_POLL_FREQUENCIES in ms, and an example of an internal timer for slow rates.
  • Documentation: Mention should_poll in the three specs and on the website, and frankly say « beyond 60 s, timer in the container ».
  • SDK: Log handler errors (onScanRequest…) at the error level, not debug.
Proofs (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 defaults to false
  • server/lib/external-integration/externalIntegration.setDiscoveredDevices.js:55-56: rejects a poll_frequency outside the list, does not check should_poll
  • front/src/routes/integration/all/external-integration/discover-page/index.js:136-151: posts the published object as-is
  • externalIntegration.getDiscoveredDevices.js:274-283: the signature only contains feature fields
  • History: UniFi 1.2.8 → 1.3.2 (5 versions in one day), SolarEdge 1.0.1 → 1.0.3, Subsonic 1.0.2 → 1.0.3, Speedtest 1.0.1 → 1.0.2, and IPP and Plex before publication

2. min and max Required, Even for text

The Observation:

  • t_device_feature declares min, max, read_only, and has_feedback as NOT NULL without a default value.
  • Discovery checks none of the four. The error only occurs when clicking « Add to Gladys »: a 422. Since #2733, it at least names the faulty feature, which helps.
  • The spec says the opposite: host-api-endpoints.md:40 talks about « every other optional column (unit, min, max) ».
  • The SDK typings also say min?: number; max?: number.

This happened with IPP, Plex, Subsonic, and Astronomy. On IPP, the limits were removed because « useless for text », then put back the same day after the 422.

Proposals:

  • Either the core sets 0/0 by default on publication, like Zigbee2MQTT does, or it rejects with a 400 as soon as POST /discovered_device. In both cases, no late failure.
  • Make min/max mandatory in index.d.ts and correct the spec’s sentence.

3. Category/type Pairs: Any Pair Passes

The Observation:

  • The discovery tests the category in a list and the type in another (setDiscoveredDevices.js:69-74). The unit is controlled without looking at the category, and DEVICE_FEATURE_UNITS_BY_CATEGORY is not used anywhere on the server side.
  • The documentation refers to constants, but generic types (decimal, integer, binary…) are accepted with any category.
  • We therefore discover the meaning of a pair in real life. What we paid for:
    • UniFi: non-existent sensor category, then speed-sensor/integer to be replaced by datarate/rate, then missing has_feedback. Three versions.
    • Android TV: button/click treated as a sensor, so application buttons not clickable (1.0.5 → 1.1.0). Had to switch to a text/select.
    • IPP: level-sensor with a generic type has no icon.
    • Astronomy: light-sensor/binary has no label, so « Device (undefined) » in Discovery. Replaced by input/binary.
    • ecojoko: the power published in energy-sensor/power with min: 0 made the needle of the gauge go off the scale in solar surplus. Needed grid-sensor/power, signed, with symmetric limits. Neither the « consumption » meaning of one nor the « signed exchange » meaning of the other are written.
  • Minor bug: front/src/utils/consts.js declares the key LIGHT_SENSOR twice in DeviceFeatureCategoriesIcon (l. 189 and 271). The second overwrites the first, so light-sensor/integer no longer has an icon.

Proposals:

  • Validate the pair (and the unit per category) in setDiscoveredDevices.
  • Export from the SDK a table « category → allowed types », with for each: sensor or command, expected read_only, typical sign and limits.
  • Have all this checked by the SDK’s fake Gladys.

4. Store: Silent Rejections

The Situation:

  • The store validator is valuable, but a repository that doesn’t pass the indexer receives no feedback.
  • The reason is only in rejected.json, whose URL is not given in either the store’s README or the website (https://integration-store-storage.gladysassistant.com/rejected.json). DEFAULT_STORE_BASE_URL even still points to GitHub Pages.
  • What we paid for this way:
    • description of more than 100 characters: ecojoko absent from the store for 2 hours, and Pat couldn’t find it;
    • non-multilingual placeholder (Speedtest);
    • cover of more than 150 KB (Astronomy);
    • types text/password to be renamed to string/secret, and display_if rejected (UniFi);
    • a version announced without an image: the integration is removed from the catalog (IPP), or the update is not offered and the tester has to uninstall and reinstall (Android TV 1.1.0).
  • Cadence: the documentation announces a « every hour » (cron 13 * * * *). In practice, GitHub only executes 3 to 6 scheduled runs per day since late September, for example 08:56 then 16:08 today. It’s better to announce « during the day ».

Proposals:

  • Give the real URL of rejected.json in the documentation.
  • Notify the developer: a commit status, or an issue automatically opened on their repository.
  • Run npx github:GladysAssistant/integration-store --skip-image-check in the template’s CI. The comment in ci.yml:15-17, which says the opposite, has become obsolete with --skip-image-check.

5. States Published Before Device Addition: 200, Then Discarded

The Situation:

  • POST /state responds 200 { success: true } for a feature that doesn’t exist yet.
  • The core then discards it with a simple logger.info (device.newStateEvent.js:16-21), and the quota of 300 states per minute is still consumed.
  • Yet the documentation rightly recommends publishing only changes. The integration has « already sent » the value, and the freshly added device remains on « no recent value » until the next change (Plex, IPP, Subsonic).
  • The workaround, clearing the cache and republishing everything in onDeviceCreated, is nowhere written.

Proposals:

  • Mention it in the documentation and in the template (onDeviceCreated → republish).
  • Better: return in the response the list of unknown external_ids, so that the SDK can keep them pending.

6. Silent Behaviors to Document, or to Log

« Update » in the Discovery tab (structure_changed).

  • It only compares external_id, category, type, unit, min, max, and step of each feature, plus the addition or removal of a feature.
  • It does not trigger on names, read_only, has_feedback, params, supported_options, nor polling. supported_options and params are however resynchronized silently on each publication.
  • The spec (host-api-endpoints.md:52) only says « features added/modified ». An explicit list would have avoided two device deletions and recreations for the tester Dreame.
  • To confirm on your side (I read it in the code, without having seen it in real life): « Update » seems to rewrite the device name chosen by the user with the published name (device.create.js:140-143). The room, however, is preserved. This would be contrary to the spec, which says that the name and the room belong to the user.

Name of a feature alone of its type.

  • The dashboard displays the generic label of the type instead of the published name, except for MQTT (DISPLAY_FEATURE_NAME_FOR_THOSE_SERVICES = { mqtt: true }). I had asked in the Dreame topic, without answer.
  • Same effect in the Discovery screen: four UniFi PoE ports were displayed as four indistinguishable « Switches ».
  • An external integration chooses its names: I propose adding it to this rule.

User’s language.

  • setValue, poll, scene.action.run, and configuration actions do not receive the language. Only widgets and weather receive it.
  • Each integration that produces text therefore adds a language field to its configuration (IPP, Astronomy, Jellyfin, Dreame).
  • I propose either transmitting language in these payloads, or documenting the limitation.

Quota of 300 states per minute.

  • The documentation only mentions the 429. It would be worth specifying three things:
    • it’s the entire batch that is rejected;
    • a batch rejected in 400 still consumes the quota;
    • each accepted state re-evaluates the scenes.

gladys_version and updates.

  • An old core rejects any unknown manifest field. Declaring a widget therefore forces gladys_version >= 5.1.0, and the index only keeps the last manifest: older cores no longer have any updates.
  • This is documented and understandable. However, on an old core:
    • isUpdateAvailable / getLatestVersion only compare the numbers, so the « Update available » badge lights up;
    • on click, the manifest is discarded with a simple warn (update.js:33-49) and the container is recreated identically;
    • the badge remains lit, without a message for the user.
  • The specs core/store.md:23 and contracts/management-api.md:13 nevertheless state that the catalog is « filtered by gladys_version ». I propose testing compatibility in isUpdateAvailable.

Energy.

  • Only a energy-sensor/index summed triggers the 30-minute consumption and cost. A energy-production-sensor/index derives nothing: the thirty-minutes-production feature is created by no one.
  • host-api-endpoints.md:63 nevertheless cites the production sliders, which leads to believe the opposite. ecojoko publishes a production index for solar producers, and it only serves the history.

7. Widgets (5.1): What Disappears Without Warning

The capability is excellent, and the SDK validator already catches a lot of things. Some gaps remain:

  • The budget for 8 components is applied before resolving references (getWidgetContent.js:62-69).
    • A tile linked to a device not yet added takes up space and is then removed. It may have displaced a valid component.
    • The truncated result remains in cache until the TTL (up to 1 hour), as adding the device does not invalidate the cache.
    • Therefore, you need to call requestWidgetRefresh on onDeviceCreated: to be documented, or better, to be invalidated on the core side.
  • Button with a taken action key: it is discarded with a warn on the server side, and the integration knows nothing about it. On Dreame, only one shortcut was displayed out of three (0.3.0 → 0.4.0). The spec says « unique », but does not say that the duplicate is removed.
  • status line without value: it is discarded without any log.
  • Action toast: it is truncated to 200 characters by slice, without an ellipsis.
    • A multilingual object without an en key gives no toast.
    • MAX_WIDGET_MESSAGE_LENGTH exists in the SDK, but nothing uses it.
  • card-list: the date replaces the subtitle instead of being added to it. The spec says « subtitle or date »; it should be specified that « the date takes precedence » (Jellyfin, Plex).
  • validateWidgetContent only runs in debug mode. I propose to run it on every onWidgetGet with a warn on the integration side, so that the developer sees what the core will remove.

8. Template and public documentation

  • .gitignore and .prettierignore: the rule data/, intended for the volume /data, also excludes src/data/. The folder is then missing from the image built by the CI (Astronomie, before 1.0.0). It needs to be anchored at /data/.

  • Decimal number fields: this is fixed on master (#3167, step="any"), but not yet published.

    • On 5.1.4, the latitude/longitude example in the template (48.8566) remains impossible to enter. Astronomie lost a version on this.
    • The manifest schema still refuses step: you cannot declare a resolution.
  • Public documentation:

    • it announces the SDK 0.12.0, while npm is at 0.14.0;
    • it describes the old release flow, without CHANGELOG or GitHub release;
    • it does not say that « View the changelog for this version » opens the GitHub release of the tag;
    • it does not yet mention the fake Gladys.
    • The template now does all this (#20, thanks): only the page is missing.
  • Publish SDK 0.15: the fake Gladys and the polling documentation are waiting on master. This fake Gladys could become the safety net if it also checks:

    • poll_frequency without should_poll;
    • the absence of min/max;
    • category/type pairs;
    • the quota of 300 states per minute;
    • the length of toasts.

    Today, { poll_frequency: 60000 } without should_poll, with a level-sensor/decimal feature without bounds, responds { success: true }.

Already fixed or already requested: I won’t ask again

  • primary button in dark mode (#3153 → #3162), secret and default in actions (#3154 → #3163, #3155 → #3164), vacuum cleaner lists and supported_options (#3156 → #3171), decimal number fields (#3167). Everything is on master, nothing is yet in a published version.
  • SDK #36 and template #19 by @prohand: unit of poll_frequency, fake Gladys, release that broke Prettier, GitHub release and changelog, 100-character limit, multilingual placeholder.
  • Limit of 200 devices per discovery, raised in August.

Thanks for reading or having Claude do it :wink: !

3 Likes