added migrated 2.x add-ons

Signed-off-by: Kai Kreuzer <kai@openhab.org>
This commit is contained in:
Kai Kreuzer
2020-09-21 01:58:32 +02:00
parent bbf1a7fd29
commit 6df6783b60
11662 changed files with 1302875 additions and 11 deletions

View File

@@ -0,0 +1,32 @@
<?xml version="1.0" encoding="UTF-8"?>
<classpath>
<classpathentry kind="src" output="target/classes" path="src/main/java">
<attributes>
<attribute name="optional" value="true"/>
<attribute name="maven.pomderived" value="true"/>
</attributes>
</classpathentry>
<classpathentry excluding="**" kind="src" output="target/classes" path="src/main/resources">
<attributes>
<attribute name="maven.pomderived" value="true"/>
</attributes>
</classpathentry>
<classpathentry kind="src" output="target/test-classes" path="src/test/java">
<attributes>
<attribute name="optional" value="true"/>
<attribute name="maven.pomderived" value="true"/>
<attribute name="test" value="true"/>
</attributes>
</classpathentry>
<classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JavaSE-11">
<attributes>
<attribute name="maven.pomderived" value="true"/>
</attributes>
</classpathentry>
<classpathentry kind="con" path="org.eclipse.m2e.MAVEN2_CLASSPATH_CONTAINER">
<attributes>
<attribute name="maven.pomderived" value="true"/>
</attributes>
</classpathentry>
<classpathentry kind="output" path="target/classes"/>
</classpath>

View File

@@ -0,0 +1,23 @@
<?xml version="1.0" encoding="UTF-8"?>
<projectDescription>
<name>org.openhab.binding.squeezebox</name>
<comment></comment>
<projects>
</projects>
<buildSpec>
<buildCommand>
<name>org.eclipse.jdt.core.javabuilder</name>
<arguments>
</arguments>
</buildCommand>
<buildCommand>
<name>org.eclipse.m2e.core.maven2Builder</name>
<arguments>
</arguments>
</buildCommand>
</buildSpec>
<natures>
<nature>org.eclipse.jdt.core.javanature</nature>
<nature>org.eclipse.m2e.core.maven2Nature</nature>
</natures>
</projectDescription>

View File

@@ -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

View File

