]> git.basschouten.com Git - openhab-addons.git/commitdiff
[volumio] Initial contribution (#14525)
authormiloit <MichaelLoercher@web.de>
Fri, 7 Jul 2023 10:18:46 +0000 (12:18 +0200)
committerGitHub <noreply@github.com>
Fri, 7 Jul 2023 10:18:46 +0000 (12:18 +0200)
Signed-off-by: Michael Loercher <MichaelLoercher@web.de>
20 files changed:
CODEOWNERS
bom/openhab-addons/pom.xml
bundles/org.openhab.binding.volumio/NOTICE [new file with mode: 0644]
bundles/org.openhab.binding.volumio/README.md [new file with mode: 0644]
bundles/org.openhab.binding.volumio/pom.xml [new file with mode: 0644]
bundles/org.openhab.binding.volumio/src/main/feature/feature.xml [new file with mode: 0644]
bundles/org.openhab.binding.volumio/src/main/java/org/openhab/binding/volumio/internal/VolumioBindingConstants.java [new file with mode: 0644]
bundles/org.openhab.binding.volumio/src/main/java/org/openhab/binding/volumio/internal/VolumioConfiguration.java [new file with mode: 0644]
bundles/org.openhab.binding.volumio/src/main/java/org/openhab/binding/volumio/internal/VolumioHandler.java [new file with mode: 0644]
bundles/org.openhab.binding.volumio/src/main/java/org/openhab/binding/volumio/internal/VolumioHandlerFactory.java [new file with mode: 0644]
bundles/org.openhab.binding.volumio/src/main/java/org/openhab/binding/volumio/internal/VolumioService.java [new file with mode: 0644]
bundles/org.openhab.binding.volumio/src/main/java/org/openhab/binding/volumio/internal/discovery/VolumioDiscoveryParticipant.java [new file with mode: 0644]
bundles/org.openhab.binding.volumio/src/main/java/org/openhab/binding/volumio/internal/mapping/VolumioCommands.java [new file with mode: 0644]
bundles/org.openhab.binding.volumio/src/main/java/org/openhab/binding/volumio/internal/mapping/VolumioData.java [new file with mode: 0644]
bundles/org.openhab.binding.volumio/src/main/java/org/openhab/binding/volumio/internal/mapping/VolumioEvents.java [new file with mode: 0644]
bundles/org.openhab.binding.volumio/src/main/java/org/openhab/binding/volumio/internal/mapping/VolumioServiceTypes.java [new file with mode: 0644]
bundles/org.openhab.binding.volumio/src/main/resources/OH-INF/addon/addon.xml [new file with mode: 0644]
bundles/org.openhab.binding.volumio/src/main/resources/OH-INF/i18n/volumio.properties [new file with mode: 0644]
bundles/org.openhab.binding.volumio/src/main/resources/OH-INF/thing/thing-types.xml [new file with mode: 0644]
bundles/pom.xml

index 903e746634aaaad01b18757a12215cac2cc32c1b..8857f6603cb7f9a20edf63b2d99f0833b1396ed4 100644 (file)
 /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
index 61c86ec949d2b2453f562237e91d084780e00e94..f821799122f80ca2e124c527827d183c8333aec0 100644 (file)
       <artifactId>org.openhab.binding.volvooncall</artifactId>
       <version>${project.version}</version>
     </dependency>
+    <dependency>
+      <groupId>org.openhab.addons.bundles</groupId>
+      <artifactId>org.openhab.binding.volumio</artifactId>
+      <version>${project.version}</version>
+    </dependency>
     <dependency>
       <groupId>org.openhab.addons.bundles</groupId>
       <artifactId>org.openhab.binding.warmup</artifactId>
diff --git a/bundles/org.openhab.binding.volumio/NOTICE b/bundles/org.openhab.binding.volumio/NOTICE
new file mode 100644 (file)
index 0000000..38d625e
--- /dev/null
@@ -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 (file)
index 0000000..7242de0
--- /dev/null
@@ -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]"            <musicnote>      {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 %%]"  <soundvolume>    {channel="volumio:player:VolumioLivingRoom:volume"}
+Player Volumio                     "Current Status [%s]"           <volumiologo>    {channel="volumio:player:VolumioLivingRoom:player"}
+String Volumio_CurrentTrackType        "Current Track Type [%s]"   <musicnote>      {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 (file)
index 0000000..af8eca1
--- /dev/null
@@ -0,0 +1,51 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://maven.apache.org/POM/4.0.0"
+  xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
+
+  <modelVersion>4.0.0</modelVersion>
+
+  <parent>
+    <groupId>org.openhab.addons.bundles</groupId>
+    <artifactId>org.openhab.addons.reactor.bundles</artifactId>
+    <version>4.0.0-SNAPSHOT</version>
+  </parent>
+
+  <artifactId>org.openhab.binding.volumio</artifactId>
+
+  <name>openHAB Add-ons :: Bundles :: Volumio Binding</name>
+  <properties>
+    <bnd.importpackage>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</bnd.importpackage>
+  </properties>
+  <dependencies>
+    <dependency>
+      <groupId>org.openhab.osgiify</groupId>
+      <artifactId>io.socket.socket.io-client</artifactId>
+      <version>1.0.0</version>
+      <scope>compile</scope>
+    </dependency>
+    <dependency>
+      <groupId>org.openhab.osgiify</groupId>
+      <artifactId>io.socket.engine.io-client</artifactId>
+      <version>1.0.0</version>
+      <scope>compile</scope>
+    </dependency>
+    <dependency>
+      <groupId>org.apache.servicemix.bundles</groupId>
+      <artifactId>org.apache.servicemix.bundles.okhttp</artifactId>
+      <version>3.8.1_1</version>
+      <scope>compile</scope>
+    </dependency>
+    <dependency>
+      <groupId>org.apache.servicemix.bundles</groupId>
+      <artifactId>org.apache.servicemix.bundles.okio</artifactId>
+      <version>1.13.0_1</version>
+      <scope>compile</scope>
+    </dependency>
+    <dependency>
+      <groupId>org.json</groupId>
+      <artifactId>json</artifactId>
+      <version>20230227</version>
+      <scope>compile</scope>
+    </dependency>
+  </dependencies>
+</project>
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 (file)
index 0000000..87b896a
--- /dev/null
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<features name="org.openhab.binding.volumio-${project.version}" xmlns="http://karaf.apache.org/xmlns/features/v1.4.0">
+       <repository>mvn:org.openhab.core.features.karaf/org.openhab.core.features.karaf.openhab-core/${ohc.version}/xml/features</repository>
+
+       <feature name="openhab-binding-volumio" description="Volumio Binding" version="${project.version}">
+               <feature>openhab-runtime-base</feature>
+               <bundle start-level="80">mvn:org.openhab.addons.bundles/org.openhab.binding.volumio/${project.version}</bundle>
+       </feature>
+</features>
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 (file)
index 0000000..5943c16
--- /dev/null
@@ -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 (file)
index 0000000..5cc57d7
--- /dev/null
@@ -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 (file)
index 0000000..21227a8
--- /dev/null
@@ -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 (file)
index 0000000..e17cb55
--- /dev/null
@@ -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<ThingTypeUID> 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 (file)
index 0000000..9f74a05
--- /dev/null
@@ -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 (file)
index 0000000..692495f
--- /dev/null
@@ -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<ThingTypeUID> 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<String, Object> 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 (file)
index 0000000..7459bf3
--- /dev/null
@@ -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 (file)
index 0000000..e8d11ff
--- /dev/null
@@ -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 (file)
index 0000000..cc0350a
--- /dev/null
@@ -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 (file)
index 0000000..b7c556d
--- /dev/null
@@ -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 (file)
index 0000000..8cb81e6
--- /dev/null
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<addon:addon id="volumio" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+       xmlns:addon="https://openhab.org/schemas/addon/v1.0.0"
+       xsi:schemaLocation="https://openhab.org/schemas/addon/v1.0.0 https://openhab.org/schemas/addon-1.0.0.xsd">
+
+       <type>binding</type>
+       <name>Volumio Binding</name>
+       <description>This is the binding for Volumio devices.</description>
+
+</addon:addon>
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 (file)
index 0000000..51e6159
--- /dev/null
@@ -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 (file)
index 0000000..c3ab7a6
--- /dev/null
@@ -0,0 +1,176 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<thing:thing-descriptions bindingId="volumio"
+       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+       xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
+       xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
+
+       <thing-type id="player">
+               <label>Volumio Binding Thing</label>
+               <description>A Volumio Instance</description>
+
+               <channels>
+                       <channel id="title" typeId="title"/>
+                       <channel id="artist" typeId="artist"/>
+                       <channel id="album" typeId="album"/>
+                       <channel id="volume" typeId="volume"/>
+                       <channel id="player" typeId="player"/>
+                       <channel id="track-type" typeId="track-type"/>
+                       <channel id="play-radiostream" typeId="play-radiostream"/>
+                       <channel id="play-playlist" typeId="play-playlist"/>
+                       <channel id="clear-queue" typeId="clear-queue"/>
+                       <channel id="play-uri" typeId="play-uri"/>
+                       <channel id="play-file" typeId="play-file"/>
+                       <channel id="random" typeId="play-random"/>
+                       <channel id="repeat" typeId="play-repeat"/>
+                       <channel id="system-command" typeId="system-command"/>
+                       <channel id="stop-command" typeId="stop-command"/>
+               </channels>
+
+               <config-description>
+                       <parameter name="hostname" type="text" required="true">
+                               <label>Hostname</label>
+                               <description>The hostname of your Volumio device</description>
+                       </parameter>
+                       <parameter name="port" type="integer" required="true">
+                               <label>Port</label>
+                               <description>The port of your Volumio device (default is 3000)</description>
+                               <default>3000</default>
+                       </parameter>
+                       <parameter name="protocol" type="text" required="true">
+                               <label>Protocol</label>
+                               <description>The protocol of your Volumio device (default is http)</description>
+                               <limitToOptions>true</limitToOptions>
+                               <options>
+                                       <option value="http">http</option>
+                                       <option value="https">https</option>
+                               </options>
+                       </parameter>
+                       <parameter name="timeout" type="integer" required="true">
+                               <label>Timeout</label>
+                               <description>Connection-Timeout in ms</description>
+                               <default>5000</default>
+                               <advanced>true</advanced>
+                       </parameter>
+               </config-description>
+
+       </thing-type>
+
+       <channel-type id="system-command" advanced="true">
+               <item-type>String</item-type>
+               <label>Send System Command</label>
+               <description>Sends a system command to Volumio. This allows to shutdown/reboot Volumio</description>
+               <state>
+                       <options>
+                               <option value="shutdown">Shutdown</option>
+                               <option value="reboot">Reboot</option>
+                       </options>
+               </state>
+       </channel-type>
+
+       <channel-type id="stop-command" advanced="true">
+               <item-type>String</item-type>
+               <label>Stop</label>
+               <description>Sends a Stop Command to Volumio. This allows to stop the player. Use "stop" as string command.
+               </description>
+               <state>
+                       <options>
+                               <option value="stop">Stop</option>
+                       </options>
+               </state>
+       </channel-type>
+
+       <channel-type id="title">
+               <item-type>String</item-type>
+               <label>Current Title</label>
+               <description>Title of the song currently playing</description>
+               <state readOnly="true"/>
+       </channel-type>
+
+       <channel-type id="artist">
+               <item-type>String</item-type>
+               <label>Current Artist</label>
+               <description>Name of the artist currently playing</description>
+               <state readOnly="true"/>
+       </channel-type>
+
+       <channel-type id="album">
+               <item-type>String</item-type>
+               <label>Current Album</label>
+               <description>Name of the album currently playing</description>
+               <state readOnly="true"/>
+       </channel-type>
+
+       <channel-type id="volume">
+               <item-type>Dimmer</item-type>
+               <label>Volume</label>
+               <description>Set or get the master volume</description>
+               <category>SoundVolume</category>
+               <state max="100" min="0" step="10"/>
+       </channel-type>
+
+       <channel-type id="player">
+               <item-type>Player</item-type>
+               <label>State</label>
+               <description>The State channel contains state of the Volumio Player</description>
+               <category>Player</category>
+       </channel-type>
+
+       <channel-type id="album-art" advanced="true">
+               <item-type>Image</item-type>
+               <label>Cover Art</label>
+               <description>Cover Art for the currently played track</description>
+               <state readOnly="true"/>
+       </channel-type>
+
+       <channel-type id="track-type" advanced="true">
+               <item-type>String</item-type>
+               <label>Track Type</label>
+               <description>Tracktype of the currently played track</description>
+               <state readOnly="true"/>
+       </channel-type>
+
+       <channel-type id="play-radiostream" advanced="true">
+               <item-type>String</item-type>
+               <label>Play Radio Stream</label>
+               <description>Play the given radio stream</description>
+       </channel-type>
+
+       <channel-type id="play-playlist" advanced="true">
+               <item-type>String</item-type>
+               <label>Play Playlist</label>
+               <description>Playback a playlist identified by its name</description>
+       </channel-type>
+
+       <channel-type id="clear-queue" advanced="true">
+               <item-type>Switch</item-type>
+               <label>Clear Queue</label>
+               <description>Clear the current queue</description>
+       </channel-type>
+
+       <channel-type id="play-random" advanced="true">
+               <item-type>Switch</item-type>
+               <label>Random</label>
+               <description>Activate random mode</description>
+       </channel-type>
+
+       <channel-type id="play-repeat" advanced="true">
+               <item-type>Switch</item-type>
+               <label>Repeat</label>
+               <description>Activate repeat mode</description>
+       </channel-type>
+
+       <channel-type id="play-uri" advanced="true">
+               <item-type>String</item-type>
+               <label>Play URI</label>
+               <description>Play the stream at given URI</description>
+       </channel-type>
+
+       <channel-type id="play-file" advanced="true">
+               <item-type>String</item-type>
+               <label>Play File</label>
+               <description>Play a file, located on your Volumio device at the given absolute path, e.g.
+                       "mnt/INTERNAL/song.mp3"
+               </description>
+       </channel-type>
+
+</thing:thing-descriptions>
index 0fff040f0341976e7ccce38b2db9bdd737342d63..4dd92d58950635cb2ebaa688d30dbf7b0b071c22 100644 (file)
     <module>org.openhab.binding.vitotronic</module>
     <module>org.openhab.binding.vizio</module>
     <module>org.openhab.binding.volvooncall</module>
+    <module>org.openhab.binding.volumio</module>
     <module>org.openhab.binding.warmup</module>
     <module>org.openhab.binding.weathercompany</module>
     <module>org.openhab.binding.weatherunderground</module>