From de1eebd174459ebe8e02666ebc583904c4d80d3f Mon Sep 17 00:00:00 2001 From: miloit Date: Fri, 7 Jul 2023 12:18:46 +0200 Subject: [PATCH] [volumio] Initial contribution (#14525) Signed-off-by: Michael Loercher --- CODEOWNERS | 1 + bom/openhab-addons/pom.xml | 5 + bundles/org.openhab.binding.volumio/NOTICE | 13 + bundles/org.openhab.binding.volumio/README.md | 101 +++++ bundles/org.openhab.binding.volumio/pom.xml | 51 +++ .../src/main/feature/feature.xml | 9 + .../internal/VolumioBindingConstants.java | 62 +++ .../internal/VolumioConfiguration.java | 66 ++++ .../volumio/internal/VolumioHandler.java | 366 ++++++++++++++++++ .../internal/VolumioHandlerFactory.java | 54 +++ .../volumio/internal/VolumioService.java | 273 +++++++++++++ .../VolumioDiscoveryParticipant.java | 97 +++++ .../internal/mapping/VolumioCommands.java | 88 +++++ .../volumio/internal/mapping/VolumioData.java | 357 +++++++++++++++++ .../internal/mapping/VolumioEvents.java | 30 ++ .../internal/mapping/VolumioServiceTypes.java | 29 ++ .../src/main/resources/OH-INF/addon/addon.xml | 10 + .../resources/OH-INF/i18n/volumio.properties | 60 +++ .../resources/OH-INF/thing/thing-types.xml | 176 +++++++++ bundles/pom.xml | 1 + 20 files changed, 1849 insertions(+) create mode 100644 bundles/org.openhab.binding.volumio/NOTICE create mode 100644 bundles/org.openhab.binding.volumio/README.md create mode 100644 bundles/org.openhab.binding.volumio/pom.xml create mode 100644 bundles/org.openhab.binding.volumio/src/main/feature/feature.xml create mode 100644 bundles/org.openhab.binding.volumio/src/main/java/org/openhab/binding/volumio/internal/VolumioBindingConstants.java create mode 100644 bundles/org.openhab.binding.volumio/src/main/java/org/openhab/binding/volumio/internal/VolumioConfiguration.java create mode 100644 bundles/org.openhab.binding.volumio/src/main/java/org/openhab/binding/volumio/internal/VolumioHandler.java create mode 100644 bundles/org.openhab.binding.volumio/src/main/java/org/openhab/binding/volumio/internal/VolumioHandlerFactory.java create mode 100644 bundles/org.openhab.binding.volumio/src/main/java/org/openhab/binding/volumio/internal/VolumioService.java create mode 100644 bundles/org.openhab.binding.volumio/src/main/java/org/openhab/binding/volumio/internal/discovery/VolumioDiscoveryParticipant.java create mode 100644 bundles/org.openhab.binding.volumio/src/main/java/org/openhab/binding/volumio/internal/mapping/VolumioCommands.java create mode 100644 bundles/org.openhab.binding.volumio/src/main/java/org/openhab/binding/volumio/internal/mapping/VolumioData.java create mode 100644 bundles/org.openhab.binding.volumio/src/main/java/org/openhab/binding/volumio/internal/mapping/VolumioEvents.java create mode 100644 bundles/org.openhab.binding.volumio/src/main/java/org/openhab/binding/volumio/internal/mapping/VolumioServiceTypes.java create mode 100644 bundles/org.openhab.binding.volumio/src/main/resources/OH-INF/addon/addon.xml create mode 100644 bundles/org.openhab.binding.volumio/src/main/resources/OH-INF/i18n/volumio.properties create mode 100644 bundles/org.openhab.binding.volumio/src/main/resources/OH-INF/thing/thing-types.xml 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