diff --git a/CODEOWNERS b/CODEOWNERS
index 903e74663..8857f6603 100644
--- a/CODEOWNERS
+++ b/CODEOWNERS
@@ -388,6 +388,7 @@
/bundles/org.openhab.binding.yamahareceiver/ @davidgraeff @zarusz
/bundles/org.openhab.binding.yeelight/ @claell
/bundles/org.openhab.binding.yioremote/ @miloit
+/bundles/org.openhab.binding.volumio/ @miloit
/bundles/org.openhab.binding.zoneminder/ @mhilbush
/bundles/org.openhab.binding.zway/ @pathec
/bundles/org.openhab.io.homekit/ @andylintner @ccutrer @yfre
diff --git a/bom/openhab-addons/pom.xml b/bom/openhab-addons/pom.xml
index 61c86ec94..f82179912 100644
--- a/bom/openhab-addons/pom.xml
+++ b/bom/openhab-addons/pom.xml
@@ -1841,6 +1841,11 @@
org.openhab.binding.volvooncall
${project.version}
+
+ org.openhab.addons.bundles
+ org.openhab.binding.volumio
+ ${project.version}
+
org.openhab.addons.bundles
org.openhab.binding.warmup
diff --git a/bundles/org.openhab.binding.volumio/NOTICE b/bundles/org.openhab.binding.volumio/NOTICE
new file mode 100644
index 000000000..38d625e34
--- /dev/null
+++ b/bundles/org.openhab.binding.volumio/NOTICE
@@ -0,0 +1,13 @@
+This content is produced and maintained by the openHAB project.
+
+* Project home: https://www.openhab.org
+
+== Declared Project Licenses
+
+This program and the accompanying materials are made available under the terms
+of the Eclipse Public License 2.0 which is available at
+https://www.eclipse.org/legal/epl-2.0/.
+
+== Source Code
+
+https://github.com/openhab/openhab-addons
diff --git a/bundles/org.openhab.binding.volumio/README.md b/bundles/org.openhab.binding.volumio/README.md
new file mode 100644
index 000000000..7242de0cb
--- /dev/null
+++ b/bundles/org.openhab.binding.volumio/README.md
@@ -0,0 +1,101 @@
+# Volumio Binding
+
+This binding integrates the open-source Music Player [Volumio](https://www.volumio.com).
+
+## Supported Things
+
+
+All available Volumio (playback) modes are supported by this binding.
+
+## Discovery
+
+The Volumio devices are discovered through mDNS in the local network and all devices are put in the Inbox.
+
+
+## Binding Configuration
+
+The binding has the following configuration options, which can be set:
+
+| Parameter | Name | Description | Required |
+| ----------- | ---------------- | -------------------------------------------------------------------------- | -------- |
+| hostname | Hostname | The hostname of the Volumio player. | yes |
+| port | Port | The port of your volumio2 device (default is 3000) | yes |
+| protocol | Protocol | The protocol of your volumio2 device (default is http) | yes |
+| timeout | Timeout | Connection-Timeout in ms | no |
+
+
+## Thing Configuration
+
+The Volumio Thing requires the hostname, port and protocol as a configuration value in order for the binding to know how to access it.
+Additionally, a connection timeout (in ms) can be configured.
+In the thing file, this looks e.g. like
+
+```java
+Thing volumio:player:VolumioLivingRoom "Volumio" @ "Living Room" [hostname="volumio.local", protocol="http"]
+```
+
+### `sample` Thing Configuration
+
+| Name | Type | Description | Default | Required | Advanced |
+|-----------------|---------|---------------------------------------|---------|----------|----------|
+| hostname | text | The hostname of the Volumio player. | N/A | yes | no |
+| port | text | The port of your Volumio device. | 3000 | yes | no |
+| protocol | text | The protocol of your Volumio device. | http | yes | no |
+| timeout | integer | Connection-Timeout in ms. | 5000 | no | yes |
+
+## Channels
+
+The devices support the following channels:
+
+
+| Channel | Type | Read/Write | Description |
+|-------------------|--------|------------|----------------------------------------------------------------------------------------------------------------------|
+| title | String | R | Title of the song currently playing. |
+| artist | String | R | Name of the artist currently playing. |
+| album | String | R | Name of the album currently playing. |
+| volume | Dimmer | RW | Set or get the master volume. |
+| player | Player | RW | The State channel contains state of the Volumio Player. |
+| albumArt | Image | R | Cover Art for the currently played track. |
+| track-type | String | R | Tracktype of the currently played track. |
+| play-radiostream | String | RW | Play the given radio stream. |
+| play-playlist | String | RW | Playback a playlist identified by its name. |
+| clear-queue | Switch | RW | Clear the current queue. |
+| play-uri | Switch | RW | Play the stream at given uri. |
+| play-file | Switch | RW | Play a file, located on your Volumio device at the given absolute path, e.g."mnt/INTERNAL/song.mp3" |
+| random | Switch | RW | Activate random mode. |
+| repeat | Switch | RW | Activate repeat mode. |
+| system-command | Switch | RW | Sends a system command to Volumio. This allows to shutdown/reboot Volumio. Use "Shutdown"/"Reboot" as String command.|
+| stop-command | Switch | RW | Sends a Stop Command to Volumio. This allows to stop the player. Use "stop" as string command. |
+
+
+## Full Example
+
+demo.things:
+
+```java
+Thing volumio:player:VolumioLivingRoom "Volumio" @ "Living Room" [hostname="volumio.local", protocol="http"]
+```
+
+demo.items:
+
+```java
+String Volumio_CurrentTitle "Current Title [%s]" {channel="volumio:player:VolumioLivingRoom:title"}
+String Volumio_CurrentArtist "Current Artist [%s]" {channel="volumio:player:VolumioLivingRoom:artist"}
+String Volumio_CurrentAlbum "Current Album [%s]" {channel="volumio:player:VolumioLivingRoom:album"}
+Dimmer Volumio_CurrentVolume "Current Volume [%.1f %%]" {channel="volumio:player:VolumioLivingRoom:volume"}
+Player Volumio "Current Status [%s]" {channel="volumio:player:VolumioLivingRoom:player"}
+String Volumio_CurrentTrackType "Current Track Type [%s]" {channel="volumio:player:VolumioLivingRoom:track-type"}
+```
+
+demo.sitemap:
+
+```perl
+sitemap demo label="Main Menu"
+{
+ Frame label="Volumio" {
+ Slider item=Volumio_CurrentVolume
+ Text item=Volumio
+ Text item=Volumio_CurrentTitle
+ }
+}
+```
diff --git a/bundles/org.openhab.binding.volumio/pom.xml b/bundles/org.openhab.binding.volumio/pom.xml
new file mode 100644
index 000000000..af8eca1cc
--- /dev/null
+++ b/bundles/org.openhab.binding.volumio/pom.xml
@@ -0,0 +1,51 @@
+
+
+
+ 4.0.0
+
+
+ org.openhab.addons.bundles
+ org.openhab.addons.reactor.bundles
+ 4.0.0-SNAPSHOT
+
+
+ org.openhab.binding.volumio
+
+ openHAB Add-ons :: Bundles :: Volumio Binding
+
+ org.apache.http.*;io.socket.thread;io.socket.engineio.client;io.socket.emitter;android.*;resolution:=optional,com.android.org.*;resolution:=optional,dalvik.*;resolution:=optional,javax.annotation.meta.*;resolution:=optional,org.apache.harmony.*;resolution:=optional,org.conscrypt.*;resolution:=optional,sun.security.*;resolution:=optional
+
+
+
+ org.openhab.osgiify
+ io.socket.socket.io-client
+ 1.0.0
+ compile
+
+
+ org.openhab.osgiify
+ io.socket.engine.io-client
+ 1.0.0
+ compile
+
+
+ org.apache.servicemix.bundles
+ org.apache.servicemix.bundles.okhttp
+ 3.8.1_1
+ compile
+
+
+ org.apache.servicemix.bundles
+ org.apache.servicemix.bundles.okio
+ 1.13.0_1
+ compile
+
+
+ org.json
+ json
+ 20230227
+ compile
+
+
+
diff --git a/bundles/org.openhab.binding.volumio/src/main/feature/feature.xml b/bundles/org.openhab.binding.volumio/src/main/feature/feature.xml
new file mode 100644
index 000000000..87b896a5c
--- /dev/null
+++ b/bundles/org.openhab.binding.volumio/src/main/feature/feature.xml
@@ -0,0 +1,9 @@
+
+
+ mvn:org.openhab.core.features.karaf/org.openhab.core.features.karaf.openhab-core/${ohc.version}/xml/features
+
+
+ openhab-runtime-base
+ mvn:org.openhab.addons.bundles/org.openhab.binding.volumio/${project.version}
+
+
diff --git a/bundles/org.openhab.binding.volumio/src/main/java/org/openhab/binding/volumio/internal/VolumioBindingConstants.java b/bundles/org.openhab.binding.volumio/src/main/java/org/openhab/binding/volumio/internal/VolumioBindingConstants.java
new file mode 100644
index 000000000..5943c1645
--- /dev/null
+++ b/bundles/org.openhab.binding.volumio/src/main/java/org/openhab/binding/volumio/internal/VolumioBindingConstants.java
@@ -0,0 +1,62 @@
+/**
+ * Copyright (c) 2010-2023 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.volumio.internal;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.core.thing.ThingTypeUID;
+
+/**
+ * The {@link VolumioBindingConstants} class defines common constants, which are
+ * used across the whole binding.
+ *
+ * @author Patrick Sernetz - Initial Contribution
+ * @author Chris Wohlbrecht - Adaption for openHAB 3
+ * @author Michael Loercher - Adaption for openHAB 3
+ */
+@NonNullByDefault
+public class VolumioBindingConstants {
+
+ private static final String BINDING_ID = "volumio";
+
+ // List of all Thing Type UIDs
+ public static final ThingTypeUID THING_TYPE_VOLUMIO = new ThingTypeUID(BINDING_ID, "player");
+
+ // List of all Channel ids
+ public static final String CHANNEL_TITLE = "title";
+ public static final String CHANNEL_ARTIST = "artist";
+ public static final String CHANNEL_ALBUM = "album";
+ public static final String CHANNEL_VOLUME = "volume";
+ public static final String CHANNEL_PLAYER = "player";
+ public static final String CHANNEL_COVER_ART = "album-art";
+ public static final String CHANNEL_TRACK_TYPE = "track-type";
+ public static final String CHANNEL_PLAY_RADIO_STREAM = "play-radiostream";
+ public static final String CHANNEL_PLAY_PLAYLIST = "play-playlist";
+ public static final String CHANNEL_CLEAR_QUEUE = "clear-queue";
+ public static final String CHANNEL_PLAY_RANDOM = "random";
+ public static final String CHANNEL_PLAY_REPEAT = "repeat";
+ public static final String CHANNEL_PLAY_URI = "play-uri";
+ public static final String CHANNEL_PLAY_FILE = "play-file";
+ public static final String CHANNEL_SYSTEM_COMMAND = "system-command";
+ public static final String CHANNEL_STOP = "stop-command";
+
+ // discovery properties
+ public static final String DISCOVERY_SERVICE_TYPE = "_Volumio._tcp.local.";
+ public static final String DISCOVERY_NAME_PROPERTY = "volumioName";
+ public static final String DISCOVERY_UUID_PROPERTY = "UUID";
+
+ // config
+ public static final String CONFIG_PROPERTY_HOSTNAME = "hostname";
+ public static final String CONFIG_PROPERTY_PORT = "port";
+ public static final String CONFIG_PROPERTY_PROTOCOL = "protocol";
+ public static final String CONFIG_PROPERTY_TIMEOUT = "timeout";
+}
diff --git a/bundles/org.openhab.binding.volumio/src/main/java/org/openhab/binding/volumio/internal/VolumioConfiguration.java b/bundles/org.openhab.binding.volumio/src/main/java/org/openhab/binding/volumio/internal/VolumioConfiguration.java
new file mode 100644
index 000000000..5cc57d747
--- /dev/null
+++ b/bundles/org.openhab.binding.volumio/src/main/java/org/openhab/binding/volumio/internal/VolumioConfiguration.java
@@ -0,0 +1,66 @@
+/**
+ * Copyright (c) 2010-2023 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.volumio.internal;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+/**
+ * The {@link VolumioConfiguration} class contains fields mapping thing configuration parameters.
+ *
+ * @author Patrick Sernetz - Initial contribution
+ * @author Chris Wohlbrecht - Adapt for openHAB 3
+ * @author Michael Loercher - Adaption for openHAB 3
+ */
+@NonNullByDefault
+public class VolumioConfiguration {
+
+ private String hostName = "";
+
+ private int port;
+
+ private String protocol = "";
+
+ private int timeout;
+
+ public String getHost() {
+ return hostName;
+ }
+
+ public void setHost(String host) {
+ this.hostName = host;
+ }
+
+ public int getPort() {
+ return port;
+ }
+
+ public void setPort(int port) {
+ this.port = port;
+ }
+
+ public String getProtocol() {
+ return protocol;
+ }
+
+ public void setProtocol(String protocol) {
+ this.protocol = protocol;
+ }
+
+ public int getTimeout() {
+ return timeout;
+ }
+
+ public void setTimeout(int timeout) {
+ this.timeout = timeout;
+ }
+}
diff --git a/bundles/org.openhab.binding.volumio/src/main/java/org/openhab/binding/volumio/internal/VolumioHandler.java b/bundles/org.openhab.binding.volumio/src/main/java/org/openhab/binding/volumio/internal/VolumioHandler.java
new file mode 100644
index 000000000..21227a8f8
--- /dev/null
+++ b/bundles/org.openhab.binding.volumio/src/main/java/org/openhab/binding/volumio/internal/VolumioHandler.java
@@ -0,0 +1,366 @@
+/**
+ * Copyright (c) 2010-2023 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.volumio.internal;
+
+import java.math.BigDecimal;
+import java.util.concurrent.TimeUnit;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.json.JSONException;
+import org.json.JSONObject;
+import org.openhab.binding.volumio.internal.mapping.VolumioData;
+import org.openhab.binding.volumio.internal.mapping.VolumioEvents;
+import org.openhab.binding.volumio.internal.mapping.VolumioServiceTypes;
+import org.openhab.core.library.types.NextPreviousType;
+import org.openhab.core.library.types.OnOffType;
+import org.openhab.core.library.types.PercentType;
+import org.openhab.core.library.types.PlayPauseType;
+import org.openhab.core.library.types.RewindFastforwardType;
+import org.openhab.core.library.types.StringType;
+import org.openhab.core.thing.Channel;
+import org.openhab.core.thing.ChannelUID;
+import org.openhab.core.thing.Thing;
+import org.openhab.core.thing.ThingStatus;
+import org.openhab.core.thing.ThingStatusDetail;
+import org.openhab.core.thing.binding.BaseThingHandler;
+import org.openhab.core.types.Command;
+import org.openhab.core.types.RefreshType;
+import org.openhab.core.types.UnDefType;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import io.socket.client.Socket;
+import io.socket.emitter.Emitter;
+
+/**
+ * The {@link VolumioHandler} is responsible for handling commands, which are
+ * sent to one of the channels.
+ *
+ * @author Patrick Sernetz - Initial Contribution
+ * @author Chris Wohlbrecht - Adaption for openHAB 3
+ * @author Michael Loercher - Adaption for openHAB 3
+ */
+@NonNullByDefault
+public class VolumioHandler extends BaseThingHandler {
+
+ private final Logger logger = LoggerFactory.getLogger(VolumioHandler.class);
+
+ private @Nullable VolumioService volumio;
+
+ private final VolumioData state = new VolumioData();
+
+ public VolumioHandler(Thing thing) {
+ super(thing);
+ }
+
+ @Override
+ public void handleCommand(ChannelUID channelUID, Command command) {
+ logger.debug("channelUID: {}", channelUID);
+
+ if (volumio == null) {
+ logger.debug("Ignoring command {} = {} because device is offline.", channelUID.getId(), command);
+ if (ThingStatus.ONLINE.equals(getThing().getStatus())) {
+ updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, "device is offline");
+ }
+ return;
+ }
+
+ try {
+ switch (channelUID.getId()) {
+ case VolumioBindingConstants.CHANNEL_PLAYER:
+ handlePlaybackCommands(command);
+ break;
+ case VolumioBindingConstants.CHANNEL_VOLUME:
+ handleVolumeCommand(command);
+ break;
+
+ case VolumioBindingConstants.CHANNEL_ARTIST:
+ case VolumioBindingConstants.CHANNEL_ALBUM:
+ case VolumioBindingConstants.CHANNEL_TRACK_TYPE:
+ case VolumioBindingConstants.CHANNEL_TITLE:
+ break;
+
+ case VolumioBindingConstants.CHANNEL_PLAY_RADIO_STREAM:
+ if (command instanceof StringType) {
+ final String uri = command.toFullString();
+ volumio.replacePlay(uri, "Radio", VolumioServiceTypes.WEBRADIO);
+ }
+
+ break;
+
+ case VolumioBindingConstants.CHANNEL_PLAY_URI:
+ if (command instanceof StringType) {
+ final String uri = command.toFullString();
+ volumio.replacePlay(uri, "URI", VolumioServiceTypes.WEBRADIO);
+ }
+
+ break;
+
+ case VolumioBindingConstants.CHANNEL_PLAY_FILE:
+ if (command instanceof StringType) {
+ final String uri = command.toFullString();
+ volumio.replacePlay(uri, "", VolumioServiceTypes.MPD);
+ }
+
+ break;
+
+ case VolumioBindingConstants.CHANNEL_PLAY_PLAYLIST:
+ if (command instanceof StringType) {
+ final String playlistName = command.toFullString();
+ volumio.playPlaylist(playlistName);
+ }
+
+ break;
+ case VolumioBindingConstants.CHANNEL_CLEAR_QUEUE:
+ if ((command instanceof OnOffType) && (command == OnOffType.ON)) {
+ volumio.clearQueue();
+ // Make it feel like a toggle button ...
+ updateState(channelUID, OnOffType.OFF);
+ }
+ break;
+ case VolumioBindingConstants.CHANNEL_PLAY_RANDOM:
+ if (command instanceof OnOffType) {
+ boolean enableRandom = command == OnOffType.ON;
+ volumio.setRandom(enableRandom);
+ }
+ break;
+ case VolumioBindingConstants.CHANNEL_PLAY_REPEAT:
+ if (command instanceof OnOffType) {
+ boolean enableRepeat = command == OnOffType.ON;
+ volumio.setRepeat(enableRepeat);
+ }
+ break;
+ case "REFRESH":
+ logger.debug("Called Refresh");
+ volumio.getState();
+ break;
+ case VolumioBindingConstants.CHANNEL_SYSTEM_COMMAND:
+ if (command instanceof StringType) {
+ sendSystemCommand(command);
+ updateState(VolumioBindingConstants.CHANNEL_SYSTEM_COMMAND, UnDefType.UNDEF);
+ } else if (RefreshType.REFRESH == command) {
+ updateState(VolumioBindingConstants.CHANNEL_SYSTEM_COMMAND, UnDefType.UNDEF);
+ }
+ break;
+ case VolumioBindingConstants.CHANNEL_STOP:
+ if (command instanceof StringType) {
+ handleStopCommand(command);
+ updateState(VolumioBindingConstants.CHANNEL_STOP, UnDefType.UNDEF);
+ } else if (RefreshType.REFRESH == command) {
+ updateState(VolumioBindingConstants.CHANNEL_STOP, UnDefType.UNDEF);
+ }
+ break;
+ default:
+ logger.error("Unknown channel: {}", channelUID.getId());
+ }
+ } catch (Exception e) {
+ updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
+ }
+ }
+
+ private void sendSystemCommand(Command command) {
+ if (command instanceof StringType) {
+ volumio.sendSystemCommand(command.toString());
+ updateState(VolumioBindingConstants.CHANNEL_SYSTEM_COMMAND, UnDefType.UNDEF);
+ } else if (command.equals(RefreshType.REFRESH)) {
+ updateState(VolumioBindingConstants.CHANNEL_SYSTEM_COMMAND, UnDefType.UNDEF);
+ }
+ }
+
+ /**
+ * Set all channel of thing to UNDEF during connection.
+ */
+ private void clearChannels() {
+ for (Channel channel : getThing().getChannels()) {
+ updateState(channel.getUID(), UnDefType.UNDEF);
+ }
+ }
+
+ private void handleVolumeCommand(Command command) {
+ if (command instanceof PercentType) {
+ volumio.setVolume((PercentType) command);
+ } else if (command instanceof RefreshType) {
+ volumio.getState();
+ } else {
+ logger.error("Command is not handled");
+ }
+ }
+
+ private void handleStopCommand(Command command) {
+ if (command instanceof StringType) {
+ volumio.stop();
+ updateState(VolumioBindingConstants.CHANNEL_STOP, UnDefType.UNDEF);
+ } else if (command.equals(RefreshType.REFRESH)) {
+ updateState(VolumioBindingConstants.CHANNEL_STOP, UnDefType.UNDEF);
+ }
+ }
+
+ private void handlePlaybackCommands(Command command) {
+ if (command instanceof PlayPauseType playPauseCmd) {
+ switch (playPauseCmd) {
+ case PLAY:
+ volumio.play();
+ break;
+ case PAUSE:
+ volumio.pause();
+ break;
+ }
+ } else if (command instanceof NextPreviousType nextPreviousType) {
+ switch (nextPreviousType) {
+ case PREVIOUS:
+ volumio.previous();
+ break;
+ case NEXT:
+ volumio.next();
+ break;
+ }
+ } else if (command instanceof RewindFastforwardType fastForwardType) {
+ switch (fastForwardType) {
+ case FASTFORWARD:
+ case REWIND:
+ logger.warn("Not implemented yet");
+ break;
+ }
+ } else if (command instanceof RefreshType) {
+ volumio.getState();
+ } else {
+ logger.error("Command is not handled: {}", command);
+ }
+ }
+
+ /**
+ * Bind default listeners to volumio session.
+ * - EVENT_CONNECT - Connection to volumio was established
+ * - EVENT_DISCONNECT - Connection was disconnected
+ * - PUSH.STATE -
+ */
+ private void bindDefaultListener() {
+ volumio.on(Socket.EVENT_CONNECT, connectListener());
+ volumio.on(Socket.EVENT_DISCONNECT, disconnectListener());
+ volumio.on(VolumioEvents.PUSH_STATE, pushStateListener());
+ }
+
+ /**
+ * Read the configuration and connect to volumio device. The Volumio impl. is
+ * async so it should not block the process in any way.
+ */
+ @Override
+ public void initialize() {
+ String hostname = (String) getThing().getConfiguration().get(VolumioBindingConstants.CONFIG_PROPERTY_HOSTNAME);
+ int port = ((BigDecimal) getThing().getConfiguration().get(VolumioBindingConstants.CONFIG_PROPERTY_PORT))
+ .intValueExact();
+ String protocol = (String) getThing().getConfiguration().get(VolumioBindingConstants.CONFIG_PROPERTY_PROTOCOL);
+ int timeout = ((BigDecimal) getThing().getConfiguration().get(VolumioBindingConstants.CONFIG_PROPERTY_TIMEOUT))
+ .intValueExact();
+
+ if (hostname == null) {
+ updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
+ "Configuration incomplete, missing hostname");
+ } else if (protocol == null) {
+ updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
+ "Configuration incomplete, missing protocol");
+ } else {
+ logger.debug("Trying to connect to Volumio on {}://{}:{}", protocol, hostname, port);
+ try {
+ volumio = new VolumioService(protocol, hostname, port, timeout);
+ clearChannels();
+ bindDefaultListener();
+ updateStatus(ThingStatus.OFFLINE);
+ volumio.connect();
+ } catch (Exception e) {
+ updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
+ }
+ }
+ }
+
+ @Override
+ public void dispose() {
+ if (volumio != null) {
+ scheduler.schedule(() -> {
+ if (volumio.isConnected()) {
+ logger.warn("Timeout during disconnect event");
+ } else {
+ volumio.close();
+ }
+ clearChannels();
+ }, 30, TimeUnit.SECONDS);
+
+ volumio.disconnect();
+ }
+ }
+
+ /** Listener **/
+
+ /**
+ * As soon as the Connect Listener is executed
+ * the ThingStatus is set to ONLINE.
+ */
+ private Emitter.Listener connectListener() {
+ return arg -> updateStatus(ThingStatus.ONLINE);
+ }
+
+ /**
+ * As soon as the Disconnect Listener is executed
+ * the ThingStatus is set to OFFLINE.
+ */
+ private Emitter.Listener disconnectListener() {
+ return arg0 -> updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR);
+ }
+
+ /**
+ * On received a pushState Event, the ThingChannels are
+ * updated if there is a change and they are linked.
+ */
+ private Emitter.Listener pushStateListener() {
+ return data -> {
+ try {
+ JSONObject jsonObject = (JSONObject) data[0];
+ logger.debug("{}", jsonObject.toString());
+ state.update(jsonObject);
+ if (isLinked(VolumioBindingConstants.CHANNEL_TITLE) && state.isTitleDirty()) {
+ updateState(VolumioBindingConstants.CHANNEL_TITLE, state.getTitle());
+ }
+ if (isLinked(VolumioBindingConstants.CHANNEL_ARTIST) && state.isArtistDirty()) {
+ updateState(VolumioBindingConstants.CHANNEL_ARTIST, state.getArtist());
+ }
+ if (isLinked(VolumioBindingConstants.CHANNEL_ALBUM) && state.isAlbumDirty()) {
+ updateState(VolumioBindingConstants.CHANNEL_ALBUM, state.getAlbum());
+ }
+ if (isLinked(VolumioBindingConstants.CHANNEL_VOLUME) && state.isVolumeDirty()) {
+ updateState(VolumioBindingConstants.CHANNEL_VOLUME, state.getVolume());
+ }
+ if (isLinked(VolumioBindingConstants.CHANNEL_PLAYER) && state.isStateDirty()) {
+ updateState(VolumioBindingConstants.CHANNEL_PLAYER, state.getState());
+ }
+ if (isLinked(VolumioBindingConstants.CHANNEL_TRACK_TYPE) && state.isTrackTypeDirty()) {
+ updateState(VolumioBindingConstants.CHANNEL_TRACK_TYPE, state.getTrackType());
+ }
+
+ if (isLinked(VolumioBindingConstants.CHANNEL_PLAY_RANDOM) && state.isRandomDirty()) {
+ updateState(VolumioBindingConstants.CHANNEL_PLAY_RANDOM, state.getRandom());
+ }
+ if (isLinked(VolumioBindingConstants.CHANNEL_PLAY_REPEAT) && state.isRepeatDirty()) {
+ updateState(VolumioBindingConstants.CHANNEL_PLAY_REPEAT, state.getRepeat());
+ }
+ /**
+ * if (isLinked(CHANNEL_COVER_ART) && state.isCoverArtDirty()) {
+ * updateState(CHANNEL_COVER_ART, state.getCoverArt());
+ * }
+ */
+ } catch (JSONException e) {
+ logger.error("Could not refresh channel: {}", e.getMessage());
+ }
+ };
+ }
+}
diff --git a/bundles/org.openhab.binding.volumio/src/main/java/org/openhab/binding/volumio/internal/VolumioHandlerFactory.java b/bundles/org.openhab.binding.volumio/src/main/java/org/openhab/binding/volumio/internal/VolumioHandlerFactory.java
new file mode 100644
index 000000000..e17cb55a6
--- /dev/null
+++ b/bundles/org.openhab.binding.volumio/src/main/java/org/openhab/binding/volumio/internal/VolumioHandlerFactory.java
@@ -0,0 +1,54 @@
+/**
+ * Copyright (c) 2010-2023 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.volumio.internal;
+
+import java.util.Set;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.core.thing.Thing;
+import org.openhab.core.thing.ThingTypeUID;
+import org.openhab.core.thing.binding.BaseThingHandlerFactory;
+import org.openhab.core.thing.binding.ThingHandler;
+import org.openhab.core.thing.binding.ThingHandlerFactory;
+import org.osgi.service.component.annotations.Component;
+
+/**
+ * The {@link VolumioHandlerFactory} is responsible for creating things and thing
+ * handlers.
+ *
+ * @author Patrick Sernetz - Initial Contribution
+ * @author Chris Wohlbrecht - Adaption for openHAB 3
+ */
+@NonNullByDefault
+@Component(configurationPid = "binding.volumio", service = ThingHandlerFactory.class)
+public class VolumioHandlerFactory extends BaseThingHandlerFactory {
+
+ private static final Set SUPPORTED_THING_TYPES_UIDS = Set
+ .of(VolumioBindingConstants.THING_TYPE_VOLUMIO);
+
+ @Override
+ public boolean supportsThingType(ThingTypeUID thingTypeUID) {
+ return SUPPORTED_THING_TYPES_UIDS.contains(thingTypeUID);
+ }
+
+ @Override
+ protected @Nullable ThingHandler createHandler(Thing thing) {
+ ThingTypeUID thingTypeUID = thing.getThingTypeUID();
+
+ if (VolumioBindingConstants.THING_TYPE_VOLUMIO.equals(thingTypeUID)) {
+ return new VolumioHandler(thing);
+ }
+ return null;
+ }
+}
diff --git a/bundles/org.openhab.binding.volumio/src/main/java/org/openhab/binding/volumio/internal/VolumioService.java b/bundles/org.openhab.binding.volumio/src/main/java/org/openhab/binding/volumio/internal/VolumioService.java
new file mode 100644
index 000000000..9f74a0533
--- /dev/null
+++ b/bundles/org.openhab.binding.volumio/src/main/java/org/openhab/binding/volumio/internal/VolumioService.java
@@ -0,0 +1,273 @@
+/**
+ * Copyright (c) 2010-2023 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.volumio.internal;
+
+import java.net.InetAddress;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.net.UnknownHostException;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.json.JSONException;
+import org.json.JSONObject;
+import org.openhab.binding.volumio.internal.mapping.VolumioCommands;
+import org.openhab.core.library.types.PercentType;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import io.socket.client.IO;
+import io.socket.client.Socket;
+import io.socket.emitter.Emitter;
+
+/**
+ * @author Patrick Sernetz - Initial Contribution
+ * @author Chris Wohlbrecht - Adaption for openHAB 3
+ * @author Michael Loercher - Adaption for openHAB 3
+ */
+@NonNullByDefault
+public class VolumioService {
+
+ private final Logger logger = LoggerFactory.getLogger(VolumioService.class);
+
+ private final Socket socket;
+
+ private boolean connected;
+
+ public VolumioService(String protocol, String hostname, int port, int timeout)
+ throws URISyntaxException, UnknownHostException {
+ String uriString = String.format("%s://%s:%d", protocol, hostname, port);
+
+ URI destUri = new URI(uriString);
+
+ IO.Options opts = new IO.Options();
+ opts.reconnection = true;
+ opts.reconnectionDelay = 1000 * 30;
+ opts.reconnectionDelayMax = 1000 * 60;
+ opts.timeout = timeout;
+
+ // Connection to mdns endpoint is only available after fetching ip.
+ InetAddress ipaddress = InetAddress.getByName(hostname);
+ logger.debug("Resolving {} to IP {}", hostname, ipaddress.getHostAddress());
+
+ socket = IO.socket(destUri, opts);
+
+ bindDefaultEvents(hostname);
+ }
+
+ private void bindDefaultEvents(String hostname) {
+ socket.on(Socket.EVENT_CONNECTING, arg0 -> logger.debug("Trying to connect to Volumio on {}", hostname));
+
+ socket.on(Socket.EVENT_RECONNECTING, arg0 -> logger.debug("Trying to reconnect to Volumio on {}", hostname));
+
+ socket.on(Socket.EVENT_CONNECT_ERROR, arg0 -> logger.error("Could not connect to Volumio on {}", hostname));
+
+ socket.on(Socket.EVENT_CONNECT_TIMEOUT,
+ arg0 -> logger.error("Timedout while conntecting to Volumio on {}", hostname));
+
+ socket.on(Socket.EVENT_CONNECT, arg0 -> {
+ logger.info("Connected to Volumio on {}", hostname);
+ setConnected(true);
+
+ }).on(Socket.EVENT_DISCONNECT, arg0 -> {
+ logger.warn("Disconnected from Volumio on {}", hostname);
+ setConnected(false);
+ });
+ }
+
+ public void connect() throws InterruptedException {
+ socket.connect();
+ }
+
+ public void disconnect() {
+ socket.disconnect();
+ }
+
+ public void close() {
+ socket.off();
+ socket.close();
+ }
+
+ public void on(String eventName, Emitter.Listener listener) {
+ socket.on(eventName, listener);
+ }
+
+ public void once(String eventName, Emitter.Listener listener) {
+ socket.once(eventName, listener);
+ }
+
+ public void getState() {
+ socket.emit(VolumioCommands.GET_STATE);
+ }
+
+ public void play() {
+ socket.emit(VolumioCommands.PLAY);
+ }
+
+ public void pause() {
+ socket.emit(VolumioCommands.PAUSE);
+ }
+
+ public void stop() {
+ socket.emit(VolumioCommands.STOP);
+ }
+
+ public void play(Integer index) {
+ socket.emit(VolumioCommands.PLAY, index);
+ }
+
+ public void next() {
+ socket.emit(VolumioCommands.NEXT);
+ }
+
+ public void previous() {
+ socket.emit(VolumioCommands.PREVIOUS);
+ }
+
+ public void setVolume(PercentType level) {
+ socket.emit(VolumioCommands.VOLUME, level.intValue());
+ }
+
+ public void shutdown() {
+ socket.emit(VolumioCommands.SHUTDOWN);
+ }
+
+ public void reboot() {
+ socket.emit(VolumioCommands.REBOOT);
+ }
+
+ public void playPlaylist(String playlistName) {
+ JSONObject item = new JSONObject();
+
+ try {
+ item.put("name", playlistName);
+
+ socket.emit(VolumioCommands.PLAY_PLAYLIST, item);
+ } catch (JSONException e) {
+ logger.error("The following error occurred {}", e.getMessage());
+ }
+ }
+
+ public void clearQueue() {
+ socket.emit(VolumioCommands.CLEAR_QUEUE);
+ }
+
+ public void setRandom(boolean val) {
+ JSONObject item = new JSONObject();
+
+ try {
+ item.put("value", val);
+
+ socket.emit(VolumioCommands.RANDOM, item);
+ } catch (JSONException e) {
+ logger.error("The following error occurred {}", e.getMessage());
+ }
+ }
+
+ public void setRepeat(boolean val) {
+ JSONObject item = new JSONObject();
+
+ try {
+ item.put("value", val);
+
+ socket.emit(VolumioCommands.REPEAT, item);
+ } catch (JSONException e) {
+ logger.error("The following error occurred {}", e.getMessage());
+ }
+ }
+
+ public void playFavorites(String favoriteName) {
+ JSONObject item = new JSONObject();
+
+ try {
+ item.put("name", favoriteName);
+
+ socket.emit(VolumioCommands.PLAY_FAVOURITES, item);
+ } catch (JSONException e) {
+ logger.error("The following error occurred {}", e.getMessage());
+ }
+ }
+
+ /**
+ * Play a radio station from volumio´s Radio Favourites identifed by
+ * its index.
+ */
+ public void playRadioFavourite(final Integer index) {
+ logger.debug("socket.emit({})", VolumioCommands.PLAY_RADIO_FAVOURITES);
+
+ socket.once("pushPlayRadioFavourites", arg -> play(index));
+
+ socket.emit(VolumioCommands.PLAY_RADIO_FAVOURITES);
+ }
+
+ public void playURI(String uri) {
+ JSONObject item = new JSONObject();
+ logger.debug("PlayURI: {}", uri);
+ try {
+ item.put("uri", uri);
+
+ socket.emit(VolumioCommands.PLAY, uri);
+ } catch (JSONException e) {
+ logger.error("The following error occurred {}", e.getMessage());
+ }
+ }
+
+ public void addPlay(String uri, String title, String serviceType) {
+ JSONObject item = new JSONObject();
+
+ try {
+ item.put("uri", uri);
+ item.put("title", title);
+ item.put("service", serviceType);
+
+ socket.emit(VolumioCommands.ADD_PLAY, item);
+ } catch (JSONException e) {
+ logger.error("The following error occurred {}", e.getMessage());
+ }
+ }
+
+ public void replacePlay(String uri, String title, String serviceType) {
+ JSONObject item = new JSONObject();
+
+ try {
+ item.put("uri", uri);
+ item.put("title", title);
+ item.put("service", serviceType);
+
+ socket.emit(VolumioCommands.REPLACE_AND_PLAY, item);
+ } catch (JSONException e) {
+ logger.error("The following error occurred {}", e.getMessage());
+ }
+ }
+
+ public boolean isConnected() {
+ return this.connected;
+ }
+
+ public void setConnected(boolean status) {
+ this.connected = status;
+ }
+
+ public void sendSystemCommand(String string) {
+ logger.warn("Jukebox Command: {}", string);
+ switch (string) {
+ case VolumioCommands.SHUTDOWN:
+ shutdown();
+ break;
+ case VolumioCommands.REBOOT:
+ reboot();
+ break;
+ default:
+ break;
+ }
+ }
+}
diff --git a/bundles/org.openhab.binding.volumio/src/main/java/org/openhab/binding/volumio/internal/discovery/VolumioDiscoveryParticipant.java b/bundles/org.openhab.binding.volumio/src/main/java/org/openhab/binding/volumio/internal/discovery/VolumioDiscoveryParticipant.java
new file mode 100644
index 000000000..692495f89
--- /dev/null
+++ b/bundles/org.openhab.binding.volumio/src/main/java/org/openhab/binding/volumio/internal/discovery/VolumioDiscoveryParticipant.java
@@ -0,0 +1,97 @@
+/**
+ * Copyright (c) 2010-2023 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.volumio.internal.discovery;
+
+import static org.openhab.binding.volumio.internal.VolumioBindingConstants.THING_TYPE_VOLUMIO;
+
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Set;
+
+import javax.jmdns.ServiceInfo;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.binding.volumio.internal.VolumioBindingConstants;
+import org.openhab.core.config.discovery.DiscoveryResult;
+import org.openhab.core.config.discovery.DiscoveryResultBuilder;
+import org.openhab.core.config.discovery.mdns.MDNSDiscoveryParticipant;
+import org.openhab.core.thing.ThingTypeUID;
+import org.openhab.core.thing.ThingUID;
+import org.osgi.service.component.annotations.Component;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * @author Patrick Sernetz - Initial contribution
+ * @author Chris Wohlbrecht - Adaption for openHAB 3
+ * @author Michael Loercher - Adaption for openHAB 3
+ */
+@NonNullByDefault
+@Component(configurationPid = "discovery.volumio")
+public class VolumioDiscoveryParticipant implements MDNSDiscoveryParticipant {
+
+ private final Logger logger = LoggerFactory.getLogger(VolumioDiscoveryParticipant.class);
+
+ @Override
+ public Set getSupportedThingTypeUIDs() {
+ return Set.of(THING_TYPE_VOLUMIO);
+ }
+
+ @Override
+ public String getServiceType() {
+ return VolumioBindingConstants.DISCOVERY_SERVICE_TYPE;
+ }
+
+ @Override
+ public @Nullable DiscoveryResult createResult(ServiceInfo serviceInfo) {
+ String volumioName = serviceInfo.getPropertyString(VolumioBindingConstants.DISCOVERY_NAME_PROPERTY);
+ Map properties = new HashMap<>();
+ ThingUID thingUID = getThingUID(serviceInfo);
+
+ logger.debug("Service Device: {}", serviceInfo);
+ logger.debug("Thing UID: {}", thingUID);
+
+ DiscoveryResult discoveryResult = null;
+ if (thingUID != null) {
+ properties.put("hostname", serviceInfo.getServer());
+ properties.put("port", serviceInfo.getPort());
+ properties.put("protocol", "http");
+
+ discoveryResult = DiscoveryResultBuilder.create(thingUID).withProperties(properties).withLabel(volumioName)
+ .build();
+ logger.debug("DiscoveryResult: {}", discoveryResult);
+ }
+ return discoveryResult;
+ }
+
+ @Override
+ public @Nullable ThingUID getThingUID(ServiceInfo serviceInfo) {
+ Collections.list(serviceInfo.getPropertyNames()).forEach(s -> logger.debug("PropertyName: {}", s));
+
+ String volumioName = serviceInfo.getPropertyString("volumioName");
+ if (volumioName == null) {
+ return null;
+ }
+
+ String uuid = serviceInfo.getPropertyString("UUID");
+ if (uuid == null) {
+ return null;
+ }
+
+ String uuidAndServername = String.format("%s-%s", uuid, volumioName);
+ logger.debug("return new ThingUID({}, {});", THING_TYPE_VOLUMIO, uuidAndServername);
+ return new ThingUID(THING_TYPE_VOLUMIO, uuidAndServername);
+ }
+}
diff --git a/bundles/org.openhab.binding.volumio/src/main/java/org/openhab/binding/volumio/internal/mapping/VolumioCommands.java b/bundles/org.openhab.binding.volumio/src/main/java/org/openhab/binding/volumio/internal/mapping/VolumioCommands.java
new file mode 100644
index 000000000..7459bf37d
--- /dev/null
+++ b/bundles/org.openhab.binding.volumio/src/main/java/org/openhab/binding/volumio/internal/mapping/VolumioCommands.java
@@ -0,0 +1,88 @@
+/**
+ * Copyright (c) 2010-2023 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.volumio.internal.mapping;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+/**
+ * @see https://github.com/volumio/Volumio2-UI/blob/master/src/app/services/player.service.js
+ * @see https://github.com/volumio/Volumio2/blob/master/app/plugins/user_interface/websocket/index.js
+ *
+ * @author Patrick Sernetz - Initial Contribution
+ * @author Chris Wohlbrecht - Adaption for openHAB 3
+ * @author Michael Loercher - Adaption for openHAB 3
+ *
+ */
+@NonNullByDefault
+public class VolumioCommands {
+
+ /* Player Status */
+
+ public static final String GET_STATE = "get-state";
+
+ /* Player Controls */
+
+ public static final String PLAY = "play";
+
+ public static final String PAUSE = "pause";
+
+ public static final String STOP = "stop";
+
+ public static final String PREVIOUS = "prev";
+
+ public static final String NEXT = "next";
+
+ public static final String SEEK = "seek";
+
+ public static final String RANDOM = "set-random";
+
+ public static final String REPEAT = "set-repeat";
+
+ /* Search */
+
+ public static final String SEARCH = "search";
+
+ /* Volume */
+
+ public static final String VOLUME = "volume";
+
+ public static final String MUTE = "mute";
+
+ public static final String UNMUTE = "unmute";
+
+ /* MultiRoom */
+
+ public static final String GET_MULTIROOM_DEVICES = "get-multi-room-devices";
+
+ /* Queue */
+
+ /**
+ * Replace the complete queue and play add/play the delivered entry.
+ */
+ public static final String REPLACE_AND_PLAY = "replace-and-play";
+
+ public static final String ADD_PLAY = "addPlay";
+
+ public static final String CLEAR_QUEUE = "clear-queue";
+
+ /* ... */
+ public static final String SHUTDOWN = "shutdown";
+
+ public static final String REBOOT = "reboot";
+
+ public static final String PLAY_PLAYLIST = "play-playlist";
+
+ public static final String PLAY_FAVOURITES = "play-favourites";
+
+ public static final String PLAY_RADIO_FAVOURITES = "play-radio-favourites";
+}
diff --git a/bundles/org.openhab.binding.volumio/src/main/java/org/openhab/binding/volumio/internal/mapping/VolumioData.java b/bundles/org.openhab.binding.volumio/src/main/java/org/openhab/binding/volumio/internal/mapping/VolumioData.java
new file mode 100644
index 000000000..e8d11ff00
--- /dev/null
+++ b/bundles/org.openhab.binding.volumio/src/main/java/org/openhab/binding/volumio/internal/mapping/VolumioData.java
@@ -0,0 +1,357 @@
+/**
+ * Copyright (c) 2010-2023 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.volumio.internal.mapping;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.URL;
+import java.net.URLConnection;
+import java.util.Objects;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.json.JSONException;
+import org.json.JSONObject;
+import org.openhab.binding.volumio.internal.VolumioBindingConstants;
+import org.openhab.core.library.types.OnOffType;
+import org.openhab.core.library.types.PercentType;
+import org.openhab.core.library.types.PlayPauseType;
+import org.openhab.core.library.types.RawType;
+import org.openhab.core.library.types.StringType;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * The {@link VolumioData} class defines state data of volumio.
+ *
+ * @author Patrick Sernetz - Initial Contribution
+ * @author Chris Wohlbrecht - Adaption for openHAB 3
+ * @author Michael Loercher - Adaption for openHAB 3
+ */
+@NonNullByDefault
+public class VolumioData {
+
+ private final Logger logger = LoggerFactory.getLogger(VolumioData.class);
+
+ private String title = "";
+ private boolean titleDirty;
+
+ private String album = "";
+ private boolean albumDirty;
+
+ private String artist = "";
+ private boolean artistDirty;
+
+ private int volume = 0;
+ private boolean volumeDirty;
+
+ private String state = "";
+ private boolean stateDirty;
+
+ private String trackType = "";
+ private boolean trackTypeDirty;
+
+ private String position = "";
+ private boolean positionDirty;
+
+ private byte @Nullable [] coverArt = null;
+ private String coverArtUrl = "";
+ private boolean coverArtDirty;
+
+ private boolean repeat = false;
+ private boolean repeatDirty;
+
+ private boolean random = false;
+ private boolean randomDirty;
+
+ public void update(JSONObject jsonObject) throws JSONException {
+ if (jsonObject.has(VolumioBindingConstants.CHANNEL_TITLE)) {
+ setTitle(jsonObject.getString(VolumioBindingConstants.CHANNEL_TITLE));
+ } else {
+ setTitle("");
+ }
+
+ if (jsonObject.has(VolumioBindingConstants.CHANNEL_ALBUM)
+ && !jsonObject.isNull(VolumioBindingConstants.CHANNEL_ALBUM)) {
+ setAlbum(jsonObject.getString(VolumioBindingConstants.CHANNEL_ALBUM));
+ } else {
+ setAlbum("");
+ }
+
+ if (jsonObject.has(VolumioBindingConstants.CHANNEL_VOLUME)) {
+ setVolume(jsonObject.getInt(VolumioBindingConstants.CHANNEL_VOLUME));
+ } else {
+ setVolume(0);
+ }
+
+ if (jsonObject.has(VolumioBindingConstants.CHANNEL_ARTIST)) {
+ setArtist(jsonObject.getString(VolumioBindingConstants.CHANNEL_ARTIST));
+ } else {
+ setArtist("");
+ }
+
+ /* Special */
+ if (jsonObject.has("status")) {
+ setState(jsonObject.getString("status"));
+ } else {
+ setState("pause");
+ }
+
+ if (jsonObject.has(VolumioBindingConstants.CHANNEL_TRACK_TYPE)) {
+ setTrackType(jsonObject.getString(VolumioBindingConstants.CHANNEL_TRACK_TYPE));
+ } else {
+ setTrackType("");
+ }
+
+ if (jsonObject.has(VolumioBindingConstants.CHANNEL_COVER_ART)
+ && !jsonObject.isNull(VolumioBindingConstants.CHANNEL_COVER_ART)) {
+ setCoverArt(jsonObject.getString(VolumioBindingConstants.CHANNEL_COVER_ART));
+ } else {
+ setCoverArt(null);
+ }
+
+ if (jsonObject.has(VolumioBindingConstants.CHANNEL_PLAY_RANDOM)
+ && !jsonObject.isNull(VolumioBindingConstants.CHANNEL_PLAY_RANDOM)) {
+ setRandom(jsonObject.getBoolean(VolumioBindingConstants.CHANNEL_PLAY_RANDOM));
+ } else {
+ setRandom(false);
+ }
+
+ if (jsonObject.has(VolumioBindingConstants.CHANNEL_PLAY_REPEAT)
+ && !jsonObject.isNull(VolumioBindingConstants.CHANNEL_PLAY_REPEAT)) {
+ setRepeat(jsonObject.getBoolean(VolumioBindingConstants.CHANNEL_PLAY_REPEAT));
+ } else {
+ setRepeat(false);
+ }
+ }
+
+ public StringType getTitle() {
+ return new StringType(title);
+ }
+
+ public void setTitle(String title) {
+ if (!title.equals(this.title)) {
+ this.title = title;
+ this.titleDirty = true;
+ } else {
+ this.titleDirty = false;
+ }
+ }
+
+ public StringType getAlbum() {
+ return new StringType(album);
+ }
+
+ public void setAlbum(String album) {
+ if ("null".equals(album)) {
+ album = "";
+ }
+
+ if (!album.equals(this.album)) {
+ this.album = album;
+ this.albumDirty = true;
+ } else {
+ this.albumDirty = false;
+ }
+ }
+
+ public StringType getArtist() {
+ return new StringType(artist);
+ }
+
+ public void setArtist(String artist) {
+ if ("null".equals(artist)) {
+ this.artist = "";
+ }
+
+ if (!artist.equals(this.artist)) {
+ this.artist = artist;
+ this.artistDirty = true;
+ } else {
+ this.artistDirty = false;
+ }
+ }
+
+ public PercentType getVolume() {
+ return new PercentType(volume);
+ }
+
+ public void setVolume(int volume) {
+ if (volume != this.volume) {
+ this.volume = volume;
+ this.volumeDirty = true;
+ } else {
+ this.volumeDirty = false;
+ }
+ }
+
+ public void setState(String state) {
+ if (!state.equals(this.state)) {
+ this.state = state;
+ this.stateDirty = true;
+ } else {
+ this.stateDirty = false;
+ }
+ }
+
+ public PlayPauseType getState() {
+ PlayPauseType playPauseStatus;
+
+ if ("play".equals(state)) {
+ playPauseStatus = PlayPauseType.PLAY;
+ } else {
+ playPauseStatus = PlayPauseType.PAUSE;
+ }
+
+ return playPauseStatus;
+ }
+
+ public void setTrackType(String trackType) {
+ if (!trackType.equals(this.trackType)) {
+ this.trackType = trackType;
+ this.trackTypeDirty = true;
+ } else {
+ this.trackTypeDirty = false;
+ }
+ }
+
+ public StringType getTrackType() {
+ return new StringType(trackType);
+ }
+
+ public void setPosition(String position) {
+ if (!position.equals(this.position)) {
+ this.position = position;
+ this.positionDirty = true;
+ } else {
+ this.positionDirty = false;
+ }
+ }
+
+ public void setCoverArt(@Nullable String coverArtUrl) {
+ if (coverArtUrl != null) {
+ if (!Objects.equals(coverArtUrl, this.coverArtUrl)) {
+ if (!coverArtUrl.startsWith("http")) {
+ return;
+ }
+
+ try {
+ URL url = new URL(coverArtUrl);
+ URLConnection connection = url.openConnection();
+ InputStream inStream = null;
+ inStream = connection.getInputStream();
+ coverArt = inputStreamToByte(inStream);
+ } catch (IOException ioe) {
+ coverArt = null;
+ }
+ this.coverArtDirty = true;
+ } else {
+ this.coverArtDirty = false;
+ }
+ } else {
+ coverArt = null;
+ }
+ }
+
+ private byte @Nullable [] inputStreamToByte(InputStream is) {
+ byte @Nullable [] imgdata = null;
+ try (ByteArrayOutputStream bytestream = new ByteArrayOutputStream()) {
+ int ch;
+ while ((ch = is.read()) != -1) {
+ bytestream.write(ch);
+ }
+ imgdata = bytestream.toByteArray();
+ return imgdata;
+ } catch (Exception e) {
+ logger.error("Could not open or read input stream {}", e.getMessage());
+ }
+
+ return imgdata;
+ }
+
+ public @Nullable RawType getCoverArt() {
+ byte[] localCoverArt = coverArt;
+ return localCoverArt == null ? null : new RawType(localCoverArt, "image/jpeg");
+ }
+
+ public OnOffType getRandom() {
+ return OnOffType.from(random);
+ }
+
+ public void setRandom(boolean val) {
+ if (val != this.random) {
+ this.random = val;
+ this.randomDirty = true;
+ } else {
+ this.randomDirty = false;
+ }
+ }
+
+ public OnOffType getRepeat() {
+ return OnOffType.from(repeat);
+ }
+
+ public void setRepeat(boolean val) {
+ if (val != this.repeat) {
+ this.repeat = val;
+ this.repeatDirty = true;
+ } else {
+ this.repeatDirty = false;
+ }
+ }
+
+ public StringType getPosition() {
+ return new StringType(position);
+ }
+
+ public boolean isPositionDirty() {
+ return positionDirty;
+ }
+
+ public boolean isStateDirty() {
+ return stateDirty;
+ }
+
+ public boolean isTitleDirty() {
+ return titleDirty;
+ }
+
+ public boolean isAlbumDirty() {
+ return albumDirty;
+ }
+
+ public boolean isArtistDirty() {
+ return artistDirty;
+ }
+
+ public boolean isVolumeDirty() {
+ return volumeDirty;
+ }
+
+ public boolean isTrackTypeDirty() {
+ return trackTypeDirty;
+ }
+
+ public boolean isCoverArtDirty() {
+ return coverArtDirty;
+ }
+
+ public boolean isRandomDirty() {
+ return randomDirty;
+ }
+
+ public boolean isRepeatDirty() {
+ return repeatDirty;
+ }
+}
diff --git a/bundles/org.openhab.binding.volumio/src/main/java/org/openhab/binding/volumio/internal/mapping/VolumioEvents.java b/bundles/org.openhab.binding.volumio/src/main/java/org/openhab/binding/volumio/internal/mapping/VolumioEvents.java
new file mode 100644
index 000000000..cc0350a2f
--- /dev/null
+++ b/bundles/org.openhab.binding.volumio/src/main/java/org/openhab/binding/volumio/internal/mapping/VolumioEvents.java
@@ -0,0 +1,30 @@
+/**
+ * Copyright (c) 2010-2023 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.volumio.internal.mapping;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+/**
+ * @author Patrick Sernetz - Initial Contribution
+ * @author Michael Loercher - Adaption for openHAB 3
+ */
+@NonNullByDefault
+public class VolumioEvents {
+
+ /**
+ * Pushes the current state of Volumio2. For example
+ * track, artist, title, volume, ...
+ *
+ */
+ public static final String PUSH_STATE = "pushState";
+}
diff --git a/bundles/org.openhab.binding.volumio/src/main/java/org/openhab/binding/volumio/internal/mapping/VolumioServiceTypes.java b/bundles/org.openhab.binding.volumio/src/main/java/org/openhab/binding/volumio/internal/mapping/VolumioServiceTypes.java
new file mode 100644
index 000000000..b7c556dd5
--- /dev/null
+++ b/bundles/org.openhab.binding.volumio/src/main/java/org/openhab/binding/volumio/internal/mapping/VolumioServiceTypes.java
@@ -0,0 +1,29 @@
+/**
+ * Copyright (c) 2010-2023 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.volumio.internal.mapping;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+/**
+ * @author Patrick Sernetz - Initial Contribution
+ * @author Michael Loercher - Adaption for openHAB 3
+ */
+@NonNullByDefault
+public class VolumioServiceTypes {
+
+ public static final String WEBRADIO = "webradio";
+
+ public static final String SPOTIFY = "spotify";
+
+ public static final String MPD = "mpd";
+}
diff --git a/bundles/org.openhab.binding.volumio/src/main/resources/OH-INF/addon/addon.xml b/bundles/org.openhab.binding.volumio/src/main/resources/OH-INF/addon/addon.xml
new file mode 100644
index 000000000..8cb81e631
--- /dev/null
+++ b/bundles/org.openhab.binding.volumio/src/main/resources/OH-INF/addon/addon.xml
@@ -0,0 +1,10 @@
+
+
+
+ binding
+ Volumio Binding
+ This is the binding for Volumio devices.
+
+
diff --git a/bundles/org.openhab.binding.volumio/src/main/resources/OH-INF/i18n/volumio.properties b/bundles/org.openhab.binding.volumio/src/main/resources/OH-INF/i18n/volumio.properties
new file mode 100644
index 000000000..51e6159a6
--- /dev/null
+++ b/bundles/org.openhab.binding.volumio/src/main/resources/OH-INF/i18n/volumio.properties
@@ -0,0 +1,60 @@
+# add-on
+
+addon.volumio.name = Volumio Binding
+addon.volumio.description = This is the binding for Volumio devices.
+
+# thing types
+
+thing-type.volumio.player.label = Volumio Binding Thing
+thing-type.volumio.player.description = A Volumio Instance
+
+# thing types config
+
+thing-type.config.volumio.player.hostname.label = Hostname
+thing-type.config.volumio.player.hostname.description = The hostname of your Volumio device
+thing-type.config.volumio.player.port.label = Port
+thing-type.config.volumio.player.port.description = The port of your Volumio device (default is 3000)
+thing-type.config.volumio.player.protocol.label = Protocol
+thing-type.config.volumio.player.protocol.description = The protocol of your Volumio device (default is http)
+thing-type.config.volumio.player.protocol.option.http = http
+thing-type.config.volumio.player.protocol.option.https = https
+thing-type.config.volumio.player.timeout.label = Timeout
+thing-type.config.volumio.player.timeout.description = Connection-Timeout in ms
+
+# channel types
+
+channel-type.volumio.album-art.label = Cover Art
+channel-type.volumio.album-art.description = Cover Art for the currently played track
+channel-type.volumio.album.label = Current Album
+channel-type.volumio.album.description = Name of the album currently playing
+channel-type.volumio.artist.label = Current Artist
+channel-type.volumio.artist.description = Name of the artist currently playing
+channel-type.volumio.clear-queue.label = Clear Queue
+channel-type.volumio.clear-queue.description = Clear the current queue
+channel-type.volumio.play-file.label = Play File
+channel-type.volumio.play-file.description = Play a file, located on your Volumio device at the given absolute path, e.g. "mnt/INTERNAL/song.mp3"
+channel-type.volumio.play-playlist.label = Play Playlist
+channel-type.volumio.play-playlist.description = Playback a playlist identified by its name
+channel-type.volumio.play-radiostream.label = Play Radio Stream
+channel-type.volumio.play-radiostream.description = Play the given radio stream
+channel-type.volumio.play-random.label = Random
+channel-type.volumio.play-random.description = Activate random mode
+channel-type.volumio.play-repeat.label = Repeat
+channel-type.volumio.play-repeat.description = Activate repeat mode
+channel-type.volumio.play-uri.label = Play URI
+channel-type.volumio.play-uri.description = Play the stream at given URI
+channel-type.volumio.player.label = State
+channel-type.volumio.player.description = The State channel contains state of the Volumio Player
+channel-type.volumio.stop-command.label = Stop
+channel-type.volumio.stop-command.description = Sends a Stop Command to Volumio. This allows to stop the player. Use "stop" as string command.
+channel-type.volumio.stop-command.state.option.stop = Stop
+channel-type.volumio.system-command.label = Send System Command
+channel-type.volumio.system-command.description = Sends a system command to Volumio. This allows to shutdown/reboot Volumio
+channel-type.volumio.system-command.state.option.shutdown = Shutdown
+channel-type.volumio.system-command.state.option.reboot = Reboot
+channel-type.volumio.title.label = Current Title
+channel-type.volumio.title.description = Title of the song currently playing
+channel-type.volumio.track-type.label = Track Type
+channel-type.volumio.track-type.description = Tracktype of the currently played track
+channel-type.volumio.volume.label = Volume
+channel-type.volumio.volume.description = Set or get the master volume
diff --git a/bundles/org.openhab.binding.volumio/src/main/resources/OH-INF/thing/thing-types.xml b/bundles/org.openhab.binding.volumio/src/main/resources/OH-INF/thing/thing-types.xml
new file mode 100644
index 000000000..c3ab7a677
--- /dev/null
+++ b/bundles/org.openhab.binding.volumio/src/main/resources/OH-INF/thing/thing-types.xml
@@ -0,0 +1,176 @@
+
+
+
+
+
+ A Volumio Instance
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ The hostname of your Volumio device
+
+
+
+ The port of your Volumio device (default is 3000)
+ 3000
+
+
+
+ The protocol of your Volumio device (default is http)
+ true
+
+
+
+
+
+
+
+ Connection-Timeout in ms
+ 5000
+ true
+
+
+
+
+
+
+ String
+
+ Sends a system command to Volumio. This allows to shutdown/reboot Volumio
+
+
+
+
+
+
+
+
+
+ String
+
+ Sends a Stop Command to Volumio. This allows to stop the player. Use "stop" as string command.
+
+
+
+
+
+
+
+
+
+ String
+
+ Title of the song currently playing
+
+
+
+
+ String
+
+ Name of the artist currently playing
+
+
+
+
+ String
+
+ Name of the album currently playing
+
+
+
+
+ Dimmer
+
+ Set or get the master volume
+ SoundVolume
+
+
+
+
+ Player
+
+ The State channel contains state of the Volumio Player
+ Player
+
+
+
+ Image
+
+ Cover Art for the currently played track
+
+
+
+
+ String
+
+ Tracktype of the currently played track
+
+
+
+
+ String
+
+ Play the given radio stream
+
+
+
+ String
+
+ Playback a playlist identified by its name
+
+
+
+ Switch
+
+ Clear the current queue
+
+
+
+ Switch
+
+ Activate random mode
+
+
+
+ Switch
+
+ Activate repeat mode
+
+
+
+ String
+
+ Play the stream at given URI
+
+
+
+ String
+
+ Play a file, located on your Volumio device at the given absolute path, e.g.
+ "mnt/INTERNAL/song.mp3"
+
+
+
+
diff --git a/bundles/pom.xml b/bundles/pom.xml
index 0fff040f0..4dd92d589 100644
--- a/bundles/pom.xml
+++ b/bundles/pom.xml
@@ -403,6 +403,7 @@
org.openhab.binding.vitotronic
org.openhab.binding.vizio
org.openhab.binding.volvooncall
+ org.openhab.binding.volumio
org.openhab.binding.warmup
org.openhab.binding.weathercompany
org.openhab.binding.weatherunderground