@@ -0,0 +1,235 @@
# Logitech Squeezebox Binding
This binding integrates the [Logitech Media Server](https://www.mysqueezebox.com) and compatible Squeeze players.
## Introduction
Slim Devices was established in 2000, and was first known for its SlimServer used for streaming music, but launched a hardware player named SliMP3 able to play these streams in 2001.
Although the first player was fairly simple only supporting wired Ethernet and MP3 natively, it was followed two years later by a slightly more advanced player which was renamed to Squeezebox.
Other versions followed, gradually adding native support for additional file formats, Wi-Fi-support, gradually adding larger and more advanced displays as well as a version targeting audiophile users.
Support for playing music from external streaming platforms such as Pandora, Napster, Last.fm and Sirius were also added.
The devices in general have two operating modes; either standalone where the device connects to an internet streaming service directly, or to a local computer running the Logitech Media Server or a network-attached storage device.
Both the server software and large parts of the firmware on the most recent players are released under open source licenses.
In 2006, Slim Devices was acquired by Logitech for $20 million USD.
Logitech continued the development of the player until they announced in August 2012 that it would be discontinued.
Given the cross-platform nature of the server and software client, some users have ensured the continued use of the platform by utilizing the Raspberry Pi as dedicated Squeezebox device (both client and server).
Taken from: [Wiki](https://en.wikipedia.org/wiki/Squeezebox_%28network_music_player%29)
## Supported Things
At least one Squeeze Server is required to act as a bridge for Squeeze players on the network.
Squeeze players may be official Logitech products or other players like [Squeeze Lites](https://code.google.com/p/squeezelite/).
## Discovery
A Squeeze Server is discovered through UPnP in the local network.
Once it is added as a Thing the Squeeze Server bridge will discover Squeeze Players automatically.
## Binding Configuration
The binding has the following configuration options, which can be set for "binding:squeezebox":
| Parameter | Name | Description | Required |
|-------------|------------------|--------------------------------------------------------------------------|----------|
| callbackUrl | Callback URL | URL to use for playing notification sounds, e.g. http://192.168.0.2:8080 | no |
When a SqueezeBox is used as an audio sink, the SqueezeBox player connects to openHAB to get the audio stream.
By default, the binding sends the SqueezeBox the URL for getting the audio stream based on the Primary
Address (Network Settings configuration) and the openHAB HTTP port.
Sometimes it is necessary to use the Callback URL to override the default, such as when using a reverse proxy or with some Docker implementations.
## Thing Configuration
The Squeeze Server bridge requires the ip address, web port, and cli port to access it on.
If Squeeze Server authentication is enabled, the userId and password also are required.
Squeeze Players are identified by their MAC address, which is required.
In addition, the notification timeout can be specified.
If omitted, the default timeout value will be used.
A notification volume can be optionally specified, which, if provided, will override the player's current volume level when playing notifications.
Here are some examples of how to define the Squeeze Server and Player things in a things file.
```
Bridge squeezebox:squeezeboxserver:myServer [ ipAddress="192.168.1.10", webport=9000, cliport=9090 ]
{
Thing squeezeboxplayer myplayer[ mac="00:f1:bb:00:00:f1" ]
}
```
If Squeeze Server authentication is enabled, the user ID and password can be specified for the Squeeze Server:
```
Bridge squeezebox:squeezeboxserver:myServer [ ipAddress="192.168.1.10", webport=9000, cliport=9090, userId="yourid", password="yourpassword" ]
{
Thing squeezeboxplayer myplayer[ mac="00:f1:bb:00:00:f1" ]
}
```
The notification timeout and/or notification volume can be specified for the Squeeze Player:
```
Bridge squeezebox:squeezeboxserver:myServer [ ipAddress="192.168.1.10", webport=9000, cliport=9090 ]
{
Thing squeezeboxplayer myplayer[ mac="00:f1:bb:00:00:f1", notificationTimeout=30, notificationVolume=35 ]
}
```
## Server Channels
The Squeezebox server supports the following channel:
| Channel Type ID | Item Type | Description |
|-------------------------|-----------|----------------------------------------------------------------------------------------|
| favoritesList | String | Comma-separated list of favorite IDs & names, updated whenever list changes on server |
## Player Channels
All devices support some of the following channels:
| Channel Type ID | Item Type | Description |
|-------------------------|-----------|----------------------------------------------------------------------------------------|
| power | Switch | Power on/off your device |
| mute | Switch | Mute/unmute your device |
| volume | Dimmer | Volume of your device |
| stop | Switch | Stop the current title |
| control | Player | Control the Zone Player, e.g. play/pause/next/previous/ffward/rewind |
| stream | String | Play the given HTTP or file stream (file:// or http://) |
| source | String | Shows the source of the currently playing playlist entry. (i.e. http://radio.org/radio.mp3 |
| sync | String | Add another player to your device for synchronized playback (other player mac address) |
| playListIndex | Number | Playlist Index |
| currentPlayingTime | Number | Current Playing Time |
| duration | Number | Duration of currently playing track (in seconds) |
| currentPlaylistShuffle | Number | Current playlist shuffle mode (0 No Shuffle, 1 Shuffle Songs, 2 Shuffle Albums) |
| currentPlaylistRepeat | Number | Current playlist repeat Mode (0 No Repeat, 1 Repeat Song, 2 Repeat Playlist) |
| title | String | Title of the current song |
| remotetitle | String | Remote Title (Radio) of the current song |
| album | String | Album name of the current song |
| artist | String | Artist name of the current song |
| year | String | Release year of the current song |
| genre | String | Genre name of the current song |
| coverartdata | Image | Image data of cover art of the current song |
| 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) |
| rate | Switch | "Like" or "unlike" the currently playing song (if supported by the streaming service) |
## Example .Items File
Add the items you want to the .items file. A few examples are shown below, the power, volume and album art channels are connected here to the items by copying across the channel discriptions from the Paper UI. Make suure each channel is linked for your needs See [openHAB New User Configuration documentation](https://www.openhab.org/docs/tutorial/configuration.html) for further details on linking and channels.
```
Switch YourPlayer_Power "Squeezebox Power" {channel="squeezebox:squeezeboxplayer:736549a3:00042016e7a0:power"}
Dimmer YourPlayer_Volume "Squeezebox Volume" {channel="squeezebox:squeezeboxplayer:736549a3:00042016e7a0:volume"}
Image YourPlayer_AlbumArt "Squeezebox Cover" {channel="squeezebox:squeezeboxplayer:736549a3:00042016e7a0:coverartdata"}
```
## Playing Favorites
Using the **playFavorite** channel, you can play a favorite from the *Favorites* list on the Logitech Media Server (LMS).
The favorites from the LMS will be populated into the state options of the **playFavorite** channel.
The Selection widget in HABpanel can be used to present the favorites as a choice list.
Selecting from that choice list will play the favorite on the SqueezeBox player.
Currently, only favorites from the root level of the LMS favorites list are exposed on the **playFavorite** channel.
### How to Set Up Favorites
- Add some favorites to your favorites list in LMS (local music playlists, Pandora, Slacker, Internet radio, etc.).
Keep all favorites at the root level (i.e. favorites in sub-folders will be ignored).
- If you're on an older openHAB build, you may need to delete and re-add your squeezebox server and player things to pick up the new channels.
- Create a new item on each player
```
String YourPlayer_PlayFavorite "Play Favorite [%s]" { channel="squeezebox:squeezeboxplayer:736549a3:00042016e7a0:playFavorite" }
```
#### For HABpanel (do this for each player)
- Add a Selection widget to your dashboard
- In the Selection widget settings
- Enter the **YourPlayer_PlayFavorite** item
- Select *Choices source* of *Server-provided item options*
- Modify other settings to suite your taste
- When you load the dashboard and click on the selection widget, you should see the favorites. Selecting a favorite from the list will play it.
#### For Sitemap
- To use state options on the playFavorite channel, simply omit the *mappings* from the Selection widget.
```
Selection item=YourPlayer_PlayFavorite label="Play Favorite"
```
## Notifications
### How To Set Up
Squeeze Players can be set up as audio sinks in openHAB.
Please follow the [openHAB multimedia documentation](https://www.openhab.org/docs/configuration/multimedia.html) for setup guidance.
You can set the default notification volume in the player thing configuration.
You can override the default notification volume by supplying it as a parameter to `say` and `playSound`.
You can play notifications from within rules.
```
rule "Garage Door Open Notification"
when
Item GarageDoorOpenNotification received command ON
then
// Play the notification on the default sink at a specified volume level
say("The garage door is open!", "voicerss:enUS", new PercentType(35))
// Play the notification on a specific sink
say("The garage door is open!", "voicerss:enUS", "squeezebox:squeezeboxplayer:5919BEA2-764B-4590-BC70-D74DCC15491B:20cfbf221510")
end
```
And, you can play sounds from the `conf/sounds` directory.
```
rule "Play Sounds"
when
Item PlaySounds received command ON
then
// Play the sound on the default sink
playSound("doorbell.mp3")
// Play the sound on a specific sink at a specified volume level
playSound("squeezebox:squeezeboxplayer:5919BEA2-764B-4590-BC70-D74DCC15491B:20cfbf221510", "doorbell.mp3", new PercentType(45))
end
```
## Rating Songs
Some streaming services, such as Pandora and Slacker, all songs to be rated.
When playing from these streaming services, sending commands to the `rate` channel can be used to *like* or *unlike* the currently playing song.
Sending the ON command will *like* the song.
Sending the OFF command will *unlike* the song.
If the streaming service doesn't support rating, sending commands to the `rate` channel has no effect.
### 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.
- 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 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.

View File

@@ -0,0 +1,17 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.openhab.addons.bundles</groupId>
<artifactId>org.openhab.addons.reactor.bundles</artifactId>
<version>3.0.0-SNAPSHOT</version>
</parent>
<artifactId>org.openhab.binding.squeezebox</artifactId>
<name>openHAB Add-ons :: Bundles :: SqueezeBox Binding</name>
</project>

View File

@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<features name="org.openhab.binding.squeezebox-${project.version}" xmlns="http://karaf.apache.org/xmlns/features/v1.4.0">
<repository>mvn:org.openhab.core.features.karaf/org.openhab.core.features.karaf.openhab-core/${ohc.version}/xml/features</repository>
<feature name="openhab-binding-squeezebox" description="Squeezebox Binding" version="${project.version}">
<feature>openhab-runtime-base</feature>
<feature>openhab-transport-upnp</feature>
<bundle start-level="80">mvn:org.openhab.addons.bundles/org.openhab.binding.squeezebox/${project.version}</bundle>
</feature>
</features>

View File

@@ -0,0 +1,141 @@
/**
* 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;
import java.util.HashSet;
import java.util.Locale;
import java.util.Set;
import org.apache.commons.lang.StringUtils;
import org.openhab.binding.squeezebox.internal.handler.SqueezeBoxPlayerHandler;
import org.openhab.core.audio.AudioFormat;
import org.openhab.core.audio.AudioHTTPServer;
import org.openhab.core.audio.AudioSink;
import org.openhab.core.audio.AudioStream;
import org.openhab.core.audio.FileAudioStream;
import org.openhab.core.audio.FixedLengthAudioStream;
import org.openhab.core.audio.URLAudioStream;
import org.openhab.core.audio.UnsupportedAudioFormatException;
import org.openhab.core.audio.UnsupportedAudioStreamException;
import org.openhab.core.audio.utils.AudioStreamUtils;
import org.openhab.core.library.types.PercentType;
import org.openhab.core.library.types.StringType;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* This makes a SqueezeBox Player serve as an {@link AudioSink}-
*
* @author Mark Hilbush - Initial contribution
* @author Mark Hilbush - Add callbackUrl
*/
public class SqueezeBoxAudioSink implements AudioSink {
private final Logger logger = LoggerFactory.getLogger(SqueezeBoxAudioSink.class);
private static final HashSet<AudioFormat> SUPPORTED_FORMATS = new HashSet<>();
private static final HashSet<Class<? extends AudioStream>> SUPPORTED_STREAMS = new HashSet<>();
// Needed because Squeezebox does multiple requests for the stream
private static final int STREAM_TIMEOUT = 15;
private String callbackUrl;
static {
SUPPORTED_FORMATS.add(AudioFormat.WAV);
SUPPORTED_FORMATS.add(AudioFormat.MP3);
SUPPORTED_STREAMS.add(FixedLengthAudioStream.class);
SUPPORTED_STREAMS.add(URLAudioStream.class);
}
private AudioHTTPServer audioHTTPServer;
private SqueezeBoxPlayerHandler playerHandler;
public SqueezeBoxAudioSink(SqueezeBoxPlayerHandler playerHandler, AudioHTTPServer audioHTTPServer,
String callbackUrl) {
this.playerHandler = playerHandler;
this.audioHTTPServer = audioHTTPServer;
this.callbackUrl = callbackUrl;
if (StringUtils.isNotEmpty(callbackUrl)) {
logger.debug("SqueezeBox AudioSink created with callback URL {}", callbackUrl);
}
}
@Override
public String getId() {
return playerHandler.getThing().getUID().toString();
}
@Override
public String getLabel(Locale locale) {
return playerHandler.getThing().getLabel();
}
@Override
public void process(AudioStream audioStream)
throws UnsupportedAudioFormatException, UnsupportedAudioStreamException {
AudioFormat format = audioStream.getFormat();
if (!AudioFormat.WAV.isCompatible(format) && !AudioFormat.MP3.isCompatible(format)) {
throw new UnsupportedAudioFormatException("Currently only MP3 and WAV formats are supported: ", format);
}
String url;
if (audioStream instanceof URLAudioStream) {
url = ((URLAudioStream) audioStream).getURL();
} else if (audioStream instanceof FixedLengthAudioStream) {
// Since Squeezebox will make multiple requests for the stream, set a timeout on the stream
url = audioHTTPServer.serve((FixedLengthAudioStream) audioStream, STREAM_TIMEOUT).toString();
if (AudioFormat.WAV.isCompatible(format)) {
url += AudioStreamUtils.EXTENSION_SEPARATOR + FileAudioStream.WAV_EXTENSION;
} else if (AudioFormat.MP3.isCompatible(format)) {
url += AudioStreamUtils.EXTENSION_SEPARATOR + FileAudioStream.MP3_EXTENSION;
}
// Form the URL for streaming the notification from the OH2 web server
// Use the callback URL if it is set in the binding configuration
String host = StringUtils.isEmpty(callbackUrl) ? playerHandler.getHostAndPort() : callbackUrl;
if (host == null) {
logger.warn("Unable to get host/port from which to stream notification");
return;
}
url = host + url;
} else {
throw new UnsupportedAudioStreamException(
"SqueezeBox can only handle URLAudioStream or FixedLengthAudioStreams.", null);
}
logger.debug("Processing audioStream {} of format {}", url, format);
playerHandler.playNotificationSoundURI(new StringType(url));
}
@Override
public Set<AudioFormat> getSupportedFormats() {
return SUPPORTED_FORMATS;
}
@Override
public Set<Class<? extends AudioStream>> getSupportedStreams() {
return SUPPORTED_STREAMS;
}
@Override
public PercentType getVolume() {
return playerHandler.getNotificationSoundVolume();
}
@Override
public void setVolume(PercentType volume) {
playerHandler.setNotificationSoundVolume(volume);
}
}

View File

@@ -0,0 +1,71 @@
/**
* 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;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.core.thing.ThingTypeUID;
/**
* The {@link SqueezeBoxBinding} class defines common constants, which are used
* across the whole binding.
*
* @author Dan Cunningham - Initial contribution
* @author Mark Hilbush - Added duration channel
*/
@NonNullByDefault
public class SqueezeBoxBindingConstants {
public static final String BINDING_ID = "squeezebox";
// List of all Thing Type UIDs
public static final ThingTypeUID SQUEEZEBOXPLAYER_THING_TYPE = new ThingTypeUID(BINDING_ID, "squeezeboxplayer");
public static final ThingTypeUID SQUEEZEBOXSERVER_THING_TYPE = new ThingTypeUID(BINDING_ID, "squeezeboxserver");
// List of all Server Channel Ids
public static final String CHANNEL_FAVORITES_LIST = "favoritesList";
// List of all Player Channel Ids
public static final String CHANNEL_POWER = "power";
public static final String CHANNEL_MUTE = "mute";
public static final String CHANNEL_VOLUME = "volume";
public static final String CHANNEL_STOP = "stop";
public static final String CHANNEL_PLAY_PAUSE = "playPause";
public static final String CHANNEL_NEXT = "next";
public static final String CHANNEL_PREV = "prev";
public static final String CHANNEL_CONTROL = "control";
public static final String CHANNEL_STREAM = "stream";
public static final String CHANNEL_SOURCE = "source";
public static final String CHANNEL_SYNC = "sync";
public static final String CHANNEL_UNSYNC = "unsync";
public static final String CHANNEL_PLAYLIST_INDEX = "playListIndex";
public static final String CHANNEL_CURRENT_PLAYING_TIME = "currentPlayingTime";
public static final String CHANNEL_DURATION = "duration";
public static final String CHANNEL_NUMBER_PLAYLIST_TRACKS = "numberPlaylistTracks";
public static final String CHANNEL_CURRENT_PLAYLIST_SHUFFLE = "currentPlaylistShuffle";
public static final String CHANNEL_CURRENT_PLAYLIST_REPEAT = "currentPlaylistRepeat";
public static final String CHANNEL_TITLE = "title";
public static final String CHANNEL_REMOTE_TITLE = "remotetitle";
public static final String CHANNEL_ALBUM = "album";
public static final String CHANNEL_ARTIST = "artist";
public static final String CHANNEL_YEAR = "year";
public static final String CHANNEL_GENRE = "genre";
public static final String CHANNEL_COVERART_DATA = "coverartdata";
public static final String CHANNEL_IRCODE = "ircode";
public static final String CHANNEL_IP = "ip";
public static final String CHANNEL_UID = "uid";
public static final String CHANNEL_TYPEID = "typeId";
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_RATE = "rate";
}

View File

@@ -0,0 +1,209 @@
/**
* 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;
import static org.openhab.binding.squeezebox.internal.SqueezeBoxBindingConstants.*;
import java.util.Dictionary;
import java.util.HashMap;
import java.util.Hashtable;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.openhab.binding.squeezebox.internal.discovery.SqueezeBoxPlayerDiscoveryParticipant;
import org.openhab.binding.squeezebox.internal.handler.SqueezeBoxPlayerEventListener;
import org.openhab.binding.squeezebox.internal.handler.SqueezeBoxPlayerHandler;
import org.openhab.binding.squeezebox.internal.handler.SqueezeBoxServerHandler;
import org.openhab.core.audio.AudioHTTPServer;
import org.openhab.core.audio.AudioSink;
import org.openhab.core.config.discovery.DiscoveryService;
import org.openhab.core.net.HttpServiceUtil;
import org.openhab.core.net.NetworkAddressService;
import org.openhab.core.thing.Bridge;
import org.openhab.core.thing.Thing;
import org.openhab.core.thing.ThingTypeUID;
import org.openhab.core.thing.ThingUID;
import org.openhab.core.thing.binding.BaseThingHandlerFactory;
import org.openhab.core.thing.binding.ThingHandler;
import org.openhab.core.thing.binding.ThingHandlerFactory;
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;
import org.slf4j.LoggerFactory;
/**
* The {@link SqueezeBoxHandlerFactory} is responsible for creating things and
* thing handlers.
*
* @author Dan Cunningham - Initial contribution
* @author Mark Hilbush - Cancel request player job when handler removed
* @author Mark Hilbush - Add callbackUrl
*/
@Component(service = ThingHandlerFactory.class, configurationPid = "binding.squeezebox")
public class SqueezeBoxHandlerFactory extends BaseThingHandlerFactory {
private final Logger logger = LoggerFactory.getLogger(SqueezeBoxHandlerFactory.class);
private static final Set<ThingTypeUID> SUPPORTED_THING_TYPES_UIDS = Stream
.concat(SqueezeBoxServerHandler.SUPPORTED_THING_TYPES_UIDS.stream(),
SqueezeBoxPlayerHandler.SUPPORTED_THING_TYPES_UIDS.stream())
.collect(Collectors.toSet());
private Map<ThingUID, ServiceRegistration<?>> discoveryServiceRegs = new HashMap<>();
private final AudioHTTPServer audioHTTPServer;
private final NetworkAddressService networkAddressService;
private final SqueezeBoxStateDescriptionOptionsProvider stateDescriptionProvider;
private Map<String, ServiceRegistration<AudioSink>> audioSinkRegistrations = new ConcurrentHashMap<>();
// 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);
}
@Override
protected void activate(ComponentContext componentContext) {
super.activate(componentContext);
Dictionary<String, Object> properties = componentContext.getProperties();
callbackUrl = (String) properties.get("callbackUrl");
}
@Override
protected ThingHandler createHandler(Thing thing) {
ThingTypeUID thingTypeUID = thing.getThingTypeUID();
if (thingTypeUID.equals(SQUEEZEBOXSERVER_THING_TYPE)) {
logger.trace("creating handler for bridge thing {}", thing);
SqueezeBoxServerHandler bridge = new SqueezeBoxServerHandler((Bridge) thing);
registerSqueezeBoxPlayerDiscoveryService(bridge);
return bridge;
}
if (thingTypeUID.equals(SQUEEZEBOXPLAYER_THING_TYPE)) {
logger.trace("creating handler for player thing {}", thing);
SqueezeBoxPlayerHandler playerHandler = new SqueezeBoxPlayerHandler(thing, createCallbackUrl(),
stateDescriptionProvider);
// Register the player as an audio sink
logger.trace("Registering an audio sink for player thing {}", thing.getUID());
SqueezeBoxAudioSink audioSink = new SqueezeBoxAudioSink(playerHandler, audioHTTPServer, callbackUrl);
@SuppressWarnings("unchecked")
ServiceRegistration<AudioSink> reg = (ServiceRegistration<AudioSink>) bundleContext
.registerService(AudioSink.class.getName(), audioSink, new Hashtable<>());
audioSinkRegistrations.put(thing.getUID().toString(), reg);
return playerHandler;
}
return null;
}
/**
* Adds SqueezeBoxServerHandlers to the discovery service to find SqueezeBox
* Players
*
* @param squeezeBoxServerHandler
*/
private synchronized void registerSqueezeBoxPlayerDiscoveryService(
SqueezeBoxServerHandler squeezeBoxServerHandler) {
logger.trace("registering player discovery service");
SqueezeBoxPlayerDiscoveryParticipant discoveryService = new SqueezeBoxPlayerDiscoveryParticipant(
squeezeBoxServerHandler);
// Register the PlayerListener with the SqueezeBoxServerHandler
squeezeBoxServerHandler.registerSqueezeBoxPlayerListener(discoveryService);
// Register the service, then add the service to the ServiceRegistration map
discoveryServiceRegs.put(squeezeBoxServerHandler.getThing().getUID(),
bundleContext.registerService(DiscoveryService.class.getName(), discoveryService, new Hashtable<>()));
}
@Override
protected synchronized void removeHandler(ThingHandler thingHandler) {
if (thingHandler instanceof SqueezeBoxServerHandler) {
logger.trace("removing handler for bridge thing {}", thingHandler.getThing());
ServiceRegistration<?> serviceReg = this.discoveryServiceRegs.get(thingHandler.getThing().getUID());
if (serviceReg != null) {
logger.trace("unregistering player discovery service");
// Get the discovery service object and use it to cancel the RequestPlayerJob
SqueezeBoxPlayerDiscoveryParticipant discoveryService = (SqueezeBoxPlayerDiscoveryParticipant) bundleContext
.getService(serviceReg.getReference());
discoveryService.cancelRequestPlayerJob();
// Unregister the PlayerListener from the SqueezeBoxServerHandler
((SqueezeBoxServerHandler) thingHandler).unregisterSqueezeBoxPlayerListener(
(SqueezeBoxPlayerEventListener) bundleContext.getService(serviceReg.getReference()));
// Unregister the PlayerListener service
serviceReg.unregister();
// Remove the service from the ServiceRegistration map
discoveryServiceRegs.remove(thingHandler.getThing().getUID());
}
}
if (thingHandler instanceof SqueezeBoxPlayerHandler) {
SqueezeBoxServerHandler bridge = ((SqueezeBoxPlayerHandler) thingHandler).getSqueezeBoxServerHandler();
if (bridge != null) {
// Unregister the player's audio sink
logger.trace("Unregistering the audio sync service for player thing {}",
thingHandler.getThing().getUID());
ServiceRegistration<AudioSink> reg = audioSinkRegistrations
.get(thingHandler.getThing().getUID().toString());
if (reg != null) {
reg.unregister();
}
logger.trace("removing handler for player thing {}", thingHandler.getThing());
bridge.removePlayerCache(((SqueezeBoxPlayerHandler) thingHandler).getMac());
}
}
}
private String createCallbackUrl() {
final String ipAddress = networkAddressService.getPrimaryIpv4HostAddress();
if (ipAddress == null) {
logger.warn("No network interface could be found.");
return null;
}
final int port = HttpServiceUtil.getHttpServicePort(bundleContext);
if (port == -1) {
logger.warn("Cannot find port of the http service.");
return null;
}
return "http://" + ipAddress + ":" + port;
}
}

View File

@@ -0,0 +1,38 @@
/**
* 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;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.core.thing.binding.BaseDynamicStateDescriptionProvider;
import org.openhab.core.thing.i18n.ChannelTypeI18nLocalizationService;
import org.openhab.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;
/**
* Dynamic provider of state options while leaving other state description fields as original.
*
* @author Gregory Moyer - Initial contribution
* @author Mark Hilbush - Adapted to squeezebox binding
*/
@Component(service = { DynamicStateDescriptionProvider.class, SqueezeBoxStateDescriptionOptionsProvider.class })
@NonNullByDefault
public class SqueezeBoxStateDescriptionOptionsProvider extends BaseDynamicStateDescriptionProvider {
@Activate
public SqueezeBoxStateDescriptionOptionsProvider(
@Reference ChannelTypeI18nLocalizationService channelTypeI18nLocalizationService) {
this.channelTypeI18nLocalizationService = channelTypeI18nLocalizationService;
}
}

View File

@@ -0,0 +1,37 @@
/**
* 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.config;
/**
* Configuration for a player
*
* @author Dan Cunningham - Initial contribution
* @author Mark Hilbush - Convert sound notification volume from channel to config parameter
*
*/
public class SqueezeBoxPlayerConfig {
/**
* MAC address of player
*/
public String mac;
/**
* Number of seconds to wait to time out a notification
*/
public int notificationTimeout;
/**
* Volume used for playing notifications
*/
public Integer notificationVolume;
}

View File

@@ -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.config;
/**
* Configuration of a server.
*
* @author Dan Cunningham - Initial contribution
* @author Mark Hilbush - Added user ID and password
*
*/
public class SqueezeBoxServerConfig {
/**
* Server ip address
*/
public String ipAddress;
/**
* Server web port for REST calls
*/
public int webport;
/**
* Server cli port
*/
public int cliport;
/**
* Language for TTS
*/
public String language;
/*
* User ID (when authentication enabled in LMS)
*/
public String userId;
/*
* User ID (when authentication enabled in LMS)
*/
public String password;
}

View File

@@ -0,0 +1,214 @@
/**
* 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.discovery;
import static org.openhab.binding.squeezebox.internal.SqueezeBoxBindingConstants.SQUEEZEBOXPLAYER_THING_TYPE;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import org.openhab.binding.squeezebox.internal.handler.SqueezeBoxPlayer;
import org.openhab.binding.squeezebox.internal.handler.SqueezeBoxPlayerEventListener;
import org.openhab.binding.squeezebox.internal.handler.SqueezeBoxPlayerHandler;
import org.openhab.binding.squeezebox.internal.handler.SqueezeBoxServerHandler;
import org.openhab.binding.squeezebox.internal.model.Favorite;
import org.openhab.core.config.discovery.AbstractDiscoveryService;
import org.openhab.core.config.discovery.DiscoveryResult;
import org.openhab.core.config.discovery.DiscoveryResultBuilder;
import org.openhab.core.thing.ThingUID;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* When a {@link SqueezeBoxServerHandler} finds a new SqueezeBox Player we will
* add it to the system.
*
* @author Dan Cunningham - Initial contribution
* @author Mark Hilbush - added method to cancel request player job, and to set thing properties
* @author Mark Hilbush - Added duration channel
* @author Mark Hilbush - Added event to update favorites list
*
*/
public class SqueezeBoxPlayerDiscoveryParticipant extends AbstractDiscoveryService
implements SqueezeBoxPlayerEventListener {
private final Logger logger = LoggerFactory.getLogger(SqueezeBoxPlayerDiscoveryParticipant.class);
private static final int TIMEOUT = 60;
private static final int TTL = 60;
private SqueezeBoxServerHandler squeezeBoxServerHandler;
private ScheduledFuture<?> requestPlayerJob;
/**
* Discovers SqueezeBox Players attached to a SqueezeBox Server
*
* @param squeezeBoxServerHandler
*/
public SqueezeBoxPlayerDiscoveryParticipant(SqueezeBoxServerHandler squeezeBoxServerHandler) {
super(SqueezeBoxPlayerHandler.SUPPORTED_THING_TYPES_UIDS, TIMEOUT, true);
this.squeezeBoxServerHandler = squeezeBoxServerHandler;
setupRequestPlayerJob();
}
@Override
protected void startScan() {
logger.debug("startScan invoked in SqueezeBoxPlayerDiscoveryParticipant");
this.squeezeBoxServerHandler.requestPlayers();
this.squeezeBoxServerHandler.requestFavorites();
}
/*
* Allows request player job to be canceled when server handler is removed
*/
public void cancelRequestPlayerJob() {
logger.debug("canceling RequestPlayerJob");
if (requestPlayerJob != null) {
requestPlayerJob.cancel(true);
requestPlayerJob = null;
}
}
@Override
public void playerAdded(SqueezeBoxPlayer player) {
ThingUID bridgeUID = squeezeBoxServerHandler.getThing().getUID();
ThingUID thingUID = new ThingUID(SQUEEZEBOXPLAYER_THING_TYPE, bridgeUID,
player.getMacAddress().replace(":", ""));
if (!playerThingExists(thingUID)) {
logger.debug("player added {} : {} ", player.getMacAddress(), player.getName());
Map<String, Object> properties = new HashMap<>(1);
String representationPropertyName = "mac";
properties.put(representationPropertyName, player.getMacAddress());
// Added other properties
properties.put("modelId", player.getModel());
properties.put("name", player.getName());
properties.put("uid", player.getUuid());
properties.put("ip", player.getIpAddr());
DiscoveryResult discoveryResult = DiscoveryResultBuilder.create(thingUID).withProperties(properties)
.withRepresentationProperty(representationPropertyName).withBridge(bridgeUID)
.withLabel(player.getName()).build();
thingDiscovered(discoveryResult);
}
}
private boolean playerThingExists(ThingUID newThingUID) {
return squeezeBoxServerHandler.getThing().getThing(newThingUID) != null ? true : false;
}
/**
* Tells the bridge to request a list of players
*/
private void setupRequestPlayerJob() {
logger.debug("Request player job scheduled to run every {} seconds", TTL);
requestPlayerJob = scheduler.scheduleWithFixedDelay(() -> {
squeezeBoxServerHandler.requestPlayers();
}, 10, TTL, TimeUnit.SECONDS);
}
// we can ignore the other events
@Override
public void powerChangeEvent(String mac, boolean power) {
}
@Override
public void modeChangeEvent(String mac, String mode) {
}
@Override
public void absoluteVolumeChangeEvent(String mac, int volume) {
}
@Override
public void relativeVolumeChangeEvent(String mac, int volumeChange) {
}
@Override
public void muteChangeEvent(String mac, boolean mute) {
}
@Override
public void currentPlaylistIndexEvent(String mac, int index) {
}
@Override
public void currentPlayingTimeEvent(String mac, int time) {
}
@Override
public void durationEvent(String mac, int duration) {
}
@Override
public void numberPlaylistTracksEvent(String mac, int track) {
}
@Override
public void currentPlaylistShuffleEvent(String mac, int shuffle) {
}
@Override
public void currentPlaylistRepeatEvent(String mac, int repeat) {
}
@Override
public void titleChangeEvent(String mac, String title) {
}
@Override
public void albumChangeEvent(String mac, String album) {
}
@Override
public void artistChangeEvent(String mac, String artist) {
}
@Override
public void coverArtChangeEvent(String mac, String coverArtUrl) {
}
@Override
public void yearChangeEvent(String mac, String year) {
}
@Override
public void genreChangeEvent(String mac, String genre) {
}
@Override
public void remoteTitleChangeEvent(String mac, String title) {
}
@Override
public void irCodeChangeEvent(String mac, String ircode) {
}
@Override
public void updateFavoritesListEvent(List<Favorite> favorites) {
}
@Override
public void sourceChangeEvent(String mac, String source) {
}
@Override
public void buttonsChangeEvent(String mac, String likeCommand, String unlikeCommand) {
}
}

View File

@@ -0,0 +1,113 @@
/**
* 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.discovery;
import static org.openhab.binding.squeezebox.internal.SqueezeBoxBindingConstants.SQUEEZEBOXSERVER_THING_TYPE;
import java.net.URI;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import org.jupnp.model.meta.RemoteDevice;
import org.openhab.binding.squeezebox.internal.utils.HttpUtils;
import org.openhab.binding.squeezebox.internal.utils.SqueezeBoxCommunicationException;
import org.openhab.binding.squeezebox.internal.utils.SqueezeBoxNotAuthorizedException;
import org.openhab.core.config.discovery.DiscoveryResult;
import org.openhab.core.config.discovery.DiscoveryResultBuilder;
import org.openhab.core.config.discovery.upnp.UpnpDiscoveryParticipant;
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;
/**
* Discovers a SqueezeServer on the network using UPNP
*
* @author Dan Cunningham - Initial contribution
* @author Mark Hilbush - Add support for LMS authentication
*
*/
@Component(immediate = true)
public class SqueezeBoxServerDiscoveryParticipant implements UpnpDiscoveryParticipant {
private final Logger logger = LoggerFactory.getLogger(SqueezeBoxServerDiscoveryParticipant.class);
/**
* Name of a Squeeze Server
*/
private static final String MODEL_NAME = "Logitech Media Server";
@Override
public Set<ThingTypeUID> getSupportedThingTypeUIDs() {
return Collections.singleton(SQUEEZEBOXSERVER_THING_TYPE);
}
@Override
public DiscoveryResult createResult(RemoteDevice device) {
ThingUID uid = getThingUID(device);
if (uid != null) {
Map<String, Object> properties = new HashMap<>(3);
URI uri = device.getDetails().getPresentationURI();
String host = uri.getHost();
int webPort = uri.getPort();
int cliPort = 0;
int defaultCliPort = 9090;
try {
cliPort = HttpUtils.getCliPort(host, webPort);
} catch (SqueezeBoxNotAuthorizedException e) {
logger.debug("Not authorized to query CLI port. Using default of {}", defaultCliPort);
cliPort = defaultCliPort;
} catch (NumberFormatException e) {
logger.debug("Badly formed CLI port. Using default of {}", defaultCliPort);
cliPort = defaultCliPort;
} catch (SqueezeBoxCommunicationException e) {
logger.debug("Could not get cli port: {}", e.getMessage(), e);
return null;
}
String label = device.getDetails().getFriendlyName();
String representationPropertyName = "ipAddress";
properties.put(representationPropertyName, host);
properties.put("webport", new Integer(webPort));
properties.put("cliPort", new Integer(cliPort));
DiscoveryResult result = DiscoveryResultBuilder.create(uid).withProperties(properties)
.withRepresentationProperty(representationPropertyName).withLabel(label).build();
logger.debug("Created a DiscoveryResult for device '{}' with UDN '{}'",
device.getDetails().getFriendlyName(), device.getIdentity().getUdn().getIdentifierString());
return result;
} else {
return null;
}
}
@Override
public ThingUID getThingUID(RemoteDevice device) {
if (device.getDetails().getFriendlyName() != null) {
if (device.getDetails().getModelDetails().getModelName().contains(MODEL_NAME)) {
logger.debug("Discovered a {} thing with UDN '{}'", device.getDetails().getFriendlyName(),
device.getIdentity().getUdn().getIdentifierString());
return new ThingUID(SQUEEZEBOXSERVER_THING_TYPE,
device.getIdentity().getUdn().getIdentifierString().toUpperCase());
}
}
return null;
}
}

View File

@@ -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;
}
}

View File

@@ -0,0 +1,54 @@
/**
* 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.stream.Collectors;
import java.util.stream.StreamSupport;
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<ButtonDTO> {
@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 = false;
button.enabled = value != 0;
} else if (jsonElement.isJsonObject()) {
JsonObject jsonObject = jsonElement.getAsJsonObject();
button = new ButtonDTO();
button.custom = true;
button.icon = jsonObject.get("icon").getAsString();
button.jiveStyle = jsonObject.get("jiveStyle").getAsString();
button.toolTip = jsonObject.get("tooltip").getAsString();
button.command = StreamSupport.stream(jsonObject.getAsJsonArray("command").spliterator(), false)
.map(JsonElement::getAsString).collect(Collectors.joining(" "));
}
return button;
}
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -0,0 +1,223 @@
/**
* 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.handler;
import java.util.List;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import org.openhab.binding.squeezebox.internal.model.Favorite;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* This {@link SqueezeBoxNotificationListener}- is a type of PlayerEventListener
* that's used to monitor certain events related to the notification functionality.
*
* @author Mark Hilbush - Initial contribution
* @author Mark Hilbush - Added event to update favorites list
*/
public final class SqueezeBoxNotificationListener implements SqueezeBoxPlayerEventListener {
private final Logger logger = LoggerFactory.getLogger(SqueezeBoxNotificationListener.class);
private final String playerMAC;
// Used to monitor when the player stops
private final AtomicBoolean started = new AtomicBoolean(false);
private final AtomicBoolean stopped = new AtomicBoolean(false);
// Used to monitor when the player pauses
private final AtomicBoolean paused = new AtomicBoolean(false);
// Used to monitor for updates to the playlist
private final AtomicBoolean playlistUpdated = new AtomicBoolean(false);
// Used to monitor when the player volume changes to a specific target value
private final AtomicInteger volume = new AtomicInteger(-1);
SqueezeBoxNotificationListener(String playerMAC) {
this.playerMAC = playerMAC;
}
// Stopped
public void resetStopped() {
this.started.set(false);
this.stopped.set(false);
}
public boolean isStopped() {
return this.stopped.get();
}
// Paused
public void resetPaused() {
this.paused.set(false);
}
public boolean isPaused() {
return this.paused.get();
}
// Playlist updated
public void resetPlaylistUpdated() {
this.playlistUpdated.set(false);
}
public boolean isPlaylistUpdated() {
return this.playlistUpdated.get();
}
// Volume updated
public void resetVolumeUpdated() {
this.volume.set(-1);
}
public boolean isVolumeUpdated(int volume) {
return this.volume.get() == volume;
}
// Implementation of listener interfaces
@Override
public void playerAdded(SqueezeBoxPlayer player) {
}
@Override
public void powerChangeEvent(String mac, boolean power) {
}
/*
* Monitor for player mode changing to stop.
*/
@Override
public void modeChangeEvent(String mac, String mode) {
if (!this.playerMAC.equals(mac)) {
return;
}
logger.trace("Mode is {} for player {}", mode, mac);
if (mode.equals("play")) {
this.started.set(true);
} else if (this.started.get() && mode.equals("stop")) {
this.stopped.set(true);
}
if (mode.equals("pause")) {
this.paused.set(true);
}
}
/*
* Monitor for when the volume is updated to a specific target value
*/
@Override
public void absoluteVolumeChangeEvent(String mac, int volume) {
if (!this.playerMAC.equals(mac)) {
return;
}
this.volume.set(volume);
logger.trace("Volume is {} for player {}", volume, mac);
}
@Override
public void relativeVolumeChangeEvent(String mac, int volumeChange) {
if (!this.playerMAC.equals(mac)) {
return;
}
int newVolume = this.volume.get() + volumeChange;
newVolume = Math.min(newVolume, 100);
newVolume = Math.max(newVolume, 0);
this.volume.set(newVolume);
logger.trace("Volume changed [{}] for player {}. New volume: {}", volumeChange, mac, volume);
}
@Override
public void muteChangeEvent(String mac, boolean mute) {
}
@Override
public void currentPlaylistIndexEvent(String mac, int index) {
}
@Override
public void currentPlayingTimeEvent(String mac, int time) {
}
@Override
public void durationEvent(String mac, int duration) {
}
/*
* Monitor for when the playlist is updated
*/
@Override
public void numberPlaylistTracksEvent(String mac, int track) {
if (!this.playerMAC.equals(mac)) {
return;
}
logger.trace("Number of playlist tracks is {} for player {}", track, mac);
playlistUpdated.set(true);
}
@Override
public void currentPlaylistShuffleEvent(String mac, int shuffle) {
}
@Override
public void currentPlaylistRepeatEvent(String mac, int repeat) {
}
@Override
public void titleChangeEvent(String mac, String title) {
}
@Override
public void albumChangeEvent(String mac, String album) {
}
@Override
public void artistChangeEvent(String mac, String artist) {
}
@Override
public void coverArtChangeEvent(String mac, String coverArtUrl) {
}
@Override
public void yearChangeEvent(String mac, String year) {
}
@Override
public void genreChangeEvent(String mac, String genre) {
}
@Override
public void remoteTitleChangeEvent(String mac, String title) {
}
@Override
public void irCodeChangeEvent(String mac, String ircode) {
}
@Override
public void updateFavoritesListEvent(List<Favorite> favorites) {
}
@Override
public void sourceChangeEvent(String mac, String source) {
}
@Override
public void buttonsChangeEvent(String mac, String likeCommand, String unlikeCommand) {
}
}

View File

@@ -0,0 +1,276 @@
/**
* 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.handler;
import java.io.Closeable;
import org.openhab.binding.squeezebox.internal.utils.SqueezeBoxTimeoutException;
import org.openhab.core.library.types.StringType;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/***
* Utility class to play a notification message. The message is added
* to the playlist, played and the previous state of the playlist and the
* player is restored.
*
* @author Mark Hilbush - Initial Contribution
* @author Patrik Gfeller - Utility class added reduce complexity and length of SqueezeBoxPlayerHandler.java
* @author Mark Hilbush - Convert sound notification volume from channel to config parameter
*
*/
class SqueezeBoxNotificationPlayer implements Closeable {
private final Logger logger = LoggerFactory.getLogger(SqueezeBoxNotificationPlayer.class);
// An exception is thrown if we do not receive an acknowledge
// for a volume set command in the given amount of time [s].
private static final int VOLUME_COMMAND_TIMEOUT = 4;
// We expect the media server to acknowledge a playlist command.
// An exception is thrown if the playlist command was not processed
// after the defined amount in [s]
private static final int PLAYLIST_COMMAND_TIMEOUT = 5;
private final SqueezeBoxPlayerState playerState;
private final SqueezeBoxPlayerHandler squeezeBoxPlayerHandler;
private final SqueezeBoxServerHandler squeezeBoxServerHandler;
private final StringType uri;
private final String mac;
boolean playlistModified;
private int notificationMessagePlaylistsIndex;
SqueezeBoxNotificationPlayer(SqueezeBoxPlayerHandler squeezeBoxPlayerHandler,
SqueezeBoxServerHandler squeezeBoxServerHandler, StringType uri) {
this.squeezeBoxPlayerHandler = squeezeBoxPlayerHandler;
this.squeezeBoxServerHandler = squeezeBoxServerHandler;
this.mac = squeezeBoxPlayerHandler.getMac();
this.uri = uri;
this.playerState = new SqueezeBoxPlayerState(squeezeBoxPlayerHandler);
}
void play() throws InterruptedException, SqueezeBoxTimeoutException {
if (squeezeBoxServerHandler == null) {
logger.warn("Server handler is null");
return;
}
setupPlayerForNotification();
addNotificationMessageToPlaylist();
playNotification();
}
@Override
public void close() {
restorePlayerState();
}
private void setupPlayerForNotification() throws InterruptedException, SqueezeBoxTimeoutException {
logger.debug("Setting up player for notification");
if (!playerState.isPoweredOn()) {
logger.debug("Powering on the player");
squeezeBoxServerHandler.powerOn(mac);
}
if (playerState.isShuffling()) {
logger.debug("Turning off shuffle");
squeezeBoxServerHandler.setShuffleMode(mac, 0);
}
if (playerState.isRepeating()) {
logger.debug("Turning off repeat");
squeezeBoxServerHandler.setRepeatMode(mac, 0);
}
if (playerState.isPlaying()) {
squeezeBoxServerHandler.stop(mac);
}
setVolume(squeezeBoxPlayerHandler.getNotificationSoundVolume().intValue());
}
/**
* Sends a volume set command if target volume is not equal to the current volume.
*
* @param requestedVolume The requested volume value.
* @throws InterruptedException Thread interrupted during while we were waiting for an answer from the media server.
* @throws SqueezeBoxTimeoutException Volume command was not acknowledged by the media server.
*/
private void setVolume(int requestedVolume) throws InterruptedException, SqueezeBoxTimeoutException {
if (playerState.getVolume() == requestedVolume) {
return;
}
SqueezeBoxNotificationListener listener = new SqueezeBoxNotificationListener(mac);
listener.resetVolumeUpdated();
squeezeBoxServerHandler.registerSqueezeBoxPlayerListener(listener);
squeezeBoxServerHandler.setVolume(mac, requestedVolume);
logger.trace("Waiting up to {} s for volume to be updated...", VOLUME_COMMAND_TIMEOUT);
try {
int timeoutCount = 0;
while (!listener.isVolumeUpdated(requestedVolume)) {
Thread.sleep(100);
if (timeoutCount++ > VOLUME_COMMAND_TIMEOUT * 10) {
throw new SqueezeBoxTimeoutException("Unable to update volume.");
}
}
} finally {
squeezeBoxServerHandler.unregisterSqueezeBoxPlayerListener(listener);
}
}
private void addNotificationMessageToPlaylist() throws InterruptedException, SqueezeBoxTimeoutException {
logger.debug("Adding notification message to playlist");
SqueezeBoxNotificationListener listener = new SqueezeBoxNotificationListener(mac);
listener.resetPlaylistUpdated();
squeezeBoxServerHandler.registerSqueezeBoxPlayerListener(listener);
squeezeBoxServerHandler.addPlaylistItem(mac, uri.toString(), "Notification");
try {
updatePlaylist(listener);
this.playlistModified = true;
} finally {
squeezeBoxServerHandler.unregisterSqueezeBoxPlayerListener(listener);
}
}
private void removeNotificationMessageFromPlaylist() throws InterruptedException, SqueezeBoxTimeoutException {
logger.debug("Removing notification message from playlist");
SqueezeBoxNotificationListener listener = new SqueezeBoxNotificationListener(mac);
listener.resetPlaylistUpdated();
squeezeBoxServerHandler.registerSqueezeBoxPlayerListener(listener);
squeezeBoxServerHandler.deletePlaylistItem(mac, notificationMessagePlaylistsIndex);
try {
updatePlaylist(listener);
} finally {
squeezeBoxServerHandler.unregisterSqueezeBoxPlayerListener(listener);
}
}
/**
* Monitor the number of playlist entries. When it changes, then we know the playlist
* has been updated with the notification URL. There's probably an edge case here where
* someone is updating the playlist at the same time, but that should be rare.
*
* @param listener
* @throws InterruptedException
* @throws SqueezeBoxTimeoutException
*/
private void updatePlaylist(SqueezeBoxNotificationListener listener)
throws InterruptedException, SqueezeBoxTimeoutException {
logger.trace("Waiting up to {} s for playlist to be updated...", PLAYLIST_COMMAND_TIMEOUT);
int timeoutCount = 0;
while (!listener.isPlaylistUpdated()) {
Thread.sleep(100);
if (timeoutCount++ > PLAYLIST_COMMAND_TIMEOUT * 10) {
logger.debug("Update playlist timed out after {} seconds", PLAYLIST_COMMAND_TIMEOUT);
throw new SqueezeBoxTimeoutException("Unable to update playlist.");
}
}
logger.debug("Playlist updated");
}
private void playNotification() throws InterruptedException, SqueezeBoxTimeoutException {
logger.debug("Playing notification");
notificationMessagePlaylistsIndex = squeezeBoxPlayerHandler.currentNumberPlaylistTracks() - 1;
SqueezeBoxNotificationListener listener = new SqueezeBoxNotificationListener(mac);
listener.resetStopped();
squeezeBoxServerHandler.registerSqueezeBoxPlayerListener(listener);
squeezeBoxServerHandler.playPlaylistItem(mac, notificationMessagePlaylistsIndex);
try {
int notificationTimeout = squeezeBoxPlayerHandler.getNotificationTimeout();
int timeoutCount = 0;
logger.trace("Waiting up to {} s for stop...", notificationTimeout);
while (!listener.isStopped()) {
Thread.sleep(100);
if (timeoutCount++ > notificationTimeout * 10) {
logger.debug("Notification message timed out after {} seconds", notificationTimeout);
throw new SqueezeBoxTimeoutException("Notification message timed out");
}
}
} finally {
squeezeBoxServerHandler.unregisterSqueezeBoxPlayerListener(listener);
}
}
private void restorePlayerState() {
logger.debug("Restoring player state");
// Mute the player to prevent any noise during the transition to saved state
// Don't wait for the volume acknowledge as there´s nothing to do about it at this point.
squeezeBoxServerHandler.setVolume(mac, 0);
if (playlistModified) {
try {
removeNotificationMessageFromPlaylist();
} catch (InterruptedException | SqueezeBoxTimeoutException e) {
// Not much we can do here except log it and continue on
logger.debug("Exception while removing notification from playlist: {}", e.getMessage());
}
}
// Resume playing saved playlist item.
// Note that setting the time doesn't work for remote streams.
squeezeBoxServerHandler.playPlaylistItem(mac, playerState.getPlaylistIndex());
squeezeBoxServerHandler.setPlayingTime(mac, playerState.getPlayingTime());
switch (playerState.getPlayState()) {
case PLAY:
logger.debug("Resuming last item playing");
break;
case PAUSE:
/*
* If the player was paused, stop it. We stop it because the LMS
* doesn't respond to a pause command while it's processing the
* above 'playPlaylist item' command. The consequence of this is
* we lose the ability to resume local music from saved playing time.
*/
logger.debug("Stopping the player");
squeezeBoxServerHandler.stop(mac);
break;
case STOP:
logger.debug("Stopping the player");
squeezeBoxServerHandler.stop(mac);
break;
}
// Restore the saved volume level
squeezeBoxServerHandler.setVolume(mac, playerState.getVolume());
if (playerState.isShuffling()) {
logger.debug("Restoring shuffle mode");
squeezeBoxServerHandler.setShuffleMode(mac, playerState.getShuffle());
}
if (playerState.isRepeating()) {
logger.debug("Restoring repeat mode");
squeezeBoxServerHandler.setRepeatMode(mac, playerState.getRepeat());
}
if (playerState.isMuted()) {
logger.debug("Re-muting the player");
squeezeBoxServerHandler.mute(mac);
}
if (!playerState.isPoweredOn()) {
logger.debug("Powering off the player");
squeezeBoxServerHandler.powerOff(mac);
}
}
}

View File

@@ -0,0 +1,121 @@
/**
* 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.handler;
/**
* Represents a Squeeze Player
*
* @author Dan Cunningham - Initial contribution
*
*/
public class SqueezeBoxPlayer {
public String macAddress;
public String name;
public String ipAddr;
public String model;
public String uuid;
public SqueezeBoxPlayer() {
super();
}
/**
* UID of player
*
* @return
*/
public String getUuid() {
return uuid;
}
/**
* UID of player
*
* @param uuid
*/
public void setUuid(String uuid) {
this.uuid = uuid;
}
/**
* Mac Address of player
*
* @param macAddress
*/
public String getMacAddress() {
return macAddress;
}
/**
* Mac Address of player
*
* @param macAddress
*/
public void setMacAddress(String macAddress) {
this.macAddress = macAddress;
}
/**
* The name (label) of a player
*
* @return
*/
public String getName() {
return name;
}
/**
* The name (label) of a player
*
* @param name
*/
public void setName(String name) {
this.name = name;
}
/**
* The ip address of a player
*
* @return
*/
public String getIpAddr() {
return ipAddr;
}
/**
* The ip address of a player
*
* @param ipAddr
*/
public void setIpAddr(String ipAddr) {
this.ipAddr = ipAddr;
}
/**
* The type of player
*
* @return
*/
public String getModel() {
return model;
}
/**
* The type of player
*
* @param model
*/
public void setModel(String model) {
this.model = model;
}
}

View File

@@ -0,0 +1,85 @@
/**
* 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.handler;
import java.util.List;
import org.openhab.binding.squeezebox.internal.model.Favorite;
/**
* @author Markus Wolters - Initial contribution
* @author Ben Jones - ?
* @author Dan Cunningham - OH2 port
* @author Mark Hilbush - Added durationEvent
* @author Mark Hilbush - Added event to update favorites list
*/
public interface SqueezeBoxPlayerEventListener {
void playerAdded(SqueezeBoxPlayer player);
void powerChangeEvent(String mac, boolean power);
void modeChangeEvent(String mac, String mode);
/**
* Reports a new absolute volume for a given player.
*
* @param mac
* @param volume
*/
void absoluteVolumeChangeEvent(String mac, int volume);
/**
* Reports a relative volume change for a given player.
*
* @param mac
* @param volumeChange
*/
void relativeVolumeChangeEvent(String mac, int volumeChange);
void muteChangeEvent(String mac, boolean mute);
void currentPlaylistIndexEvent(String mac, int index);
void currentPlayingTimeEvent(String mac, int time);
void durationEvent(String mac, int duration);
void numberPlaylistTracksEvent(String mac, int track);
void currentPlaylistShuffleEvent(String mac, int shuffle);
void currentPlaylistRepeatEvent(String mac, int repeat);
void titleChangeEvent(String mac, String title);
void albumChangeEvent(String mac, String album);
void artistChangeEvent(String mac, String artist);
void coverArtChangeEvent(String mac, String coverArtUrl);
void yearChangeEvent(String mac, String year);
void genreChangeEvent(String mac, String genre);
void remoteTitleChangeEvent(String mac, String title);
void irCodeChangeEvent(String mac, String ircode);
void updateFavoritesListEvent(List<Favorite> favorites);
void sourceChangeEvent(String mac, String source);
void buttonsChangeEvent(String mac, String likeCommand, String unlikeCommand);
}

View File

@@ -0,0 +1,728 @@
/**
* 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.handler;
import static org.openhab.binding.squeezebox.internal.SqueezeBoxBindingConstants.*;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import org.apache.commons.lang.StringUtils;
import org.eclipse.jdt.annotation.NonNull;
import org.openhab.binding.squeezebox.internal.SqueezeBoxStateDescriptionOptionsProvider;
import org.openhab.binding.squeezebox.internal.config.SqueezeBoxPlayerConfig;
import org.openhab.binding.squeezebox.internal.model.Favorite;
import org.openhab.binding.squeezebox.internal.utils.SqueezeBoxTimeoutException;
import org.openhab.core.cache.ExpiringCacheMap;
import org.openhab.core.io.net.http.HttpUtil;
import org.openhab.core.library.types.DecimalType;
import org.openhab.core.library.types.IncreaseDecreaseType;
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.RawType;
import org.openhab.core.library.types.RewindFastforwardType;
import org.openhab.core.library.types.StringType;
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.ThingStatusInfo;
import org.openhab.core.thing.ThingTypeUID;
import org.openhab.core.thing.binding.BaseThingHandler;
import org.openhab.core.types.Command;
import org.openhab.core.types.RefreshType;
import org.openhab.core.types.State;
import org.openhab.core.types.StateOption;
import org.openhab.core.types.UnDefType;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The {@link SqueezeBoxPlayerHandler} is responsible for handling states, which
* are sent to/from channels.
*
* @author Dan Cunningham - Initial contribution
* @author Mark Hilbush - Improved handling of player status, prevent REFRESH from causing exception
* @author Mark Hilbush - Implement AudioSink and notifications
* @author Mark Hilbush - Added duration channel
* @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);
public static final Set<ThingTypeUID> SUPPORTED_THING_TYPES_UIDS = Collections
.singleton(SQUEEZEBOXPLAYER_THING_TYPE);
/**
* We need to remember some states to change offsets in volume, time index,
* etc..
*/
protected Map<String, State> stateMap = Collections.synchronizedMap(new HashMap<>());
/**
* Keeps current track time
*/
private ScheduledFuture<?> timeCounterJob;
/**
* Local reference to our bridge
*/
private SqueezeBoxServerHandler squeezeBoxServerHandler;
/**
* Our mac address, needed everywhere
*/
private String mac;
/**
* The server sends us the current time on play/pause/stop events, we
* increment it locally from there on
*/
private int currentTime = 0;
/**
* Our we playing something right now or not, need to keep current track
* time
*/
private boolean playing;
/**
* Separate volume level for notifications
*/
private Integer notificationSoundVolume = null;
private String callbackUrl;
private SqueezeBoxStateDescriptionOptionsProvider stateDescriptionProvider;
private static final ExpiringCacheMap<String, RawType> IMAGE_CACHE = new ExpiringCacheMap<>(
TimeUnit.MINUTES.toMillis(15)); // 15min
private String likeCommand;
private String unlikeCommand;
/**
* Creates SqueezeBox Player Handler
*
* @param thing
* @param stateDescriptionProvider
*/
public SqueezeBoxPlayerHandler(@NonNull Thing thing, String callbackUrl,
SqueezeBoxStateDescriptionOptionsProvider stateDescriptionProvider) {
super(thing);
this.callbackUrl = callbackUrl;
this.stateDescriptionProvider = stateDescriptionProvider;
}
@Override
public void initialize() {
mac = getConfig().as(SqueezeBoxPlayerConfig.class).mac;
timeCounter();
updateBridgeStatus();
logger.debug("player thing {} initialized with mac {}", getThing().getUID(), mac);
}
@Override
public void bridgeStatusChanged(ThingStatusInfo bridgeStatusInfo) {
updateBridgeStatus();
}
private void updateBridgeStatus() {
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");
}
}
@Override
public void dispose() {
// stop our duration counter
if (timeCounterJob != null && !timeCounterJob.isCancelled()) {
timeCounterJob.cancel(true);
timeCounterJob = null;
}
if (squeezeBoxServerHandler != null) {
squeezeBoxServerHandler.removePlayerCache(mac);
}
logger.debug("player thing {} disposed for mac {}", getThing().getUID(), mac);
super.dispose();
}
@Override
public void handleCommand(ChannelUID channelUID, Command command) {
if (squeezeBoxServerHandler == null) {
logger.debug("Player {} has no server configured, ignoring command: {}", getThing().getUID(), command);
return;
}
// Some of the code below is not designed to handle REFRESH, only reply to channels where cached values exist
if (command == RefreshType.REFRESH) {
String channelID = channelUID.getId();
if (stateMap.containsKey(channelID)) {
updateState(channelID, stateMap.get(channelID));
}
return;
}
switch (channelUID.getIdWithoutGroup()) {
case CHANNEL_POWER:
if (command.equals(OnOffType.ON)) {
squeezeBoxServerHandler.powerOn(mac);
} else {
squeezeBoxServerHandler.powerOff(mac);
}
break;
case CHANNEL_MUTE:
if (command.equals(OnOffType.ON)) {
squeezeBoxServerHandler.mute(mac);
} else {
squeezeBoxServerHandler.unMute(mac);
}
break;
case CHANNEL_STOP:
if (command.equals(OnOffType.ON)) {
squeezeBoxServerHandler.stop(mac);
} else if (command.equals(OnOffType.OFF)) {
squeezeBoxServerHandler.play(mac);
}
break;
case CHANNEL_PLAY_PAUSE:
if (command.equals(OnOffType.ON)) {
squeezeBoxServerHandler.play(mac);
} else if (command.equals(OnOffType.OFF)) {
squeezeBoxServerHandler.pause(mac);
}
break;
case CHANNEL_PREV:
if (command.equals(OnOffType.ON)) {
squeezeBoxServerHandler.prev(mac);
}
break;
case CHANNEL_NEXT:
if (command.equals(OnOffType.ON)) {
squeezeBoxServerHandler.next(mac);
}
break;
case CHANNEL_VOLUME:
if (command instanceof PercentType) {
squeezeBoxServerHandler.setVolume(mac, ((PercentType) command).intValue());
} else if (command.equals(IncreaseDecreaseType.INCREASE)) {
squeezeBoxServerHandler.volumeUp(mac, currentVolume());
} else if (command.equals(IncreaseDecreaseType.DECREASE)) {
squeezeBoxServerHandler.volumeDown(mac, currentVolume());
} else if (command.equals(OnOffType.OFF)) {
squeezeBoxServerHandler.mute(mac);
} else if (command.equals(OnOffType.ON)) {
squeezeBoxServerHandler.unMute(mac);
}
break;
case CHANNEL_CONTROL:
if (command instanceof PlayPauseType) {
if (command.equals(PlayPauseType.PLAY)) {
squeezeBoxServerHandler.play(mac);
} else if (command.equals(PlayPauseType.PAUSE)) {
squeezeBoxServerHandler.pause(mac);
}
}
if (command instanceof NextPreviousType) {
if (command.equals(NextPreviousType.NEXT)) {
squeezeBoxServerHandler.next(mac);
} else if (command.equals(NextPreviousType.PREVIOUS)) {
squeezeBoxServerHandler.prev(mac);
}
}
if (command instanceof RewindFastforwardType) {
if (command.equals(RewindFastforwardType.REWIND)) {
squeezeBoxServerHandler.setPlayingTime(mac, currentPlayingTime() - 5);
} else if (command.equals(RewindFastforwardType.FASTFORWARD)) {
squeezeBoxServerHandler.setPlayingTime(mac, currentPlayingTime() + 5);
}
}
break;
case CHANNEL_STREAM:
squeezeBoxServerHandler.playUrl(mac, command.toString());
break;
case CHANNEL_SYNC:
if (StringUtils.isBlank(command.toString())) {
squeezeBoxServerHandler.unSyncPlayer(mac);
} else {
squeezeBoxServerHandler.syncPlayer(mac, command.toString());
}
break;
case CHANNEL_UNSYNC:
if (command.equals(OnOffType.ON)) {
squeezeBoxServerHandler.unSyncPlayer(mac);
}
break;
case CHANNEL_PLAYLIST_INDEX:
squeezeBoxServerHandler.playPlaylistItem(mac, ((DecimalType) command).intValue());
break;
case CHANNEL_CURRENT_PLAYING_TIME:
squeezeBoxServerHandler.setPlayingTime(mac, ((DecimalType) command).intValue());
break;
case CHANNEL_CURRENT_PLAYLIST_SHUFFLE:
squeezeBoxServerHandler.setShuffleMode(mac, ((DecimalType) command).intValue());
break;
case CHANNEL_CURRENT_PLAYLIST_REPEAT:
squeezeBoxServerHandler.setRepeatMode(mac, ((DecimalType) command).intValue());
break;
case CHANNEL_FAVORITES_PLAY:
squeezeBoxServerHandler.playFavorite(mac, command.toString());
break;
case CHANNEL_RATE:
if (command.equals(OnOffType.ON)) {
squeezeBoxServerHandler.rate(mac, likeCommand);
} else if (command.equals(OnOffType.OFF)) {
squeezeBoxServerHandler.rate(mac, unlikeCommand);
}
break;
default:
break;
}
}
@Override
public void playerAdded(SqueezeBoxPlayer player) {
// Player properties are saved in SqueezeBoxPlayerDiscoveryParticipant
}
@Override
public void powerChangeEvent(String mac, boolean power) {
updateChannel(mac, CHANNEL_POWER, power ? OnOffType.ON : OnOffType.OFF);
if (!power && isMe(mac)) {
playing = false;
}
}
@Override
public synchronized void modeChangeEvent(String mac, String mode) {
updateChannel(mac, CHANNEL_CONTROL, "play".equals(mode) ? PlayPauseType.PLAY : PlayPauseType.PAUSE);
updateChannel(mac, CHANNEL_PLAY_PAUSE, "play".equals(mode) ? OnOffType.ON : OnOffType.OFF);
updateChannel(mac, CHANNEL_STOP, "stop".equals(mode) ? OnOffType.ON : OnOffType.OFF);
if (isMe(mac)) {
playing = "play".equalsIgnoreCase(mode);
}
}
@Override
public void sourceChangeEvent(String mac, String source) {
updateChannel(mac, CHANNEL_SOURCE, StringType.valueOf(source));
}
@Override
public void absoluteVolumeChangeEvent(String mac, int volume) {
int newVolume = volume;
newVolume = Math.min(100, newVolume);
newVolume = Math.max(0, newVolume);
updateChannel(mac, CHANNEL_VOLUME, new PercentType(newVolume));
}
@Override
public void relativeVolumeChangeEvent(String mac, int volumeChange) {
int newVolume = currentVolume() + volumeChange;
newVolume = Math.min(100, newVolume);
newVolume = Math.max(0, newVolume);
updateChannel(mac, CHANNEL_VOLUME, new PercentType(newVolume));
if (isMe(mac)) {
logger.trace("Volume changed [{}] for player {}. New volume: {}", volumeChange, mac, newVolume);
}
}
@Override
public void muteChangeEvent(String mac, boolean mute) {
updateChannel(mac, CHANNEL_MUTE, mute ? OnOffType.ON : OnOffType.OFF);
}
@Override
public void currentPlaylistIndexEvent(String mac, int index) {
updateChannel(mac, CHANNEL_PLAYLIST_INDEX, new DecimalType(index));
}
@Override
public void currentPlayingTimeEvent(String mac, int time) {
updateChannel(mac, CHANNEL_CURRENT_PLAYING_TIME, new DecimalType(time));
if (isMe(mac)) {
currentTime = time;
}
}
@Override
public void durationEvent(String mac, int duration) {
if (getThing().getChannel(CHANNEL_DURATION) == null) {
logger.debug("Channel 'duration' does not exist. Delete and readd player thing to pick up channel.");
return;
}
updateChannel(mac, CHANNEL_DURATION, new DecimalType(duration));
}
@Override
public void numberPlaylistTracksEvent(String mac, int track) {
updateChannel(mac, CHANNEL_NUMBER_PLAYLIST_TRACKS, new DecimalType(track));
}
@Override
public void currentPlaylistShuffleEvent(String mac, int shuffle) {
updateChannel(mac, CHANNEL_CURRENT_PLAYLIST_SHUFFLE, new DecimalType(shuffle));
}
@Override
public void currentPlaylistRepeatEvent(String mac, int repeat) {
updateChannel(mac, CHANNEL_CURRENT_PLAYLIST_REPEAT, new DecimalType(repeat));
}
@Override
public void titleChangeEvent(String mac, String title) {
updateChannel(mac, CHANNEL_TITLE, new StringType(title));
}
@Override
public void albumChangeEvent(String mac, String album) {
updateChannel(mac, CHANNEL_ALBUM, new StringType(album));
}
@Override
public void artistChangeEvent(String mac, String artist) {
updateChannel(mac, CHANNEL_ARTIST, new StringType(artist));
}
@Override
public void coverArtChangeEvent(String mac, String coverArtUrl) {
updateChannel(mac, CHANNEL_COVERART_DATA, createImage(downloadImage(mac, coverArtUrl)));
}
/**
* Download and cache the image data from an URL.
*
* @param url The URL of the image to be downloaded.
* @return A RawType object containing the image, null if the content type could not be found or the content type is
* not an image.
*/
private RawType downloadImage(String mac, String url) {
// Only get the image if this is my PlayerHandler instance
if (isMe(mac)) {
if (StringUtils.isNotEmpty(url)) {
String sanitizedUrl = sanitizeUrl(url);
RawType image = IMAGE_CACHE.putIfAbsentAndGet(url, () -> {
logger.debug("Trying to download the content of URL {}", sanitizedUrl);
try {
return HttpUtil.downloadImage(url);
} catch (IllegalArgumentException e) {
logger.debug("IllegalArgumentException when downloading image from {}", sanitizedUrl, e);
return null;
}
});
if (image == null) {
logger.debug("Failed to download the content of URL {}", sanitizedUrl);
return null;
} else {
return image;
}
}
}
return null;
}
/*
* Replaces the password in the URL, if present
*/
private String sanitizeUrl(String url) {
String sanitizedUrl = url;
try {
URI uri = new URI(url);
String userInfo = uri.getUserInfo();
if (userInfo != null) {
String[] userInfoParts = userInfo.split(":");
if (userInfoParts.length == 2) {
sanitizedUrl = url.replace(userInfoParts[1], "**********");
}
}
} catch (URISyntaxException e) {
// Just return what was passed in
}
return sanitizedUrl;
}
/**
* Wrap the given RawType and return it as {@link State} or return {@link UnDefType#UNDEF} if the RawType is null.
*/
private State createImage(RawType image) {
if (image == null) {
return UnDefType.UNDEF;
} else {
return image;
}
}
@Override
public void yearChangeEvent(String mac, String year) {
updateChannel(mac, CHANNEL_YEAR, new StringType(year));
}
@Override
public void genreChangeEvent(String mac, String genre) {
updateChannel(mac, CHANNEL_GENRE, new StringType(genre));
}
@Override
public void remoteTitleChangeEvent(String mac, String title) {
updateChannel(mac, CHANNEL_REMOTE_TITLE, new StringType(title));
}
@Override
public void irCodeChangeEvent(String mac, String ircode) {
if (isMe(mac)) {
postCommand(CHANNEL_IRCODE, new StringType(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<Favorite> favorites) {
logger.trace("Player {} updating favorites list with {} favorites", mac, favorites.size());
List<StateOption> options = new ArrayList<>();
for (Favorite favorite : favorites) {
options.add(new StateOption(favorite.shortId, favorite.name));
}
stateDescriptionProvider.setStateOptions(new ChannelUID(getThing().getUID(), CHANNEL_FAVORITES_PLAY), options);
}
/**
* Update a channel if the mac matches our own
*
* @param mac
* @param channelID
* @param state
*/
private void updateChannel(String mac, String channelID, State state) {
if (isMe(mac)) {
State prevState = stateMap.put(channelID, state);
if (prevState == null || !prevState.equals(state)) {
logger.trace("Updating channel {} for thing {} with mac {} to state {}", channelID, getThing().getUID(),
mac, state);
updateState(channelID, state);
}
}
}
/**
* Helper methods to get the current state of the player
*
* @return
*/
int currentVolume() {
if (stateMap.containsKey(CHANNEL_VOLUME)) {
return ((DecimalType) stateMap.get(CHANNEL_VOLUME)).intValue();
} else {
return 0;
}
}
int currentPlayingTime() {
if (stateMap.containsKey(CHANNEL_CURRENT_PLAYING_TIME)) {
return ((DecimalType) stateMap.get(CHANNEL_CURRENT_PLAYING_TIME)).intValue();
} else {
return 0;
}
}
int currentNumberPlaylistTracks() {
if (stateMap.containsKey(CHANNEL_NUMBER_PLAYLIST_TRACKS)) {
return ((DecimalType) stateMap.get(CHANNEL_NUMBER_PLAYLIST_TRACKS)).intValue();
} else {
return 0;
}
}
int currentPlaylistIndex() {
if (stateMap.containsKey(CHANNEL_PLAYLIST_INDEX)) {
return ((DecimalType) stateMap.get(CHANNEL_PLAYLIST_INDEX)).intValue();
} else {
return 0;
}
}
boolean currentPower() {
if (stateMap.containsKey(CHANNEL_POWER)) {
return (stateMap.get(CHANNEL_POWER).equals(OnOffType.ON) ? true : false);
} else {
return false;
}
}
boolean currentStop() {
if (stateMap.containsKey(CHANNEL_STOP)) {
return (stateMap.get(CHANNEL_STOP).equals(OnOffType.ON) ? true : false);
} else {
return false;
}
}
boolean currentControl() {
if (stateMap.containsKey(CHANNEL_CONTROL)) {
return (stateMap.get(CHANNEL_CONTROL).equals(PlayPauseType.PLAY) ? true : false);
} else {
return false;
}
}
boolean currentMute() {
if (stateMap.containsKey(CHANNEL_MUTE)) {
return (stateMap.get(CHANNEL_MUTE).equals(OnOffType.ON) ? true : false);
} else {
return false;
}
}
int currentShuffle() {
if (stateMap.containsKey(CHANNEL_CURRENT_PLAYLIST_SHUFFLE)) {
return ((DecimalType) stateMap.get(CHANNEL_CURRENT_PLAYLIST_SHUFFLE)).intValue();
} else {
return 0;
}
}
int currentRepeat() {
if (stateMap.containsKey(CHANNEL_CURRENT_PLAYLIST_REPEAT)) {
return ((DecimalType) stateMap.get(CHANNEL_CURRENT_PLAYLIST_REPEAT)).intValue();
} else {
return 0;
}
}
/**
* Ticks away when in a play state to keep current track time
*/
private void timeCounter() {
timeCounterJob = scheduler.scheduleWithFixedDelay(() -> {
if (playing) {
updateChannel(mac, CHANNEL_CURRENT_PLAYING_TIME, new DecimalType(currentTime++));
}
}, 0, 1, TimeUnit.SECONDS);
}
private boolean isMe(String mac) {
return mac.equals(this.mac);
}
/**
* Returns our server handler if set
*
* @return
*/
public SqueezeBoxServerHandler getSqueezeBoxServerHandler() {
return this.squeezeBoxServerHandler;
}
/**
* Returns the MAC address for this player
*
* @return
*/
public String getMac() {
return this.mac;
}
/*
* Give the notification player access to the notification timeout
*/
public int getNotificationTimeout() {
return getConfigAs(SqueezeBoxPlayerConfig.class).notificationTimeout;
}
/*
* Used by the AudioSink to get the volume level that should be used for the notification.
* Priority for determining volume is:
* - volume is provided in the say/playSound actions
* - volume is contained in the player thing's configuration
* - current player volume setting
*/
public PercentType getNotificationSoundVolume() {
// Get the notification sound volume from this player thing's configuration
Integer configNotificationSoundVolume = getConfigAs(SqueezeBoxPlayerConfig.class).notificationVolume;
// Determine which volume to use
Integer currentNotificationSoundVolume;
if (notificationSoundVolume != null) {
currentNotificationSoundVolume = notificationSoundVolume;
} else if (configNotificationSoundVolume != null) {
currentNotificationSoundVolume = configNotificationSoundVolume;
} else {
currentNotificationSoundVolume = Integer.valueOf(currentVolume());
}
return new PercentType(currentNotificationSoundVolume.intValue());
}
/*
* Used by the AudioSink to set the volume level that should be used to play the notification
*/
public void setNotificationSoundVolume(PercentType newNotificationSoundVolume) {
if (newNotificationSoundVolume != null) {
notificationSoundVolume = Integer.valueOf(newNotificationSoundVolume.intValue());
}
}
/*
* Play the notification.
*/
public void playNotificationSoundURI(StringType uri) {
logger.debug("Play notification sound on player {} at URI {}", mac, uri);
try (SqueezeBoxNotificationPlayer notificationPlayer = new SqueezeBoxNotificationPlayer(this,
squeezeBoxServerHandler, uri)) {
notificationPlayer.play();
} catch (InterruptedException e) {
logger.warn("Notification playback was interrupted", e);
} catch (SqueezeBoxTimeoutException e) {
logger.debug("SqueezeBoxTimeoutException during notification: {}", e.getMessage());
} finally {
notificationSoundVolume = null;
}
}
/*
* Return the IP and port of the OH2 web server
*/
public String getHostAndPort() {
return callbackUrl;
}
}

View File

@@ -0,0 +1,25 @@
/**
* 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.handler;
/***
* Enumeration of the play states of a player.
*
* @author Patrik Gfeller - Initial contribution
*
*/
enum SqueezeBoxPlayerPlayState {
STOP,
PLAY,
PAUSE
}

View File

@@ -0,0 +1,163 @@
/**
* 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.handler;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The {@link SqueezeBoxPlayerState} is responsible for saving the state of a player.
*
* @author Mark Hilbush - Initial contribution
* @author Patrik Gfeller - Moved class to its own file.
*/
class SqueezeBoxPlayerState {
private final Logger logger = LoggerFactory.getLogger(SqueezeBoxPlayerState.class);
private boolean savedMute;
private boolean savedPower;
private boolean savedStop;
private boolean savedControl;
private int savedVolume;
private int savedShuffle;
private int savedRepeat;
private int savedPlaylistIndex;
private int savedNumberPlaylistTracks;
private int savedPlayingTime;
private SqueezeBoxPlayerHandler playerHandler;
public SqueezeBoxPlayerState(SqueezeBoxPlayerHandler playerHandler) {
this.playerHandler = playerHandler;
save();
}
boolean isMuted() {
return savedMute;
}
boolean isPoweredOn() {
return savedPower;
}
boolean isStopped() {
return savedStop;
}
boolean isPlaying() {
return savedControl;
}
boolean isShuffling() {
return savedShuffle == 0 ? false : true;
}
int getShuffle() {
return savedShuffle;
}
boolean isRepeating() {
return savedRepeat == 0 ? false : true;
}
int getRepeat() {
return savedRepeat;
}
int getVolume() {
return savedVolume;
}
int getPlaylistIndex() {
return savedPlaylistIndex;
}
private int getNumberPlaylistTracks() {
return savedNumberPlaylistTracks;
}
int getPlayingTime() {
return savedPlayingTime;
}
private void save() {
savedVolume = playerHandler.currentVolume();
savedMute = playerHandler.currentMute();
savedPower = playerHandler.currentPower();
savedStop = playerHandler.currentStop();
savedControl = playerHandler.currentControl();
savedShuffle = playerHandler.currentShuffle();
savedRepeat = playerHandler.currentRepeat();
savedPlaylistIndex = playerHandler.currentPlaylistIndex();
savedNumberPlaylistTracks = playerHandler.currentNumberPlaylistTracks();
savedPlayingTime = playerHandler.currentPlayingTime();
logger.debug("Cur State: vol={}, mut={}, pwr={}, stp={}, ctl={}, shf={}, rpt={}, tix={}, tnm={}, tim={}",
savedVolume, muteAsString(), powerAsString(), stopAsString(), controlAsString(), shuffleAsString(),
repeatAsString(), getPlaylistIndex(), getNumberPlaylistTracks(), getPlayingTime());
}
private String muteAsString() {
return isMuted() ? "MUTED" : "NOT MUTED";
}
private String powerAsString() {
return isPoweredOn() ? "ON" : "OFF";
}
private String stopAsString() {
return isStopped() ? "STOPPED" : "NOT STOPPED";
}
private String controlAsString() {
return isPlaying() ? "PLAYING" : "PAUSED";
}
private String shuffleAsString() {
String shuffle = "OFF";
if (getShuffle() == 1) {
shuffle = "SONG";
} else if (getShuffle() == 2) {
shuffle = "ALBUM";
}
return shuffle;
}
private String repeatAsString() {
String repeat = "OFF";
if (getRepeat() == 1) {
repeat = "SONG";
} else if (getRepeat() == 2) {
repeat = "PLAYLIST";
}
return repeat;
}
/***
* Return the player state as {@link SqueezeBoxPlayerPlayState}
*
* @return {@link SqueezeBoxPlayerPlayState}
*/
SqueezeBoxPlayerPlayState getPlayState() {
if (!isPlaying() && !isStopped()) {
return SqueezeBoxPlayerPlayState.PAUSE;
}
if (isPlaying()) {
return SqueezeBoxPlayerPlayState.PLAY;
}
return SqueezeBoxPlayerPlayState.STOP;
}
}

View File

@@ -0,0 +1,57 @@
/**
* 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.model;
/**
* Attributes of a Squeezebox Server favorite
*
* @author Mark Hilbush - Initial contribution
*
*/
public class Favorite {
/**
* Favorite id is of form xxxxxxxx.nn
*/
public String id;
/**
* Just the nn part of the id
*/
public String shortId;
/**
* The name given to the favorite in the Squeezebox Server.
*/
public String name;
/**
* Creates a preset from the given favorite id
*
* @param id Squeezebox Server internal identifier for favorite
*/
public Favorite(String id) {
this.id = id;
this.shortId = id;
if (id.indexOf(".") != -1) {
this.shortId = id.substring(id.indexOf(".") + 1);
}
}
@Override
public String toString() {
StringBuilder sb = new StringBuilder();
sb.append("Favorite {id=").append(id).append(", shortId=").append(shortId).append(", name=").append(name)
.append("}");
return sb.toString();
}
}

View File

@@ -0,0 +1,111 @@
/**
* 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.utils;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import org.eclipse.jetty.client.HttpClient;
import org.eclipse.jetty.client.api.ContentResponse;
import org.eclipse.jetty.client.util.StringContentProvider;
import org.eclipse.jetty.http.HttpMethod;
import org.eclipse.jetty.http.HttpStatus;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.gson.JsonElement;
import com.google.gson.JsonParser;
/**
* Collection of methods to help retrieve HTTP data from a SqueezeServer
*
* @author Dan Cunningham - Initial contribution
* @author Svilen Valkanov - replaced Apache HttpClient with Jetty
* @author Mark Hilbush - Add support for LMS authentication
* @author Mark Hilbush - Rework exception handling
*/
public class HttpUtils {
private static Logger logger = LoggerFactory.getLogger(HttpUtils.class);
private static final int TIMEOUT = 5000;
private static HttpClient client = new HttpClient();
/**
* JSON request to get the CLI port from a Squeeze Server
*/
private static final String JSON_REQ = "{\"params\": [\"\", [\"pref\" ,\"plugin.cli:cliport\",\"?\"]], \"id\": 1, \"method\": \"slim.request\"}";
/**
* Simple logic to perform a post request
*
* @param url URL to be sent to LMS server
* @param postData Data to be sent to LMS server
* @return Content received from LMS
* @throws SqueezeBoxCommunicationException
* @throws SqueezeBoxNotAuthorizedException
*/
public static String post(String url, String postData)
throws SqueezeBoxNotAuthorizedException, SqueezeBoxCommunicationException {
if (!client.isStarted()) {
try {
client.start();
} catch (Exception e) {
throw new SqueezeBoxCommunicationException("Jetty http client exception: " + e.getMessage());
}
}
ContentResponse response;
try {
response = client.newRequest(url).method(HttpMethod.POST).content(new StringContentProvider(postData))
.timeout(TIMEOUT, TimeUnit.MILLISECONDS).send();
} catch (InterruptedException | TimeoutException | ExecutionException e) {
throw new SqueezeBoxCommunicationException("Jetty http client exception: " + e.getMessage());
}
int statusCode = response.getStatus();
if (statusCode == HttpStatus.UNAUTHORIZED_401) {
String statusLine = response.getStatus() + " " + response.getReason();
logger.error("Received '{}' from squeeze server", statusLine);
throw new SqueezeBoxNotAuthorizedException("Unauthorized: " + statusLine);
}
if (statusCode != HttpStatus.OK_200) {
String statusLine = response.getStatus() + " " + response.getReason();
logger.error("HTTP POST method failed: {}", statusLine);
throw new SqueezeBoxCommunicationException("Http post to server failed: " + statusLine);
}
return response.getContentAsString();
}
/**
* Retrieves the command line port (cli) from a SqueezeServer
*
* @param ip
* @param webPort
* @return Command Line Interpreter (CLI) port number
* @throws SqueezeBoxNotAuthorizedException
* @throws SqueezeBoxCommunicationException
* @throws NumberFormatException
*/
public static int getCliPort(String ip, int webPort)
throws SqueezeBoxNotAuthorizedException, SqueezeBoxCommunicationException {
String url = "http://" + ip + ":" + webPort + "/jsonrpc.js";
String json = HttpUtils.post(url, JSON_REQ);
logger.trace("Recieved json from server {}", json);
JsonElement resp = new JsonParser().parse(json);
String cliPort = resp.getAsJsonObject().get("result").getAsJsonObject().get("_p2").getAsString();
return Integer.parseInt(cliPort);
}
}

View File

@@ -0,0 +1,30 @@
/**
* 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.utils;
/**
* Exception thrown when unable to communicate with LMS server.
*
* @author Mark Hilbush - Initial contribution
*/
public class SqueezeBoxCommunicationException extends Exception {
private static final long serialVersionUID = 1540489268747099161L;
public SqueezeBoxCommunicationException(String message) {
super(message);
}
public SqueezeBoxCommunicationException(String message, Throwable cause) {
super(message, cause);
}
}

View File

@@ -0,0 +1,32 @@
/**
* 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.utils;
/**
* Exception thrown when calling LMS command line interface, and
* the LMS is set up to require authentication.
*
* @author Mark Hilbush - Initial contribution
*/
public class SqueezeBoxNotAuthorizedException extends Exception {
private static final long serialVersionUID = -5190671725971757821L;
public SqueezeBoxNotAuthorizedException(String message) {
super(message);
}
public SqueezeBoxNotAuthorizedException(String message, Throwable cause) {
super(message, cause);
}
}

View File

@@ -0,0 +1,29 @@
/**
* 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.utils;
/***
*
* Exception class to indicate a timeout during comminication with
* the media server.
*
* @author Patrik Gfeller - Initial contribution
*
*/
public class SqueezeBoxTimeoutException extends Exception {
private static final long serialVersionUID = 4542388088266882905L;
public SqueezeBoxTimeoutException(String message) {
super(message);
}
}

View File

@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="UTF-8"?>
<binding:binding id="squeezebox" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:binding="https://openhab.org/schemas/binding/v1.0.0"
xsi:schemaLocation="https://openhab.org/schemas/binding/v1.0.0 https://openhab.org/schemas/binding-1.0.0.xsd">
<name>SqueezeBox Binding</name>
<description>This is the binding for the Logitech Squeeze Server and Players.</description>
<author>Dan Cunningham</author>
<config-description>
<parameter name="callbackUrl" type="text">
<label>Callback URL</label>
<description>URL to use for playing notification sounds, e.g. http://192.168.0.2:8080</description>
<required>false</required>
</parameter>
</config-description>
</binding:binding>

View File

@@ -0,0 +1,307 @@
<?xml version="1.0" encoding="UTF-8"?>
<thing:thing-descriptions bindingId="squeezebox"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
<bridge-type id="squeezeboxserver">
<label>SqueezeBox Server</label>
<description>This is a SqueezeBox Server instance.</description>
<channels>
<channel id="favoritesList" typeId="favoritesList"/>
</channels>
<representation-property>ipAddress</representation-property>
<config-description>
<parameter name="ipAddress" type="text" required="true">
<label>IP or Host Name</label>
<description>The IP or host name of the SqueezeServer
</description>
</parameter>
<parameter name="webport" type="integer" required="true" min="1" max="65535">
<label>SqueezeServer Web Port</label>
<description>Webport interface of the SqueezeServer</description>
<default>9000</default>
</parameter>
<parameter name="cliport" type="integer" required="true" min="1" max="65535">
<label>SqueezeServer CLI Port</label>
<description>Port of the CLI interface of the SqueezeServer</description>
<default>9090</default>
</parameter>
<parameter name="language" type="text" required="false">
<label>Language</label>
<description>Language to use when using Google speech</description>
<default>en</default>
</parameter>
<parameter name="userId" type="text" required="false">
<label>User Id</label>
<description>User ID used to login to Squeeze Server</description>
</parameter>
<parameter name="password" type="text" required="false">
<label>Password</label>
<description>Password used to login to Squeeze Server</description>
<context>password</context>
</parameter>
</config-description>
</bridge-type>
<thing-type id="squeezeboxplayer">
<supported-bridge-type-refs>
<bridge-type-ref id="squeezeboxserver"/>
</supported-bridge-type-refs>
<label>SqueezeBox Player</label>
<description>This is a SqueezeBox Player instance</description>
<channels>
<channel id="power" typeId="power"/>
<channel id="mute" typeId="mute"/>
<channel id="volume" typeId="volume"/>
<channel id="stop" typeId="stop"/>
<channel id="playPause" typeId="playPause"/>
<channel id="next" typeId="next"/>
<channel id="prev" typeId="prev"/>
<channel id="control" typeId="control"/>
<channel id="stream" typeId="stream"/>
<channel id="source" typeId="source"/>
<channel id="sync" typeId="sync"/>
<channel id="unsync" typeId="unsync"/>
<channel id="playListIndex" typeId="playListIndex"/>
<channel id="currentPlayingTime" typeId="currentPlayingTime"/>
<channel id="duration" typeId="duration"/>
<channel id="currentPlaylistShuffle" typeId="currentPlaylistShuffle"/>
<channel id="currentPlaylistRepeat" typeId="currentPlaylistRepeat"/>
<channel id="title" typeId="title"/>
<channel id="remotetitle" typeId="remotetitle"/>
<channel id="album" typeId="album"/>
<channel id="artist" typeId="artist"/>
<channel id="year" typeId="year"/>
<channel id="genre" typeId="genre"/>
<channel id="coverartdata" typeId="coverartdata"/>
<channel id="ircode" typeId="ircode"/>
<channel id="numberPlaylistTracks" typeId="numberPlaylistTracks"/>
<channel id="playFavorite" typeId="playFavorite"/>
<channel id="rate" typeId="rate"/>
</channels>
<properties>
<property name="vendor">Logitech</property>
<property name="modelId"></property>
<property name="name"></property>
<property name="uid"></property>
<property name="ip"></property>
</properties>
<representation-property>mac</representation-property>
<config-description>
<parameter name="mac" type="text" required="true">
<label>MAC Address</label>
<description>SqueezeBox Players are identified by their MAC address</description>
</parameter>
<parameter name="notificationTimeout" type="integer" unit="s">
<label>Notification Timeout</label>
<description>Maximum amount of time in seconds for which the notification will be played</description>
<default>20</default>
</parameter>
<parameter name="notificationVolume" type="integer" min="0" max="100" step="1" unit="%">
<label>Notification Sound Volume</label>
<description>Volume used for notifications. Leaving blank uses current player volume.</description>
</parameter>
</config-description>
</thing-type>
<!-- Favorites -->
<channel-type id="favoritesList">
<item-type>String</item-type>
<label>Favorites List</label>
<description>Comma-separated list of favorites of form favoriteId=favoriteName</description>
<state readOnly="true" pattern="%s"></state>
<config-description>
<parameter name="quoteList" type="boolean" required="true">
<label>Quote Favorites</label>
<description>Wrap the right hand side of the favorites in quotes</description>
<default>false</default>
</parameter>
</config-description>
</channel-type>
<channel-type id="playFavorite">
<item-type>String</item-type>
<label>Play a Favorite</label>
<description>Play favorite by sending command with favoriteId</description>
<state pattern="%s"></state>
</channel-type>
<!-- Commands -->
<channel-type id="power" advanced="true">
<item-type>Switch</item-type>
<label>Power</label>
<description>Power on/off your device</description>
</channel-type>
<channel-type id="mute" advanced="true">
<item-type>Switch</item-type>
<label>Mute</label>
<description>Mute/unmute your device</description>
</channel-type>
<channel-type id="volume">
<item-type>Dimmer</item-type>
<label>Volume</label>
<description>Volume of your device</description>
<state min="0" max="100" step="1" pattern="%d %%">
</state>
</channel-type>
<channel-type id="stop" advanced="true">
<item-type>Switch</item-type>
<label>Stop</label>
<description>Stop the current title</description>
</channel-type>
<channel-type id="playPause" advanced="true">
<item-type>Switch</item-type>
<label>Play/Pause</label>
<description>Plays (on) or pauses (off) the player</description>
</channel-type>
<channel-type id="pause" advanced="true">
<item-type>Switch</item-type>
<label>Pause</label>
<description>Send a pause command to the player</description>
</channel-type>
<channel-type id="next" advanced="true">
<item-type>Switch</item-type>
<label>Next</label>
<description>Send a next command to the player</description>
</channel-type>
<channel-type id="prev" advanced="true">
<item-type>Switch</item-type>
<label>Previous</label>
<description>Send a previous command to the player</description>
</channel-type>
<channel-type id="control">
<item-type>Player</item-type>
<label>Control</label>
<description>Control the Zone Player, e.g. start/stop/next/previous/ffward/rewind</description>
<category>Player</category>
</channel-type>
<channel-type id="stream" advanced="true">
<item-type>String</item-type>
<label>Stream URL</label>
<description>Play the given HTTP or file stream (file:// or http://). </description>
</channel-type>
<channel-type id="source" advanced="true">
<item-type>String</item-type>
<label>Source</label>
<description>Shows the source of the currently playing playlist entry.</description>
<state readOnly="true" pattern="%s"></state>
</channel-type>
<channel-type id="sync" advanced="true">
<item-type>String</item-type>
<label>Sync Player</label>
<description>Add another player to your device for synchronized playback (other player mac address)</description>
</channel-type>
<channel-type id="unsync" advanced="true">
<item-type>Switch</item-type>
<label>UnSync Player</label>
<description>Remove this device from synchronization</description>
</channel-type>
<channel-type id="playListIndex" advanced="true">
<item-type>Number</item-type>
<label>Playlist Index</label>
<description>Playlist index</description>
</channel-type>
<channel-type id="currentPlayingTime">
<item-type>Number</item-type>
<label>Current Playing Time</label>
<description>Current Playing Time</description>
</channel-type>
<channel-type id="duration">
<item-type>Number</item-type>
<label>Track Duration</label>
<description>Duration of Current Track (in seconds)</description>
</channel-type>
<channel-type id="currentPlaylistShuffle">
<item-type>Number</item-type>
<label>Shuffle Mode</label>
<description>Current playlist shuffle mode</description>
<state>
<options>
<option value="0">No Shuffle</option>
<option value="1">Shuffle Songs</option>
<option value="2">Shuffle Albums</option>
</options>
</state>
</channel-type>
<channel-type id="currentPlaylistRepeat">
<item-type>Number</item-type>
<label>Repeat Mode</label>
<description>Current playlist repeat Mode</description>
<state>
<options>
<option value="0">No Repeat</option>
<option value="1">Repeat Song</option>
<option value="2">Repeat Playlist</option>
</options>
</state>
</channel-type>
<!-- Squeezebox variables -->
<channel-type id="title">
<item-type>String</item-type>
<label>Title</label>
<description>Title of the current song</description>
<state readOnly="true" pattern="%s"></state>
</channel-type>
<channel-type id="remotetitle" advanced="true">
<item-type>String</item-type>
<label>Remote Title</label>
<description>Remote Title (Radio) of the current song</description>
<state readOnly="true" pattern="%s"></state>
</channel-type>
<channel-type id="album">
<item-type>String</item-type>
<label>Album</label>
<description>Album name of the current song</description>
<state readOnly="true" pattern="%s"></state>
</channel-type>
<channel-type id="artist">
<item-type>String</item-type>
<label>Artist</label>
<description>Artist name of the current song</description>
<state readOnly="true" pattern="%s"></state>
</channel-type>
<channel-type id="year" advanced="true">
<item-type>String</item-type>
<label>Year</label>
<description>Release year of the current song</description>
<state readOnly="true" pattern="%s"></state>
</channel-type>
<channel-type id="genre" advanced="true">
<item-type>String</item-type>
<label>Genre</label>
<description>Genre name of the current song</description>
<state readOnly="true" pattern="%s"></state>
</channel-type>
<channel-type id="coverartdata">
<item-type>Image</item-type>
<label>Cover Art</label>
<description>Image data of cover art of the current song</description>
<state readOnly="true"></state>
</channel-type>
<channel-type id="ircode" advanced="true">
<item-type>String</item-type>
<label>IR Code</label>
<description>String of the cached IR code</description>
<state readOnly="true" pattern="%s"></state>
</channel-type>
<channel-type id="numberPlaylistTracks" advanced="true">
<item-type>Number</item-type>
<label>Number of Playlist Tracks</label>
<description>Number of playlist tracks</description>
<state readOnly="true" pattern="%d"></state>
</channel-type>
<channel-type id="rate" advanced="true">
<item-type>Switch</item-type>
<label>Like or Unlike Song</label>
<description>Likes or unlikes the current song (if the service supports it)</description>
</channel-type>
</thing:thing-descriptions>