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
- Polling:
should_poll: trueis mandatory, but no spec nor the public documentation mentions it. The template publishespoll_frequency: 300(in seconds, withoutshould_poll), and its own Discovery page is therefore rejected with a 400. Cost for me: approximately 11 published versions, across 5 integrations. min/maxrequired on all features, includingtext. Discovery accepts them as absent, then « Add » fails with a 422. The spec even qualifies them as « optional ». 4 integrations affected.- 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.
- Store: A rejection by the indexer notifies no one. The reason is only in a
rejected.jsonwhose documentation does not provide the URL. 7 integrations affected, including ecojoko, which remained unfindable for its tester for 2 hours. - 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 === trueandpoll_frequencyis defined. should_polldefaults tofalse. Nothing is inferred frompoll_frequency, and the Discovery page posts the device as-is.- Result: A device published with only
poll_frequencyis 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, andcommand-routing.md:5describe polling « for a device with apoll_frequency». No file indocs/specs/external-integrations/mentionsshould_poll. - The public documentation (
/docs/dev/external-integrations/) has only one line aboutonPoll. - The SDK README on
masterhas 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:19containspoll_frequency: 300, // seconds, reused byweatherStation.js:45andplug.js:64, withoutshould_poll.- The core rejects this batch with
400 devices[0].poll_frequency: invalid poll frequency. The fake Gladys in the SDK onmasterreproduces 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_frequencyandshould_pollnever reapply to an already created device. They are not part of thestructure_changedsignature, 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 = trueas soon as a validpoll_frequencyis published. Failing that, reject with a 400 apoll_frequencywithoutshould_poll. - Core: Include
should_pollandpoll_frequencyin thestructure_changedsignature. - Template:
should_poll: true, a value ofDEVICE_POLL_FREQUENCIESin ms, and an example of an internal timer for slow rates. - Documentation: Mention
should_pollin the three specs and on the website, and frankly say « beyond 60 s, timer in the container ». - SDK: Log handler errors (
onScanRequest…) at theerrorlevel, notdebug.
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_polldefaults tofalseserver/lib/external-integration/externalIntegration.setDiscoveredDevices.js:55-56: rejects apoll_frequencyoutside the list, does not checkshould_pollfront/src/routes/integration/all/external-integration/discover-page/index.js:136-151: posts the published object as-isexternalIntegration.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_featuredeclaresmin,max,read_only, andhas_feedbackas 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:40talks 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/0by default on publication, like Zigbee2MQTT does, or it rejects with a 400 as soon asPOST /discovered_device. In both cases, no late failure. - Make
min/maxmandatory inindex.d.tsand 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, andDEVICE_FEATURE_UNITS_BY_CATEGORYis 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
sensorcategory, thenspeed-sensor/integerto be replaced bydatarate/rate, then missinghas_feedback. Three versions. - Android TV:
button/clicktreated as a sensor, so application buttons not clickable (1.0.5 → 1.1.0). Had to switch to atext/select. - IPP:
level-sensorwith a generic type has no icon. - Astronomy:
light-sensor/binaryhas no label, so « Device (undefined) » in Discovery. Replaced byinput/binary. - ecojoko: the power published in
energy-sensor/powerwithmin: 0made the needle of the gauge go off the scale in solar surplus. Neededgrid-sensor/power, signed, with symmetric limits. Neither the « consumption » meaning of one nor the « signed exchange » meaning of the other are written.
- UniFi: non-existent
- Minor bug:
front/src/utils/consts.jsdeclares the keyLIGHT_SENSORtwice inDeviceFeatureCategoriesIcon(l. 189 and 271). The second overwrites the first, solight-sensor/integerno 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_URLeven 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/passwordto be renamed tostring/secret, anddisplay_ifrejected (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.jsonin the documentation. - Notify the developer: a commit status, or an issue automatically opened on their repository.
- Run
npx github:GladysAssistant/integration-store --skip-image-checkin the template’s CI. The comment inci.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 /stateresponds200 { 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, andstepof 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_optionsandparamsare 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
languagefield to its configuration (IPP, Astronomy, Jellyfin, Dreame). - I propose either transmitting
languagein 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/getLatestVersiononly 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:23andcontracts/management-api.md:13nevertheless state that the catalog is « filtered bygladys_version». I propose testing compatibility inisUpdateAvailable.
Energy.
- Only a
energy-sensor/indexsummed triggers the 30-minute consumption and cost. Aenergy-production-sensor/indexderives nothing: thethirty-minutes-productionfeature is created by no one. host-api-endpoints.md:63nevertheless 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
requestWidgetRefreshononDeviceCreated: to be documented, or better, to be invalidated on the core side.
- Button with a taken action key: it is discarded with a
warnon 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. statusline withoutvalue: it is discarded without any log.- Action toast: it is truncated to 200 characters by
slice, without an ellipsis.- A multilingual object without an
enkey gives no toast. MAX_WIDGET_MESSAGE_LENGTHexists in the SDK, but nothing uses it.
- A multilingual object without an
card-list: thedatereplaces 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).validateWidgetContentonly runs in debug mode. I propose to run it on everyonWidgetGetwith awarnon the integration side, so that the developer sees what the core will remove.
8. Template and public documentation
-
.gitignoreand.prettierignore: the ruledata/, intended for the volume/data, also excludessrc/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
numberfields: this is fixed onmaster(#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.
- On 5.1.4, the latitude/longitude example in the template (
-
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.
- it announces the SDK
-
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_frequencywithoutshould_poll;- the absence of
min/max; - category/type pairs;
- the quota of 300 states per minute;
- the length of toasts.
Today,
{ poll_frequency: 60000 }withoutshould_poll, with alevel-sensor/decimalfeature without bounds, responds{ success: true }.
Already fixed or already requested: I won’t ask again
primarybutton in dark mode (#3153 → #3162),secretanddefaultin actions (#3154 → #3163, #3155 → #3164), vacuum cleaner lists andsupported_options(#3156 → #3171), decimalnumberfields (#3167). Everything is onmaster, 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, multilingualplaceholder. - Limit of 200 devices per discovery, raised in August.
Thanks for reading or having Claude do it
!