From 5f37700428e75a15e20d38c8acbb975f796166db Mon Sep 17 00:00:00 2001 From: Mark Hilbush Date: Fri, 3 Apr 2020 12:20:50 -0400 Subject: [PATCH] Implement like/unlike for remote streaming services Signed-off-by: Mark Hilbush --- .../org.openhab.binding.squeezebox/README.md | 14 ++- .../internal/SqueezeBoxBindingConstants.java | 2 + .../internal/SqueezeBoxHandlerFactory.java | 44 +++---- ...ezeBoxStateDescriptionOptionsProvider.java | 12 +- .../SqueezeBoxPlayerDiscoveryParticipant.java | 5 + .../squeezebox/internal/dto/ButtonDTO.java | 65 +++++++++++ .../internal/dto/ButtonDTODeserializer.java | 58 ++++++++++ .../squeezebox/internal/dto/ButtonsDTO.java | 53 +++++++++ .../internal/dto/RemoteMetaDTO.java | 77 +++++++++++++ .../internal/dto/StatusResponseDTO.java | 47 ++++++++ .../internal/dto/StatusResultDTO.java | 93 +++++++++++++++ .../SqueezeBoxNotificationListener.java | 4 + .../SqueezeBoxPlayerEventListener.java | 2 + .../handler/SqueezeBoxPlayerHandler.java | 52 ++++++--- .../handler/SqueezeBoxServerHandler.java | 107 ++++++++++++++++-- .../resources/ESH-INF/thing/thing-types.xml | 12 ++ 16 files changed, 580 insertions(+), 67 deletions(-) create mode 100644 bundles/org.openhab.binding.squeezebox/src/main/java/org/openhab/binding/squeezebox/internal/dto/ButtonDTO.java create mode 100644 bundles/org.openhab.binding.squeezebox/src/main/java/org/openhab/binding/squeezebox/internal/dto/ButtonDTODeserializer.java create mode 100644 bundles/org.openhab.binding.squeezebox/src/main/java/org/openhab/binding/squeezebox/internal/dto/ButtonsDTO.java create mode 100644 bundles/org.openhab.binding.squeezebox/src/main/java/org/openhab/binding/squeezebox/internal/dto/RemoteMetaDTO.java create mode 100644 bundles/org.openhab.binding.squeezebox/src/main/java/org/openhab/binding/squeezebox/internal/dto/StatusResponseDTO.java create mode 100644 bundles/org.openhab.binding.squeezebox/src/main/java/org/openhab/binding/squeezebox/internal/dto/StatusResultDTO.java diff --git a/bundles/org.openhab.binding.squeezebox/README.md b/bundles/org.openhab.binding.squeezebox/README.md index 05ebe0a973a96..6e27361da72f8 100644 --- a/bundles/org.openhab.binding.squeezebox/README.md +++ b/bundles/org.openhab.binding.squeezebox/README.md @@ -114,6 +114,8 @@ All devices support some of the following channels: | ircode | String | Received IR code | | numberPlaylistTracks | Number | Number of playlist tracks | | playFavorite | String | ID of Favorite to play (channel's state options contains available favorites) | +| like | Switch | "Like" the currently playing song (if supported by the streaming service) | +| unlike | Switch | "Unlike" the currently playing song (if supported by the streaming service) | ## Playing Favorites @@ -199,12 +201,18 @@ end ### Known Issues -- There are some versions of squeezelite that will not correctly play very short duration mp3 files. Versions of squeezelite after v1.7 and before v1.8.6 will not play very short duration mp3 files reliably. For example, if you're using piCorePlayer (which uses squeezelite), please check your version of squeezelite if you're having trouble playing notifications. This bug has been fixed in squeezelite version 1.8.6-985, which is included in piCorePlayer version 3.20. +- There are some versions of squeezelite that will not correctly play very short duration mp3 files. +Versions of squeezelite after v1.7 and before v1.8.6 will not play very short duration mp3 files reliably. +For example, if you're using piCorePlayer (which uses squeezelite), please check your version of squeezelite if you're having trouble playing notifications. +This bug has been fixed in squeezelite version 1.8.6-985, which is included in piCorePlayer version 3.20. - When streaming from a remote service (such as Pandora or Spotify), after the notification plays, the Squeezebox Server starts playing a new track, instead of picking up from where it left off on the currently playing track. -- There have been reports that notifications do not play reliably, or do not play at all, when using Logitech Media Server (LMS) version 7.7.5. Therefore, it is recommended that the LMS be on a more current version than 7.7.5. +- There have been reports that notifications do not play reliably, or do not play at all, when using Logitech Media Server (LMS) version 7.7.5. +Therefore, it is recommended that the LMS be on a more current version than 7.7.5. - There have been reports that the LMS does not play some WAV files reliably. If you're using a TTS service that produces WAV files, and the notifications are not playing, try using an MP3-formatted TTS notification. +This issue reportedly was [fixed in the LMS](https://github.com/Logitech/slimserver/issues/307) by accepting additional MIME types for WAV files. -- The LMS treats player MAC addresses as case-sensitive. Therefore, the case of MAC addresses in the Squeeze Player thing configuration must match the case displayed on the *Information* tab in the LMS Settings. +- The LMS treats player MAC addresses as case-sensitive. +Therefore, the case of MAC addresses in the Squeeze Player thing configuration must match the case displayed on the *Information* tab in the LMS Settings. diff --git a/bundles/org.openhab.binding.squeezebox/src/main/java/org/openhab/binding/squeezebox/internal/SqueezeBoxBindingConstants.java b/bundles/org.openhab.binding.squeezebox/src/main/java/org/openhab/binding/squeezebox/internal/SqueezeBoxBindingConstants.java index 657307dc35ce0..5e8dfd4d62964 100644 --- a/bundles/org.openhab.binding.squeezebox/src/main/java/org/openhab/binding/squeezebox/internal/SqueezeBoxBindingConstants.java +++ b/bundles/org.openhab.binding.squeezebox/src/main/java/org/openhab/binding/squeezebox/internal/SqueezeBoxBindingConstants.java @@ -67,4 +67,6 @@ public class SqueezeBoxBindingConstants { public static final String CHANNEL_NAME = "name"; public static final String CHANNEL_MODEL = "model"; public static final String CHANNEL_FAVORITES_PLAY = "playFavorite"; + public static final String CHANNEL_LIKE = "like"; + public static final String CHANNEL_UNLIKE = "unlike"; } diff --git a/bundles/org.openhab.binding.squeezebox/src/main/java/org/openhab/binding/squeezebox/internal/SqueezeBoxHandlerFactory.java b/bundles/org.openhab.binding.squeezebox/src/main/java/org/openhab/binding/squeezebox/internal/SqueezeBoxHandlerFactory.java index 82460f10c9df3..5867daf39ecec 100644 --- a/bundles/org.openhab.binding.squeezebox/src/main/java/org/openhab/binding/squeezebox/internal/SqueezeBoxHandlerFactory.java +++ b/bundles/org.openhab.binding.squeezebox/src/main/java/org/openhab/binding/squeezebox/internal/SqueezeBoxHandlerFactory.java @@ -41,6 +41,7 @@ import org.openhab.binding.squeezebox.internal.handler.SqueezeBoxServerHandler; import org.osgi.framework.ServiceRegistration; import org.osgi.service.component.ComponentContext; +import org.osgi.service.component.annotations.Activate; import org.osgi.service.component.annotations.Component; import org.osgi.service.component.annotations.Reference; import org.slf4j.Logger; @@ -65,16 +66,24 @@ public class SqueezeBoxHandlerFactory extends BaseThingHandlerFactory { private Map> discoveryServiceRegs = new HashMap<>(); - private AudioHTTPServer audioHTTPServer; - private NetworkAddressService networkAddressService; + private final AudioHTTPServer audioHTTPServer; + private final NetworkAddressService networkAddressService; + private final SqueezeBoxStateDescriptionOptionsProvider stateDescriptionProvider; private Map> audioSinkRegistrations = new ConcurrentHashMap<>(); - private SqueezeBoxStateDescriptionOptionsProvider stateDescriptionProvider; - // Callback url (scheme+server+port) to use for playing notification sounds private String callbackUrl = null; + @Activate + public SqueezeBoxHandlerFactory(@Reference AudioHTTPServer audioHTTPServer, + @Reference NetworkAddressService networkAddressService, + @Reference SqueezeBoxStateDescriptionOptionsProvider stateDescriptionProvider) { + this.audioHTTPServer = audioHTTPServer; + this.networkAddressService = networkAddressService; + this.stateDescriptionProvider = stateDescriptionProvider; + } + @Override public boolean supportsThingType(ThingTypeUID thingTypeUID) { return SUPPORTED_THING_TYPES_UIDS.contains(thingTypeUID); @@ -197,31 +206,4 @@ private String createCallbackUrl() { return "http://" + ipAddress + ":" + port; } - - @Reference - protected void setAudioHTTPServer(AudioHTTPServer audioHTTPServer) { - this.audioHTTPServer = audioHTTPServer; - } - - protected void unsetAudioHTTPServer(AudioHTTPServer audioHTTPServer) { - this.audioHTTPServer = null; - } - - @Reference - protected void setNetworkAddressService(NetworkAddressService networkAddressService) { - this.networkAddressService = networkAddressService; - } - - protected void unsetNetworkAddressService(NetworkAddressService networkAddressService) { - this.networkAddressService = null; - } - - @Reference - protected void setDynamicStateDescriptionProvider(SqueezeBoxStateDescriptionOptionsProvider provider) { - this.stateDescriptionProvider = provider; - } - - protected void unsetDynamicStateDescriptionProvider(SqueezeBoxStateDescriptionOptionsProvider provider) { - this.stateDescriptionProvider = null; - } } diff --git a/bundles/org.openhab.binding.squeezebox/src/main/java/org/openhab/binding/squeezebox/internal/SqueezeBoxStateDescriptionOptionsProvider.java b/bundles/org.openhab.binding.squeezebox/src/main/java/org/openhab/binding/squeezebox/internal/SqueezeBoxStateDescriptionOptionsProvider.java index ec8b60a4be0ba..9b7fe06344c0d 100644 --- a/bundles/org.openhab.binding.squeezebox/src/main/java/org/openhab/binding/squeezebox/internal/SqueezeBoxStateDescriptionOptionsProvider.java +++ b/bundles/org.openhab.binding.squeezebox/src/main/java/org/openhab/binding/squeezebox/internal/SqueezeBoxStateDescriptionOptionsProvider.java @@ -16,6 +16,7 @@ import org.eclipse.smarthome.core.thing.binding.BaseDynamicStateDescriptionProvider; import org.eclipse.smarthome.core.thing.i18n.ChannelTypeI18nLocalizationService; import org.eclipse.smarthome.core.thing.type.DynamicStateDescriptionProvider; +import org.osgi.service.component.annotations.Activate; import org.osgi.service.component.annotations.Component; import org.osgi.service.component.annotations.Reference; @@ -29,14 +30,9 @@ @NonNullByDefault public class SqueezeBoxStateDescriptionOptionsProvider extends BaseDynamicStateDescriptionProvider { - @Reference - protected void setChannelTypeI18nLocalizationService( - final ChannelTypeI18nLocalizationService channelTypeI18nLocalizationService) { + @Activate + public SqueezeBoxStateDescriptionOptionsProvider( + @Reference ChannelTypeI18nLocalizationService channelTypeI18nLocalizationService) { this.channelTypeI18nLocalizationService = channelTypeI18nLocalizationService; } - - protected void unsetChannelTypeI18nLocalizationService( - final ChannelTypeI18nLocalizationService channelTypeI18nLocalizationService) { - this.channelTypeI18nLocalizationService = null; - } } diff --git a/bundles/org.openhab.binding.squeezebox/src/main/java/org/openhab/binding/squeezebox/internal/discovery/SqueezeBoxPlayerDiscoveryParticipant.java b/bundles/org.openhab.binding.squeezebox/src/main/java/org/openhab/binding/squeezebox/internal/discovery/SqueezeBoxPlayerDiscoveryParticipant.java index 627623739c162..914db342b3827 100644 --- a/bundles/org.openhab.binding.squeezebox/src/main/java/org/openhab/binding/squeezebox/internal/discovery/SqueezeBoxPlayerDiscoveryParticipant.java +++ b/bundles/org.openhab.binding.squeezebox/src/main/java/org/openhab/binding/squeezebox/internal/discovery/SqueezeBoxPlayerDiscoveryParticipant.java @@ -67,6 +67,7 @@ public SqueezeBoxPlayerDiscoveryParticipant(SqueezeBoxServerHandler squeezeBoxSe protected void startScan() { logger.debug("startScan invoked in SqueezeBoxPlayerDiscoveryParticipant"); this.squeezeBoxServerHandler.requestPlayers(); + this.squeezeBoxServerHandler.requestFavorites(); } /* @@ -204,4 +205,8 @@ public void updateFavoritesListEvent(List favorites) { @Override public void sourceChangeEvent(String mac, String source) { } + + @Override + public void buttonsChangeEvent(String mac, String likeCommand, String unlikeCommand) { + } } diff --git a/bundles/org.openhab.binding.squeezebox/src/main/java/org/openhab/binding/squeezebox/internal/dto/ButtonDTO.java b/bundles/org.openhab.binding.squeezebox/src/main/java/org/openhab/binding/squeezebox/internal/dto/ButtonDTO.java new file mode 100644 index 0000000000000..38681045c855b --- /dev/null +++ b/bundles/org.openhab.binding.squeezebox/src/main/java/org/openhab/binding/squeezebox/internal/dto/ButtonDTO.java @@ -0,0 +1,65 @@ +/** + * Copyright (c) 2010-2020 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.squeezebox.internal.dto; + +import com.google.gson.annotations.SerializedName; + +/** + * The {@link ButtonDTO} represents a custom button that overrides existing + * button functionality. For example, "like song" replaces the repeat button. + * + * @author Mark Hilbush - Initial contribution + */ +public class ButtonDTO { + + /** + * Indicates whether button is standard or custom + */ + public Boolean custom; + + /** + * Indicates if standard button is enabled or disabled + */ + public Boolean enabled; + + /** + * Concatenation of elements of command array + */ + public String command; + + /** + * Currently not used + */ + @SerializedName("icon") + public String icon; + + /** + * Currently not used + */ + @SerializedName("jiveStyle") + public String jiveStyle; + + /** + * Currently not used + */ + @SerializedName("tooltip") + public String toolTip; + + public boolean isCustom() { + return custom == null ? Boolean.FALSE : custom; + } + + public boolean isEnabled() { + return enabled == null ? Boolean.FALSE : enabled; + } +} diff --git a/bundles/org.openhab.binding.squeezebox/src/main/java/org/openhab/binding/squeezebox/internal/dto/ButtonDTODeserializer.java b/bundles/org.openhab.binding.squeezebox/src/main/java/org/openhab/binding/squeezebox/internal/dto/ButtonDTODeserializer.java new file mode 100644 index 0000000000000..41266065a3a50 --- /dev/null +++ b/bundles/org.openhab.binding.squeezebox/src/main/java/org/openhab/binding/squeezebox/internal/dto/ButtonDTODeserializer.java @@ -0,0 +1,58 @@ +/** + * Copyright (c) 2010-2020 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.squeezebox.internal.dto; + +import java.lang.reflect.Type; +import java.util.List; +import java.util.stream.Collectors; +import java.util.stream.StreamSupport; + +import org.apache.commons.lang.StringUtils; + +import com.google.gson.JsonDeserializationContext; +import com.google.gson.JsonDeserializer; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonParseException; + +/** + * The {@link ButtonDTODeserializer} is responsible for deserializing a button object, which + * can either be an Integer, or a custom button specification. + * + * @author Mark Hilbush - Initial contribution + */ +public class ButtonDTODeserializer implements JsonDeserializer { + + @Override + public ButtonDTO deserialize(JsonElement jsonElement, Type tyoeOfT, JsonDeserializationContext context) + throws JsonParseException { + ButtonDTO button = null; + if (jsonElement.isJsonPrimitive() && jsonElement.getAsJsonPrimitive().isNumber()) { + Integer value = jsonElement.getAsInt(); + button = new ButtonDTO(); + button.custom = Boolean.FALSE; + button.enabled = value == 0 ? Boolean.FALSE : Boolean.TRUE; + } else if (jsonElement.isJsonObject()) { + JsonObject jsonObject = jsonElement.getAsJsonObject(); + button = new ButtonDTO(); + button.custom = Boolean.TRUE; + button.icon = jsonObject.get("icon").getAsString(); + button.jiveStyle = jsonObject.get("jiveStyle").getAsString(); + button.toolTip = jsonObject.get("tooltip").getAsString(); + List commandList = StreamSupport.stream(jsonObject.getAsJsonArray("command").spliterator(), false) + .map(JsonElement::getAsString).collect(Collectors.toList()); + button.command = StringUtils.join(commandList, " "); + } + return button; + } +} diff --git a/bundles/org.openhab.binding.squeezebox/src/main/java/org/openhab/binding/squeezebox/internal/dto/ButtonsDTO.java b/bundles/org.openhab.binding.squeezebox/src/main/java/org/openhab/binding/squeezebox/internal/dto/ButtonsDTO.java new file mode 100644 index 0000000000000..f65c028fa695d --- /dev/null +++ b/bundles/org.openhab.binding.squeezebox/src/main/java/org/openhab/binding/squeezebox/internal/dto/ButtonsDTO.java @@ -0,0 +1,53 @@ +/** + * Copyright (c) 2010-2020 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.squeezebox.internal.dto; + +import com.google.gson.annotations.SerializedName; + +/** + * The {@link ButtonsDTO} contains information about the forward, rewind, repeat, + * and shuffle buttons, including any custom definitions, such as replacing repeat + * and shuffle with like and unlike, respectively. + * + * @author Mark Hilbush - Initial contribution + */ +public class ButtonsDTO { + + /** + * Indicates if forward button is enabled/disabled, + * or if there is a custom button definition. + */ + @SerializedName("fwd") + public ButtonDTO forward; + + /** + * Indicates if rewind button is enabled/disabled, + * or if there is a custom button definition. + */ + @SerializedName("rew") + public ButtonDTO rewind; + + /** + * Indicates if repeat button is enabled/disabled, + * or if there is a custom button definition. + */ + @SerializedName("repeat") + public ButtonDTO repeat; + + /** + * Indicates if shuffle button is enabled/disabled, + * or if there is a custom button definition. + */ + @SerializedName("shuffle") + public ButtonDTO shuffle; +} diff --git a/bundles/org.openhab.binding.squeezebox/src/main/java/org/openhab/binding/squeezebox/internal/dto/RemoteMetaDTO.java b/bundles/org.openhab.binding.squeezebox/src/main/java/org/openhab/binding/squeezebox/internal/dto/RemoteMetaDTO.java new file mode 100644 index 0000000000000..196da72b20d60 --- /dev/null +++ b/bundles/org.openhab.binding.squeezebox/src/main/java/org/openhab/binding/squeezebox/internal/dto/RemoteMetaDTO.java @@ -0,0 +1,77 @@ +/** + * Copyright (c) 2010-2020 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.squeezebox.internal.dto; + +import com.google.gson.annotations.SerializedName; + +/** + * The {@link RemoteMetaDTO} contains remote metadata information, including button and + * button override functionality. + * + * @author Mark Hilbush - Initial contribution + */ +public class RemoteMetaDTO { + + /** + * Contains button specifications for forward, rewind, repeat, shuffle + */ + public ButtonsDTO buttons; + + /** + * Currently unused + */ + @SerializedName("id") + public String id; + + /** + * Currently unused + */ + @SerializedName("title") + public String title; + + /** + * Currently unused + */ + @SerializedName("artist") + public String artist; + + /** + * Currently unused + */ + @SerializedName("album") + public String album; + + /** + * Currently unused + */ + @SerializedName("artwork_url") + public String artworkUrl; + + /** + * Currently unused + */ + @SerializedName("coverart") + public String coverart; + + /** + * Currently unused + */ + @SerializedName("coverid") + public String coverid; + + /** + * Currently unused + */ + @SerializedName("year") + public String year; +} diff --git a/bundles/org.openhab.binding.squeezebox/src/main/java/org/openhab/binding/squeezebox/internal/dto/StatusResponseDTO.java b/bundles/org.openhab.binding.squeezebox/src/main/java/org/openhab/binding/squeezebox/internal/dto/StatusResponseDTO.java new file mode 100644 index 0000000000000..c463f15476e3c --- /dev/null +++ b/bundles/org.openhab.binding.squeezebox/src/main/java/org/openhab/binding/squeezebox/internal/dto/StatusResponseDTO.java @@ -0,0 +1,47 @@ +/** + * Copyright (c) 2010-2020 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.squeezebox.internal.dto; + +import com.google.gson.annotations.SerializedName; + +/** + * The {@link StatusResponseDTO} is the response received from a player status request. + * + * @author Mark Hilbush - Initial contribution + */ +public class StatusResponseDTO { + + /** + * Id. Currently unused. + */ + @SerializedName("id") + public String id; + + /** + * Method name. Normally "slim.request" + */ + @SerializedName("method") + public String method; + + /** + * Parameters passed in the query. Currently unused. + */ + @SerializedName("params") + public Object params; + + /** + * Contains the result of the query + */ + @SerializedName("result") + public StatusResultDTO result; +} diff --git a/bundles/org.openhab.binding.squeezebox/src/main/java/org/openhab/binding/squeezebox/internal/dto/StatusResultDTO.java b/bundles/org.openhab.binding.squeezebox/src/main/java/org/openhab/binding/squeezebox/internal/dto/StatusResultDTO.java new file mode 100644 index 0000000000000..63a37607a6e54 --- /dev/null +++ b/bundles/org.openhab.binding.squeezebox/src/main/java/org/openhab/binding/squeezebox/internal/dto/StatusResultDTO.java @@ -0,0 +1,93 @@ +/** + * Copyright (c) 2010-2020 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.squeezebox.internal.dto; + +import com.google.gson.annotations.SerializedName; + +/** + * The {@link StatusResultDTO} represents the result of a status request. + * + * @author Mark Hilbush - Initial contribution + */ +public class StatusResultDTO { + + /** + * Remote metadata information, including button definitions/redefinitions. + */ + @SerializedName("remoteMeta") + public RemoteMetaDTO remoteMeta; + + /** + * These remaining fields are currently unused by the binding, + * as they also are returned by the Command Line Interface (CLI). + */ + @SerializedName("current_title") + public String currentTitle; + + @SerializedName("digital_volume_control") + public Integer digitalVolumeControl; + + @SerializedName("duration") + public Double duration; + + @SerializedName("mixer volume") + public Integer mixerVolume; + + @SerializedName("player_connected") + public Integer playerConnected; + + @SerializedName("player_ip") + public String playerIpAddress; + + @SerializedName("player_name") + public String playerName; + + @SerializedName("playlist mode") + public String playlistMode; + + @SerializedName("playlist repeat") + public Integer playlistRepeat; + + @SerializedName("playlist shuffle") + public Integer playlistShuffle; + + @SerializedName("playlist_cur_index") + public String playListCurrentIndex; + + @SerializedName("playlist_timestamp") + public String playlistTimestamp; + + @SerializedName("playlist_tracks") + public Integer playlistTracks; + + @SerializedName("power") + public String power; + + @SerializedName("rate") + public String rate; + + @SerializedName("remote") + public String remote; + + @SerializedName("repeating_stream") + public Integer repeatingStream; + + @SerializedName("seq_no") + public Integer sequenceNumber; + + @SerializedName("signalstrength") + public Integer signalStrength; + + @SerializedName("time") + public String time; +} diff --git a/bundles/org.openhab.binding.squeezebox/src/main/java/org/openhab/binding/squeezebox/internal/handler/SqueezeBoxNotificationListener.java b/bundles/org.openhab.binding.squeezebox/src/main/java/org/openhab/binding/squeezebox/internal/handler/SqueezeBoxNotificationListener.java index 60bb6318659a4..1318668b71a20 100644 --- a/bundles/org.openhab.binding.squeezebox/src/main/java/org/openhab/binding/squeezebox/internal/handler/SqueezeBoxNotificationListener.java +++ b/bundles/org.openhab.binding.squeezebox/src/main/java/org/openhab/binding/squeezebox/internal/handler/SqueezeBoxNotificationListener.java @@ -216,4 +216,8 @@ public void updateFavoritesListEvent(List favorites) { @Override public void sourceChangeEvent(String mac, String source) { } + + @Override + public void buttonsChangeEvent(String mac, String likeCommand, String unlikeCommand) { + } } diff --git a/bundles/org.openhab.binding.squeezebox/src/main/java/org/openhab/binding/squeezebox/internal/handler/SqueezeBoxPlayerEventListener.java b/bundles/org.openhab.binding.squeezebox/src/main/java/org/openhab/binding/squeezebox/internal/handler/SqueezeBoxPlayerEventListener.java index 913bff31d4aad..8fca220d1e272 100644 --- a/bundles/org.openhab.binding.squeezebox/src/main/java/org/openhab/binding/squeezebox/internal/handler/SqueezeBoxPlayerEventListener.java +++ b/bundles/org.openhab.binding.squeezebox/src/main/java/org/openhab/binding/squeezebox/internal/handler/SqueezeBoxPlayerEventListener.java @@ -80,4 +80,6 @@ public interface SqueezeBoxPlayerEventListener { void updateFavoritesListEvent(List favorites); void sourceChangeEvent(String mac, String source); + + void buttonsChangeEvent(String mac, String likeCommand, String unlikeCommand); } diff --git a/bundles/org.openhab.binding.squeezebox/src/main/java/org/openhab/binding/squeezebox/internal/handler/SqueezeBoxPlayerHandler.java b/bundles/org.openhab.binding.squeezebox/src/main/java/org/openhab/binding/squeezebox/internal/handler/SqueezeBoxPlayerHandler.java index 855429deeb375..bd18aa81274c0 100644 --- a/bundles/org.openhab.binding.squeezebox/src/main/java/org/openhab/binding/squeezebox/internal/handler/SqueezeBoxPlayerHandler.java +++ b/bundles/org.openhab.binding.squeezebox/src/main/java/org/openhab/binding/squeezebox/internal/handler/SqueezeBoxPlayerHandler.java @@ -68,6 +68,7 @@ * @author Patrik Gfeller - Timeout for TTS messages increased from 30 to 90s. * @author Mark Hilbush - Get favorites from server and play favorite * @author Mark Hilbush - Convert sound notification volume from channel to config parameter + * @author Mark Hilbush - Add like/unlike functionality */ public class SqueezeBoxPlayerHandler extends BaseThingHandler implements SqueezeBoxPlayerEventListener { private final Logger logger = LoggerFactory.getLogger(SqueezeBoxPlayerHandler.class); @@ -84,7 +85,7 @@ public class SqueezeBoxPlayerHandler extends BaseThingHandler implements Squeeze /** * Keeps current track time */ - ScheduledFuture timeCounterJob; + private ScheduledFuture timeCounterJob; /** * Local reference to our bridge @@ -120,6 +121,9 @@ public class SqueezeBoxPlayerHandler extends BaseThingHandler implements Squeeze private static final ExpiringCacheMap IMAGE_CACHE = new ExpiringCacheMap<>( TimeUnit.MINUTES.toMillis(15)); // 15min + private String likeCommand; + private String unlikeCommand; + /** * Creates SqueezeBox Player Handler * @@ -138,7 +142,7 @@ public void initialize() { mac = getConfig().as(SqueezeBoxPlayerConfig.class).mac; timeCounter(); updateBridgeStatus(); - logger.debug("player thing {} initialized.", getThing().getUID()); + logger.debug("player thing {} initialized with mac {}", getThing().getUID(), mac); } @Override @@ -147,12 +151,17 @@ public void bridgeStatusChanged(ThingStatusInfo bridgeStatusInfo) { } private void updateBridgeStatus() { - ThingStatus bridgeStatus = getBridge().getStatus(); - if (bridgeStatus == ThingStatus.ONLINE && getThing().getStatus() != ThingStatus.ONLINE) { - updateStatus(ThingStatus.ONLINE, ThingStatusDetail.NONE); - squeezeBoxServerHandler = (SqueezeBoxServerHandler) getBridge().getHandler(); - } else if (bridgeStatus == ThingStatus.OFFLINE) { - updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE); + Thing bridge = getBridge(); + if (bridge != null) { + squeezeBoxServerHandler = (SqueezeBoxServerHandler) bridge.getHandler(); + ThingStatus bridgeStatus = bridge.getStatus(); + if (bridgeStatus == ThingStatus.ONLINE && getThing().getStatus() != ThingStatus.ONLINE) { + updateStatus(ThingStatus.ONLINE, ThingStatusDetail.NONE); + } else if (bridgeStatus == ThingStatus.OFFLINE) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE); + } + } else { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "Bridge not found"); } } @@ -167,18 +176,16 @@ public void dispose() { if (squeezeBoxServerHandler != null) { squeezeBoxServerHandler.removePlayerCache(mac); } - logger.debug("player thing {} disposed.", getThing().getUID()); + logger.debug("player thing {} disposed for mac {}", getThing().getUID(), mac); super.dispose(); } @Override public void handleCommand(ChannelUID channelUID, Command command) { if (squeezeBoxServerHandler == null) { - logger.info("player thing {} has no server configured, ignoring command: {}", getThing().getUID(), command); + logger.debug("Player {} has no server configured, ignoring command: {}", getThing().getUID(), command); return; } - String mac = getConfigAs(SqueezeBoxPlayerConfig.class).mac; - // Some of the code below is not designed to handle REFRESH if (command == RefreshType.REFRESH) { return; @@ -289,6 +296,16 @@ public void handleCommand(ChannelUID channelUID, Command command) { case CHANNEL_FAVORITES_PLAY: squeezeBoxServerHandler.playFavorite(mac, command.toString()); break; + case CHANNEL_LIKE: + if (likeCommand != null) { + squeezeBoxServerHandler.like(mac, likeCommand); + } + break; + case CHANNEL_UNLIKE: + if (unlikeCommand != null) { + squeezeBoxServerHandler.unlike(mac, unlikeCommand); + } + break; default: break; } @@ -489,9 +506,18 @@ public void irCodeChangeEvent(String mac, String ircode) { } } + @Override + public void buttonsChangeEvent(String mac, String likeCommand, String unlikeCommand) { + if (isMe(mac)) { + this.likeCommand = likeCommand; + this.unlikeCommand = unlikeCommand; + logger.trace("Player {} got a button change event: like='{}' unlike='{}'", mac, likeCommand, unlikeCommand); + } + } + @Override public void updateFavoritesListEvent(List favorites) { - logger.debug("Player {} updating favorites list", mac); + logger.trace("Player {} updating favorites list with {} favorites", mac, favorites.size()); List options = new ArrayList<>(); for (Favorite favorite : favorites) { options.add(new StateOption(favorite.shortId, favorite.name)); diff --git a/bundles/org.openhab.binding.squeezebox/src/main/java/org/openhab/binding/squeezebox/internal/handler/SqueezeBoxServerHandler.java b/bundles/org.openhab.binding.squeezebox/src/main/java/org/openhab/binding/squeezebox/internal/handler/SqueezeBoxServerHandler.java index acb37c1d99c23..380ee5b88fc22 100644 --- a/bundles/org.openhab.binding.squeezebox/src/main/java/org/openhab/binding/squeezebox/internal/handler/SqueezeBoxServerHandler.java +++ b/bundles/org.openhab.binding.squeezebox/src/main/java/org/openhab/binding/squeezebox/internal/handler/SqueezeBoxServerHandler.java @@ -24,8 +24,10 @@ import java.net.URLDecoder; import java.net.URLEncoder; import java.nio.charset.StandardCharsets; +import java.time.Duration; import java.util.ArrayList; import java.util.Arrays; +import java.util.Base64; import java.util.Collections; import java.util.HashMap; import java.util.List; @@ -47,11 +49,20 @@ import org.eclipse.smarthome.core.thing.binding.ThingHandler; import org.eclipse.smarthome.core.types.Command; import org.eclipse.smarthome.core.types.UnDefType; +import org.eclipse.smarthome.io.net.http.HttpRequestBuilder; import org.openhab.binding.squeezebox.internal.config.SqueezeBoxServerConfig; +import org.openhab.binding.squeezebox.internal.dto.ButtonDTO; +import org.openhab.binding.squeezebox.internal.dto.ButtonDTODeserializer; +import org.openhab.binding.squeezebox.internal.dto.ButtonsDTO; +import org.openhab.binding.squeezebox.internal.dto.StatusResponseDTO; import org.openhab.binding.squeezebox.internal.model.Favorite; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.JsonSyntaxException; + /** * Handles connection and event handling to a SqueezeBox Server. * @@ -66,6 +77,7 @@ * @author Philippe Siem - Improve refresh of cover art url,remote title, artist, album, genre, year. * @author Patrik Gfeller - Support for mixer volume message added * @author Mark Hilbush - Get favorites from LMS; update channel and send to players + * @author Mark Hilbush - Add like/unlike functionality */ public class SqueezeBoxServerHandler extends BaseBridgeHandler { private final Logger logger = LoggerFactory.getLogger(SqueezeBoxServerHandler.class); @@ -86,6 +98,8 @@ public class SqueezeBoxServerHandler extends BaseBridgeHandler { private static final String CHANNEL_CONFIG_QUOTE_LIST = "quoteList"; + private static final String JSONRPC_STATUS_REQUEST = "{\"id\":1,\"method\":\"slim.request\",\"params\":[\"@@MAC@@\",[\"status\",\"-\",\"tags:yagJlNKjcB\"]]}"; + private List squeezeBoxPlayerListeners = Collections .synchronizedList(new ArrayList<>()); @@ -106,6 +120,11 @@ public class SqueezeBoxServerHandler extends BaseBridgeHandler { private String password; + private final Gson gson = new GsonBuilder().registerTypeAdapter(ButtonDTO.class, new ButtonDTODeserializer()) + .create(); + private String jsonRpcUrl; + private String basicAuthorization; + public SqueezeBoxServerHandler(Bridge bridge) { super(bridge); } @@ -275,6 +294,14 @@ public void playFavorite(String mac, String favorite) { sendCommand(mac + " favorites playlist play item_id:" + favorite); } + public void like(String mac, String command) { + sendCommand(mac + " " + command); + } + + public void unlike(String mac, String command) { + sendCommand(mac + " " + command); + } + /** * Send a generic command to a given player * @@ -306,6 +333,8 @@ public void login() { if (StringUtils.isEmpty(userId)) { return; } + // Create basic auth string for jsonrpc interface + basicAuthorization = new String(Base64.getEncoder().encode((userId + ":" + password).getBytes())); logger.debug("Logging into Squeeze Server using userId={}", userId); sendCommand("login " + userId + " " + password); } @@ -361,6 +390,9 @@ private void connect() { updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.CONFIGURATION_ERROR, "host is not set"); return; } + // Create URL for jsonrpc interface + jsonRpcUrl = String.format("http://%s:%d/jsonrpc.js", host, webport); + try { clientSocket = new Socket(host, cliport); } catch (IOException e) { @@ -377,7 +409,6 @@ private void connect() { } catch (IllegalThreadStateException e) { updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage()); } - // Mark the server ONLINE. bridgeStatusChanged will cause the players to come ONLINE updateStatus(ThingStatus.ONLINE); } @@ -426,7 +457,7 @@ public void run() { login(); updateStatus(ThingStatus.ONLINE); requestPlayers(); - requestFavorites(); + scheduleRequestFavorites(); sendCommand("listen 1"); String message = null; @@ -474,7 +505,6 @@ public void run() { "end of stream on socket read"); scheduleReconnect(); } - logger.debug("Squeeze Server listener exiting."); } @@ -598,7 +628,6 @@ private void handleMixerMessage(String mac, String[] messageParts) { switch (action) { case "volume": String volumeStringValue = decode(messageParts[3]); - updatePlayer(new PlayerUpdateEvent() { @Override public void updateListener(SqueezeBoxPlayerEventListener listener) { @@ -836,6 +865,10 @@ private void handlePlaylistMessage(final String mac, String[] messageParts) { String mode; if (action.equals("newsong")) { mode = "play"; + // Execute in separate thread to avoid delaying listener + scheduler.execute(() -> { + updateCustomButtons(mac); + }); // Set the track duration to 0 updatePlayer(new PlayerUpdateEvent() { @Override @@ -862,7 +895,6 @@ public void updateListener(SqueezeBoxPlayerEventListener listener) { } final String value = mode; updatePlayer(new PlayerUpdateEvent() { - @Override public void updateListener(SqueezeBoxPlayerEventListener listener) { listener.modeChangeEvent(mac, value); @@ -872,9 +904,7 @@ public void updateListener(SqueezeBoxPlayerEventListener listener) { private void handleSourceChangeMessage(String mac, String rawSource) { String source = URLDecoder.decode(rawSource); - updatePlayer(new PlayerUpdateEvent() { - @Override public void updateListener(SqueezeBoxPlayerEventListener listener) { listener.sourceChangeEvent(mac, source); @@ -886,12 +916,10 @@ private void handlePrefsetMessage(final String mac, String[] messageParts) { if (messageParts.length < 5) { return; } - // server prefsets if (messageParts[2].equals("server")) { String function = messageParts[3]; String value = messageParts[4]; - if (function.equals("power")) { final boolean power = value.equals("1"); updatePlayer(new PlayerUpdateEvent() { @@ -903,7 +931,6 @@ public void updateListener(SqueezeBoxPlayerEventListener listener) { } else if (function.equals("volume")) { final int volume = (int) Double.parseDouble(value); updatePlayer(new PlayerUpdateEvent() { - @Override public void updateListener(SqueezeBoxPlayerEventListener listener) { listener.absoluteVolumeChangeEvent(mac, volume); @@ -914,8 +941,6 @@ public void updateListener(SqueezeBoxPlayerEventListener listener) { } private void handleFavorites(String message) { - logger.trace("Handle favorites message: {}", message); - String[] messageParts = message.split("\\s"); if (messageParts.length == 2 && "changed".equals(messageParts[1])) { // LMS informing us that favorites have changed; request an update to the favorites list @@ -998,6 +1023,64 @@ private void updateChannelFavoritesList(List favorites) { updateState(CHANNEL_FAVORITES_LIST, new StringType(favoritesList)); } } + + private void scheduleRequestFavorites() { + // Delay the execution to give the player thing handlers a chance to initialize + scheduler.schedule(() -> { + requestFavorites(); + }, 3L, TimeUnit.SECONDS); + } + + private void updateCustomButtons(final String mac) { + String response = executePost(jsonRpcUrl, JSONRPC_STATUS_REQUEST.replace("@@MAC@@", mac)); + if (response != null) { + logger.trace("Status response: {}", response); + String likeCommand = null; + String unlikeCommand = null; + try { + StatusResponseDTO status = gson.fromJson(response, StatusResponseDTO.class); + if (status != null && status.result != null && status.result.remoteMeta != null + && status.result.remoteMeta.buttons != null) { + ButtonsDTO buttons = status.result.remoteMeta.buttons; + if (buttons.repeat != null && buttons.repeat.isCustom()) { + likeCommand = buttons.repeat.command; + } + if (buttons.shuffle != null && buttons.shuffle.isCustom()) { + unlikeCommand = buttons.shuffle.command; + } + } + } catch (JsonSyntaxException e) { + logger.debug("JsonSyntaxException parsing status response: {}", response, e); + } + final String like = likeCommand; + final String unlike = unlikeCommand; + updatePlayer(new PlayerUpdateEvent() { + @Override + public void updateListener(SqueezeBoxPlayerEventListener listener) { + listener.buttonsChangeEvent(mac, like, unlike); + } + }); + } + } + + private String executePost(String url, String content) { + // @formatter:off + HttpRequestBuilder builder = HttpRequestBuilder.postTo(url) + .withTimeout(Duration.ofSeconds(5)) + .withContent(content) + .withHeader("charset", "utf-8") + .withHeader("Content-Type", "application/json"); + // @formatter:on + if (basicAuthorization != null) { + builder = builder.withHeader("Authorization", "Basic " + basicAuthorization); + } + try { + return builder.getContentAsString(); + } catch (IOException e) { + logger.debug("Bridge: IOException on jsonrpc call: {}", e.getMessage(), e); + return null; + } + } } /** diff --git a/bundles/org.openhab.binding.squeezebox/src/main/resources/ESH-INF/thing/thing-types.xml b/bundles/org.openhab.binding.squeezebox/src/main/resources/ESH-INF/thing/thing-types.xml index 6484988745efa..47af286cd635e 100644 --- a/bundles/org.openhab.binding.squeezebox/src/main/resources/ESH-INF/thing/thing-types.xml +++ b/bundles/org.openhab.binding.squeezebox/src/main/resources/ESH-INF/thing/thing-types.xml @@ -81,6 +81,8 @@ + + @@ -294,4 +296,14 @@ Number of playlist tracks + + Switch + + Likes the current song (if the service supports it) + + + Switch + + Unlikes the current song (if the service supports it) +