I just updated my installation to 4.2.2 and the widget works as expected. Are there any supicious entries in your logfile?
nothing I am aware of, but the widget also doesn’t appear even in the widget editor:
btw I have the rules file in: /etc/openhab/automation/js/sonos_coordinator_rule.js
@L_B
I would have a look into openhab.log once more regarding messages on RINCON and sonos_coordinator_rule.js entries.
Even if in your case the proxyitems still show up which could mean, the script did not unload, it might show issues there. At least for me it pointed to problems.
Not sure if I can properly recall every detail or in correct order and not claiming you face anything similar, but:
I had this widget working on first try in OH3 back then (only favorites never worked) but after upgrading to OH4 it caused me quite a lot of pain to get it finally working again.
In my case it threw java exceptions claiming (some) items would already exist. Removing and re-adding the script did not fix it.
But at that time also RR4Dj persistance (was my default) was working with constantly growing delays, so it might have made it worse here or could have been the primary reason. I ended up cleaning the database from postponed proxyitems not to cause them to be recovered upon startup.
And after switching to influxdb as default persistance (to fix the massive delays) even the favorites started working for the first time.
Hi silchez,
I am total rookie using openHAB. I am pretty impressed of your “Sonos coordinator widget”.
After downloading the js-file and copying it to /etc/openhab/automation/js, unfortunately, the widget is not configured as it should. From 5 Sonos speakers only one is found and the favorite list is also empty. Is there anything I missed to do?
Thanks for your help in advance.
Florian
P.S.: I am using openHAB 3.4.2 on a RasPI 4.
Hello, i love this widget & zero conf rule but it stopped working in version 5.1. of openhab. as i could find out with AI this is due to some changes in how JS is handled. but i couldnt fix it. has someone the same experience and can help?
@sichelz can you help?
I used Gemini to fix the script. It seems to work for me, use it at your own risk ofcourse ![]()
your code goes here/**
* Sonos Coordinator Script - Fixed for openHAB 4.x
*/
const { rules, triggers, items, time, osgi } = require('openhab');
const osgiService = osgi || (typeof osgi !== 'undefined' ? osgi : null);
if (!osgiService) {
console.error("CRITICAL ERROR: Kan OSGi service niet laden. Zorg dat de 'JavaScript Scripting' add-on up-to-date is.");
}
const controllerGroupName = "SonosControllerGroup";
const proxyItemTagName = "SonosProxyItem";
const coordinatorProxyItemTagName = "SonosCoordinatorProxyItem";
const zoneVolumeProxyItemTagName = "ZoneVolume_SonosProxyItem";
const groupSwitcherItemTagName = "GroupSwitcher_SonosProxyItem";
const zoneMuteProxyItemTagName = "ZoneMute_SonosProxyItem";
const channelIds = {
playerChannelId: "control",
albumChannelId: "currentalbum",
artistChannelId: "currentartist",
coverArtChannelId: "currentalbumart",
coverArtChannelUrlId: "currentalbumarturl",
titleChannelId: "currenttitle",
masterChannelId: "coordinator",
localMasterChannelId: "localcoordinator",
volumeChannelId: "volume",
zoneNameChannelId: "zonename",
addChannelId: "add",
removeChannelId: "remove",
standaloneChannelId: "standalone",
trackChannelId: "currenttrack",
muteChannelId: "mute",
favoriteChannelId: "favorite"
};
let channelLinkRegistry, thingRegistry, managedLinkProvider;
try {
channelLinkRegistry = osgiService.getService("org.openhab.core.thing.link.ItemChannelLinkRegistry");
thingRegistry = osgiService.getService("org.openhab.core.thing.ThingRegistry");
managedLinkProvider = osgiService.getService("org.openhab.core.thing.link.ManagedItemChannelLinkProvider");
} catch (e) {
console.error("Fout bij ophalen OSGi services: " + e);
}
const ItemChannelLink = Java.type("org.openhab.core.thing.link.ItemChannelLink");
const constants = {
sonosIdentifierString: "RINCON_"
}
var createItemChannelLink = function (itemName, channel) {
if (!managedLinkProvider) return;
// console.log("Linking item " + itemName + " to channel " + channel.getUID());
var link = new ItemChannelLink(itemName, channel.getUID());
managedLinkProvider.add(link);
}
var getChannelUidFromItem = function (item) {
if (!channelLinkRegistry) return undefined;
let itemObj = (typeof item === 'string') ? items.getItem(item) : item;
if (!itemObj) return undefined;
let foundChannels = Array.from(channelLinkRegistry.getBoundChannels(itemObj.name));
return foundChannels.find(channel => channel.getThingUID().getId().includes(constants.sonosIdentifierString));
}
var getItemBoundToChannel = function (thingUidString, channelIdString) {
// Zoek in items met de proxy tag om performance te sparen
var foundItems = items.getItemsByTag(proxyItemTagName);
var foundItem = foundItems.find(item => {
let channelUid = getChannelUidFromItem(item);
if (channelUid === undefined) return false;
return channelUid.getThingUID().getId() === thingUidString && channelUid.getId() === channelIdString;
});
return foundItem || null;
}
var getAllSonosThings = function () {
if (!thingRegistry) return [];
let foundThings = Array.from(thingRegistry.getAll());
return foundThings.filter(thing => thing.getUID().getId().startsWith(constants.sonosIdentifierString));
}
var itemExists = function (itemName) {
try {
return items.getItem(itemName) !== null;
} catch (e) {
return false;
}
}
class SonosCoordinator {
constructor() {
if (itemExists(controllerGroupName)) {
this.group = items.getItem(controllerGroupName);
} else {
this.group = items.addItem({
name: controllerGroupName,
type: "Group",
tags: [proxyItemTagName]
});
}
this.allSonosThings = getAllSonosThings().map(thing => new SonosThing(thing));
var coordinatorTriggerArray = [];
var volumeTriggerArray = [];
var zoneVolumeTriggerArray = [];
var groupSwitchTriggerArray = [];
var muteTriggerArray = [];
var zoneMuteTriggerArray = [];
this.allSonosThings.forEach(sonosThing => {
if (sonosThing.allThingItemNames[channelIds.localMasterChannelId]) coordinatorTriggerArray.push(triggers.ItemStateChangeTrigger(sonosThing.allThingItemNames[channelIds.localMasterChannelId]));
if (sonosThing.allThingItemNames[channelIds.masterChannelId]) coordinatorTriggerArray.push(triggers.ItemStateChangeTrigger(sonosThing.allThingItemNames[channelIds.masterChannelId]));
if (sonosThing.allThingItemNames[channelIds.muteChannelId]) muteTriggerArray.push(triggers.ItemStateChangeTrigger(sonosThing.allThingItemNames[channelIds.muteChannelId]));
if (sonosThing.allThingItemNames[channelIds.volumeChannelId]) volumeTriggerArray.push(triggers.ItemStateChangeTrigger(sonosThing.allThingItemNames[channelIds.volumeChannelId]));
zoneVolumeTriggerArray.push(triggers.ItemCommandTrigger(sonosThing.zoneVolumeItemName));
groupSwitchTriggerArray.push(triggers.ItemCommandTrigger(sonosThing.groupSwitcherItemName));
zoneMuteTriggerArray.push(triggers.ItemCommandTrigger(sonosThing.zoneMuteItemName));
});
this.updatingItems = {};
if (!itemExists(coordinatorProxyItemTagName)) {
items.addItem({
name: coordinatorProxyItemTagName,
type: "String",
groups: [controllerGroupName],
tags: [proxyItemTagName, coordinatorProxyItemTagName]
});
}
setTimeout(() => this.updateCoordinatorProxyItems(), 5000);
rules.JSRule({
name: "Sonos: Coordinator Changed",
description: "Refreshes proxy items when coordinator changes",
triggers: coordinatorTriggerArray,
execute: data => {
this.updateCoordinatorProxyItems();
}
});
rules.JSRule({
name: "Sonos: Zone Volume Command",
description: "Adjusts group volume",
triggers: zoneVolumeTriggerArray,
execute: data => {
var changedThing = this.getSonosThingFromItemName(data.itemName);
if (changedThing) this.onZoneVolumeChanged(changedThing);
}
});
rules.JSRule({
name: "Sonos: Volume Item Command",
description: "Adjusts zone volume based on single volume change",
triggers: volumeTriggerArray,
execute: data => {
var changedThing = this.getSonosThingFromItemName(data.itemName);
if (changedThing) this.onVolumeChanged(changedThing);
}
});
rules.JSRule({
name: "Sonos: Group Switch Command",
description: "Rearranges sonos group",
triggers: groupSwitchTriggerArray,
execute: data => {
var changedThing = this.getSonosThingFromItemName(data.itemName);
if (changedThing) this.onGroupSwitched(changedThing);
}
});
rules.JSRule({
name: "Sonos: Mute Item Command",
description: "Syncs mute state",
triggers: muteTriggerArray,
execute: data => {
var changedThing = this.getSonosThingFromItemName(data.itemName);
if (changedThing) this.onMuteChanged(changedThing);
}
});
rules.JSRule({
name: "Sonos: Zone Mute Command",
description: "Syncs zone mute state",
triggers: zoneMuteTriggerArray,
execute: data => {
var changedThing = this.getSonosThingFromItemName(data.itemName);
if (changedThing) this.onGroupMuteChanged(changedThing);
}
});
}
onZoneVolumeChanged(sonosThing) {
if (this.wasOwnUpdate(sonosThing.zoneVolumeItemName)) return;
if (!sonosThing.isZoneCoordinator()) return;
var allGroupedThings = this.allSonosThings.filter(st => st.getMasterId() === sonosThing.thing.getUID().getId());
if (allGroupedThings.length === 0) return;
var groupedAvgVolume = 0.0;
allGroupedThings.forEach(gt => groupedAvgVolume += gt.getVolume());
groupedAvgVolume = groupedAvgVolume / allGroupedThings.length;
var delta = Math.round(sonosThing.getZoneVolume() - groupedAvgVolume);
if (delta === 0) return;
allGroupedThings.forEach(st => {
var newValue = st.getVolume() + delta;
if (newValue > 100.0) newValue = 100.0;
if (newValue < 0.0) newValue = 0.0;
this.setNewVolume(st.allThingItemNames[channelIds.volumeChannelId], newValue)
});
}
onVolumeChanged(sonosThing) {
if (this.wasOwnUpdate(sonosThing.allThingItemNames[channelIds.volumeChannelId])) return;
var masterId = sonosThing.getMasterId();
var masterThing = this.allSonosThings.find(st => st.thing.getUID().getId() === masterId);
if (!masterThing) return;
var newVolume = 0.0;
var allSonosThingsInGroup = this.allSonosThings.filter(st => st.getMasterId() === masterId);
if (allSonosThingsInGroup.length === 0) return;
allSonosThingsInGroup.forEach(st => newVolume += st.getVolume());
newVolume = Math.round(newVolume / allSonosThingsInGroup.length);
this.setNewVolume(masterThing.zoneVolumeItemName, newVolume);
}
onGroupSwitched(sonosThing) {
var groupConfigurationString = items.getItem(sonosThing.groupSwitcherItemName).state;
if (!sonosThing.isZoneCoordinator()) return;
try {
var groupConfiguration = JSON.parse(groupConfigurationString);
Object.keys(groupConfiguration).forEach(configKey => {
var groupThing = this.allSonosThings.find(st => st.thing.getUID().getId() === configKey);
if (!groupThing) return;
if (groupConfiguration[configKey]) {
// ADD to group
// console.log("Adding " + groupThing.thing.getLabel() + " to " + sonosThing.thing.getLabel());
items.getItem(sonosThing.allThingItemNames[channelIds.addChannelId]).sendCommand(groupThing.thing.getUID().getId());
} else {
// REMOVE from group
// console.log("Removing " + groupThing.thing.getLabel() + " from " + sonosThing.thing.getLabel());
items.getItem(groupThing.allThingItemNames[channelIds.standaloneChannelId]).sendCommand("ON");
}
});
} catch (e) {
console.error("Error parsing group config: " + e);
}
}
onMuteChanged(sonosThing) {
if (this.wasOwnUpdate(sonosThing.allThingItemNames[channelIds.muteChannelId])) return;
var masterThing = this.allSonosThings.find(st => st.thing.getUID().getId() === sonosThing.getMasterId());
if (!masterThing) return;
var muteValue = this.getZoneMuteState(masterThing);
this.setMute(masterThing.zoneMuteItemName, muteValue);
}
getZoneMuteState(sonosThing) {
var unmutedThingFound = false;
this.getAllGroupedSonosThings(sonosThing).forEach(gt => {
if (unmutedThingFound) return;
if (items.getItem(gt.allThingItemNames[channelIds.muteChannelId]).state === "OFF") {
unmutedThingFound = true;
}
});
return unmutedThingFound ? "OFF" : "ON";
}
onGroupMuteChanged(sonosThing) {
if (!sonosThing.isZoneCoordinator()) return;
if (this.wasOwnUpdate(sonosThing.zoneMuteItemName)) return;
var groupedThings = this.getAllGroupedSonosThings(sonosThing);
var muteState = items.getItem(sonosThing.zoneMuteItemName).state;
groupedThings.forEach(gt => {
var muteItemName = gt.allThingItemNames[channelIds.muteChannelId];
this.setMute(muteItemName, muteState);
});
}
setMute(itemName, muteValue) {
var item = items.getItem(itemName);
if (item.state === muteValue) return;
this.updatingItems[itemName] = muteValue;
item.sendCommand(muteValue);
}
getSonosThingFromItemName(itemName) {
return this.allSonosThings.find(sonosThing =>
Object.keys(sonosThing.allThingItemNames).some(key => sonosThing.allThingItemNames[key] === itemName) ||
sonosThing.groupSwitcherItemName === itemName ||
sonosThing.zoneVolumeItemName === itemName ||
sonosThing.zoneMuteItemName === itemName
);
}
wasOwnUpdate(itemName) {
if (this.updatingItems.hasOwnProperty(itemName)) {
delete this.updatingItems[itemName];
return true;
}
return false;
}
setNewVolume(itemName, newVolume) {
var item = items.getItem(itemName);
var currentVol = parseFloat(item.state);
if (currentVol === newVolume) return;
this.updatingItems[itemName] = newVolume;
item.sendCommand(newVolume.toString());
}
getAllGroupedSonosThings(sonosThing) {
if (!sonosThing.isZoneCoordinator()) {
return [sonosThing];
}
return this.allSonosThings.filter(st => st.getMasterId() === sonosThing.thing.getUID().getId() || st.thing.getUID().getId() === sonosThing.thing.getUID().getId());
}
updateCoordinatorProxyItems() {
var allCoordinators = this.allSonosThings.filter(sonosThing => sonosThing.isZoneCoordinator());
var coordinatorArray = [];
allCoordinators.forEach(sonosCoordinator => {
var groupedSonosThings = this.getAllGroupedSonosThings(sonosCoordinator);
var coordinatorProxyItem = {};
coordinatorProxyItem.id = sonosCoordinator.thing.getUID().getId();
// Items
coordinatorProxyItem.zoneVolumeItemName = sonosCoordinator.zoneVolumeItemName;
coordinatorProxyItem.zoneMuteItemName = sonosCoordinator.zoneMuteItemName;
coordinatorProxyItem.groupSwitcherItemName = sonosCoordinator.groupSwitcherItemName;
coordinatorProxyItem.artistItemName = sonosCoordinator.allThingItemNames[channelIds.artistChannelId];
coordinatorProxyItem.titelItemName = sonosCoordinator.allThingItemNames[channelIds.titleChannelId];
coordinatorProxyItem.albumItemName = sonosCoordinator.allThingItemNames[channelIds.albumChannelId];
coordinatorProxyItem.coverArtItemName = sonosCoordinator.allThingItemNames[channelIds.coverArtChannelId];
coordinatorProxyItem.coverArtUrlItemName = sonosCoordinator.allThingItemNames[channelIds.coverArtChannelUrlId];
coordinatorProxyItem.playerItemName = sonosCoordinator.allThingItemNames[channelIds.playerChannelId];
coordinatorProxyItem.trackItemName = sonosCoordinator.allThingItemNames[channelIds.trackChannelId];
coordinatorProxyItem.muteItemName = sonosCoordinator.allThingItemNames[channelIds.muteChannelId];
coordinatorProxyItem.favoriteItemName = sonosCoordinator.allThingItemNames[channelIds.favoriteChannelId];
var zoneItemNames = [];
var volumeItemsInformation = [];
var groupedItemsInformation = [];
var groupedThingDeleteVars = [];
var groupVolume = 0.0;
groupedThingDeleteVars.push(sonosCoordinator.thing.getUID().getId() + "_group");
groupedThingDeleteVars.push(sonosCoordinator.thing.getUID().getId() + "_volume");
groupedThingDeleteVars.push(sonosCoordinator.thing.getUID().getId() + "_favorite");
groupedSonosThings.forEach(gt => {
let zoneNameItem = items.getItem(gt.allThingItemNames[channelIds.zoneNameChannelId]);
if(zoneNameItem) zoneItemNames.push(zoneNameItem.state);
var gtVolumeInformation = {};
gtVolumeInformation.zoneItemName = gt.allThingItemNames[channelIds.zoneNameChannelId];
gtVolumeInformation.volumeItemName = gt.allThingItemNames[channelIds.volumeChannelId];
gtVolumeInformation.muteItemName = gt.allThingItemNames[channelIds.muteChannelId];
volumeItemsInformation.push(gtVolumeInformation);
groupVolume += gt.getVolume();
});
if (groupedSonosThings.length > 0)
groupVolume = Math.round(groupVolume / groupedSonosThings.length);
items.getItem(sonosCoordinator.zoneVolumeItemName).postUpdate(groupVolume.toString());
items.getItem(sonosCoordinator.zoneMuteItemName).postUpdate(this.getZoneMuteState(sonosCoordinator));
var allOtherSonosItems = this.allSonosThings.filter(ast => {
return ast.thing.getUID().getId() !== sonosCoordinator.thing.getUID().getId();
});
var idCounter = 0;
allOtherSonosItems.forEach(aosi => {
var groupedItemInformation = {};
groupedItemInformation.isInGroup = groupedSonosThings.some(gst => aosi.thing.getUID().getId() === gst.thing.getUID().getId());
let otherZoneItem = items.getItem(aosi.allThingItemNames[channelIds.zoneNameChannelId]);
groupedItemInformation.name = otherZoneItem ? otherZoneItem.state : "Unknown";
groupedItemInformation.thingUid = aosi.thing.getUID().getId();
groupedItemInformation.id = "itemInfo_" + idCounter;
groupedItemsInformation.push(groupedItemInformation);
groupedThingDeleteVars.push(aosi.thing.getUID().getId());
idCounter++;
});
coordinatorProxyItem.zoneNames = zoneItemNames.join(" + ");
coordinatorProxyItem.volumeInformation = volumeItemsInformation;
coordinatorProxyItem.groupedItemsInformation = groupedItemsInformation;
coordinatorProxyItem.groupedThingDeleteVars = groupedThingDeleteVars;
coordinatorArray.push(coordinatorProxyItem);
});
items.getItem(coordinatorProxyItemTagName).sendCommand(JSON.stringify(coordinatorArray));
}
}
class SonosThing {
constructor(thing) {
this.thing = thing;
this.allThingItemNames = {};
Object.keys(channelIds).forEach(key => {
var channelId = channelIds[key];
var channel = thing.getChannel(channelId);
if(!channel) return;
var cleanThingId = thing.getUID().getId().replace(/:/g, "_").replace(/-/g, "_");
var proxyItemName = cleanThingId + "_" + channelId + "_" + proxyItemTagName;
if (itemExists(proxyItemName)) {
this.allThingItemNames[channelId] = proxyItemName;
} else {
var itemType = channel.getAcceptedItemType() || "String";
items.addItem({
name: proxyItemName,
type: itemType,
groups: [controllerGroupName],
tags: [proxyItemTagName]
});
createItemChannelLink(proxyItemName, channel);
this.allThingItemNames[channelId] = proxyItemName;
}
});
var cleanThingId = thing.getUID().getId().replace(/:/g, "_").replace(/-/g, "_");
this.zoneVolumeItemName = cleanThingId + "_" + zoneVolumeProxyItemTagName;
if (!itemExists(this.zoneVolumeItemName)) {
items.addItem({ name: this.zoneVolumeItemName, type: "Dimmer", tags: [proxyItemTagName, zoneVolumeProxyItemTagName] });
}
this.groupSwitcherItemName = cleanThingId + "_" + groupSwitcherItemTagName;
if (!itemExists(this.groupSwitcherItemName)) {
items.addItem({ name: this.groupSwitcherItemName, type: "String", tags: [proxyItemTagName, groupSwitcherItemTagName] });
}
this.zoneMuteItemName = cleanThingId + "_" + zoneMuteProxyItemTagName;
if (!itemExists(this.zoneMuteItemName)) {
items.addItem({ name: this.zoneMuteItemName, type: "Switch", tags: [proxyItemTagName, zoneMuteProxyItemTagName] });
}
}
isZoneCoordinator() {
var thingName = this.allThingItemNames[channelIds.localMasterChannelId];
if(!thingName) return false;
var localMasterItem = items.getItem(thingName);
return localMasterItem.state === "ON";
}
getMasterId() {
var item = items.getItem(this.allThingItemNames[channelIds.masterChannelId]);
if(!item) return this.thing.getUID().getId();
var masterString = item.state;
if (!masterString || masterString === "NULL" || masterString === "UNDEF") {
return this.thing.getUID().getId();
}
if (!masterString.includes(constants.sonosIdentifierString)) {
return this.thing.getUID().getId();
}
return masterString;
}
getVolume() {
var item = items.getItem(this.allThingItemNames[channelIds.volumeChannelId]);
if(!item) return 0.0;
var volume = parseFloat(item.state);
return isNaN(volume) ? 0.0 : volume;
}
getZoneVolume() {
var zoneVolume = parseFloat(items.getItem(this.zoneVolumeItemName).state);
return isNaN(zoneVolume) ? 0.0 : zoneVolume;
}
}
console.log("Sonos Modern Coordinator Rule: Starting...");
new SonosCoordinator();
hi @stevendp thanks for that but for me it’s still not working. do you have openhab 5.1 running? in your top comment in the code it says it is fixed for openhab 4.x?
i tried some times with gemini and didnt get it running…
Yes, I’m using 5.1 as well. Check your openhab log what error it’s giving.
Hey guys, sorry for the late reply. I’ll have a look into the script next week. For now, are there any log files with errors of the script which you could provide me?
hi @sichelz i will try to look into the logs tomorrow and post it. acutally i am not a dev and not sure in which log part i should look as it seems the js is totally not loaded. so i dont have problems in the interface as in the interface nothing is loaded. the proxy items are in general not generated. Gemini says in 5.1. there where changes in the core with which it is not possible any more to autocreate items or so. so the error in the log will be something like xyz deprecated or so … but i will see what ich can find.
and thanks that you can look into it. your widget and JS is in daily great use at home!
dont know how i could make it without …
@sichelz on startup i get the following error in the log when initializing the js:
2026-01-14 20:47:47.254 [ERROR] [pting.file.sonos_coordinator_rule.js] - Failed to execute script: Cannot add element, because an element with same UID (RINCON_542A1B5C672A01400_Entfernen_SonosProxyItem -> sonos:SYMFONISK:RINCON_542A1B5C672A01400:remove) already exists.
at org.openhab.core.common.registry.AbstractManagedProvider.add(AbstractManagedProvider.java:61)
at com.oracle.truffle.host.HostMethodDesc$SingleMethod$MHBase.invokeHandle(HostMethodDesc.java:372)
at com.oracle.truffle.host.GuestToHostCodeCache$GuestToHostInvokeHandle.executeImpl(GuestToHostCodeCache.java:88)
at com.oracle.truffle.host.GuestToHostRootNode.execute(GuestToHostRootNode.java:80)
at com.oracle.truffle.api.impl.DefaultCallTarget.call(DefaultCallTarget.java:118)
... 203 more
2026-01-14 20:47:47.254 [ERROR] [ipt.internal.ScriptEngineManagerImpl] - Error during evaluation of script '/etc/openhab/automation/js/sonos_coordinator_rule.js': java.lang.IllegalArgumentException: Cannot add element, because an element with same UID (RINCON_542A1B5C672A01400_Entfernen_SonosProxyItem -> sonos:SYMFONISK:RINCON_542A1B5C672A01400:remove) already exists.
2026-01-14 20:47:47.254 [WARN ] [ort.loader.AbstractScriptFileWatcher] - Script loading error, ignoring file '/etc/openhab/automation/js/sonos_coordinator_rule.js'
hope that helps, the problem is only after update to 5.1. now i am running 5.1.1. i have also manually configured some sonos items but as i understood your script that is not a problem and was also not a problem before the update. let me know if i should test something else or send other logs.
It could be, that the problem is indeed, that you have manually configured a sonos item. Could you try to remove the items temporarly and see if the situation improves?
Nervertheless, normally the script should detect whether an item is already bound to a channel. I have to take a look if this is somehow not working anymore.
@sichelz unfortunately i can not do that as i have several other rules and implementations that need their sonos items. so i can not solely rely on the autogenerated items by the js.
as far as i could find out there where changes in the javascript scripting automation in openhab in version 5.1. that included some core chanages that prohibit the autogeneration of items. but it was an “AI chat” so definitely possible there is some halucination.
i try to create some other widget that works without your js but it will never be so comfortable as you built it but needs all those items added to my widget so still hope you can fix it or reproduce the problem. another one wrote here he had problems too so i guess its broken with 5.1. in general.
After upgrading to 5.1.1 I also had some issues. Hereby the updated js code, for me this works:
/**
* Sonos Coordinator Script - Final Fix (Clean Names)
*/
const { rules, triggers, items, time, osgi } = require('openhab');
// Veiligheidscheck
const osgiService = osgi || (typeof osgi !== 'undefined' ? osgi : null);
if (!osgiService) {
console.error("CRITICAL ERROR: Kan OSGi service niet laden.");
}
const controllerGroupName = "SonosControllerGroup";
const proxyItemTagName = "SonosProxyItem";
const coordinatorProxyItemTagName = "SonosCoordinatorProxyItem";
const zoneVolumeProxyItemTagName = "ZoneVolume_SonosProxyItem";
const groupSwitcherItemTagName = "GroupSwitcher_SonosProxyItem";
const zoneMuteProxyItemTagName = "ZoneMute_SonosProxyItem";
const channelIds = {
playerChannelId: "control",
albumChannelId: "currentalbum",
artistChannelId: "currentartist",
coverArtChannelId: "currentalbumart",
coverArtChannelUrlId: "currentalbumarturl",
titleChannelId: "currenttitle",
masterChannelId: "coordinator",
localMasterChannelId: "localcoordinator",
volumeChannelId: "volume",
zoneNameChannelId: "zonename",
addChannelId: "add",
removeChannelId: "remove",
standaloneChannelId: "standalone",
trackChannelId: "currenttrack",
muteChannelId: "mute",
favoriteChannelId: "favorite"
};
// Services ophalen
let channelLinkRegistry, thingRegistry, managedLinkProvider;
try {
channelLinkRegistry = osgiService.getService("org.openhab.core.thing.link.ItemChannelLinkRegistry");
thingRegistry = osgiService.getService("org.openhab.core.thing.ThingRegistry");
managedLinkProvider = osgiService.getService("org.openhab.core.thing.link.ManagedItemChannelLinkProvider");
} catch (e) {
console.error("Fout bij ophalen OSGi services: " + e);
}
const ItemChannelLink = Java.type("org.openhab.core.thing.link.ItemChannelLink");
const constants = {
sonosIdentifierString: "RINCON_"
}
// Veilige Link Creatie
var createItemChannelLink = function (itemName, channel) {
if (!managedLinkProvider) return;
var link = new ItemChannelLink(itemName, channel.getUID());
try {
managedLinkProvider.add(link);
} catch (e) {
// Link bestaat al, negeren.
}
}
var getChannelUidFromItem = function (item) {
if (!channelLinkRegistry) return undefined;
let itemObj = (typeof item === 'string') ? items.getItem(item) : item;
if (!itemObj) return undefined;
let foundChannels = Array.from(channelLinkRegistry.getBoundChannels(itemObj.name));
return foundChannels.find(channel => channel.getThingUID().getId().includes(constants.sonosIdentifierString));
}
var getAllSonosThings = function () {
if (!thingRegistry) return [];
let foundThings = Array.from(thingRegistry.getAll());
return foundThings.filter(thing => thing.getUID().getId().startsWith(constants.sonosIdentifierString));
}
var itemExists = function (itemName) {
try {
return items.getItem(itemName) !== null;
} catch (e) {
return false;
}
}
class SonosCoordinator {
constructor() {
this.group = items.replaceItem({
name: controllerGroupName,
type: "Group",
tags: [proxyItemTagName]
});
this.allSonosThings = getAllSonosThings().map(thing => new SonosThing(thing));
var coordinatorTriggerArray = [];
var volumeTriggerArray = [];
var zoneVolumeTriggerArray = [];
var groupSwitchTriggerArray = [];
var muteTriggerArray = [];
var zoneMuteTriggerArray = [];
var zoneNameTriggerArray = [];
this.allSonosThings.forEach(sonosThing => {
if (sonosThing.allThingItemNames[channelIds.localMasterChannelId]) coordinatorTriggerArray.push(triggers.ItemStateChangeTrigger(sonosThing.allThingItemNames[channelIds.localMasterChannelId]));
if (sonosThing.allThingItemNames[channelIds.masterChannelId]) coordinatorTriggerArray.push(triggers.ItemStateChangeTrigger(sonosThing.allThingItemNames[channelIds.masterChannelId]));
if (sonosThing.allThingItemNames[channelIds.muteChannelId]) muteTriggerArray.push(triggers.ItemStateChangeTrigger(sonosThing.allThingItemNames[channelIds.muteChannelId]));
if (sonosThing.allThingItemNames[channelIds.volumeChannelId]) volumeTriggerArray.push(triggers.ItemStateChangeTrigger(sonosThing.allThingItemNames[channelIds.volumeChannelId]));
if (sonosThing.allThingItemNames[channelIds.zoneNameChannelId]) {
zoneNameTriggerArray.push(triggers.ItemStateChangeTrigger(sonosThing.allThingItemNames[channelIds.zoneNameChannelId]));
}
zoneVolumeTriggerArray.push(triggers.ItemCommandTrigger(sonosThing.zoneVolumeItemName));
groupSwitchTriggerArray.push(triggers.ItemCommandTrigger(sonosThing.groupSwitcherItemName));
zoneMuteTriggerArray.push(triggers.ItemCommandTrigger(sonosThing.zoneMuteItemName));
});
this.updatingItems = {};
items.replaceItem({
name: coordinatorProxyItemTagName,
type: "String",
groups: [controllerGroupName],
tags: [proxyItemTagName, coordinatorProxyItemTagName]
});
setTimeout(() => this.updateCoordinatorProxyItems(), 5000);
rules.JSRule({
name: "Sonos: Coordinator Changed",
description: "Refreshes proxy items when coordinator changes",
triggers: coordinatorTriggerArray,
execute: data => { this.updateCoordinatorProxyItems(); }
});
rules.JSRule({
name: "Sonos: Zone Name Changed",
description: "Updates proxies when a zone name updates",
triggers: zoneNameTriggerArray,
execute: data => { this.updateCoordinatorProxyItems(); }
});
rules.JSRule({
name: "Sonos: Zone Volume Command",
description: "Adjusts group volume",
triggers: zoneVolumeTriggerArray,
execute: data => {
var changedThing = this.getSonosThingFromItemName(data.itemName);
if (changedThing) this.onZoneVolumeChanged(changedThing);
}
});
rules.JSRule({
name: "Sonos: Volume Item Command",
description: "Adjusts zone volume based on single volume change",
triggers: volumeTriggerArray,
execute: data => {
var changedThing = this.getSonosThingFromItemName(data.itemName);
if (changedThing) this.onVolumeChanged(changedThing);
}
});
rules.JSRule({
name: "Sonos: Group Switch Command",
description: "Rearranges sonos group",
triggers: groupSwitchTriggerArray,
execute: data => {
var changedThing = this.getSonosThingFromItemName(data.itemName);
if (changedThing) this.onGroupSwitched(changedThing, data.receivedCommand.toString());
}
});
rules.JSRule({
name: "Sonos: Mute Item Command",
description: "Syncs mute state",
triggers: muteTriggerArray,
execute: data => {
var changedThing = this.getSonosThingFromItemName(data.itemName);
if (changedThing) this.onMuteChanged(changedThing);
}
});
rules.JSRule({
name: "Sonos: Zone Mute Command",
description: "Syncs zone mute state",
triggers: zoneMuteTriggerArray,
execute: data => {
var changedThing = this.getSonosThingFromItemName(data.itemName);
if (changedThing) this.onGroupMuteChanged(changedThing);
}
});
}
onZoneVolumeChanged(sonosThing) {
if (this.wasOwnUpdate(sonosThing.zoneVolumeItemName)) return;
if (!sonosThing.isZoneCoordinator()) return;
var allGroupedThings = this.allSonosThings.filter(st => st.getMasterId() === sonosThing.thing.getUID().getId());
if (allGroupedThings.length === 0) return;
var groupedAvgVolume = 0.0;
allGroupedThings.forEach(gt => groupedAvgVolume += gt.getVolume());
groupedAvgVolume = groupedAvgVolume / allGroupedThings.length;
var delta = Math.round(sonosThing.getZoneVolume() - groupedAvgVolume);
if (delta === 0) return;
allGroupedThings.forEach(st => {
var newValue = st.getVolume() + delta;
if (newValue > 100.0) newValue = 100.0;
if (newValue < 0.0) newValue = 0.0;
this.setNewVolume(st.allThingItemNames[channelIds.volumeChannelId], newValue)
});
}
onVolumeChanged(sonosThing) {
if (this.wasOwnUpdate(sonosThing.allThingItemNames[channelIds.volumeChannelId])) return;
var masterId = sonosThing.getMasterId();
var masterThing = this.allSonosThings.find(st => st.thing.getUID().getId() === masterId);
if (!masterThing) return;
var newVolume = 0.0;
var allSonosThingsInGroup = this.allSonosThings.filter(st => st.getMasterId() === masterId);
if (allSonosThingsInGroup.length === 0) return;
allSonosThingsInGroup.forEach(st => newVolume += st.getVolume());
newVolume = Math.round(newVolume / allSonosThingsInGroup.length);
this.setNewVolume(masterThing.zoneVolumeItemName, newVolume);
}
onGroupSwitched(sonosThing, groupConfigurationString) {
if (!sonosThing.isZoneCoordinator()) return;
try {
var groupConfiguration = JSON.parse(groupConfigurationString);
Object.keys(groupConfiguration).forEach(configKey => {
var groupThing = this.allSonosThings.find(st => st.thing.getUID().getId() === configKey);
if (!groupThing) return;
if (groupConfiguration[configKey]) {
items.getItem(sonosThing.allThingItemNames[channelIds.addChannelId]).sendCommand(groupThing.thing.getUID().getId());
} else {
items.getItem(groupThing.allThingItemNames[channelIds.standaloneChannelId]).sendCommand("ON");
}
});
} catch (e) {
console.error("Error parsing group config: " + e);
}
}
onMuteChanged(sonosThing) {
if (this.wasOwnUpdate(sonosThing.allThingItemNames[channelIds.muteChannelId])) return;
var masterThing = this.allSonosThings.find(st => st.thing.getUID().getId() === sonosThing.getMasterId());
if (!masterThing) return;
var muteValue = this.getZoneMuteState(masterThing);
this.setMute(masterThing.zoneMuteItemName, muteValue);
}
getZoneMuteState(sonosThing) {
var unmutedThingFound = false;
this.getAllGroupedSonosThings(sonosThing).forEach(gt => {
if (unmutedThingFound) return;
if (items.getItem(gt.allThingItemNames[channelIds.muteChannelId]).state === "OFF") {
unmutedThingFound = true;
}
});
return unmutedThingFound ? "OFF" : "ON";
}
onGroupMuteChanged(sonosThing) {
if (!sonosThing.isZoneCoordinator()) return;
if (this.wasOwnUpdate(sonosThing.zoneMuteItemName)) return;
var groupedThings = this.getAllGroupedSonosThings(sonosThing);
var muteState = items.getItem(sonosThing.zoneMuteItemName).state;
groupedThings.forEach(gt => {
var muteItemName = gt.allThingItemNames[channelIds.muteChannelId];
this.setMute(muteItemName, muteState);
});
}
setMute(itemName, muteValue) {
var item = items.getItem(itemName);
if (item.state === muteValue) return;
this.updatingItems[itemName] = muteValue;
item.sendCommand(muteValue);
}
getSonosThingFromItemName(itemName) {
return this.allSonosThings.find(sonosThing =>
Object.keys(sonosThing.allThingItemNames).some(key => sonosThing.allThingItemNames[key] === itemName) ||
sonosThing.groupSwitcherItemName === itemName ||
sonosThing.zoneVolumeItemName === itemName ||
sonosThing.zoneMuteItemName === itemName
);
}
wasOwnUpdate(itemName) {
if (this.updatingItems.hasOwnProperty(itemName)) {
delete this.updatingItems[itemName];
return true;
}
return false;
}
setNewVolume(itemName, newVolume) {
var item = items.getItem(itemName);
var currentVol = parseFloat(item.state);
if (currentVol === newVolume) return;
this.updatingItems[itemName] = newVolume;
item.sendCommand(newVolume.toString());
}
getAllGroupedSonosThings(sonosThing) {
if (!sonosThing.isZoneCoordinator()) {
return [sonosThing];
}
return this.allSonosThings.filter(st => st.getMasterId() === sonosThing.thing.getUID().getId() || st.thing.getUID().getId() === sonosThing.thing.getUID().getId());
}
updateCoordinatorProxyItems() {
var allCoordinators = this.allSonosThings.filter(sonosThing => sonosThing.isZoneCoordinator());
var coordinatorArray = [];
allCoordinators.forEach(sonosCoordinator => {
var groupedSonosThings = this.getAllGroupedSonosThings(sonosCoordinator);
var coordinatorProxyItem = {};
coordinatorProxyItem.id = sonosCoordinator.thing.getUID().getId();
// Items
coordinatorProxyItem.zoneVolumeItemName = sonosCoordinator.zoneVolumeItemName;
coordinatorProxyItem.zoneMuteItemName = sonosCoordinator.zoneMuteItemName;
coordinatorProxyItem.groupSwitcherItemName = sonosCoordinator.groupSwitcherItemName;
coordinatorProxyItem.artistItemName = sonosCoordinator.allThingItemNames[channelIds.artistChannelId];
coordinatorProxyItem.titelItemName = sonosCoordinator.allThingItemNames[channelIds.titleChannelId];
coordinatorProxyItem.albumItemName = sonosCoordinator.allThingItemNames[channelIds.albumChannelId];
coordinatorProxyItem.coverArtItemName = sonosCoordinator.allThingItemNames[channelIds.coverArtChannelId];
coordinatorProxyItem.coverArtUrlItemName = sonosCoordinator.allThingItemNames[channelIds.coverArtChannelUrlId];
coordinatorProxyItem.playerItemName = sonosCoordinator.allThingItemNames[channelIds.playerChannelId];
coordinatorProxyItem.trackItemName = sonosCoordinator.allThingItemNames[channelIds.trackChannelId];
coordinatorProxyItem.muteItemName = sonosCoordinator.allThingItemNames[channelIds.muteChannelId];
coordinatorProxyItem.favoriteItemName = sonosCoordinator.allThingItemNames[channelIds.favoriteChannelId];
var zoneItemNames = [];
var volumeItemsInformation = [];
var groupedItemsInformation = [];
var groupedThingDeleteVars = [];
var groupVolume = 0.0;
groupedThingDeleteVars.push(sonosCoordinator.thing.getUID().getId() + "_group");
groupedThingDeleteVars.push(sonosCoordinator.thing.getUID().getId() + "_volume");
groupedThingDeleteVars.push(sonosCoordinator.thing.getUID().getId() + "_favorite");
groupedSonosThings.forEach(gt => {
let zoneNameItem = items.getItem(gt.allThingItemNames[channelIds.zoneNameChannelId]);
let zoneName = "Unknown";
if (zoneNameItem) {
if (zoneNameItem.state !== "NULL" && zoneNameItem.state !== "UNDEF") {
zoneName = zoneNameItem.state;
} else {
zoneName = gt.thing.getLabel();
}
}
var match = zoneName.match(/.*\((.*)\)$/);
if (match && match[1]) {
zoneName = match[1];
}
if (zoneNameItem && zoneNameItem.state !== zoneName) {
zoneNameItem.postUpdate(zoneName);
}
zoneItemNames.push(zoneName);
var gtVolumeInformation = {};
gtVolumeInformation.zoneItemName = gt.allThingItemNames[channelIds.zoneNameChannelId];
gtVolumeInformation.zoneName = zoneName;
gtVolumeInformation.volumeItemName = gt.allThingItemNames[channelIds.volumeChannelId];
gtVolumeInformation.muteItemName = gt.allThingItemNames[channelIds.muteChannelId];
volumeItemsInformation.push(gtVolumeInformation);
groupVolume += gt.getVolume();
});
if (groupedSonosThings.length > 0)
groupVolume = Math.round(groupVolume / groupedSonosThings.length);
items.getItem(sonosCoordinator.zoneVolumeItemName).postUpdate(groupVolume.toString());
items.getItem(sonosCoordinator.zoneMuteItemName).postUpdate(this.getZoneMuteState(sonosCoordinator));
var allOtherSonosItems = this.allSonosThings.filter(ast => {
return ast.thing.getUID().getId() !== sonosCoordinator.thing.getUID().getId();
});
var idCounter = 0;
allOtherSonosItems.forEach(aosi => {
var groupedItemInformation = {};
groupedItemInformation.isInGroup = groupedSonosThings.some(gst => aosi.thing.getUID().getId() === gst.thing.getUID().getId());
// Dezelfde logica voor groepen lijst
let otherZoneItem = items.getItem(aosi.allThingItemNames[channelIds.zoneNameChannelId]);
let otherName = "Unknown";
if (otherZoneItem) {
if (otherZoneItem.state !== "NULL" && otherZoneItem.state !== "UNDEF") {
otherName = otherZoneItem.state;
} else {
otherName = aosi.thing.getLabel();
}
}
// CLEANUP ook hier
var match = otherName.match(/.*\((.*)\)$/);
if (match && match[1]) {
otherName = match[1];
}
if (otherZoneItem && otherZoneItem.state !== otherName) {
otherZoneItem.postUpdate(otherName);
}
groupedItemInformation.name = otherName;
groupedItemInformation.thingUid = aosi.thing.getUID().getId();
groupedItemInformation.id = "itemInfo_" + idCounter;
groupedItemsInformation.push(groupedItemInformation);
groupedThingDeleteVars.push(aosi.thing.getUID().getId());
idCounter++;
});
coordinatorProxyItem.zoneNames = zoneItemNames.join(" + ");
coordinatorProxyItem.volumeInformation = volumeItemsInformation;
coordinatorProxyItem.groupedItemsInformation = groupedItemsInformation;
coordinatorProxyItem.groupedThingDeleteVars = groupedThingDeleteVars;
coordinatorArray.push(coordinatorProxyItem);
});
items.getItem(coordinatorProxyItemTagName).sendCommand(JSON.stringify(coordinatorArray));
}
}
class SonosThing {
constructor(thing) {
this.thing = thing;
this.allThingItemNames = {};
Object.keys(channelIds).forEach(key => {
var channelId = channelIds[key];
var channel = thing.getChannel(channelId);
if(!channel) return;
var cleanThingId = thing.getUID().getId().replace(/:/g, "_").replace(/-/g, "_");
var proxyItemName = cleanThingId + "_" + channelId + "_" + proxyItemTagName;
var itemType = channel.getAcceptedItemType() || "String";
items.replaceItem({
name: proxyItemName,
type: itemType,
groups: [controllerGroupName],
tags: [proxyItemTagName]
});
createItemChannelLink(proxyItemName, channel);
this.allThingItemNames[channelId] = proxyItemName;
});
var cleanThingId = thing.getUID().getId().replace(/:/g, "_").replace(/-/g, "_");
this.zoneVolumeItemName = cleanThingId + "_" + zoneVolumeProxyItemTagName;
items.replaceItem({ name: this.zoneVolumeItemName, type: "Dimmer", tags: [proxyItemTagName, zoneVolumeProxyItemTagName] });
this.groupSwitcherItemName = cleanThingId + "_" + groupSwitcherItemTagName;
items.replaceItem({ name: this.groupSwitcherItemName, type: "String", tags: [proxyItemTagName, groupSwitcherItemTagName] });
this.zoneMuteItemName = cleanThingId + "_" + zoneMuteProxyItemTagName;
items.replaceItem({ name: this.zoneMuteItemName, type: "Switch", tags: [proxyItemTagName, zoneMuteProxyItemTagName] });
}
isZoneCoordinator() {
var thingName = this.allThingItemNames[channelIds.localMasterChannelId];
if(!thingName) return false;
var localMasterItem = items.getItem(thingName);
return localMasterItem.state === "ON";
}
getMasterId() {
var item = items.getItem(this.allThingItemNames[channelIds.masterChannelId]);
if(!item) return this.thing.getUID().getId();
var masterString = item.state;
if (!masterString || masterString === "NULL" || masterString === "UNDEF") {
return this.thing.getUID().getId();
}
if (!masterString.includes(constants.sonosIdentifierString)) {
return this.thing.getUID().getId();
}
return masterString;
}
getVolume() {
var item = items.getItem(this.allThingItemNames[channelIds.volumeChannelId]);
if(!item) return 0.0;
var volume = parseFloat(item.state);
return isNaN(volume) ? 0.0 : volume;
}
getZoneVolume() {
var zoneVolume = parseFloat(items.getItem(this.zoneVolumeItemName).state);
return isNaN(zoneVolume) ? 0.0 : zoneVolume;
}
}
console.log("Sonos Modern Coordinator Rule: Starting...");
new SonosCoordinator();
@stevendp thanks - now the items are also generated for me again. but i still have some rendering issue. the players are shown below each other und are not in the swiper element as before.
but i noticed i have in general a problem with the slider element in openhab 5.1.1. it seems broken - having nothing todo with the sonos stuff. is the slider working for you? however this is then part of the widget code i guess and js is now working… but still before trying to fix the widget code i thing the oh-swiper should work again.
would be glad to know if this swiper is working for you.
i created this issue regarding the swiper as it also happens in the official openhab demo: oh-swiper not working in 5.1.1 · Issue #3782 · openhab/openhab-webui · GitHub
For me the swiper is working Mathias. No issues on that level.
@stevendp do you use openhab 5.1.1. ? the issue i opened on github is also happening in the official demo. so i guess there is some problem…
Yes, also using 5.1.1.
I adapted the widget as well to make it smaller. This is the widget code I use:
uid: sonos_coordinator_widget_v1_2_ultra_compact-v2
tags:
- marketplace:132732
props:
parameters:
- description: Text for the title caption
label: Title caption
name: titleCaption
required: false
type: TEXT
- description: Text for the artist caption
label: Artist caption
name: artistCaption
required: false
type: TEXT
- description: Text for the album caption
label: Album caption
name: albumCaption
required: false
type: TEXT
- description: Text for the volume controls caption
label: Volume controls caption
name: volumeControlsCaption
required: false
type: TEXT
- description: Text for the group configuration caption
label: Group configuration caption
name: groupConfigurationCaption
required: false
type: TEXT
- description: Text for the favorite selection popup caption
label: Favorite popup selection caption
name: favoriteSelectionCaption
required: false
type: TEXT
parameterGroups: []
timestamp: Jan 9, 2026, 10:09:06 PM
component: f7-card
config:
style:
background: transparent
box-shadow: none
margin: 0
padding: 0
slots:
default:
- component: oh-swiper
config:
navigation: true
pagination: true
style:
width: 100%
slots:
slides:
- component: oh-repeater
config:
for: sonosInfo
fragment: true
in: =JSON.parse(items['SonosCoordinatorProxyItem'].state)
sourceType: array
slots:
default:
- component: f7-swiper-slide
slots:
default:
- component: f7-card
config:
style:
background: white
border-radius: 12px
display: flex
flex-direction: column
height: 120px
margin: 2px
overflow: hidden
padding: 0px
slots:
default:
- component: div
config:
style:
display: flex
flex-direction: row
height: 80px
width: 100%
slots:
default:
- component: div
config:
style:
flex-shrink: 0
height: 80px
position: relative
width: 80px
slots:
default:
- component: oh-image
config:
item: =loop.sonosInfo.coverArtItemName
style:
border-bottom-right-radius: 12px
height: 100%
object-fit: cover
width: 100%
visible: =items[loop.sonosInfo.coverArtItemName].state !== 'UNDEF' &&
items[loop.sonosInfo.coverArtItemName].state
!== 'NULL'
- component: div
config:
style:
display: flex
flex-direction: column
flex-grow: 1
justify-content: space-between
overflow: hidden
padding: 10px 15px 5px 5px
position: relative
slots:
default:
- component: Label
config:
style:
color: "#aaa"
font-size: 7px
font-weight: 700
letter-spacing: 0.5px
max-width: 95%
overflow: hidden
position: absolute
right: 5px
text-align: right
text-overflow: ellipsis
text-transform: uppercase
top: 3px
white-space: nowrap
z-index: 5
text: =loop.sonosInfo.zoneNames
- component: div
config:
style:
padding-top: 3px
slots:
default:
- component: Label
config:
style:
color: "#333"
font-size: 14px
font-weight: 700
overflow: hidden
padding-right: 20px
text-overflow: ellipsis
white-space: nowrap
text: "=items[loop.sonosInfo.artistItemName].state.length > 1 ?
items[loop.sonosInfo.titelI\
temName].state :
items[loop.sonosInfo.trackI\
temName].state"
- component: Label
config:
style:
color: "#888"
font-size: 13px
margin-top: 2px
overflow: hidden
text-overflow: ellipsis
white-space: nowrap
text: "=items[loop.sonosInfo.artistItemName].state.length > 1 ?
items[loop.sonosInfo.artist\
ItemName].state : ''"
- component: div
config:
style:
align-items: center
display: flex
gap: 20px
justify-content: flex-start
slots:
default:
- component: oh-link
config:
action: command
actionCommand: PREVIOUS
actionItem: =loop.sonosInfo.playerItemName
color: black
iconF7: backward_end_fill
iconSize: 16
- component: oh-link
config:
action: command
actionCommand: "=(items[loop.sonosInfo.playerItemName].state === 'PLAY') ?
'PAUSE' : 'PLAY'"
actionItem: =loop.sonosInfo.playerItemName
color: blue
iconF7: "=(items[loop.sonosInfo.playerItemName].state === 'PLAY') ?
'pause_circle_fill' :
'play_circle_fill'"
iconSize: 24
- component: oh-link
config:
action: command
actionCommand: NEXT
actionItem: =loop.sonosInfo.playerItemName
color: black
iconF7: forward_end_fill
iconSize: 16
- component: div
config:
style:
align-items: center
background: "#f9f9f9"
display: grid
grid-template-columns: 40px 1fr 120px
height: 40px
padding: 0 5px
width: 100%
slots:
default:
- component: div
config:
style:
display: flex
justify-content: center
slots:
default:
- component: oh-link
config:
action: command
actionCommand: "=items[loop.sonosInfo.zoneMuteItemName].state == 'ON' ? 'OFF' :
'ON'"
actionItem: =loop.sonosInfo.zoneMuteItemName
iconF7: "=items[loop.sonosInfo.zoneMuteItemName].state == 'OFF' ?
'speaker_3_fill' :
'speaker_slash_fill' "
style:
color: "#666"
- component: oh-slider
config:
item: =loop.sonosInfo.zoneVolumeItemName
releaseOnly: true
style:
padding-left: 5px
padding-right: 5px
width: 90%
unit: "%"
- component: div
config:
style:
display: flex
gap: 15px
justify-content: flex-end
padding-right: 15px
slots:
default:
- component: oh-link
config:
action: variable
actionVariable: =loop.sonosInfo.id + '_volume'
actionVariableValue: =loop.sonosInfo
iconF7: speaker_3
style:
color: "#666"
font-size: 20px
slots:
default:
- component: f7-popup
config:
closeByBackdropClick: false
opened: =vars[loop.sonosInfo.id + '_volume'] !== undefined
style:
height: auto
slots:
default:
- component: f7-card
config:
title: "=props.volumeControlsCaption === undefined ? 'Volume controls' :
props.volumeControlsC\
aption"
slots:
default:
- component: oh-repeater
config:
for: volumeItem
fragment: true
in: =vars[loop.sonosInfo.id + '_volume'].volumeInformation
sourceType: array
slots:
default:
- component: f7-row
slots:
default:
- component: f7-col
config:
class: justify-content-center
style:
width: 33%
slots:
default:
- component: Label
config:
class: margin
text: =items[loop.volumeItem.zoneItemName].state
- component: f7-col
config:
class: display-flex justify-content-center align-items-center
style:
width: 66%
slots:
default:
- component: f7-icon
config:
class: margin-horizontal
f7: "=items[loop.volumeItem.muteItemName].state == 'OFF' ? 'speaker_3' :
'speaker_slash' "
size: 30
slots:
default:
- component: oh-button
config:
action: command
actionCommand: "=items[loop.volumeItem.muteItemName].state == 'ON' ? 'OFF' :
'ON'"
actionItem: =loop.volumeItem.muteItemName
style:
height: 100%
position: absolute
top: 0px
width: 100%
- component: oh-slider
config:
class: margin
item: =loop.volumeItem.volumeItemName
label: true
releaseOnly: true
unit: "%"
- component: oh-button
config:
clearVariable: =loop.sonosInfo.groupedThingDeleteVars
popupClose: true
text: OK
- component: oh-link
config:
action: variable
actionVariable: =loop.sonosInfo.id + '_group'
actionVariableValue: =loop.sonosInfo
iconF7: link
style:
color: "#666"
font-size: 20px
slots:
default:
- component: f7-popup
config:
closeByBackdropClick: false
opened: =vars[loop.sonosInfo.id + '_group'] !== undefined
style:
height: auto
width: 300px
slots:
default:
- component: f7-card
config:
title: Groepen
slots:
default:
- component: oh-repeater
config:
for: groupInfo
fragment: true
in: =vars[loop.sonosInfo.id + '_group'].groupedItemsInformation
sourceType: array
slots:
default:
- component: f7-row
config:
style:
align-items: center
padding: 5px
slots:
default:
- component: oh-button
config:
action: variable
actionVariable: =loop.groupInfo.thingUid
actionVariableValue: "=vars[loop.groupInfo.thingUid] === undefined ?
!loop.groupInfo.isI\
nGroup :
!vars[loop.groupInf\
o.thingUid]"
iconF7: "=vars[loop.groupInfo.thingUid] === undefined ?
(loop.groupInfo.isInGroup
? 'checkmark_square'
: 'square') :
(vars[loop.groupInf\
o.thingUid] ?
'checkmark_square' :
'square')"
- component: Label
config:
style:
margin-left: 10px
text: =loop.groupInfo.name
- component: oh-button
config:
action: command
actionCommand: =JSON.stringify(vars)
actionItem: =loop.sonosInfo.groupSwitcherItemName
class: margin
clearVariable: =loop.sonosInfo.groupedThingDeleteVars
popupClose: true
text: OK
- component: oh-link
config:
action: variable
actionVariable: =loop.sonosInfo.id + '_favorite'
actionVariableValue: =loop.sonosInfo
iconF7: star
style:
color: "#666"
font-size: 20px
slots:
default:
- component: f7-popup
config:
closeByBackdropClick: false
opened: =vars[loop.sonosInfo.id + '_favorite'] !== undefined
style:
height: auto
slots:
default:
- component: f7-card
config:
style:
margin: 0
padding: 0
slots:
default:
- component: div
config:
style:
align-items: center
background: "#f9f9f9"
border-bottom: 1px solid
display: flex
justify-content: space-between
padding: 10px 15px
slots:
default:
- component: Label
config:
style:
color: "#666"
font-size: 16px
font-weight: bold
text: Favorieten
- component: oh-link
config:
action: variable
actionVariable: =loop.sonosInfo.id + '_favorite'
iconF7: multiply
popupClose: true
style:
color: red
font-size: 22px
- component: div
config:
style:
max-height: 350px
overflow-y: auto
slots:
default:
- component: oh-repeater
config:
for: currentFavorit
fragment: true
itemOptions: =loop.sonosInfo.favoriteItemName
sourceType: itemCommandOptions
slots:
default:
- component: oh-button
config:
action: command
actionCommand: =loop.currentFavorit.command
actionItem: =loop.sonosInfo.favoriteItemName
actionVariable: =loop.sonosInfo.id + '_favorite'
popupClose: true
style:
border-bottom: 1px solid
height: 40px
padding-left: 20px
text-align: left
width: 100%
text: =loop.currentFavorit.label
- component: oh-button
config:
actionVariable: =loop.sonosInfo.id + '_favorite'
clearVariable: =loop.sonosInfo.groupedThingDeleteVars
popupClose: true
style:
color: gray
margin: 10px
text: Sluiten
@stevendp thanks for the widget code. for me with this too the two players are below each other and not in the swiper. looks like this for me. so i wait what happens with the oh-swiper bug. thanks anyways.

