iobroker.xibo
v0.4.1
Published
Control Xibo digital signage displays: change the layout playing on a display group, revert to schedule, and read display status.
Readme
ioBroker.xibo
Tests:
Play a layout on a Xibo display group, and put it back to its schedule.
Anything in ioBroker that can write a state — a button, a script, a scene, a VIS widget — can put a layout on a display group, and hand it back to the schedule afterwards.
Xibo is an open-source digital signage platform, of which this drives the CMS.
What it does
- Inventory — 23 CMS collections mirrored on a timer, from display groups and layouts to datasets, sync groups and player versions
- Control — change layout, overlay layout, revert to schedule, collect now
- Status — how many displays in a group are online, and what they report playing
- Anything else — any of the CMS's 263 API operations, via
sendTo
Layouts are the unit of control. Media in the CMS library cannot be scheduled on its own; a layout is what a display can be told to show.
Calling the rest of the API
The state tree covers everyday control of what displays show. The CMS exposes far more — dataset editing, widget and region layout, user administration — and modelling all of it as states would be thousands of states for jobs better done in the Xibo UI. So anything not modelled is still one call away:
// Returns the response body.
const res = await sendToAsync("xibo.0", "api", {
method: "GET", path: "/layout", params: { retired: 0 },
});
if (res.ok) log(`${res.result.length} live layouts`);
// Arrays become the repeated key[] form the schedule endpoints require.
await sendToAsync("xibo.0", "api", {
method: "GET", path: "/schedule", params: { displayGroupIds: [3, 4] },
});commands.api does the same from a state — write
{"method":"POST","path":"/tag","params":{"name":"reception"}} — but a state
cannot hand a response back to whoever wrote it, so the body lands in
commands.lastResult. Prefer sendTo when you need the answer.
Paths are resolved and checked to stay under /api, and methods are limited to
GET, POST, PUT and DELETE. Beyond that the passthrough is as powerful as the
credentials it is given — a DELETE /layout/{id} really does delete a layout —
so scope the CMS application accordingly.
One thing the CMS gets wrong
GET /campaign returns an empty array on a CMS holding campaigns, because
every layout's own single-layout campaign is excluded by the default filter.
Ask for ?isLayoutSpecific=-1 — the adapter's own campaigns collection does.
Configuration
| Field | Notes |
|---|---|
| CMS URL | The CMS root, without /api |
| Client ID / Secret | Administration → Applications in the CMS. The application must allow the client_credentials grant. |
| Layout folder | Only offer layouts in this folder and below. Leave empty for the whole CMS. |
| Default duration | Seconds a layout stays before the display returns to its schedule. 0 means until reverted. |
The adapter authenticates as an application, not as a person, so CMS folder permissions do not constrain it — those govern people using the Xibo UI.
State tree (xibo.0.*)
info
info.connection— the CMS is reachable and the credentials work. Driven by the status poll, and needs two consecutive failures before it goes false, so a single timed-out request is not reported as a disconnection.info.lastError— the outstanding error, whichever poll produced itinfo.lastSync,info.cmsUrl
inventory
One inventory.<name>Json state and one count per mirrored collection, chosen
under Mirrored CMS collections. Seventeen are on by default:
displayGroups, displays, layouts, campaigns, playlists, datasets,
templates, tags, resolutions, displayProfiles, dayParts, folders,
cmsCommands, syncGroups, menuBoards, notifications, playerVersions
Six are available but off: library and modules are large, venues is a
fixed lookup list, and users, userGroups and sessions hold personal data —
mirroring those copies names, email addresses and who is signed in out of the
CMS's access control into states any script can read.
The first three keep the state ids they had in 0.2.0 (displayGroupCount, not
displayGroupsCount), so an upgrade neither loses them nor orphans them.
Unticking a collection deletes its states rather than leaving a stale value
behind, and unticking everything mirrors nothing — display groups and layouts
are still read, because the object tree needs them.
Per display group
xibo.0.displayGroups.<sanitised name>, with native.displayGroupId carrying
the CMS id. Display-specific groups — Xibo's internal one-per-display groups —
are omitted, because they are never what an operator picks.
| State | Notes |
|---|---|
| id, name | CMS identity |
| displayCount, displaysOnline | How many displays, and how many are logged in |
| currentLayout | What the first display in the group reports playing |
| playLayoutId | Writable. Write a layout id to play it on this group. |
| revert | Writable. Return this group to its schedule. |
playLayoutId is the state to bind a button or script to.
Commands
Callers write JSON to commands.<name> un-acked; the adapter executes it and
clears the state, so an identical follow-up request still triggers.
| Command | Payload |
|---|---|
| refresh | boolean button |
| changeLayout | {displayGroupId, layoutId, duration?} |
| overlayLayout | {displayGroupId, layoutId, duration?} — refused in schedule mode |
| revertToSchedule | {displayGroupId} |
| collectNow | {displayGroupId} |
| pushCriteria | {displayGroupId, metric, value, ttl?}, or {displayGroupId, updates:[…]} |
commands.lastResult records {ok, command, payload, error?, ts}.
Schedule Criteria
pushCriteria sends metric values to a display group, which is the other way
to change what a display shows. Instead of naming a layout, the CMS schedule holds
several standing entries each gated on a <criteria> condition, and the player
picks the one whose condition its current metric values satisfy.
The point is that nothing outside the CMS names a layout. A button pushes
screen1=3; which content that is lives in the schedule. So replacing a layout
with a new one cannot leave a button pointing at something that no longer
exists.
Three things to know:
- The CMS stores no value. It only forwards the push to the players over XMR, so there is no readback. What was last sent is a weaker claim than what a display is showing.
- A player that does not implement criteria fails open, not closed. It sees
every gated entry as unconditional, so four entries gated
eq 1..4all match and the display rotates through all four. Nothing errors. ttlis a floor, not a deadline. A push re-evaluates the schedule the moment it lands, but an expiry is only noticed at the player's next minute tick. To take content off at a definite time, push a new value rather than let one lapse. The default ttl is 12 hours, so a display holds what an operator chose until they choose again.
How a layout reaches the player
Setting: "How a layout reaches the player" — schedule (default) or action.
The CMS's own changeLayout action is delivered over XMR and applied instantly
— by a player that implements that message. Arexibo and gaxibo do not. The
action arrives, is logged as an unsupported XMR action and dropped, while the
CMS reports success. Nothing moves and nothing fails, which is the worst way for
this to break.
So schedule mode does not use the action. It:
- resolves the layout's campaign (the schedule names a campaign, never a layout);
- deletes the event it created last time on that display group;
- creates one "always" layout event at
schedulePriority, which outranks the group's ordinary schedule; - sends
collectNow— the one XMR action these players do implement, which is what makes the change land in seconds rather than at the next poll.
A duration becomes a custom day part bounded by toDt, which the player
enforces locally, so the layout comes down on time without hearing from the CMS
again.
revertToSchedule in this mode deletes that event rather than posting the
XMR revert action, because the event is the thing overriding the schedule.
Two constraints follow:
schedulePrioritymust be a priority nothing else uses. The CMS gives a schedule event no name or tag to stamp, so the priority is the only marker available for "the adapter owns this one and may replace it".overlayLayoutis refused inschedulemode, rather than attempted. These players render no overlay by either route, so posting the action would report success and show nothing.
Pick action only for the official Xibo player, where it is instant.
currentLayout lags
displayGroups.<group>.currentLayout is what the player last reported, and
gaxibo reports it from a field its GUI thread updates asynchronously — so the
collect that applies a new layout usually still reports the previous one.
Measured at anything from 4 seconds to 5 minutes behind. It is a status field,
not a confirmation that a command worked, and binding a button's active
indicator to it will light the wrong button.
Notes
A layout change replaces the schedule and stays until something else changes
it or the group is reverted — changeMode: replace with no duration, so what
was pressed stays on screen rather than expiring unexpectedly. Set a default
duration if you want the opposite.
downloadRequired is set, so the player fetches the layout before showing
it rather than flashing an empty screen while it downloads.
Folder scoping covers the subtree. The CMS folderId filter matches one
folder exactly, so scoping to a root folder alone finds nothing when layouts sit
in subfolders below it. The adapter walks the folder tree and filters against
it.
Changelog
0.4.1 (2026-10-01)
- Published through CI with npm provenance.
- The 0.4.0 release notes shown in admin are corrected in every language. The
machine translation had changed state and setting ids such as
commands.lastResultandschedulePriority, and cut some sentences short.
0.4.0 (2026-10-01)
Behaviour changes — read these before upgrading.
- Command payloads are checked strictly. Ids must be positive integers,
given as a number or a numeric string.
true,nulland""used to be turned into 1 or 0, so{"displayGroupId": true}targeted display group 1; they are now refused andcommands.lastResultrecordsok:false. Adurationoftrueor""is refused too, while a missing ornullone still means the configured default. A criteriattlmust be a positive number — a blank ttl used to expire the push at the next minute tick instead of using the 12-hour default. schedulePriorityis rounded to a whole number. A fractional priority meant the adapter could never find its own schedule events again, so they piled up instead of being replaced.defaultChangeDurationis capped at 86400 seconds in code as well as in admin, since a larger value sent the CMS an invalid date.
Fixes
- Display-group states created by an earlier version now receive updated definitions, and one deleted by hand is recreated on the next start.
- A command cut short by the instance stopping — saving the config restarts it
— no longer logs an error or writes
commands.lastResultfrom a stopping adapter. - Two display groups whose names fold to the same object id can no longer end up sharing a branch.
- The collection picker in admin is translated, and several admin strings that had been mistranslated are corrected.
- A new icon that reads as a screen at small sizes.
0.3.0 (2026-09-03)
- Schedule Criteria (CMS 4.1+).
commands.pushCriteriasends metric values to a display group —{displayGroupId, metric, value, ttl?}, or{displayGroupId, updates:[…]}to batch several into one request. Standing, criteria-gated schedule entries in the CMS then decide which content a value selects, so nothing outside the CMS names a layout and republishing cannot invalidate a button. Note that the CMS stores no value and the player reports none, so there is no readback; and attlis a floor, not a deadline — expiry is only noticed at the player's next minute tick.
Behaviour changes — read these before upgrading.
A
durationthat is not a number is now refused instead of ignored.Number("30s")isNaNandNumber("")is0, and both used to mean "no duration", so{"layoutId":41,"duration":"30s"}booked an indefinite play and still recordedok:true. It now throws,commands.lastResultrecordsok:false, and the error names the field. If a button or script sends a duration with units in it, fix the payload — that button was not doing what it appeared to do before.info.connectionmeans something slightly different. It now staysfalseuntil a status poll has actually succeeded, and needs two consecutive status-poll failures to gofalseagain. A single timed-out request no longer reports a disconnection, and a failing inventory refresh no longer does either — that goes toinfo.lastErrorand the log instead. Watchdogs gating on this state will see a slightly longer window after a restart, and far fewer spurious disconnects.Scheduled layouts survive a DST change. The CMS's UTC offset was read once and kept for the life of the instance, so an adapter running since summer booked every event an hour out after the October change — the display changing an hour late, or a timed layout never appearing at all, with
okreported both times. It is now re-read hourly.commands.lastResultnow describes a failure the same way as a success. A failing write recordedcommand: "commands.changeLayout"with the raw string it was sent, where a working one recordedcommand: "changeLayout"with the parsed payload — so a script matchingcommand === "changeLayout"to see whether its own press worked matched every success and no failure, and reported a failed press as still pending. Both paths now use the short name and a parsed payload.Object names and roles now reach upgraded instances, not just fresh installs. Adapter-owned metadata is merged with
extendObjectinstead of being written only when missing, so this release's renamedinventorychannel and count labels appear on existing instances too. Your own history and InfluxDB settings on those states are left alone, and a display-group channel you have renamed keeps your name.A display group renamed in the CMS keeps its branch. The branch id is folded from the group name, so a rename used to produce a second branch on the next restart while the old one stayed behind for ever, frozen at its last counts and looking live — and a button still writing the old
displayGroups.<old name>.playLayoutIdhit a state that existed and looked healthy while nothing happened. Branches are now matched on the CMS id, so the rename follows the name state and existing bindings keep working. A group deleted from the CMS has its states zeroed once and stops updating, and writing to it now fails visibly incommands.lastResultinstead of only logging a warning.A standing failure is logged once, not every poll. An application scoped without Layout access, or an estate that has never used menu boards, answers 403 for ever — which used to put the same line in the log 288 times a day and re-raise admin's "errors in the log" notice every five minutes. The first occurrence is logged, repeats go to debug, a different failure still reports, and recovery is logged so the log says when it stopped.
A display group you renamed in admin keeps your name. The previous fix compared the CMS's name against the channel's label, which made a rename in admin indistinguishable from a rename in Xibo — so the adapter reverted your name on the next restart and logged a CMS rename that had never happened. The CMS name is now recorded separately in
native.displayGroup, and only a genuine CMS rename moves the label.A duplicate branch left by 0.2.0 no longer wins. 0.2.0 created a second branch after a CMS rename, both carrying the same
displayGroupId. The first fix adopted whichever the database returned first — deterministically the older, dead one — leaving the branch your bindings had been moved to unindexed, where every press failed with "not in the CMS any more". The branch whose recorded CMS name matches now wins, and any leftover is zeroed and named in a warning so you can delete it.Unticking every collection now mirrors nothing. An empty list and an absent setting were treated the same, so clearing all 23 entries and saving left the adapter mirroring the 17 defaults anyway — 34 states recreated and 14 extra CMS requests every five minutes — while the config screen showed nothing selected. An empty list is now honoured; the adapter still drives display groups and layouts either way, since those are fetched for the object tree rather than for the mirror.
New
inventory.*now mirrors 23 CMS collections, not three: campaigns, playlists, datasets, templates, tags, resolutions, display profiles, day parts, folders, CMS commands, sync groups, menu boards, notifications and player versions are on by default, and the media library, widget modules, venues, users, user groups and sessions can be turned on. Pick them under "Mirrored CMS collections". Unticking one deletes its states rather than leaving a stale value behind.Any CMS operation can now be called, for the parts of the platform the state tree does not model:
const layouts = await sendToAsync("xibo.0", "api", { method: "GET", path: "/layout", params: { retired: 0 }, });commands.apidoes the same from a state, with the response incommands.lastResult. Paths are held to/apiand methods to GET/POST/PUT/DELETE.Release tooling:
npm run releasenow drives the version bump, sopackage.json,io-package.jsonand this changelog cannot drift apart.Integration test that starts the adapter under a real js-controller.
0.2.0
- Layout changes now reach Arexibo and gaxibo players. Those players do not
implement the CMS's
changeLayoutXMR action — it arrives, is logged as unsupported and dropped, while the CMS reports success — so a layout change did nothing and nothing failed. The new defaultschedulemode books a priority schedule event and sendscollectNowinstead, which every player honours. Verified through to a live display. layoutPlayModesetting:schedule(default, works on any player) oraction(instant, official Xibo player only).schedulePrioritysetting, which is also the marker for the events the adapter owns and may replace.revertToScheduledeletes the adapter's own schedule event inschedulemode, rather than posting an XMR action the player would ignore.overlayLayoutis refused inschedulemode rather than silently doing nothing, since those players render no overlay by either route.
0.1.0
- Initial release: display group and layout inventory, and change layout / overlay layout / revert to schedule / collect now commands.
Older changes have been moved to CHANGELOG_OLD.md
Requirements
- Node.js 22 or newer, js-controller 6.0.11 or newer and admin 7.6.20 or newer.
- A Xibo CMS (developed against 4.5) reachable from ioBroker.
- An Application in the CMS under Administration -> Applications, with the
client_credentialsgrant enabled. The adapter authenticates as that application, not as a person, so scope it to what you want it to reach — the API passthrough is as powerful as the credentials it is given. - At least one layout and one display group. Media in the library cannot be scheduled on its own.
Player compatibility
schedule mode, the default, works with any player because every player
honours its own schedule. Use it unless you have a reason not to.
action mode posts the CMS's changeLayout XMR action, which is instant but is
silently ignored by players that do not implement it — the CMS still reports
success. Arexibo and
gaxibo are in that category. Gaxibo is a
fork of Arexibo that plays video through GStreamer so the hardware decoder is
used, built for Rockchip RK3399 players. Only choose action with the official Xibo player.
License
MIT — see LICENSE.
Copyright (c) 2026 Alan Paris [email protected]
