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.heos</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,410 @@
# Denon HEOS Binding
This binding support the HEOS-System from Denon.
The binding provides control of the players and groups within the network.
It also supports selecting favorites and play them on players or groups within the HEOS-Network.
The binding first establishes a connection to one of the players of the HEOS-Network and use them as a bridge.
After a connection is established, the binding searches for all available players and groups via the bridge.
To keep the network traffic low it is recommended to establish only one connection via one bridge.
Connection to the bridge is done via a Telnet connection.
## Supported Things
Bridge:
The binding supports a bridge to connect to the HEOS-Network.
A bridge uses the thing ID "bridge".
Player:
A generic player is supported via this binding.
Currently no differences are made between the players.
A player uses the Thing ID "player"
Groups:
The binding supports HEOS groups.
A group uses the Thing ID "group"
## Discovery
This binding supports full automatic discovery of available players to be used as a bridge, players and groups.
You need to add a Bridge device first (which is also auto-discovered by the binding) which can be any HEOS device in your network (preferably which has wired connection).
__Important!__
Please note that only one bridge is required to establish a connection.
Adding a second bridge can cause trouble with the connection.
It is recommended to use the Paper UI to setup the system and add all players and groups.
The bridge is discovered through UPnP in the local network.
Once it is added the players and groups are discovered via the bridge and placed in the Paper UI Inbox.
## Binding Configuration
This binding does not require any configuration.
## Thing Configuration
### Bridge Configuration
The bridge has the following configuration parameter
| Parameter | Description | Required |
|----------------- |------------------------------------------------------------ | --------- |
| ipAddress | The network address of the Bridge | yes |
| username | The user name to login to the HEOS account | no |
| password | The password for the HEOS account | no |
| heartbeat | The time in seconds for the HEOS Heartbeat (default = 60 s) | no |
The password and the user name are used to login to the HEOS account.
This is required to load the favorites, playlists and so on from personal settings.
If no login information is provided these features can't be used.
````
Bridge heos:bridge:main "name" [ipAddress="192.168.0.1", unsername="xxx", password="123456"]
````
### Player Configuration
Player have the following configuration parameter
| Parameter | Description | Required |
|----------------- |----------------------------------------------------------- | --------- |
| pid | The internal Player ID | yes |
For manual configuration a player can be defined as followed:
````
Thing heos:player:player1 "name" [pid="123456789"]
````
PID behind the heos:player:--- should be changed as required.
It is recommended to use the Player PID.
If the PID isn't known it can be discovered by establishing a Telnet connection (port 1255) to one player and search for available players (Command: heos://player/get_players) within the network.
Another way is to use Paper UI to discover the Player via the bridge and get the PID.
For further details refer to the [HEOS CLI](https://rn.dmglobal.com/euheos/HEOS_CLI_ProtocolSpecification.pdf) specification.
### Group Configuration
Player have the following configuration parameter
| Parameter | Description | Required |
|----------------- |------------------------------------------------------------------------------------- | --------- |
| members | The members of the groups. These are the player IDs. IDs have to be separated by ";" | yes |
If you use Paper UI to manage your Things (which is the preferred way), you can also set up your group automatically from Paper UI.
Groups will automatically appear in the Inbox if that Group is active. To do this, build your Group from the HEOS app, then the group will appear in the Inbox.
```
Thing heos:group:group1 "name" [members="45345634;35534567"]
```
### Defining Bridge and Players together
Defining Player and Bridge together.
To ensure that the players and groups are attached to the bridge the definition can be like:
```
Bridge heos:bridge:main "Bridge" [ipAddress="192.168.0.1", username="userName", password="123456"] {
player Kitchen "Kitchen"[pid="434523813"]
player LivingRoom "Living Room"[pid="918797451"]
group 813793755 "Ground Level"[members="434523813;918797451"]
}
```
## Channels
### Channels of Thing type 'player'
| Channel ID | Item Type | Description |
|----------------- |----------- |--------------------------------------------------------------------- |
| Control | Player | Play (also ON) / Pause (also OFF) / Next / Previous |
| Volume | Dimmer | Volume control / also accepts "DECREASE" & "INCREASE" |
| Mute | Switch | Mute the Player |
| Title | String | Song Title |
| Artist | String | Song Artist |
| Album | String | Album Title |
| Cover | Image | The cover of the actual song |
| Inputs | String | The input to be switched to. Input values from HEOS protocol |
| CurrentPosition | Number:Time | Shows the current track position in seconds |
| Duration | Number:Time | The overall track duration in seconds |
| Type | String | The type of the played media. Station or song for example |
| Station | String | The station name if it is a station (Spotify shows track name....) |
| PlayUrl | String | Plays a media file located at the URL |
| Shuffle | Switch | Switches shuffle ON or OFF |
| RepeatMode | String | Defines the repeat mode: Inputs are: "One" , "All" or "Off" |
| Favorites | String | Plays a favorite. The selection options are retrieved automatically |
| Playlists | String | Plays a playlist. The selection options are retrieved automatically |
| Queue | String | Plays from the queue. The queue items are retrieved automatically |
| ClearQueue | Switch | Clear the queue when turned ON |
The `Favorites`, `Playlists`, `Queue` selection options are queried automatically from the HEOS system (if you set up any in the HEOS app).
This means the available options will be visible in a Selection, you don't have to specify them manually.
You can send commands to these channels from rules by sending the name of the selected item (For example: Starting a favorite radio channel from rule).
#### Example
```
Player LivingRoom_Control "Control" {channel="heos:player:main:LivingRoom:Control"}
Selection item=LivingRoom_Playlists label="Playlist" icon="music"
```
### Channels of Thing type 'group'
| Channel ID | Item Type | Description |
|----------------- |----------- |-------------------------------------------------------------------- |
| Control | Player | Play (also ON) / Pause (also OFF) / Next / Previous |
| Volume | Dimmer | Volume control / also accepts "DECREASE" & "INCREASE" |
| Mute | Switch | Mute the Group |
| Title | String | Song Title |
| Artist | String | Song Artist |
| Album | String | Album Title |
| Ungroup | Switch | Deletes the group (OFF) or generate the group again (ON) |
| Cover | Image | The cover of the actual song |
| CurrentPosition | Number:Time | Shows the current track position in seconds |
| Duration | Number:Time | The overall track duration in seconds |
| Type | String | The type of the played media. Station or song for example |
| Station | String | The station name if it is a station (Spotify shows track name....) |
| Inputs | String | The input to be switched to. Input values from HEOS protocol |
| PlayUrl | String | Plays a media file located at the URL |
| Shuffle | Switch | Switches shuffle ON or OFF |
| RepeatMode | String | Defines the repeat mode: Inputs are: "One" ; "All" or "Off" |
| Favorites | String | Plays a favorite. The selection options are retrieved automatically |
| Playlists | String | Plays a playlist. The selection options are retrieved automatically |
| Queue | String | Plays from the queue. The queue items are retrieved automatically |
| ClearQueue | Switch | Clear the queue when turned ON |
The `Favorites`, `Playlists`, `Queue` selection options are queried automatically from the HEOS system (if you set up any in the HEOS app).
This means the available options will be visible in a Selection, you don't have to specify them manually.
You can send commands to these channels from rules by sending the name of the selected item (For example: Starting a favorite radio channel from rule).
### Available inputs
| Input names |
|-------------- |
| aux_in_1 |
| aux_in_2 |
| aux_in_3 |
| aux_in_4 |
| aux1 |
| aux2 |
| aux3 |
| aux4 |
| aux5 |
| aux6 |
| aux7 |
| line_in_1 |
| line_in_2 |
| line_in_3 |
| line_in_4 |
| coax_in_1 |
| coax_in_2 |
| optical_in_1 |
| optical_in_2 |
| hdmi_in_1 |
| hdmi_arc_1 |
| cable_sat |
| dvd |
| bluray |
| game |
| mediaplayer |
| cd |
| tuner |
| hdradio |
| tvaudio |
| phono |
A current list can be found within the HEOS CLI protocol which can be found [here](https://rn.dmglobal.com/euheos/HEOS_CLI_ProtocolSpecification.pdf).
### Channels of Thing type 'bridge'
| Channel ID | Item Type | Description |
|---------------------- |----------- |-------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Reboot | Switch | Reboot the whole HEOS System. Can be used if you get in trouble with the system |
| BuildGroup | Switch | Is used to define a group. The player which shall be grouped has to be selected first. If Switch is then activated the group is built. |
For a list of the commands please refer to the [HEOS CLI protocol](https://rn.dmglobal.com/euheos/HEOS_CLI_ProtocolSpecification.pdf).
## *Dynamic Channels*
Also the bridge supports dynamic channels which represent the players of the network.
They are added dynamically if a player is found. The player and group channels are only shown on the bridge.
### Player Channels
| Channel ID | Item Type | Description |
|------------ |----------- |----------------------------------------------------------------------------------------------------- |
| {playerID} | Switch | A channel which represents the player. Please check via UI how the correct Channel Type looks like. |
Example
```
Switch Player_1 "Player [%s]" {channel="heos:bridge:main:P123456789"}
```
The {playerUID} has either a P in front of the number which indicates that this is a player or a G to indicate this is a group.
## Full Example
### demo.things:
```
Bridge heos:bridge:main "Bridge" [ipAddress="192.168.0.1", username="userName", password="123456"] {
player Kitchen "Kitchen"[pid="434523813"]
player LivingRoom "Living Room"[pid="918797451"]
group 813793755 "Ground Level"[members="434523813;918797451"]
}
```
### demo.items:
```
Player LivingRoom_Control "Control" {channel="heos:player:main:LivingRoom:Control"}
Switch LivingRoom_Mute "Mute"{channel="heos:player:main:LivingRoom:Mute"}
Dimmer LivingRoom_Volume "Volume" {channel="heos:player:main:LivingRoom:Volume"}
String LivingRoom_Title "Title [%s]" {channel="heos:player:main:LivingRoom:Title"}
String LivingRoom_Interpret "Interpret [%s]" {channel="heos:player:main:LivingRoom:Artist"}
String LivingRoom_Album "Album [%s]" {channel="heos:player:main:LivingRoom:Album"}
String LivingRoom_Favorites {channel="heos:player:main:LivingRoom:Favorites"}
String LivingRoom_Playlists {channel="heos:player:main:LivingRoom:Playlists"}
```
### demo.sitemap
```
Frame label="LivingRoom" {
Default item=LivingRoom_Control
Default item=LivingRoom_Mute
Default item=LivingRoom_Volume
Default item=LivingRoom_Title
Default item=LivingRoom_Interpret
Default item=LivingRoom_Album
Selection item=LivingRoom_Favorites label="Favorite" icon="music"
Selection item=LivingRoom_Playlists label="Playlist" icon="music"
}
```
## Detailed Explanation
This section gives some detailed explanations how to use the binding.
### Grouping Players
Players can be grouped via the binding.
The easiest way to do this is to use the created Group type Thing. To group them simply use the `Ungroup` channel on the Group. Switching this Switch ON and OFF will group and ungroup that Group.
The first player which is selected will be the Group leader.
Therefore changing play/pause and some other things at any player (which is included in that group) will also change that at the whole group.
Muting and Volume on the other hand can be changed individually for each Player also for the group leader.
If you want to change that for the whole group you have to do it via the Group thing.
### Inputs
To play inputs like the Aux_In it can be played at each player or group.
It is also possible to play an input from another player at the selected player.
To do so, first select the player channel of the player where the input is located (source) at the bridge.
Then use the input channel of the player where the source shall be played (destination) to activate the input.
#### Example
Player A = Kitchen (destination)
Player B = Living Room (source)
Items:
```
Switch HeosBridge_Play_Living "Living Room" (gHeos) {channel="heos:bridge:ed0ac1ff-0193-65c6-c1b8-506137456a50:P918797451"}
String HeosKitchen_Input (gHeos) {channel="heos:player:918797451:Inputs"}
String HeosKitchen_InputSelect "Input" (gHeos)
```
Rule for kitchen:
```
rule "Play AuxIn from Living Room"
when
Item HeosKitchen_InputSelect received command
then
if (receivedCommand.toString == "aux_in_1") {
sendCommand(HeosKitchen_Input, "aux_in_1")
} if (receivedCommand.toString == "LivingRoom") {
sendCommand(HeosBridge_Play_Living, ON)
sendCommand(HeosKitchen_Input, "aux_in_1")
sendCommand(HeosBridge_Play_Living, OFF) //Switch player channel off again to be sure that it is OFF
}
```
Sitemap:
```
Switch item=HeosKitchen_InputSelect mappings=[aux_in_1 = "Aux In" , LivingRoom = "Living Room"]
```
### The Online status of Groups and Players
The online state of a Thing can be helpful for groups to control the visibility of group items within sitemap.
So if the group is removed the visibility of those items is also changed.
#### Example
First you have to define a new Item within the Item section which is used later within the Sitemap:
Items:
```
String HeosGroup_Status
```
Then we need a rule which triggers the state if an Item goes Online or Offline.
Rules:
```
rule "Online State Heos Group"
when
Thing "heos:group:1747557118" changed
then
var thingStatus = getThingStatusInfo("heos:group:1747557118")
sendCommand(HeosGroup_Status, thingStatus.getStatus.toString)
end
```
Sitemap:
```
Frame label="Heos Group" visibility=[HeosGroup_Status==ONLINE] {
Default item=HeosGroup1_Player
Default item=HeosGroup1_Volume
Default item=HeosGroup1_Mute
Default item=HeosGroup1_Favorites
Default item=HeosGroup1_Playlist
Text item=HeosGroup1_Song {
Default item=HeosGroup1_Song
Default item=HeosGroup1_Artist
Default item=HeosGroup1_Album
Image item=HeosGroup1_Cover url=""
}
}
```
## Rule Actions
Multiple actions are supported by this binding. In classic rules these are accessible as shown in the example below:
```
val actions = getActions("heos","heos:bridge:bridgeId")
if(null === actions) {
logInfo("actions", "Actions not found, check thing ID")
return
} else {
actions.playInputFromPlayer(-3213214, "aux_in_1", 89089081)
}
```
### playInputFromPlayer(sourcePlayer, sourceInput, destination)
Allows to play a source from a player to another player.

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.heos</artifactId>
<name>openHAB Add-ons :: Bundles :: HEOS Binding</name>
</project>

View File

@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="UTF-8"?>
<features name="org.openhab.binding.heos-${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-heos" description="HEOS Binding" version="${project.version}">
<feature>openhab-runtime-base</feature>
<feature>openhab-transport-upnp</feature>
<bundle dependency="true">mvn:commons-net/commons-net/3.6</bundle>
<bundle start-level="80">mvn:org.openhab.addons.bundles/org.openhab.binding.heos/${project.version}</bundle>
</feature>
</features>

View File

@@ -0,0 +1,91 @@
/**
* 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.heos.internal;
import java.util.Collections;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.heos.internal.resources.HeosConstants;
import org.openhab.core.thing.ThingTypeUID;
import org.openhab.core.thing.type.ChannelTypeUID;
/**
* The {@link HeosBinding} class defines common constants, which are
* used across the whole binding.
*
* @author Johannes Einig - Initial contribution
*/
@NonNullByDefault
public class HeosBindingConstants extends HeosConstants {
public static final String BINDING_ID = "heos";
// List of all Bridge Type UIDs
public static final ThingTypeUID THING_TYPE_BRIDGE = new ThingTypeUID(BINDING_ID, "bridge");
public static final ThingTypeUID THING_TYPE_PLAYER = new ThingTypeUID(BINDING_ID, "player");
public static final ThingTypeUID THING_TYPE_GROUP = new ThingTypeUID(BINDING_ID, "group");
// List off all Channel Types
public static final ChannelTypeUID CH_TYPE_PLAYER = new ChannelTypeUID(BINDING_ID, "chPlayer");
// List of all Channel IDs
public static final String CH_ID_CONTROL = "Control";
public static final String CH_ID_VOLUME = "Volume";
public static final String CH_ID_MUTE = "Mute";
public static final String CH_ID_UNGROUP = "Ungroup";
public static final String CH_ID_SONG = "Title";
public static final String CH_ID_ARTIST = "Artist";
public static final String CH_ID_ALBUM = "Album";
public static final String CH_ID_BUILDGROUP = "BuildGroup";
public static final String CH_ID_REBOOT = "Reboot";
public static final String CH_ID_COVER = "Cover";
public static final String CH_ID_PLAYLISTS = "Playlists";
public static final String CH_ID_FAVORITES = "Favorites";
public static final String CH_ID_QUEUE = "Queue";
public static final String CH_ID_CLEAR_QUEUE = "ClearQueue";
public static final String CH_ID_INPUTS = "Inputs";
public static final String CH_ID_CUR_POS = "CurrentPosition";
public static final String CH_ID_DURATION = "Duration";
public static final String CH_ID_STATION = "Station";
public static final String CH_ID_RAW_COMMAND = "RawCommand";
public static final String CH_ID_TYPE = "Type";
public static final String CH_ID_PLAY_URL = "PlayUrl";
public static final String CH_ID_SHUFFLE_MODE = "Shuffle";
public static final String CH_ID_REPEAT_MODE = "RepeatMode";
// Values for Bridge, Player and Group Properties;
// Using this values to display the correct name
// within the thing properties.
public static final String PROP_PID = "pid";
public static final String PROP_GROUP_MEMBERS = "members";
public static final String PROP_NAME = "Name";
public static final String PROP_GID = "Group ID";
public static final String PROP_IP = "IP Address";
public static final String PROP_NETWORK = "Connection";
public static final String PROP_GROUP_HASH = "Members Hash value";
public static final String PROP_GROUP_LEADER = "Group leader";
public static final String USERNAME = "username";
public static final String PASSWORD = "password";
public static final String HEARTBEAT = "heartbeat";
public static final Set<ThingTypeUID> SUPPORTED_THING_TYPES_UIDS = Collections.unmodifiableSet(
Stream.of(THING_TYPE_BRIDGE, THING_TYPE_GROUP, THING_TYPE_PLAYER).collect(Collectors.toSet()));
public static final int FAILURE_COUNT_LIMIT = 5;
}

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.heos.internal;
import static org.openhab.binding.heos.internal.HeosBindingConstants.*;
import java.util.HashMap;
import java.util.Map;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.heos.internal.handler.*;
import org.openhab.binding.heos.internal.resources.HeosEventListener;
import org.openhab.core.thing.ChannelUID;
import org.openhab.core.thing.type.ChannelTypeUID;
/**
* The {@link HeosChannelHandlerFactory} is responsible for creating and returning
* of the single handler for each channel of the single things.
* It also stores already created handler for further use.
*
* @author Johannes Einig - Initial contribution
*/
@NonNullByDefault
public class HeosChannelHandlerFactory {
private final HeosBridgeHandler bridge;
private final HeosDynamicStateDescriptionProvider heosDynamicStateDescriptionProvider;
private final Map<ChannelUID, HeosChannelHandler> handlerStorageMap = new HashMap<>();
public HeosChannelHandlerFactory(HeosBridgeHandler bridge,
HeosDynamicStateDescriptionProvider heosDynamicStateDescriptionProvider) {
this.bridge = bridge;
this.heosDynamicStateDescriptionProvider = heosDynamicStateDescriptionProvider;
}
public @Nullable HeosChannelHandler getChannelHandler(ChannelUID channelUID, HeosEventListener eventListener,
@Nullable ChannelTypeUID channelTypeUID) {
if (handlerStorageMap.containsKey(channelUID)) {
return handlerStorageMap.get(channelUID);
} else {
HeosChannelHandler handler = createNewChannelHandler(channelUID, eventListener, channelTypeUID);
if (handler != null) {
handlerStorageMap.put(channelUID, handler);
}
return handler;
}
}
private @Nullable HeosChannelHandler createNewChannelHandler(ChannelUID channelUID, HeosEventListener eventListener,
@Nullable ChannelTypeUID channelTypeUID) {
switch (channelUID.getId()) {
case CH_ID_CONTROL:
return new HeosChannelHandlerControl(eventListener, bridge);
case CH_ID_VOLUME:
return new HeosChannelHandlerVolume(eventListener, bridge);
case CH_ID_MUTE:
return new HeosChannelHandlerMute(eventListener, bridge);
case CH_ID_INPUTS:
return new HeosChannelHandlerInputs(eventListener, bridge);
case CH_ID_REPEAT_MODE:
return new HeosChannelHandlerRepeatMode(eventListener, bridge);
case CH_ID_SHUFFLE_MODE:
return new HeosChannelHandlerShuffleMode(eventListener, bridge);
case CH_ID_ALBUM:
case CH_ID_SONG:
case CH_ID_ARTIST:
case CH_ID_COVER:
case CH_ID_TYPE:
case CH_ID_STATION:
return new HeosChannelHandlerNowPlaying(eventListener, bridge);
case CH_ID_QUEUE:
return new HeosChannelHandlerQueue(heosDynamicStateDescriptionProvider, bridge);
case CH_ID_CLEAR_QUEUE:
return new HeosChannelHandlerClearQueue(bridge);
case CH_ID_PLAY_URL:
return new HeosChannelHandlerPlayURL(bridge);
case CH_ID_UNGROUP:
return new HeosChannelHandlerGrouping(bridge);
case CH_ID_RAW_COMMAND:
return new HeosChannelHandlerRawCommand(eventListener, bridge);
case CH_ID_REBOOT:
return new HeosChannelHandlerReboot(bridge);
case CH_ID_BUILDGROUP:
return new HeosChannelHandlerBuildGroup(channelUID, bridge);
case CH_ID_PLAYLISTS:
return new HeosChannelHandlerPlaylist(heosDynamicStateDescriptionProvider, bridge);
case CH_ID_FAVORITES:
return new HeosChannelHandlerFavorite(heosDynamicStateDescriptionProvider, bridge);
case CH_ID_CUR_POS:
case CH_ID_DURATION:
// nothing to handle, we receive updates automatically
return null;
}
if (channelTypeUID != null) {
if (CH_TYPE_PLAYER.equals(channelTypeUID)) {
return new HeosChannelHandlerPlayerSelect(channelUID, bridge);
}
}
return null;
}
}

View File

@@ -0,0 +1,94 @@
/**
* 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.heos.internal;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
import java.util.stream.Collectors;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.core.thing.Channel;
import org.openhab.core.thing.ChannelUID;
import org.openhab.core.thing.binding.ThingHandler;
/**
* The {@link HeosChannelManager} provides the functions to
* add and remove channels from the channel list provided by the thing
* The generation of the individual channels has to be done by the thingHandler
* itself.
*
* @author Johannes Einig - Initial contribution
*/
@NonNullByDefault
public class HeosChannelManager {
private final ThingHandler handler;
public HeosChannelManager(ThingHandler handler) {
this.handler = handler;
}
public synchronized List<Channel> addSingleChannel(Channel channel) {
ChannelWrapper channelList = getChannelsFromThing();
channelList.removeChannel(channel.getUID());
channelList.add(channel);
return channelList.get();
}
public synchronized List<Channel> removeSingleChannel(String channelIdentifier) {
ChannelWrapper channelWrapper = getChannelsFromThing();
channelWrapper.removeChannel(generateChannelUID(channelIdentifier));
return channelWrapper.get();
}
/*
* Gets the channels from the Thing and makes the channel
* list editable.
*/
private ChannelWrapper getChannelsFromThing() {
return new ChannelWrapper(handler.getThing().getChannels());
}
private ChannelUID generateChannelUID(String channelIdentifier) {
return new ChannelUID(handler.getThing().getUID(), channelIdentifier);
}
/**
* Wrap a channel list
*
* @author Martin van Wingerden - Initial contribution
*/
private static class ChannelWrapper {
private final List<Channel> channels;
ChannelWrapper(List<Channel> channels) {
this.channels = new ArrayList<>(channels);
}
private void removeChannel(ChannelUID uid) {
List<Channel> itemsToBeRemoved = channels.stream().filter(Objects::nonNull)
.filter(channel -> uid.equals(channel.getUID())).collect(Collectors.toList());
channels.removeAll(itemsToBeRemoved);
}
public void add(Channel channel) {
channels.add(channel);
}
public List<Channel> get() {
return Collections.unmodifiableList(channels);
}
}
}

View File

@@ -0,0 +1,177 @@
/**
* 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.heos.internal;
import static org.openhab.binding.heos.internal.HeosBindingConstants.*;
import java.util.HashMap;
import java.util.Hashtable;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.heos.internal.api.HeosAudioSink;
import org.openhab.binding.heos.internal.discovery.HeosPlayerDiscovery;
import org.openhab.binding.heos.internal.handler.HeosBridgeHandler;
import org.openhab.binding.heos.internal.handler.HeosDynamicStateDescriptionProvider;
import org.openhab.binding.heos.internal.handler.HeosGroupHandler;
import org.openhab.binding.heos.internal.handler.HeosPlayerHandler;
import org.openhab.binding.heos.internal.handler.HeosThingBaseHandler;
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 HeosHandlerFactory} is responsible for creating things and thing
* handlers.
*
* @author Johannes Einig - Initial contribution
*/
@Component(service = ThingHandlerFactory.class, configurationPid = "binding.heos")
@NonNullByDefault
public class HeosHandlerFactory extends BaseThingHandlerFactory {
private final Logger logger = LoggerFactory.getLogger(HeosHandlerFactory.class);
private final Map<ThingUID, ServiceRegistration<?>> discoveryServiceRegs = new HashMap<>();
private final Map<String, ServiceRegistration<AudioSink>> audioSinkRegistrations = new ConcurrentHashMap<>();
private @NonNullByDefault({}) AudioHTTPServer audioHTTPServer;
private @NonNullByDefault({}) NetworkAddressService networkAddressService;
private @NonNullByDefault({}) HeosDynamicStateDescriptionProvider heosDynamicStateDescriptionProvider;
@Override
public boolean supportsThingType(ThingTypeUID thingTypeUID) {
return SUPPORTED_THING_TYPES_UIDS.contains(thingTypeUID);
}
@Override
@Activate
protected void activate(ComponentContext componentContext) {
super.activate(componentContext);
}
@Override
protected @Nullable ThingHandler createHandler(Thing thing) {
ThingTypeUID thingTypeUID = thing.getThingTypeUID();
if (THING_TYPE_BRIDGE.equals(thingTypeUID)) {
HeosBridgeHandler bridgeHandler = new HeosBridgeHandler((Bridge) thing,
heosDynamicStateDescriptionProvider);
HeosPlayerDiscovery playerDiscovery = new HeosPlayerDiscovery(bridgeHandler);
discoveryServiceRegs.put(bridgeHandler.getThing().getUID(), bundleContext
.registerService(DiscoveryService.class.getName(), playerDiscovery, new Hashtable<>()));
logger.debug("Register discovery service for HEOS player and HEOS groups by bridge '{}'",
bridgeHandler.getThing().getUID().getId());
return bridgeHandler;
}
if (THING_TYPE_PLAYER.equals(thingTypeUID)) {
HeosPlayerHandler playerHandler = new HeosPlayerHandler(thing, heosDynamicStateDescriptionProvider);
registerAudioSink(thing, playerHandler);
return playerHandler;
}
if (THING_TYPE_GROUP.equals(thingTypeUID)) {
HeosGroupHandler groupHandler = new HeosGroupHandler(thing, heosDynamicStateDescriptionProvider);
registerAudioSink(thing, groupHandler);
return groupHandler;
}
return null;
}
private void registerAudioSink(Thing thing, HeosThingBaseHandler thingBaseHandler) {
HeosAudioSink audioSink = new HeosAudioSink(thingBaseHandler, audioHTTPServer, createCallbackUrl());
@SuppressWarnings("unchecked")
ServiceRegistration<AudioSink> reg = (ServiceRegistration<AudioSink>) bundleContext
.registerService(AudioSink.class.getName(), audioSink, new Hashtable<>());
audioSinkRegistrations.put(thing.getUID().toString(), reg);
}
@Override
public void unregisterHandler(Thing thing) {
if (thing.getThingTypeUID().equals(THING_TYPE_BRIDGE)) {
super.unregisterHandler(thing);
ServiceRegistration<?> serviceRegistration = discoveryServiceRegs.get(thing.getUID());
if (serviceRegistration != null) {
serviceRegistration.unregister();
discoveryServiceRegs.remove(thing.getUID());
logger.debug("Unregister discovery service for HEOS player and HEOS groups by bridge '{}'",
thing.getUID().getId());
}
}
if (THING_TYPE_PLAYER.equals(thing.getThingTypeUID()) || THING_TYPE_GROUP.equals(thing.getThingTypeUID())) {
super.unregisterHandler(thing);
ServiceRegistration<AudioSink> reg = audioSinkRegistrations.get(thing.getUID().toString());
if (reg != null) {
reg.unregister();
}
}
}
@Reference
protected void setAudioHTTPServer(AudioHTTPServer audioHTTPServer) {
this.audioHTTPServer = audioHTTPServer;
}
protected void unsetAudioHTTPServer(AudioHTTPServer audioHTTPServer) {
this.audioHTTPServer = null;
}
@Reference
protected void setNetworkAddressService(NetworkAddressService networkAddressService) {
this.networkAddressService = networkAddressService;
}
protected void unsetNetworkAddressService(NetworkAddressService networkAddressService) {
this.networkAddressService = null;
}
@Reference
protected void setDynamicStateDescriptionProvider(HeosDynamicStateDescriptionProvider provider) {
this.heosDynamicStateDescriptionProvider = provider;
}
protected void unsetDynamicStateDescriptionProvider(HeosDynamicStateDescriptionProvider provider) {
this.heosDynamicStateDescriptionProvider = null;
}
private @Nullable String createCallbackUrl() {
final String ipAddress = networkAddressService.getPrimaryIpv4HostAddress();
if (ipAddress == null) {
logger.warn("No network interface could be found.");
return null;
}
// we do not use SSL as it can cause certificate validation issues.
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,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.heos.internal.action;
import java.io.IOException;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.heos.internal.api.HeosFacade;
import org.openhab.binding.heos.internal.exception.HeosNotConnectedException;
import org.openhab.binding.heos.internal.handler.HeosBridgeHandler;
import org.openhab.binding.heos.internal.resources.Telnet;
import org.openhab.core.automation.annotation.ActionInput;
import org.openhab.core.automation.annotation.RuleAction;
import org.openhab.core.thing.binding.ThingActions;
import org.openhab.core.thing.binding.ThingActionsScope;
import org.openhab.core.thing.binding.ThingHandler;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The class is responsible to call corresponding action on HEOS Handler
* <p>
* <b>Note:</b>The static method <b>invokeMethodOf</b> handles the case where
* the test <i>actions instanceof HeosActions</i> fails. This test can fail
* due to an issue in openHAB core v2.5.0 where the {@link HeosActions} class
* can be loaded by a different classloader than the <i>actions</i> instance.
*
* @author Martin van Wingerden - Initial contribution
*/
@ThingActionsScope(name = "heos")
@NonNullByDefault
public class HeosActions implements ThingActions, IHeosActions {
private final static Logger logger = LoggerFactory.getLogger(HeosActions.class);
private @Nullable HeosBridgeHandler handler;
@Override
public void setThingHandler(@Nullable ThingHandler handler) {
if (handler instanceof HeosBridgeHandler) {
this.handler = (HeosBridgeHandler) handler;
}
}
@Override
public @Nullable ThingHandler getThingHandler() {
return this.handler;
}
private @Nullable HeosFacade getConnection() throws HeosNotConnectedException {
if (handler == null) {
return null;
}
return handler.getApiConnection();
}
@Override
@RuleAction(label = "Play Input", description = "Play an input from another device")
public void playInputFromPlayer(
@ActionInput(name = "source", label = "Source Player", description = "Player used for input") @Nullable Integer sourcePlayer,
@ActionInput(name = "input", label = "Source Input", description = "Input source used") @Nullable String input,
@ActionInput(name = "destination", label = "Destination Player", description = "Device for audio output") @Nullable Integer destinationPlayer) {
if (sourcePlayer == null || input == null || destinationPlayer == null) {
logger.debug(
"Skipping HEOS playInputFromPlayer due to null value: sourcePlayer: {}, input: {}, destination: {}",
sourcePlayer, input, destinationPlayer);
return;
}
try {
HeosFacade connection = getConnection();
if (connection == null) {
logger.debug("Skipping HEOS playInputFromPlayer because no connection was available");
return;
}
connection.playInputSource(destinationPlayer.toString(), sourcePlayer.toString(), input);
} catch (IOException | Telnet.ReadException e) {
logger.warn("Failed to play input source!", e);
}
}
public static void playInputFromPlayer(@Nullable ThingActions actions, @Nullable Integer sourcePlayer,
@Nullable String input, @Nullable Integer destinationPlayer) {
invokeMethodOf(actions).playInputFromPlayer(sourcePlayer, input, destinationPlayer);
}
private static IHeosActions invokeMethodOf(@Nullable ThingActions actions) {
if (actions == null) {
throw new IllegalArgumentException("actions cannot be null");
}
if (actions.getClass().getName().equals(HeosActions.class.getName())) {
if (actions instanceof IHeosActions) {
return (IHeosActions) actions;
} else {
return (IHeosActions) Proxy.newProxyInstance(IHeosActions.class.getClassLoader(),
new Class[] { IHeosActions.class }, (Object proxy, Method method, Object[] args) -> {
Method m = actions.getClass().getDeclaredMethod(method.getName(),
method.getParameterTypes());
return m.invoke(actions, args);
});
}
}
throw new IllegalArgumentException("Actions is not an instance of HeosActions");
}
}

View File

@@ -0,0 +1,28 @@
/**
* 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.heos.internal.action;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
/**
* The {@link IHeosActions} defines the interface for all thing actions supported by the binding.
*
* @author Laurent Garnier - Initial contribution
*/
@NonNullByDefault
public interface IHeosActions {
public void playInputFromPlayer(@Nullable Integer sourcePlayer, @Nullable String input,
@Nullable Integer destinationPlayer);
}

View File

@@ -0,0 +1,135 @@
/**
* 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.heos.internal.api;
import java.io.IOException;
import java.util.HashSet;
import java.util.Locale;
import java.util.Set;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.heos.internal.handler.HeosThingBaseHandler;
import org.openhab.binding.heos.internal.resources.Telnet.ReadException;
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.utils.AudioStreamUtils;
import org.openhab.core.library.types.PercentType;
import org.openhab.core.thing.util.ThingHandlerHelper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* This makes HEOS to serve as an {@link AudioSink}.
*
* @author Johannes Einig - Initial contribution
*/
@NonNullByDefault
public class HeosAudioSink implements AudioSink {
private final Logger logger = LoggerFactory.getLogger(HeosAudioSink.class);
private static final Set<AudioFormat> SUPPORTED_AUDIO_FORMATS = new HashSet<>();
private static final Set<Class<? extends AudioStream>> SUPPORTED_AUDIO_STREAMS = new HashSet<>();
static {
SUPPORTED_AUDIO_FORMATS.add(AudioFormat.WAV);
SUPPORTED_AUDIO_FORMATS.add(AudioFormat.MP3);
SUPPORTED_AUDIO_FORMATS.add(AudioFormat.AAC);
SUPPORTED_AUDIO_STREAMS.add(URLAudioStream.class);
SUPPORTED_AUDIO_STREAMS.add(FixedLengthAudioStream.class);
}
private final HeosThingBaseHandler handler;
private final AudioHTTPServer audioHTTPServer;
private @Nullable final String callbackUrl;
public HeosAudioSink(HeosThingBaseHandler handler, AudioHTTPServer audioHTTPServer, @Nullable String callbackUrl) {
this.handler = handler;
this.audioHTTPServer = audioHTTPServer;
this.callbackUrl = callbackUrl;
}
@Override
public String getId() {
return handler.getThing().getUID().toString();
}
@Override
public @Nullable String getLabel(@Nullable Locale locale) {
return handler.getThing().getLabel();
}
@Override
public void process(@Nullable AudioStream audioStream) throws UnsupportedAudioFormatException {
try {
if (audioStream instanceof URLAudioStream) {
// it is an external URL, the speaker can access it itself and play it.
URLAudioStream urlAudioStream = (URLAudioStream) audioStream;
handler.playURL(urlAudioStream.getURL());
} else if (audioStream instanceof FixedLengthAudioStream) {
if (callbackUrl != null) {
// we serve it on our own HTTP server for 30 seconds as HEOS requests the stream several times
String relativeUrl = audioHTTPServer.serve((FixedLengthAudioStream) audioStream, 30);
String url = callbackUrl + relativeUrl + AudioStreamUtils.EXTENSION_SEPARATOR;
AudioFormat audioFormat = audioStream.getFormat();
if (!ThingHandlerHelper.isHandlerInitialized(handler)) {
logger.debug("HEOS speaker '{}' is not initialized - status is {}", handler.getThing().getUID(),
handler.getThing().getStatus());
} else if (AudioFormat.MP3.isCompatible(audioFormat)) {
handler.playURL(url + FileAudioStream.MP3_EXTENSION);
} else if (AudioFormat.WAV.isCompatible(audioFormat)) {
handler.playURL(url + FileAudioStream.WAV_EXTENSION);
} else if (AudioFormat.AAC.isCompatible(audioFormat)) {
handler.playURL(url + FileAudioStream.AAC_EXTENSION);
} else {
throw new UnsupportedAudioFormatException("HEOS only supports MP3, WAV and AAC.", audioFormat);
}
} else {
logger.warn("We do not have any callback url, so HEOS cannot play the audio stream!");
}
} else {
throw new UnsupportedAudioFormatException(
"HEOS can only handle FixedLengthAudioStreams & URLAudioStream.", null);
}
} catch (IOException | ReadException e) {
logger.warn("Failed to play audio stream: {}", e.getMessage());
}
}
@Override
public Set<AudioFormat> getSupportedFormats() {
return SUPPORTED_AUDIO_FORMATS;
}
@Override
public Set<Class<? extends AudioStream>> getSupportedStreams() {
return SUPPORTED_AUDIO_STREAMS;
}
@Override
public PercentType getVolume() {
return handler.getNotificationSoundVolume();
}
@Override
public void setVolume(PercentType volume) {
handler.setNotificationSoundVolume(volume);
}
}

View File

@@ -0,0 +1,119 @@
/**
* 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.heos.internal.api;
import static org.openhab.binding.heos.internal.resources.HeosConstants.*;
import java.io.IOException;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.heos.internal.json.dto.HeosCommunicationAttribute;
import org.openhab.binding.heos.internal.json.dto.HeosEvent;
import org.openhab.binding.heos.internal.json.dto.HeosEventObject;
import org.openhab.binding.heos.internal.json.dto.HeosResponseObject;
import org.openhab.binding.heos.internal.json.payload.Media;
import org.openhab.binding.heos.internal.resources.HeosCommands;
import org.openhab.binding.heos.internal.resources.HeosSystemEventListener;
import org.openhab.binding.heos.internal.resources.Telnet;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The {@link HeosEventController} is responsible for handling event, which are
* received by the HEOS system.
*
* @author Johannes Einig - Initial contribution
*/
@NonNullByDefault
public class HeosEventController extends HeosSystemEventListener {
private final Logger logger = LoggerFactory.getLogger(HeosEventController.class);
private final HeosSystem system;
private long lastEventTime;
public HeosEventController(HeosSystem system) {
this.system = system;
lastEventTime = System.currentTimeMillis();
}
public void handleEvent(HeosEventObject eventObject) {
HeosEvent command = eventObject.command;
lastEventTime = System.currentTimeMillis();
logger.debug("Handling event: {}", eventObject);
if (command == null) {
return;
}
switch (command) {
case PLAYER_NOW_PLAYING_PROGRESS:
case PLAYER_STATE_CHANGED:
case PLAYER_VOLUME_CHANGED:
case SHUFFLE_MODE_CHANGED:
case REPEAT_MODE_CHANGED:
case PLAYER_PLAYBACK_ERROR:
case GROUP_VOLUME_CHANGED:
case PLAYER_QUEUE_CHANGED:
case SOURCES_CHANGED:
fireStateEvent(eventObject);
break;
case USER_CHANGED:
fireBridgeEvent(EVENT_TYPE_SYSTEM, true, command);
break;
case PLAYER_NOW_PLAYING_CHANGED:
String pid = eventObject.getAttribute(HeosCommunicationAttribute.PLAYER_ID);
if (pid == null) {
logger.debug("HEOS did not mention which player changed, unlikely but ignore");
break;
}
try {
HeosResponseObject<Media> mediaResponse = system.send(HeosCommands.getNowPlayingMedia(pid),
Media.class);
Media responseMedia = mediaResponse.payload;
if (responseMedia != null) {
fireMediaEvent(pid, responseMedia);
}
} catch (IOException | Telnet.ReadException e) {
logger.debug("Failed to retrieve current playing media, will try again next time.", e);
}
break;
case GROUPS_CHANGED:
case PLAYERS_CHANGED:
fireBridgeEvent(EVENT_TYPE_EVENT, true, command);
break;
}
}
public void connectionToSystemLost() {
fireBridgeEvent(EVENT_TYPE_EVENT, false, CONNECTION_LOST);
}
public void eventStreamTimeout() {
fireBridgeEvent(EVENT_TYPE_EVENT, false, EVENT_STREAM_TIMEOUT);
}
public void systemReachable() {
fireBridgeEvent(EVENT_TYPE_EVENT, true, CONNECTION_RESTORED);
}
long getLastEventTime() {
return lastEventTime;
}
}

View File

@@ -0,0 +1,556 @@
/**
* 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.heos.internal.api;
import static org.openhab.binding.heos.internal.resources.HeosConstants.*;
import java.io.IOException;
import java.net.URL;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.jetbrains.annotations.NotNull;
import org.openhab.binding.heos.internal.json.dto.HeosResponseObject;
import org.openhab.binding.heos.internal.json.payload.BrowseResult;
import org.openhab.binding.heos.internal.json.payload.Group;
import org.openhab.binding.heos.internal.json.payload.Media;
import org.openhab.binding.heos.internal.json.payload.Player;
import org.openhab.binding.heos.internal.resources.HeosCommands;
import org.openhab.binding.heos.internal.resources.HeosConstants;
import org.openhab.binding.heos.internal.resources.HeosEventListener;
import org.openhab.binding.heos.internal.resources.Telnet.ReadException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.gson.JsonElement;
/**
* The {@link HeosFacade} is the interface for handling commands, which are
* sent to the HEOS system.
*
* @author Johannes Einig - Initial contribution
*/
@NonNullByDefault
public class HeosFacade {
private static final int MAX_QUEUE_PAGES = 25;
private final Logger logger = LoggerFactory.getLogger(HeosFacade.class);
private final HeosSystem heosSystem;
private final HeosEventController eventController;
public HeosFacade(HeosSystem heosSystem, HeosEventController eventController) {
this.heosSystem = heosSystem;
this.eventController = eventController;
}
public synchronized List<BrowseResult> getFavorites() throws IOException, ReadException {
return getBrowseResults(FAVORITE_SID);
}
public List<BrowseResult> getInputs() throws IOException, ReadException {
return getBrowseResults(String.valueOf(INPUT_SID));
}
public List<BrowseResult> getPlaylists() throws IOException, ReadException {
return getBrowseResults(PLAYLISTS_SID);
}
@NotNull
private List<BrowseResult> getBrowseResults(String sourceIdentifier) throws IOException, ReadException {
HeosResponseObject<BrowseResult[]> response = browseSource(sourceIdentifier);
logger.debug("Response: {}", response);
if (response.payload == null) {
return Collections.emptyList();
}
logger.debug("Received results: {}", Arrays.asList(response.payload));
return Arrays.asList(response.payload);
}
public List<Media> getQueue(String pid) throws IOException, ReadException {
List<Media> media = new ArrayList<>();
for (int page = 0; page < MAX_QUEUE_PAGES; page++) {
HeosResponseObject<Media[]> response = fetchQueue(pid, page);
if (!response.result || response.payload == null) {
break;
}
media.addAll(Arrays.asList(response.payload));
if (response.payload.length < 100) {
break;
}
if (page == MAX_QUEUE_PAGES - 1) {
logger.info("Currently only a maximum of {} pages is fetched for every queue", MAX_QUEUE_PAGES);
}
}
return media;
}
HeosResponseObject<Media[]> fetchQueue(String pid, int page) throws IOException, ReadException {
return heosSystem.send(HeosCommands.getQueue(pid, page * 100, (page + 1) * 100), Media[].class);
}
public HeosResponseObject<Player> getPlayerInfo(String pid) throws IOException, ReadException {
return heosSystem.send(HeosCommands.getPlayerInfo(pid), Player.class);
}
public HeosResponseObject<Group> getGroupInfo(String gid) throws IOException, ReadException {
return heosSystem.send(HeosCommands.getGroupInfo(gid), Group.class);
}
/**
* Pauses the HEOS player
*
* @param pid The PID of the dedicated player
*/
public void pause(String pid) throws IOException, ReadException {
heosSystem.send(HeosCommands.setPlayStatePause(pid));
}
/**
* Starts the HEOS player
*
* @param pid The PID of the dedicated player
*/
public void play(String pid) throws IOException, ReadException {
heosSystem.send(HeosCommands.setPlayStatePlay(pid));
}
/**
* Stops the HEOS player
*
* @param pid The PID of the dedicated player
*/
public void stop(String pid) throws IOException, ReadException {
heosSystem.send(HeosCommands.setPlayStateStop(pid));
}
/**
* Jumps to the next song on the HEOS player
*
* @param pid The PID of the dedicated player
*/
public void next(String pid) throws IOException, ReadException {
heosSystem.send(HeosCommands.playNext(pid));
}
/**
* Jumps to the previous song on the HEOS player
*
* @param pid The PID of the dedicated player
*/
public void previous(String pid) throws IOException, ReadException {
heosSystem.send(HeosCommands.playPrevious(pid));
}
/**
* Toggles the mute state the HEOS player
*
* @param pid The PID of the dedicated player
*/
public void mute(String pid) throws IOException, ReadException {
heosSystem.send(HeosCommands.setMuteToggle(pid));
}
/**
* Mutes the HEOS player
*
* @param pid The PID of the dedicated player
*/
public void muteON(String pid) throws IOException, ReadException {
heosSystem.send(HeosCommands.setMuteOn(pid));
}
/**
* Un-mutes the HEOS player
*
* @param pid The PID of the dedicated player
*/
public void muteOFF(String pid) throws IOException, ReadException {
heosSystem.send(HeosCommands.setMuteOff(pid));
}
/**
* Set the play mode of the player or group
*
* @param pid The PID of the dedicated player or group
* @param mode The shuffle mode: Allowed commands: on; off
*/
public void setShuffleMode(String pid, String mode) throws IOException, ReadException {
heosSystem.send(HeosCommands.setShuffleMode(pid, mode));
}
/**
* Sets the repeat mode of the player or group
*
* @param pid The ID of the dedicated player or group
* @param mode The repeat mode. Allowed commands: on_all; on_one; off
*/
public void setRepeatMode(String pid, String mode) throws IOException, ReadException {
heosSystem.send(HeosCommands.setRepeatMode(pid, mode));
}
/**
* Set the HEOS player to a dedicated volume
*
* @param vol The volume the player shall be set to (value between 0 -100)
* @param pid The ID of the dedicated player or group
*/
public void setVolume(String vol, String pid) throws IOException, ReadException {
heosSystem.send(HeosCommands.setVolume(vol, pid));
}
/**
* Increases the HEOS player volume 1 Step
*
* @param pid The ID of the dedicated player or group
*/
public void increaseVolume(String pid) throws IOException, ReadException {
heosSystem.send(HeosCommands.volumeUp(pid));
}
/**
* Decreases the HEOS player volume 1 Step
*
* @param pid The ID of the dedicated player or group
*/
public void decreaseVolume(String pid) throws IOException, ReadException {
heosSystem.send(HeosCommands.volumeDown(pid));
}
/**
* Toggles mute state of the HEOS group
*
* @param gid The GID of the group
*/
public void muteGroup(String gid) throws IOException, ReadException {
heosSystem.send(HeosCommands.setMuteToggle(gid));
}
/**
* Mutes the HEOS group
*
* @param gid The GID of the group
*/
public void muteGroupON(String gid) throws IOException, ReadException {
heosSystem.send(HeosCommands.setGroupMuteOn(gid));
}
/**
* Un-mutes the HEOS group
*
* @param gid The GID of the group
*/
public void muteGroupOFF(String gid) throws IOException, ReadException {
heosSystem.send(HeosCommands.setGroupMuteOff(gid));
}
/**
* Set the volume of the group to a specific level
*
* @param vol The volume the group shall be set to (value between 0-100)
* @param gid The GID of the group
*/
public void volumeGroup(String vol, String gid) throws IOException, ReadException {
heosSystem.send(HeosCommands.setGroupVolume(vol, gid));
}
/**
* Increases the HEOS group volume 1 Step
*
* @param gid The ID of the dedicated player or group
*/
public void increaseGroupVolume(String gid) throws IOException, ReadException {
heosSystem.send(HeosCommands.setGroupVolumeUp(gid));
}
/**
* Decreases the HEOS group volume 1 Step
*
* @param gid The ID of the dedicated player or group
*/
public void decreaseGroupVolume(String gid) throws IOException, ReadException {
heosSystem.send(HeosCommands.setGroupVolumeDown(gid));
}
/**
* Un-Group the HEOS group to single player
*
* @param gid The GID of the group
*/
public void ungroupGroup(String gid) throws IOException, ReadException {
String[] pid = new String[] { gid };
heosSystem.send(HeosCommands.setGroup(pid));
}
/**
* Builds a group from single players
*
* @param pids The single player IDs of the player which shall be grouped
* @return
*/
public boolean groupPlayer(String[] pids) throws IOException, ReadException {
return heosSystem.send(HeosCommands.setGroup(pids)).result;
}
/**
* Browses through a HEOS source. Currently no response
*
* @param sid The source sid which shall be browsed
* @return
*/
public HeosResponseObject<BrowseResult[]> browseSource(String sid) throws IOException, ReadException {
return heosSystem.send(HeosCommands.browseSource(sid), BrowseResult[].class);
}
/**
* Adds a media container to the queue and plays the media directly
* Information of the sid and cid has to be obtained via the browse function
*
* @param pid The player ID where the media object shall be played
* @param sid The source ID where the media is located
* @param cid The container ID of the media
*/
public void addContainerToQueuePlayNow(String pid, String sid, String cid) throws IOException, ReadException {
heosSystem.send(HeosCommands.addContainerToQueuePlayNow(pid, sid, cid));
}
/**
* Reboot the bridge to which the connection is established
*/
public void reboot() throws IOException, ReadException {
heosSystem.send(HeosCommands.rebootSystem());
}
/**
* Login in via the bridge to the HEOS account
*
* @param name The username
* @param password The password of the user
* @return
*/
public HeosResponseObject<Void> logIn(String name, String password) throws IOException, ReadException {
return heosSystem.send(HeosCommands.signIn(name, password));
}
/**
* Get all the players known by HEOS
*
* @return
*/
public HeosResponseObject<Player[]> getPlayers() throws IOException, ReadException {
return heosSystem.send(HeosCommands.getPlayers(), Player[].class);
}
/**
* Get all the groups known by HEOS
*
* @return
*/
public HeosResponseObject<Group[]> getGroups() throws IOException, ReadException {
return heosSystem.send(HeosCommands.getGroups(), Group[].class);
}
/**
* Plays a specific station on the HEOS player
*
* @param pid The player ID
* @param sid The source ID where the media is located
* @param cid The container ID of the media
* @param mid The media ID of the media
* @param name Station name returned by 'browse' command.
*/
public void playStream(@Nullable String pid, @Nullable String sid, @Nullable String cid, @Nullable String mid,
@Nullable String name) throws IOException, ReadException {
heosSystem.send(HeosCommands.playStream(pid, sid, cid, mid, name));
}
/**
* Plays a specific station on the HEOS player
*
* @param pid The player ID
* @param sid The source ID where the media is located
* @param mid The media ID of the media
*/
public void playStream(String pid, String sid, String mid) throws IOException, ReadException {
heosSystem.send(HeosCommands.playStream(pid, sid, mid));
}
/**
* Plays a specified local input source on the player.
* Input name as per specified in HEOS CLI Protocol
*
* @param pid
* @param input
*/
public void playInputSource(String pid, String input) throws IOException, ReadException {
heosSystem.send(HeosCommands.playInputSource(pid, pid, input));
}
/**
* Plays a specified input source from another player on the selected player.
* Input name as per specified in HEOS CLI Protocol
*
* @param destinationPid the PID where the source shall be played
* @param sourcePid the PID where the source is located.
* @param input the input name
*/
public void playInputSource(String destinationPid, String sourcePid, String input)
throws IOException, ReadException {
heosSystem.send(HeosCommands.playInputSource(destinationPid, sourcePid, input));
}
/**
* Plays a file from a URL
*
* @param pid the PID where the file shall be played
* @param url the complete URL the file is located
*/
public void playURL(String pid, URL url) throws IOException, ReadException {
heosSystem.send(HeosCommands.playURL(pid, url.toString()));
}
/**
* clear the queue
*
* @param pid The player ID the media is playing on
*/
public void clearQueue(String pid) throws IOException, ReadException {
heosSystem.send(HeosCommands.clearQueue(pid));
}
/**
* Deletes a media from the queue
*
* @param pid The player ID the media is playing on
* @param qid The queue ID of the media. (starts by 1)
*/
public void deleteMediaFromQueue(String pid, String qid) throws IOException, ReadException {
heosSystem.send(HeosCommands.deleteQueueItem(pid, qid));
}
/**
* Plays a specific media file from the queue
*
* @param pid The player ID the media shall be played on
* @param qid The queue ID of the media. (starts by 1)
*/
public void playMediaFromQueue(String pid, String qid) throws IOException, ReadException {
heosSystem.send(HeosCommands.playQueueItem(pid, qid));
}
/**
* Asks for the actual state of the player. The result has
* to be handled by the event controller. The system returns {@link HeosConstants.PLAY},
* {@link HeosConstants.PAUSE} or {@link HeosConstants.STOP}.
*
* @param id The player ID the state shall get for
* @return
*/
public HeosResponseObject<Void> getPlayState(String id) throws IOException, ReadException {
return heosSystem.send(HeosCommands.getPlayState(id));
}
/**
* Ask for the actual mute state of the player. The result has
* to be handled by the event controller. The HEOS system returns {@link HeosConstants.ON}
* or {@link HeosConstants.OFF}.
*
* @param id The player id the mute state shall get for
* @return
*/
public HeosResponseObject<Void> getPlayerMuteState(String id) throws IOException, ReadException {
return heosSystem.send(HeosCommands.getMute(id));
}
/**
* Ask for the actual volume the player. The result has
* to be handled by the event controller. The HEOS system returns
* a value between 0 and 100
*
* @param id The player id the volume shall get for
* @return
*/
public HeosResponseObject<Void> getPlayerVolume(String id) throws IOException, ReadException {
return heosSystem.send(HeosCommands.getVolume(id));
}
/**
* Ask for the actual shuffle mode of the player. The result has
* to be handled by the event controller. The HEOS system returns {@link HeosConstants.ON},
* {@link HeosConstants.HEOS_REPEAT_ALL} or {@link HeosConstants.HEOS_REPEAT_ONE}
*
* @param id The player id the shuffle mode shall get for
* @return
*/
public HeosResponseObject<Void> getPlayMode(String id) throws IOException, ReadException {
return heosSystem.send(HeosCommands.getPlayMode(id));
}
public HeosResponseObject<Void> getGroupMuteState(String id) throws IOException, ReadException {
return heosSystem.send(HeosCommands.getGroupMute(id));
}
public HeosResponseObject<Void> getGroupVolume(String id) throws IOException, ReadException {
return heosSystem.send(HeosCommands.getGroupVolume(id));
}
public HeosResponseObject<Media> getNowPlayingMedia(String id) throws IOException, ReadException {
return heosSystem.send(HeosCommands.getNowPlayingMedia(id), Media.class);
}
/**
* Sends a RAW command to the HEOS bridge. The command has to be
* in accordance with the HEOS CLI specification
*
* @param command to send
* @return
*/
public HeosResponseObject<JsonElement> sendRawCommand(String command) throws IOException, ReadException {
return heosSystem.send(command, JsonElement.class);
}
/**
* Register an {@link HeosEventListener} to get notification of system events
*
* @param listener The HeosEventListener
*/
public void registerForChangeEvents(HeosEventListener listener) {
eventController.addListener(listener);
}
/**
* Unregister an {@link HeosEventListener} to get notification of system events
*
* @param listener The HeosEventListener
*/
public void unregisterForChangeEvents(HeosEventListener listener) {
eventController.removeListener(listener);
}
public boolean isConnected() {
return heosSystem.isConnected();
}
public void closeConnection() {
heosSystem.closeConnection();
}
}

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.heos.internal.api;
import static org.openhab.binding.heos.internal.handler.FutureUtil.cancel;
import java.beans.PropertyChangeListener;
import java.io.IOException;
import java.util.concurrent.*;
import java.util.function.Consumer;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.heos.internal.json.HeosJsonParser;
import org.openhab.binding.heos.internal.json.dto.HeosResponseObject;
import org.openhab.binding.heos.internal.resources.HeosCommands;
import org.openhab.binding.heos.internal.resources.HeosSendCommand;
import org.openhab.binding.heos.internal.resources.Telnet;
import org.openhab.binding.heos.internal.resources.Telnet.ReadException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.gson.JsonSyntaxException;
/**
* The {@link HeosSystem} is handling the main commands, which are
* sent and received by the HEOS system.
*
* @author Johannes Einig - Initial contribution
*/
@NonNullByDefault
public class HeosSystem {
private final Logger logger = LoggerFactory.getLogger(HeosSystem.class);
private static final int START_DELAY_SEC = 30;
private static final long LAST_EVENT_THRESHOLD = TimeUnit.HOURS.toMillis(2);
private final ScheduledExecutorService scheduler;
private @Nullable ExecutorService singleThreadExecutor;
private final HeosEventController eventController = new HeosEventController(this);
private final Telnet eventLine = new Telnet();
private final HeosSendCommand eventSendCommand = new HeosSendCommand(eventLine);
private final Telnet commandLine = new Telnet();
private final HeosSendCommand sendCommand = new HeosSendCommand(commandLine);
private final HeosJsonParser parser = new HeosJsonParser();
private final PropertyChangeListener eventProcessor = evt -> {
String newValue = (String) evt.getNewValue();
ExecutorService executor = singleThreadExecutor;
if (executor == null) {
logger.debug("No executor available ignoring event: {}", newValue);
return;
}
try {
executor.submit(() -> eventController.handleEvent(parser.parseEvent(newValue)));
} catch (JsonSyntaxException e) {
logger.debug("Failed processing event JSON", e);
}
};
private @Nullable ScheduledFuture<?> keepAliveJob;
private @Nullable ScheduledFuture<?> reconnectJob;
public HeosSystem(ScheduledExecutorService scheduler) {
this.scheduler = scheduler;
}
/**
* Establishes the connection to the HEOS-Network if IP and Port is
* set. The caller has to handle the retry to establish the connection
* if the method returns {@code false}.
*
* @param connectionIP
* @param connectionPort
* @param heartbeat
* @return {@code true} if connection is established else returns {@code false}
*/
public HeosFacade establishConnection(String connectionIP, int connectionPort, int heartbeat)
throws IOException, ReadException {
singleThreadExecutor = Executors.newSingleThreadExecutor();
if (commandLine.connect(connectionIP, connectionPort)) {
logger.debug("HEOS command line connected at IP {} @ port {}", connectionIP, connectionPort);
send(HeosCommands.registerChangeEventOff());
}
if (eventLine.connect(connectionIP, connectionPort)) {
logger.debug("HEOS event line connected at IP {} @ port {}", connectionIP, connectionPort);
eventSendCommand.send(HeosCommands.registerChangeEventOff(), Void.class);
}
startHeartBeat(heartbeat);
startEventListener();
return new HeosFacade(this, eventController);
}
boolean isConnected() {
return sendCommand.isConnected() && eventSendCommand.isConnected();
}
/**
* Starts the HEOS Heart Beat. This held the connection open even
* if no data is transmitted. If the connection to the HEOS system
* is lost, the method reconnects to the HEOS system by calling the
* {@code establishConnection()} method. If the connection is lost or
* reconnect the method fires a bridgeEvent via the {@code HeosEvenController.class}
*/
void startHeartBeat(int heartbeatPulse) {
keepAliveJob = scheduler.scheduleWithFixedDelay(new KeepAliveRunnable(), START_DELAY_SEC, heartbeatPulse,
TimeUnit.SECONDS);
}
synchronized void startEventListener() throws IOException, ReadException {
logger.debug("HEOS System Event Listener is starting....");
eventSendCommand.startInputListener(HeosCommands.registerChangeEventOn());
logger.debug("HEOS System Event Listener successfully started");
eventLine.getReadResultListener().addPropertyChangeListener(eventProcessor);
}
void closeConnection() {
logger.debug("Shutting down HEOS Heart Beat");
cancel(keepAliveJob);
cancel(this.reconnectJob, false);
eventLine.getReadResultListener().removePropertyChangeListener(eventProcessor);
eventSendCommand.stopInputListener(HeosCommands.registerChangeEventOff());
eventSendCommand.disconnect();
sendCommand.disconnect();
@Nullable
ExecutorService executor = this.singleThreadExecutor;
if (executor != null && executor.isShutdown()) {
executor.shutdownNow();
}
}
HeosResponseObject<Void> send(String command) throws IOException, ReadException {
return send(command, Void.class);
}
synchronized <T> HeosResponseObject<T> send(String command, Class<T> clazz) throws IOException, ReadException {
return sendCommand.send(command, clazz);
}
/**
* A class which provides a runnable for the HEOS Heart Beat
*
* @author Johannes Einig
*/
private class KeepAliveRunnable implements Runnable {
@Override
public void run() {
try {
if (sendCommand.isHostReachable()) {
long timeSinceLastEvent = System.currentTimeMillis() - eventController.getLastEventTime();
logger.debug("Time since latest event: {} s", timeSinceLastEvent / 1000);
if (timeSinceLastEvent > LAST_EVENT_THRESHOLD) {
logger.debug("Events haven't been received for too long");
resetEventStream();
return;
}
logger.debug("Sending HEOS Heart Beat");
HeosResponseObject<Void> response = send(HeosCommands.heartbeat());
if (response.result) {
return;
}
}
logger.debug("Connection to HEOS Network lost!");
// catches a failure during a heart beat send message if connection was
// getting lost between last Heart Beat but Bridge is online again and not
// detected by isHostReachable()
} catch (ReadException | IOException e) {
logger.debug("Failed at {}", System.currentTimeMillis(), e);
logger.debug("Failure during HEOS Heart Beat command with message: {}", e.getMessage());
}
restartConnection();
}
private void restartConnection() {
reset(a -> eventController.connectionToSystemLost());
}
private void resetEventStream() {
reset(a -> eventController.eventStreamTimeout());
}
private void reset(Consumer<@Nullable Void> method) {
closeConnection();
method.accept(null);
cancel(HeosSystem.this.reconnectJob, false);
reconnectJob = scheduler.scheduleWithFixedDelay(this::reconnect, 1, 5, TimeUnit.SECONDS);
}
private void reconnect() {
logger.debug("Trying to reconnect to HEOS Network...");
if (!sendCommand.isHostReachable()) {
return;
}
cancel(HeosSystem.this.reconnectJob, false);
logger.debug("Reconnecting to Bridge");
scheduler.schedule(eventController::systemReachable, 15, TimeUnit.SECONDS);
}
}
}

View File

@@ -0,0 +1,46 @@
/**
* 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.heos.internal.configuration;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
/**
* Configuration wrapper for bridge configuration
*
* @author Martin van Wingerden - Initial Contribution
*/
@NonNullByDefault
public class BridgeConfiguration {
public static final String IP_ADDRESS = "ipAddress";
/**
* Network address of the HEOS bridge
*/
public String ipAddress = "";
/**
* Username for login to the HEOS account.
*/
public @Nullable String username;
/**
* Password for login to the HEOS account
*/
public @Nullable String password;
/**
* The time in seconds for the HEOS Heartbeat (default = 60 s)
*/
public int heartbeat;
}

View File

@@ -0,0 +1,28 @@
/**
* 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.heos.internal.configuration;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* Configuration wrapper for group configuration
*
* @author Martin van Wingerden - Initial Contribution
*/
@NonNullByDefault
public class GroupConfiguration {
/**
*
*/
public String members = "";
}

View File

@@ -0,0 +1,28 @@
/**
* 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.heos.internal.configuration;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* Configuration wrapper for player configuration
*
* @author Martin van Wingerden - Initial Contribution
*/
@NonNullByDefault
public class PlayerConfiguration {
/**
* The Player ID
*/
public String pid = "";
}

View File

@@ -0,0 +1,90 @@
/**
* 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.heos.internal.discovery;
import static org.openhab.binding.heos.internal.HeosBindingConstants.*;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.jupnp.model.meta.DeviceDetails;
import org.jupnp.model.meta.ModelDetails;
import org.jupnp.model.meta.RemoteDevice;
import org.openhab.binding.heos.internal.configuration.BridgeConfiguration;
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.Thing;
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;
/**
* The {@link HeosDiscoveryParticipant} discovers the HEOS Player of the
* network via an UPnP interface.
*
* @author Johannes Einig - Initial contribution
*/
@NonNullByDefault
@Component(service = UpnpDiscoveryParticipant.class, immediate = true, configurationPid = "discovery.heos")
public class HeosDiscoveryParticipant implements UpnpDiscoveryParticipant {
private final Logger logger = LoggerFactory.getLogger(HeosDiscoveryParticipant.class);
@Override
public Set<ThingTypeUID> getSupportedThingTypeUIDs() {
return Collections.singleton(THING_TYPE_BRIDGE);
}
@Override
public @Nullable DiscoveryResult createResult(RemoteDevice device) {
ThingUID uid = getThingUID(device);
if (uid != null) {
Map<String, Object> properties = new HashMap<>();
properties.put(Thing.PROPERTY_VENDOR, device.getDetails().getManufacturerDetails().getManufacturer());
properties.put(Thing.PROPERTY_MODEL_ID, getModel(device.getDetails().getModelDetails()));
properties.put(Thing.PROPERTY_SERIAL_NUMBER, device.getDetails().getSerialNumber());
properties.put(BridgeConfiguration.IP_ADDRESS, device.getIdentity().getDescriptorURL().getHost());
properties.put(PROP_NAME, device.getDetails().getFriendlyName());
DiscoveryResult result = DiscoveryResultBuilder.create(uid).withProperties(properties)
.withLabel(" Bridge - " + device.getDetails().getFriendlyName())
.withRepresentationProperty(Thing.PROPERTY_VENDOR).build();
logger.debug("Found HEOS device with UID: {}", uid.getAsString());
return result;
}
return null;
}
private String getModel(ModelDetails modelDetails) {
return String.format("%s (%s)", modelDetails.getModelName(), modelDetails.getModelNumber());
}
@Override
public @Nullable ThingUID getThingUID(RemoteDevice device) {
DeviceDetails details = device.getDetails();
String modelName = details.getModelDetails().getModelName();
String modelManufacturer = details.getManufacturerDetails().getManufacturer();
if ("Denon".equals(modelManufacturer) && (modelName.startsWith("HEOS") || modelName.endsWith("H"))) {
String deviceType = device.getType().getType();
if (deviceType.startsWith("ACT") || deviceType.startsWith("Aios")) {
return new ThingUID(THING_TYPE_BRIDGE, device.getIdentity().getUdn().getIdentifierString());
}
}
return null;
}
}

View File

@@ -0,0 +1,230 @@
/**
* 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.heos.internal.discovery;
import static org.openhab.binding.heos.internal.HeosBindingConstants.*;
import static org.openhab.binding.heos.internal.handler.FutureUtil.cancel;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.heos.internal.handler.HeosBridgeHandler;
import org.openhab.binding.heos.internal.handler.HeosPlayerHandler;
import org.openhab.binding.heos.internal.json.payload.Group;
import org.openhab.binding.heos.internal.json.payload.Player;
import org.openhab.binding.heos.internal.resources.HeosGroup;
import org.openhab.binding.heos.internal.resources.Telnet;
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.Thing;
import org.openhab.core.thing.ThingTypeUID;
import org.openhab.core.thing.ThingUID;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The {@link HeosPlayerDiscovery} discovers the player and groups within
* the HEOS network and reacts on changed groups or player.
*
* @author Johannes Einig - Initial contribution
*/
@NonNullByDefault
public class HeosPlayerDiscovery extends AbstractDiscoveryService implements HeosPlayerDiscoveryListener {
private final Logger logger = LoggerFactory.getLogger(HeosPlayerDiscovery.class);
private static final int SEARCH_TIME = 5;
private static final int INITIAL_DELAY = 5;
private static final int SCAN_INTERVAL = 20;
private final HeosBridgeHandler bridge;
private Map<Integer, Player> players = new HashMap<>();
private Map<String, Group> groups = new HashMap<>();
private @Nullable ScheduledFuture<?> scanningJob;
public HeosPlayerDiscovery(HeosBridgeHandler bridge) throws IllegalArgumentException {
super(SEARCH_TIME);
this.bridge = bridge;
bridge.registerPlayerDiscoverListener(this);
}
@Override
public Set<ThingTypeUID> getSupportedThingTypes() {
return Stream.of(THING_TYPE_GROUP, THING_TYPE_PLAYER).collect(Collectors.toSet());
}
@Override
protected void startScan() {
if (!bridge.isBridgeConnected()) {
logger.debug("Scan for Players not possible. HEOS Bridge is not connected");
return;
}
scanForPlayers();
scanForGroups();
}
private void scanForPlayers() {
logger.debug("Start scan for HEOS Player");
try {
Map<Integer, Player> currentPlayers = new HashMap<>();
for (Player player : bridge.getPlayers()) {
currentPlayers.put(player.playerId, player);
}
handleRemovedPlayers(findRemovedEntries(currentPlayers, players));
handleDiscoveredPlayers(currentPlayers);
players = currentPlayers;
} catch (IOException | Telnet.ReadException e) {
logger.debug("Failed getting/processing groups", e);
}
}
private void handleDiscoveredPlayers(Map<Integer, Player> currentPlayers) {
logger.debug("Found: {} player", currentPlayers.size());
ThingUID bridgeUID = bridge.getThing().getUID();
for (Player player : currentPlayers.values()) {
ThingUID uid = new ThingUID(THING_TYPE_PLAYER, bridgeUID, String.valueOf(player.playerId));
Map<String, Object> properties = new HashMap<>();
HeosPlayerHandler.propertiesFromPlayer(properties, player);
DiscoveryResult result = DiscoveryResultBuilder.create(uid).withLabel(player.name)
.withProperties(properties).withBridge(bridgeUID)
.withRepresentationProperty(Thing.PROPERTY_SERIAL_NUMBER).build();
thingDiscovered(result);
}
}
private void handleRemovedPlayers(Map<Integer, Player> removedPlayers) {
for (Player player : removedPlayers.values()) {
// The same as above!
ThingUID uid = new ThingUID(THING_TYPE_PLAYER, String.valueOf(player.playerId));
logger.debug("Removed HEOS Player: {} ", uid);
thingRemoved(uid);
}
}
private void scanForGroups() {
logger.debug("Start scan for HEOS Groups");
try {
HashMap<String, Group> currentGroups = new HashMap<>();
for (Group group : bridge.getGroups()) {
logger.debug("Found: Group {} with {} Players", group.name, group.players.size());
currentGroups.put(HeosGroup.calculateGroupMemberHash(group), group);
}
handleRemovedGroups(findRemovedEntries(currentGroups, groups));
handleDiscoveredGroups(currentGroups);
groups = currentGroups;
} catch (IOException | Telnet.ReadException e) {
logger.debug("Failed getting/processing groups", e);
}
}
private void handleDiscoveredGroups(HashMap<String, Group> currentGroups) {
if (currentGroups.isEmpty()) {
logger.debug("No HEOS Groups found");
return;
}
logger.debug("Found: {} new Groups", currentGroups.size());
ThingUID bridgeUID = bridge.getThing().getUID();
for (Map.Entry<String, Group> entry : currentGroups.entrySet()) {
Group group = entry.getValue();
String groupMemberHash = entry.getKey();
// Using an unsigned hashCode from the group members to identify
// the group and generates the Thing UID.
// This allows identifying the group even if the sorting within the group has changed
ThingUID uid = new ThingUID(THING_TYPE_GROUP, bridgeUID, groupMemberHash);
Map<String, Object> properties = new HashMap<>();
properties.put(PROP_NAME, group.name);
properties.put(PROP_GID, group.id);
String groupMembers = group.players.stream().map(p -> p.id).collect(Collectors.joining(";"));
properties.put(PROP_GROUP_MEMBERS, groupMembers);
properties.put(PROP_GROUP_LEADER, group.players.get(0).id);
properties.put(PROP_GROUP_HASH, groupMemberHash);
DiscoveryResult result = DiscoveryResultBuilder.create(uid).withLabel(group.name).withProperties(properties)
.withBridge(bridgeUID).withRepresentationProperty(PROP_GROUP_HASH).build();
thingDiscovered(result);
bridge.setGroupOnline(groupMemberHash, group.id);
}
}
private void handleRemovedGroups(Map<String, Group> removedGroups) {
for (String groupMemberHash : removedGroups.keySet()) {
// The same as above!
ThingUID uid = new ThingUID(THING_TYPE_GROUP, groupMemberHash);
logger.debug("Removed HEOS Group: {}", uid);
thingRemoved(uid);
bridge.setGroupOffline(groupMemberHash);
}
}
private <K, V> Map<K, V> findRemovedEntries(Map<K, V> mapNew, Map<K, V> mapOld) {
Map<K, V> removedItems = new HashMap<>();
for (K key : mapOld.keySet()) {
if (!mapNew.containsKey(key)) {
removedItems.put(key, mapOld.get(key));
}
}
return removedItems;
}
@Override
protected void startBackgroundDiscovery() {
ScheduledFuture<?> runningScanningJob = this.scanningJob;
if (runningScanningJob == null || runningScanningJob.isCancelled()) {
this.scanningJob = scheduler.scheduleWithFixedDelay(this::startScan, INITIAL_DELAY, SCAN_INTERVAL,
TimeUnit.SECONDS);
}
}
@Override
protected void stopBackgroundDiscovery() {
logger.debug("Stop HEOS Player background discovery");
cancel(scanningJob);
}
@Override
protected synchronized void stopScan() {
super.stopScan();
removeOlderResults(getTimestampOfLastScan());
}
private void scanForNewPlayers() {
removeOlderResults(getTimestampOfLastScan());
startScan();
}
@Override
public void playerChanged() {
scanForNewPlayers();
}
}

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.heos.internal.discovery;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.heos.internal.handler.HeosBridgeHandler;
/**
* The {@link HeosPlayerDiscoveryListener } is an Event Listener
* for the HEOS network. Handler which wants the get informed
* if the player or groups within the HEOS network have changed has to
* implement this class and register itself at the {@link HeosBridgeHandler}
*
* @author Johannes Einig - Initial contribution
*/
@NonNullByDefault
public interface HeosPlayerDiscoveryListener {
void playerChanged();
}

View File

@@ -0,0 +1,36 @@
/**
* 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.heos.internal.exception;
import java.io.IOException;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.heos.internal.json.dto.HeosErrorCode;
/**
* Exception to inform the caller that there is functional error reported by the HEOS system
*
* @author Martin van Wingerden - Initial contribution
*/
@NonNullByDefault
public class HeosFunctionalException extends IOException {
private final HeosErrorCode code;
public HeosFunctionalException(HeosErrorCode code) {
this.code = code;
}
public HeosErrorCode getCode() {
return code;
}
}

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.heos.internal.exception;
import java.io.IOException;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* Exception to inform the caller that there is no connection to the HEOS system (yet)
*
* @author Martin van Wingerden - Initial contribution
*/
@NonNullByDefault
public class HeosNotConnectedException extends IOException {
public HeosNotConnectedException() {
super("HEOS not connected");
}
}

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.heos.internal.exception;
import java.io.IOException;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* Exception to inform the caller that there is no connection to the HEOS system (yet)
*
* @author Martin van Wingerden - Initial contribution
*/
@NonNullByDefault
public class HeosNotFoundException extends IOException {
public HeosNotFoundException() {
super("HEOS not found");
}
}

View File

@@ -0,0 +1,35 @@
/**
* 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.heos.internal.handler;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.heos.internal.api.HeosFacade;
import org.openhab.binding.heos.internal.exception.HeosNotConnectedException;
/**
* Base class for the channel handlers
*
* @author Martin van Wingerden - Initial contribution
*/
@NonNullByDefault
public abstract class BaseHeosChannelHandler implements HeosChannelHandler {
final HeosBridgeHandler bridge;
public BaseHeosChannelHandler(HeosBridgeHandler bridge) {
this.bridge = bridge;
}
protected HeosFacade getApi() throws HeosNotConnectedException {
return bridge.getApiConnection();
}
}

View File

@@ -0,0 +1,59 @@
/**
* 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.heos.internal.handler;
import java.util.concurrent.Future;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
/**
* Instead of continously rewriting the termination of future a small util method was added
*
* @author Martin van Wingerden - Initial Contribution
*/
@NonNullByDefault
public class FutureUtil {
private FutureUtil() {
// this is a util no instances should be created
}
/**
* Cancel the future
*
* - when it is not null
* - and it is not already cancelled
*
* interrupt if still/already running
*
* @param future nullable future to be cancelled
*/
public static void cancel(@Nullable Future<?> future) {
cancel(future, true);
}
/**
* Cancel the future
*
* - when it is not null
* - and it is not already cancelled
*
* @param future nullable future to be cancelled
* @param interruptIfRunning choose whether to interrupt a running future
*/
public static void cancel(@Nullable Future<?> future, boolean interruptIfRunning) {
if (future != null && !future.isCancelled()) {
future.cancel(interruptIfRunning);
}
}
}

View File

@@ -0,0 +1,519 @@
/**
* 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.heos.internal.handler;
import static org.openhab.binding.heos.internal.HeosBindingConstants.*;
import static org.openhab.binding.heos.internal.handler.FutureUtil.cancel;
import static org.openhab.core.thing.ThingStatus.OFFLINE;
import static org.openhab.core.thing.ThingStatus.ONLINE;
import java.io.IOException;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.heos.internal.HeosChannelHandlerFactory;
import org.openhab.binding.heos.internal.HeosChannelManager;
import org.openhab.binding.heos.internal.action.HeosActions;
import org.openhab.binding.heos.internal.api.HeosFacade;
import org.openhab.binding.heos.internal.api.HeosSystem;
import org.openhab.binding.heos.internal.configuration.BridgeConfiguration;
import org.openhab.binding.heos.internal.discovery.HeosPlayerDiscoveryListener;
import org.openhab.binding.heos.internal.exception.HeosNotConnectedException;
import org.openhab.binding.heos.internal.exception.HeosNotFoundException;
import org.openhab.binding.heos.internal.json.dto.HeosError;
import org.openhab.binding.heos.internal.json.dto.HeosEvent;
import org.openhab.binding.heos.internal.json.dto.HeosEventObject;
import org.openhab.binding.heos.internal.json.dto.HeosResponseObject;
import org.openhab.binding.heos.internal.json.payload.Group;
import org.openhab.binding.heos.internal.json.payload.Media;
import org.openhab.binding.heos.internal.json.payload.Player;
import org.openhab.binding.heos.internal.resources.HeosEventListener;
import org.openhab.binding.heos.internal.resources.Telnet;
import org.openhab.binding.heos.internal.resources.Telnet.ReadException;
import org.openhab.core.library.types.OnOffType;
import org.openhab.core.thing.Bridge;
import org.openhab.core.thing.Channel;
import org.openhab.core.thing.ChannelUID;
import org.openhab.core.thing.Thing;
import org.openhab.core.thing.ThingStatus;
import org.openhab.core.thing.ThingStatusDetail;
import org.openhab.core.thing.binding.BaseBridgeHandler;
import org.openhab.core.thing.binding.ThingHandler;
import org.openhab.core.thing.binding.ThingHandlerService;
import org.openhab.core.thing.binding.builder.ChannelBuilder;
import org.openhab.core.thing.binding.builder.ThingBuilder;
import org.openhab.core.thing.type.ChannelTypeUID;
import org.openhab.core.types.Command;
import org.openhab.core.types.RefreshType;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The {@link HeosBridgeHandler} is responsible for handling commands, which are
* sent to one of the channels.
*
* @author Johannes Einig - Initial contribution
*/
@NonNullByDefault
public class HeosBridgeHandler extends BaseBridgeHandler implements HeosEventListener {
private final Logger logger = LoggerFactory.getLogger(HeosBridgeHandler.class);
private static final int HEOS_PORT = 1255;
private final List<HeosPlayerDiscoveryListener> playerDiscoveryList = new CopyOnWriteArrayList<>();
private final HeosChannelManager channelManager = new HeosChannelManager(this);
private final HeosChannelHandlerFactory channelHandlerFactory;
private final Map<String, HeosGroupHandler> groupHandlerMap = new ConcurrentHashMap<>();
private final Map<String, String> hashToGidMap = new ConcurrentHashMap<>();
private List<String[]> selectedPlayerList = new CopyOnWriteArrayList<>();
private @Nullable Future<?> startupFuture;
private final List<Future<?>> childHandlerInitializedFutures = new CopyOnWriteArrayList<>();
private final HeosSystem heosSystem;
private @Nullable HeosFacade apiConnection;
private boolean loggedIn = false;
private boolean bridgeHandlerDisposalOngoing = false;
private @NonNullByDefault({}) BridgeConfiguration configuration;
private int failureCount;
public HeosBridgeHandler(Bridge bridge, HeosDynamicStateDescriptionProvider heosDynamicStateDescriptionProvider) {
super(bridge);
heosSystem = new HeosSystem(scheduler);
channelHandlerFactory = new HeosChannelHandlerFactory(this, heosDynamicStateDescriptionProvider);
}
@Override
public void handleCommand(ChannelUID channelUID, Command command) {
if (command instanceof RefreshType) {
return;
}
@Nullable
Channel channel = this.getThing().getChannel(channelUID.getId());
if (channel == null) {
logger.debug("No valid channel found");
return;
}
@Nullable
ChannelTypeUID channelTypeUID = channel.getChannelTypeUID();
@Nullable
HeosChannelHandler channelHandler = channelHandlerFactory.getChannelHandler(channelUID, this, channelTypeUID);
if (channelHandler != null) {
try {
channelHandler.handleBridgeCommand(command, thing.getUID());
failureCount = 0;
updateStatus(ONLINE);
} catch (IOException | ReadException e) {
logger.debug("Failed to handle bridge command", e);
failureCount++;
if (failureCount > FAILURE_COUNT_LIMIT) {
updateStatus(OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
"Failed to handle command: " + e.getMessage());
}
}
}
}
@Override
public synchronized void initialize() {
configuration = thing.getConfiguration().as(BridgeConfiguration.class);
cancel(startupFuture);
startupFuture = scheduler.submit(this::delayedInitialize);
}
private void delayedInitialize() {
@Nullable
HeosFacade connection = null;
try {
logger.debug("Running scheduledStartUp job");
connection = connectBridge();
updateStatus(ThingStatus.ONLINE);
updateState(CH_ID_REBOOT, OnOffType.OFF);
logger.debug("HEOS System heart beat started. Pulse time is {}s", configuration.heartbeat);
// gets all available player and groups to ensure that the system knows
// about the conjunction between the groupMemberHash and the GID
triggerPlayerDiscovery();
@Nullable
String username = configuration.username;
@Nullable
String password = configuration.password;
if (username != null && !"".equals(username) && password != null && !"".equals(password)) {
login(connection, username, password);
} else {
updateStatus(ThingStatus.ONLINE, ThingStatusDetail.CONFIGURATION_ERROR,
"Can't log in. Username or password not set.");
}
fetchPlayersAndGroups();
} catch (Telnet.ReadException | IOException | RuntimeException e) {
logger.debug("Error occurred while connecting", e);
if (connection != null) {
connection.closeConnection();
}
updateStatus(OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, "Errors occurred: " + e.getMessage());
cancel(startupFuture, false);
startupFuture = scheduler.schedule(this::delayedInitialize, 30, TimeUnit.SECONDS);
}
}
private void fetchPlayersAndGroups() {
try {
@Nullable
Player[] onlinePlayers = getApiConnection().getPlayers().payload;
@Nullable
Group[] onlineGroups = getApiConnection().getGroups().payload;
updatePlayerStatus(onlinePlayers, onlineGroups);
} catch (ReadException | IOException e) {
logger.debug("Failed updating online state of groups/players", e);
}
}
private void updatePlayerStatus(@Nullable Player[] onlinePlayers, @Nullable Group[] onlineGroups) {
if (onlinePlayers == null || onlineGroups == null) {
return;
}
Set<String> players = Stream.of(onlinePlayers).map(p -> Objects.toString(p.playerId))
.collect(Collectors.toSet());
Set<String> groups = Stream.of(onlineGroups).map(p -> p.id).collect(Collectors.toSet());
for (Thing thing : getThing().getThings()) {
try {
@Nullable
ThingHandler handler = thing.getHandler();
if (handler instanceof HeosThingBaseHandler) {
Set<String> target = handler instanceof HeosPlayerHandler ? players : groups;
HeosThingBaseHandler heosHandler = (HeosThingBaseHandler) handler;
String id = heosHandler.getId();
if (target.contains(id)) {
heosHandler.setStatusOnline();
} else {
heosHandler.setStatusOffline();
}
}
} catch (HeosNotFoundException e) {
logger.debug("SKipping handler which reported not found", e);
}
}
}
private HeosFacade connectBridge() throws IOException, Telnet.ReadException {
loggedIn = false;
logger.debug("Initialize Bridge '{}' with IP '{}'", thing.getProperties().get(PROP_NAME),
configuration.ipAddress);
bridgeHandlerDisposalOngoing = false;
HeosFacade connection = heosSystem.establishConnection(configuration.ipAddress, HEOS_PORT,
configuration.heartbeat);
connection.registerForChangeEvents(this);
apiConnection = connection;
return connection;
}
private void login(HeosFacade connection, String username, String password) throws IOException, ReadException {
logger.debug("Logging in to HEOS account.");
HeosResponseObject<Void> response = connection.logIn(username, password);
if (response.result) {
logger.debug("successfully logged-in, event is fired to handle post-login behaviour");
return;
}
@Nullable
HeosError error = response.getError();
logger.debug("Failed to login: {}", error);
updateStatus(ONLINE, ThingStatusDetail.CONFIGURATION_ERROR,
error != null ? error.code.toString() : "Failed to login, no error was returned.");
}
@Override
public void dispose() {
bridgeHandlerDisposalOngoing = true; // Flag to prevent the handler from being updated during disposal
cancel(startupFuture);
for (Future<?> future : childHandlerInitializedFutures) {
cancel(future);
}
@Nullable
HeosFacade localApiConnection = apiConnection;
if (localApiConnection == null) {
logger.debug("Not disposing bridge because of missing apiConnection");
return;
}
localApiConnection.unregisterForChangeEvents(this);
logger.debug("HEOS bridge removed from change notifications");
logger.debug("Dispose bridge '{}'", thing.getProperties().get(PROP_NAME));
localApiConnection.closeConnection();
}
/**
* Manages the removal of the player or group channels from the bridge.
*/
@Override
public synchronized void childHandlerDisposed(ThingHandler childHandler, Thing childThing) {
logger.debug("Disposing child handler for: {}.", childThing.getUID().getId());
if (bridgeHandlerDisposalOngoing) { // Checks if bridgeHandler is going to disposed (by stopping the binding or
// openHAB for example) and prevents it from being updated which stops the
// disposal process.
} else if (childHandler instanceof HeosPlayerHandler) {
String channelIdentifier = "P" + childThing.getUID().getId();
updateThingChannels(channelManager.removeSingleChannel(channelIdentifier));
} else if (childHandler instanceof HeosGroupHandler) {
String channelIdentifier = "G" + childThing.getUID().getId();
updateThingChannels(channelManager.removeSingleChannel(channelIdentifier));
// removes the handler from the groupMemberMap that handler is no longer called
// if group is getting online
removeGroupHandlerInformation((HeosGroupHandler) childHandler);
}
}
@Override
public void childHandlerInitialized(ThingHandler childHandler, Thing childThing) {
logger.debug("Initialized child handler for: {}.", childThing.getUID().getId());
childHandlerInitializedFutures.add(scheduler.submit(() -> addPlayerChannel(childThing, null)));
}
void resetPlayerList(ChannelUID channelUID) {
selectedPlayerList.forEach(element -> updateState(element[1], OnOffType.OFF));
selectedPlayerList.clear();
updateState(channelUID, OnOffType.OFF);
}
/**
* Sets the HEOS Thing offline
*/
@SuppressWarnings("null")
public void setGroupOffline(String groupMemberHash) {
HeosGroupHandler groupHandler = groupHandlerMap.get(groupMemberHash);
if (groupHandler != null) {
groupHandler.setStatusOffline();
}
hashToGidMap.remove(groupMemberHash);
}
/**
* Sets the HEOS Thing online. Also updates the link between
* the groupMemberHash value with the actual gid of this group
*/
public void setGroupOnline(String groupMemberHash, String groupId) {
hashToGidMap.put(groupMemberHash, groupId);
Optional.ofNullable(groupHandlerMap.get(groupMemberHash)).ifPresent(handler -> {
handler.setStatusOnline();
addPlayerChannel(handler.getThing(), groupId);
});
}
/**
* Create a channel for the childThing. Depending if it is a HEOS Group
* or a player an identification prefix is added
*
* @param childThing the thing the channel is created for
* @param groupId
*/
private void addPlayerChannel(Thing childThing, @Nullable String groupId) {
try {
String channelIdentifier = "";
String pid = "";
@Nullable
ThingHandler handler = childThing.getHandler();
if (handler instanceof HeosPlayerHandler) {
channelIdentifier = "P" + childThing.getUID().getId();
pid = ((HeosPlayerHandler) handler).getId();
} else if (handler instanceof HeosGroupHandler) {
channelIdentifier = "G" + childThing.getUID().getId();
if (groupId == null) {
pid = ((HeosGroupHandler) handler).getId();
} else {
pid = groupId;
}
}
Map<String, String> properties = new HashMap<>();
@Nullable
String playerName = childThing.getLabel();
playerName = playerName == null ? pid : playerName;
ChannelUID channelUID = new ChannelUID(getThing().getUID(), channelIdentifier);
properties.put(PROP_NAME, playerName);
properties.put(PID, pid);
Channel channel = ChannelBuilder.create(channelUID, "Switch").withLabel(playerName).withType(CH_TYPE_PLAYER)
.withProperties(properties).build();
updateThingChannels(channelManager.addSingleChannel(channel));
} catch (HeosNotFoundException e) {
logger.debug("Group is not yet initialized fully");
}
}
public void addGroupHandlerInformation(HeosGroupHandler handler) {
groupHandlerMap.put(handler.getGroupMemberHash(), handler);
}
private void removeGroupHandlerInformation(HeosGroupHandler handler) {
groupHandlerMap.remove(handler.getGroupMemberHash());
}
public @Nullable String getActualGID(String groupHash) {
return hashToGidMap.get(groupHash);
}
@Override
public void playerStateChangeEvent(HeosEventObject eventObject) {
// do nothing
}
@Override
public void playerStateChangeEvent(HeosResponseObject<?> responseObject) {
// do nothing
}
@Override
public void playerMediaChangeEvent(String pid, Media media) {
// do nothing
}
@Override
public void bridgeChangeEvent(String event, boolean success, Object command) {
if (EVENT_TYPE_EVENT.equals(event)) {
if (HeosEvent.PLAYERS_CHANGED.equals(command) || HeosEvent.GROUPS_CHANGED.equals(command)) {
fetchPlayersAndGroups();
triggerPlayerDiscovery();
} else if (EVENT_STREAM_TIMEOUT.equals(command)) {
logger.debug("HEOS Bridge events timed-out might be nothing, trying to reconnect");
} else if (CONNECTION_LOST.equals(command)) {
updateStatus(OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR);
logger.debug("HEOS Bridge OFFLINE");
} else if (CONNECTION_RESTORED.equals(command)) {
initialize();
}
}
if (EVENT_TYPE_SYSTEM.equals(event) && HeosEvent.USER_CHANGED == command) {
if (success && !loggedIn) {
loggedIn = true;
}
}
}
private synchronized void updateThingChannels(List<Channel> channelList) {
ThingBuilder thingBuilder = editThing();
thingBuilder.withChannels(channelList);
updateThing(thingBuilder.build());
}
public Player[] getPlayers() throws IOException, ReadException {
HeosResponseObject<Player[]> response = getApiConnection().getPlayers();
@Nullable
Player[] players = response.payload;
if (players == null) {
throw new IOException("Received no valid payload");
}
return players;
}
public Group[] getGroups() throws IOException, ReadException {
HeosResponseObject<Group[]> response = getApiConnection().getGroups();
@Nullable
Group[] groups = response.payload;
if (groups == null) {
throw new IOException("Received no valid payload");
}
return groups;
}
/**
* The list with the currently selected player
*
* @return a HashMap which the currently selected player
*/
public Map<String, String> getSelectedPlayer() {
return selectedPlayerList.stream().collect(Collectors.toMap(a -> a[0], a -> a[1], (a, b) -> a));
}
public List<String[]> getSelectedPlayerList() {
return selectedPlayerList;
}
public void setSelectedPlayerList(List<String[]> selectedPlayerList) {
this.selectedPlayerList = selectedPlayerList;
}
public HeosChannelHandlerFactory getChannelHandlerFactory() {
return channelHandlerFactory;
}
/**
* Register an {@link HeosPlayerDiscoveryListener} to get informed
* if the amount of groups or players have changed
*
* @param listener the implementing class
*/
public void registerPlayerDiscoverListener(HeosPlayerDiscoveryListener listener) {
playerDiscoveryList.add(listener);
}
private void triggerPlayerDiscovery() {
playerDiscoveryList.forEach(HeosPlayerDiscoveryListener::playerChanged);
}
public boolean isLoggedIn() {
return loggedIn;
}
public boolean isBridgeConnected() {
@Nullable
HeosFacade connection = apiConnection;
return connection != null && connection.isConnected();
}
public HeosFacade getApiConnection() throws HeosNotConnectedException {
@Nullable
HeosFacade localApiConnection = apiConnection;
if (localApiConnection != null) {
return localApiConnection;
} else {
throw new HeosNotConnectedException();
}
}
@Override
public Collection<Class<? extends ThingHandlerService>> getServices() {
return Collections.singletonList(HeosActions.class);
}
}

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.heos.internal.handler;
import java.io.IOException;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.heos.internal.resources.Telnet.ReadException;
import org.openhab.core.thing.ThingUID;
import org.openhab.core.types.Command;
/**
* The {@link HeosChannelHandler} handles the base class for the different
* channel handler which handles the command from the channels of the things
* to the HEOS system
*
* @author Johannes Einig - Initial contribution
*/
@NonNullByDefault
public interface HeosChannelHandler {
/**
* Handle a command received from a channel. Requires the class which
* wants to handle the command to decide which subclass has to be used
*
* @param command the command to handle
* @param id of the group or player
* @param uid
*/
void handlePlayerCommand(Command command, String id, ThingUID uid) throws IOException, ReadException;
void handleGroupCommand(Command command, @Nullable String id, ThingUID uid, HeosGroupHandler heosGroupHandler)
throws IOException, ReadException;
/**
* Handles a command for classes without an id. Used
* for BridgeHandler
*
* @param command the command to handle
* @param uid
*/
void handleBridgeCommand(Command command, ThingUID uid) throws IOException, ReadException;
}

View File

@@ -0,0 +1,68 @@
/**
* 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.heos.internal.handler;
import java.io.IOException;
import java.util.List;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.heos.internal.resources.Telnet.ReadException;
import org.openhab.core.library.types.OnOffType;
import org.openhab.core.thing.ChannelUID;
import org.openhab.core.thing.ThingUID;
import org.openhab.core.types.Command;
import org.openhab.core.types.RefreshType;
/**
* The {@link HeosChannelHandlerBuildGroup} handles the BuidlGroup channel command
* from the implementing thing.
*
* @author Johannes Einig - Initial contribution *
*/
@NonNullByDefault
public class HeosChannelHandlerBuildGroup extends BaseHeosChannelHandler {
private final ChannelUID channelUID;
public HeosChannelHandlerBuildGroup(ChannelUID channelUID, HeosBridgeHandler bridge) {
super(bridge);
this.channelUID = channelUID;
}
@Override
public void handlePlayerCommand(Command command, String id, ThingUID uid) {
// not used on player
}
@Override
public void handleGroupCommand(Command command, @Nullable String id, ThingUID uid,
HeosGroupHandler heosGroupHandler) {
// not used on group
}
@Override
public void handleBridgeCommand(Command command, ThingUID uid) throws IOException, ReadException {
if (command instanceof RefreshType) {
bridge.resetPlayerList(channelUID);
return;
}
if (command == OnOffType.ON) {
List<String[]> selectedPlayerList = bridge.getSelectedPlayerList();
if (!selectedPlayerList.isEmpty()) {
getApi().groupPlayer(selectedPlayerList.stream().map(a -> a[0]).toArray(String[]::new));
bridge.resetPlayerList(channelUID);
}
}
}
}

View File

@@ -0,0 +1,66 @@
/**
* 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.heos.internal.handler;
import java.io.IOException;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.heos.internal.exception.HeosNotFoundException;
import org.openhab.binding.heos.internal.resources.Telnet.ReadException;
import org.openhab.core.library.types.OnOffType;
import org.openhab.core.thing.ThingUID;
import org.openhab.core.types.Command;
import org.openhab.core.types.RefreshType;
/**
* The {@link HeosChannelHandlerReboot} handles the Reboot channel command
* from the implementing thing.
*
* @author Johannes Einig - Initial contribution
*/
@NonNullByDefault
public class HeosChannelHandlerClearQueue extends BaseHeosChannelHandler {
public HeosChannelHandlerClearQueue(HeosBridgeHandler bridge) {
super(bridge);
}
@Override
public void handlePlayerCommand(Command command, String id, ThingUID uid) throws IOException, ReadException {
handleCommand(command, id);
}
@Override
public void handleGroupCommand(Command command, @Nullable String id, ThingUID uid,
HeosGroupHandler heosGroupHandler) throws IOException, ReadException {
if (id == null) {
throw new HeosNotFoundException();
}
handleCommand(command, id);
}
@Override
public void handleBridgeCommand(Command command, ThingUID uid) {
// Not used on bridge
}
private void handleCommand(Command command, String id) throws IOException, ReadException {
if (command instanceof RefreshType) {
return;
}
if (command == OnOffType.ON) {
getApi().clearQueue(id);
}
}
}

View File

@@ -0,0 +1,82 @@
/**
* 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.heos.internal.handler;
import java.io.IOException;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.heos.internal.exception.HeosNotFoundException;
import org.openhab.binding.heos.internal.resources.HeosEventListener;
import org.openhab.binding.heos.internal.resources.Telnet.ReadException;
import org.openhab.core.thing.ThingUID;
import org.openhab.core.types.Command;
import org.openhab.core.types.RefreshType;
/**
* The {@link HeosChannelHandlerControl} handles the control commands
* coming from the implementing thing
*
* @author Johannes Einig - Initial contribution
*/
@NonNullByDefault
public class HeosChannelHandlerControl extends BaseHeosChannelHandler {
private final HeosEventListener eventListener;
public HeosChannelHandlerControl(HeosEventListener eventListener, HeosBridgeHandler bridge) {
super(bridge);
this.eventListener = eventListener;
}
@Override
public void handlePlayerCommand(Command command, String id, ThingUID uid) throws IOException, ReadException {
handleCommand(command, id);
}
@Override
public void handleGroupCommand(Command command, @Nullable String id, ThingUID uid,
HeosGroupHandler heosGroupHandler) throws IOException, ReadException {
if (id == null) {
throw new HeosNotFoundException();
}
handleCommand(command, id);
}
@Override
public void handleBridgeCommand(Command command, ThingUID uid) {
// No such channel within bridge
}
private void handleCommand(Command command, String id) throws IOException, ReadException {
if (command instanceof RefreshType) {
eventListener.playerStateChangeEvent(getApi().getPlayState(id));
return;
}
switch (command.toString()) {
case "PLAY":
case "ON":
getApi().play(id);
break;
case "PAUSE":
case "OFF":
getApi().pause(id);
break;
case "NEXT":
getApi().next(id);
break;
case "PREVIOUS":
getApi().previous(id);
break;
}
}
}

View File

@@ -0,0 +1,74 @@
/**
* 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.heos.internal.handler;
import static org.openhab.binding.heos.internal.HeosBindingConstants.CH_ID_FAVORITES;
import static org.openhab.binding.heos.internal.resources.HeosConstants.FAVORITE_SID;
import java.io.IOException;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.heos.internal.exception.HeosNotFoundException;
import org.openhab.binding.heos.internal.resources.Telnet.ReadException;
import org.openhab.core.thing.ChannelUID;
import org.openhab.core.thing.ThingUID;
import org.openhab.core.types.Command;
import org.openhab.core.types.RefreshType;
/**
* The {@link HeosChannelHandlerFavorite} handles the playlist selection channel command
* from the implementing thing.
*
* @author Johannes Einig - Initial contribution
*/
@NonNullByDefault
public class HeosChannelHandlerFavorite extends BaseHeosChannelHandler {
private final HeosDynamicStateDescriptionProvider heosDynamicStateDescriptionProvider;
public HeosChannelHandlerFavorite(HeosDynamicStateDescriptionProvider heosDynamicStateDescriptionProvider,
HeosBridgeHandler bridge) {
super(bridge);
this.heosDynamicStateDescriptionProvider = heosDynamicStateDescriptionProvider;
}
@Override
public void handlePlayerCommand(Command command, String id, ThingUID uid) throws IOException, ReadException {
handleCommand(command, id, uid);
}
@Override
public void handleGroupCommand(Command command, @Nullable String id, ThingUID uid,
HeosGroupHandler heosGroupHandler) throws IOException, ReadException {
if (id == null) {
throw new HeosNotFoundException();
}
handleCommand(command, id, uid);
}
@Override
public void handleBridgeCommand(Command command, ThingUID uid) {
// not used on bridge
}
private void handleCommand(Command command, String id, ThingUID uid) throws IOException, ReadException {
ChannelUID channelUID = new ChannelUID(uid, CH_ID_FAVORITES);
if (command instanceof RefreshType) {
heosDynamicStateDescriptionProvider.setFavorites(channelUID, getApi().getFavorites());
return;
}
String idCommand = heosDynamicStateDescriptionProvider.getValueByLabel(channelUID, command.toString());
getApi().playStream(id, FAVORITE_SID, idCommand);
}
}

View File

@@ -0,0 +1,64 @@
/**
* 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.heos.internal.handler;
import java.io.IOException;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.heos.internal.exception.HeosNotFoundException;
import org.openhab.binding.heos.internal.resources.Telnet.ReadException;
import org.openhab.core.library.types.OnOffType;
import org.openhab.core.thing.ThingUID;
import org.openhab.core.types.Command;
import org.openhab.core.types.RefreshType;
/**
* The {@link HeosChannelHandlerGrouping} handles the grouping channel command
* from the implementing thing.
*
* @author Johannes Einig - Initial contribution
*/
@NonNullByDefault
public class HeosChannelHandlerGrouping extends BaseHeosChannelHandler {
public HeosChannelHandlerGrouping(HeosBridgeHandler bridge) {
super(bridge);
}
@Override
public void handlePlayerCommand(Command command, String id, ThingUID uid) {
// No such channel on player
}
@Override
public void handleGroupCommand(Command command, @Nullable String id, ThingUID uid,
HeosGroupHandler heosGroupHandler) throws IOException, ReadException {
if (command instanceof RefreshType) {
return;
}
if (OnOffType.OFF == command) {
if (id == null) {
throw new HeosNotFoundException();
}
getApi().ungroupGroup(id);
} else if (OnOffType.ON == command) {
getApi().groupPlayer(heosGroupHandler.getGroupMemberPidList());
}
}
@Override
public void handleBridgeCommand(Command command, ThingUID uid) {
// No such channel on Bridge
}
}

View File

@@ -0,0 +1,89 @@
/**
* 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.heos.internal.handler;
import java.io.IOException;
import java.util.Map;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.heos.internal.exception.HeosNotFoundException;
import org.openhab.binding.heos.internal.json.payload.Media;
import org.openhab.binding.heos.internal.resources.HeosEventListener;
import org.openhab.binding.heos.internal.resources.Telnet.ReadException;
import org.openhab.core.thing.ThingUID;
import org.openhab.core.types.Command;
import org.openhab.core.types.RefreshType;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The {@link HeosChannelHandlerInputs} handles the Input channel command
* from the implementing thing.
*
* @author Johannes Einig - Initial contribution
*/
@NonNullByDefault
public class HeosChannelHandlerInputs extends BaseHeosChannelHandler {
protected final Logger logger = LoggerFactory.getLogger(HeosChannelHandlerInputs.class);
private final HeosEventListener eventListener;
public HeosChannelHandlerInputs(HeosEventListener eventListener, HeosBridgeHandler bridge) {
super(bridge);
this.eventListener = eventListener;
}
@Override
public void handlePlayerCommand(Command command, String id, ThingUID uid) throws IOException, ReadException {
handleCommand(command, id);
}
@Override
public void handleGroupCommand(Command command, @Nullable String id, ThingUID uid,
HeosGroupHandler heosGroupHandler) throws IOException, ReadException {
if (id == null) {
throw new HeosNotFoundException();
}
handleCommand(command, id);
}
@Override
public void handleBridgeCommand(Command command, ThingUID uid) {
// not used on bridge
}
private void handleCommand(Command command, String id) throws IOException, ReadException {
if (command instanceof RefreshType) {
@Nullable
Media payload = getApi().getNowPlayingMedia(id).payload;
if (payload != null) {
eventListener.playerMediaChangeEvent(id, payload);
}
return;
}
Map<String, String> selectedPlayers = bridge.getSelectedPlayer();
if (selectedPlayers.isEmpty()) {
// no selected player, just play it from the player itself
getApi().playInputSource(id, command.toString());
} else if (selectedPlayers.size() > 1) {
logger.debug("Only one source can be selected for HEOS Input. Selected amount of sources: {} ",
selectedPlayers.size());
} else {
for (String sourcePid : selectedPlayers.keySet()) {
getApi().playInputSource(id, sourcePid, command.toString());
}
}
}
}

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.heos.internal.handler;
import java.io.IOException;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.heos.internal.exception.HeosNotFoundException;
import org.openhab.binding.heos.internal.resources.HeosEventListener;
import org.openhab.binding.heos.internal.resources.Telnet.ReadException;
import org.openhab.core.library.types.OnOffType;
import org.openhab.core.thing.ThingUID;
import org.openhab.core.types.Command;
import org.openhab.core.types.RefreshType;
/**
* The {@link HeosChannelHandlerMute} handles the Mute channel command
* from the implementing thing.
*
* @author Johannes Einig - Initial contribution
*/
@NonNullByDefault
public class HeosChannelHandlerMute extends BaseHeosChannelHandler {
private final HeosEventListener eventListener;
public HeosChannelHandlerMute(HeosEventListener eventListener, HeosBridgeHandler bridge) {
super(bridge);
this.eventListener = eventListener;
}
@Override
public void handlePlayerCommand(Command command, String id, ThingUID uid) throws IOException, ReadException {
if (command instanceof RefreshType) {
eventListener.playerStateChangeEvent(getApi().getPlayerMuteState(id));
return;
}
if (command.equals(OnOffType.ON)) {
getApi().muteON(id);
} else if (command.equals(OnOffType.OFF)) {
getApi().muteOFF(id);
}
}
@Override
public void handleGroupCommand(Command command, @Nullable String id, ThingUID uid,
HeosGroupHandler heosGroupHandler) throws IOException, ReadException {
if (id == null) {
throw new HeosNotFoundException();
}
if (command instanceof RefreshType) {
eventListener.playerStateChangeEvent(getApi().getGroupMuteState(id));
return;
}
if (command.equals(OnOffType.ON)) {
getApi().muteGroupON(id);
} else if (command.equals(OnOffType.OFF)) {
getApi().muteGroupOFF(id);
}
}
@Override
public void handleBridgeCommand(Command command, ThingUID uid) {
// No such channel on bridge
}
}

View File

@@ -0,0 +1,72 @@
/**
* 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.heos.internal.handler;
import java.io.IOException;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.heos.internal.exception.HeosNotFoundException;
import org.openhab.binding.heos.internal.json.payload.Media;
import org.openhab.binding.heos.internal.resources.HeosEventListener;
import org.openhab.binding.heos.internal.resources.Telnet.ReadException;
import org.openhab.core.thing.ThingUID;
import org.openhab.core.types.Command;
import org.openhab.core.types.RefreshType;
/**
* The {@link HeosChannelHandlerNowPlaying} handles the refresh commands
* coming from the implementing thing
*
* @author Johannes Einig - Initial contribution
*/
@NonNullByDefault
public class HeosChannelHandlerNowPlaying extends BaseHeosChannelHandler {
private final HeosEventListener eventListener;
public HeosChannelHandlerNowPlaying(HeosEventListener eventListener, HeosBridgeHandler bridge) {
super(bridge);
this.eventListener = eventListener;
}
@Override
public void handlePlayerCommand(Command command, String id, ThingUID uid) throws IOException, ReadException {
handleCommand(command, id);
}
@Override
public void handleGroupCommand(Command command, @Nullable String id, ThingUID uid,
HeosGroupHandler heosGroupHandler) throws IOException, ReadException {
if (id == null) {
throw new HeosNotFoundException();
}
handleCommand(command, id);
}
@Override
public void handleBridgeCommand(Command command, ThingUID uid) {
// No such channel on bridge
}
private void handleCommand(Command command, String id) throws IOException, ReadException {
if (command instanceof RefreshType) {
// TODO consider caching this somehow, this method is triggered from a lot of channels for the same player
@Nullable
Media payload = getApi().getNowPlayingMedia(id).payload;
if (payload != null) {
eventListener.playerMediaChangeEvent(id, payload);
}
}
}
}

View File

@@ -0,0 +1,74 @@
/**
* 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.heos.internal.handler;
import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URL;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.heos.internal.exception.HeosNotFoundException;
import org.openhab.binding.heos.internal.resources.Telnet.ReadException;
import org.openhab.core.thing.ThingUID;
import org.openhab.core.types.Command;
import org.openhab.core.types.RefreshType;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The {@link HeosChannelHandlerPlayURL} handles the PlayURL channel command
* from the implementing thing.
*
* @author Johannes Einig - Initial contribution
*/
@NonNullByDefault
public class HeosChannelHandlerPlayURL extends BaseHeosChannelHandler {
protected final Logger logger = LoggerFactory.getLogger(HeosChannelHandlerPlayURL.class);
public HeosChannelHandlerPlayURL(HeosBridgeHandler bridge) {
super(bridge);
}
@Override
public void handlePlayerCommand(Command command, String id, ThingUID uid) throws IOException, ReadException {
handleCommand(command, id);
}
@Override
public void handleGroupCommand(Command command, @Nullable String id, ThingUID uid,
HeosGroupHandler heosGroupHandler) throws IOException, ReadException {
if (id == null) {
throw new HeosNotFoundException();
}
handleCommand(command, id);
}
@Override
public void handleBridgeCommand(Command command, ThingUID uid) {
// not used on bridge
}
private void handleCommand(Command command, String id) throws IOException, ReadException {
if (command instanceof RefreshType) {
return;
}
try {
URL url = new URL(command.toString());
getApi().playURL(id, url);
} catch (MalformedURLException e) {
logger.debug("Command '{}' is not a proper URL. Error: {}", command.toString(), e.getMessage());
}
}
}

View File

@@ -0,0 +1,88 @@
/**
* 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.heos.internal.handler;
import static org.openhab.binding.heos.internal.resources.HeosConstants.PID;
import java.util.List;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.core.library.types.OnOffType;
import org.openhab.core.thing.Channel;
import org.openhab.core.thing.ChannelUID;
import org.openhab.core.thing.ThingUID;
import org.openhab.core.types.Command;
import org.openhab.core.types.RefreshType;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The {@link HeosChannelHandlerPlayerSelect} handles the player selection channel command
* from the implementing thing.
*
* @author Johannes Einig - Initial contribution
*/
@NonNullByDefault
public class HeosChannelHandlerPlayerSelect extends BaseHeosChannelHandler {
protected final Logger logger = LoggerFactory.getLogger(HeosChannelHandlerPlayerSelect.class);
private final ChannelUID channelUID;
public HeosChannelHandlerPlayerSelect(ChannelUID channelUID, HeosBridgeHandler bridge) {
super(bridge);
this.channelUID = channelUID;
}
@Override
public void handlePlayerCommand(Command command, String id, ThingUID uid) {
// not used on player
}
@Override
public void handleGroupCommand(Command command, @Nullable String id, ThingUID uid,
HeosGroupHandler heosGroupHandler) {
// not used on group
}
@Override
public void handleBridgeCommand(Command command, ThingUID uid) {
if (command instanceof RefreshType) {
return;
}
Channel channel = bridge.getThing().getChannel(channelUID.getId());
if (channel == null) {
logger.debug("Channel {} not found", channelUID);
return;
}
List<String[]> selectedPlayerList = bridge.getSelectedPlayerList();
if (command.equals(OnOffType.ON)) {
String[] selectedPlayerInfo = new String[2];
selectedPlayerInfo[0] = channel.getProperties().get(PID);
selectedPlayerInfo[1] = channelUID.getId();
selectedPlayerList.add(selectedPlayerInfo);
} else if (!selectedPlayerList.isEmpty()) {
int indexPlayerChannel = -1;
for (int i = 0; i < selectedPlayerList.size(); i++) {
String localPID = selectedPlayerList.get(i)[0];
if (localPID.equals(channel.getProperties().get(PID))) {
indexPlayerChannel = i;
}
}
selectedPlayerList.remove(indexPlayerChannel);
bridge.setSelectedPlayerList(selectedPlayerList);
}
}
}

View File

@@ -0,0 +1,75 @@
/**
* 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.heos.internal.handler;
import static org.openhab.binding.heos.internal.HeosBindingConstants.CH_ID_PLAYLISTS;
import static org.openhab.binding.heos.internal.resources.HeosConstants.PLAYLISTS_SID;
import java.io.IOException;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.heos.internal.exception.HeosNotFoundException;
import org.openhab.binding.heos.internal.resources.Telnet.ReadException;
import org.openhab.core.thing.ChannelUID;
import org.openhab.core.thing.ThingUID;
import org.openhab.core.types.Command;
import org.openhab.core.types.RefreshType;
/**
* The {@link HeosChannelHandlerPlaylist} handles the playlist selection channel command
* from the implementing thing.
*
* @author Johannes Einig - Initial contribution
*/
@NonNullByDefault
public class HeosChannelHandlerPlaylist extends BaseHeosChannelHandler {
private final HeosDynamicStateDescriptionProvider heosDynamicStateDescriptionProvider;
public HeosChannelHandlerPlaylist(HeosDynamicStateDescriptionProvider heosDynamicStateDescriptionProvider,
HeosBridgeHandler bridge) {
super(bridge);
this.heosDynamicStateDescriptionProvider = heosDynamicStateDescriptionProvider;
}
@Override
public void handlePlayerCommand(Command command, String id, ThingUID uid) throws IOException, ReadException {
handleCommand(command, id, uid);
}
@Override
public void handleGroupCommand(Command command, @Nullable String id, ThingUID uid,
HeosGroupHandler heosGroupHandler) throws IOException, ReadException {
if (id == null) {
throw new HeosNotFoundException();
}
handleCommand(command, id, uid);
}
@Override
public void handleBridgeCommand(Command command, ThingUID uid) {
// not used on bridge
}
private void handleCommand(Command command, String id, ThingUID uid) throws IOException, ReadException {
ChannelUID channelUID = new ChannelUID(uid, CH_ID_PLAYLISTS);
if (command instanceof RefreshType) {
heosDynamicStateDescriptionProvider.setPlaylists(channelUID, getApi().getPlaylists());
return;
}
String idCommand = heosDynamicStateDescriptionProvider.getValueByLabel(channelUID, command.toString());
getApi().addContainerToQueuePlayNow(id, PLAYLISTS_SID, idCommand);
}
}

View File

@@ -0,0 +1,74 @@
/**
* 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.heos.internal.handler;
import static org.openhab.binding.heos.internal.HeosBindingConstants.CH_ID_QUEUE;
import java.io.IOException;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.heos.internal.exception.HeosNotFoundException;
import org.openhab.binding.heos.internal.resources.Telnet.ReadException;
import org.openhab.core.thing.ChannelUID;
import org.openhab.core.thing.ThingUID;
import org.openhab.core.types.Command;
import org.openhab.core.types.RefreshType;
/**
* The {@link HeosChannelHandlerFavorite} handles the playlist selection channel command
* from the implementing thing.
*
* @author Johannes Einig - Initial contribution
*/
@NonNullByDefault
public class HeosChannelHandlerQueue extends BaseHeosChannelHandler {
private final HeosDynamicStateDescriptionProvider heosDynamicStateDescriptionProvider;
public HeosChannelHandlerQueue(HeosDynamicStateDescriptionProvider heosDynamicStateDescriptionProvider,
HeosBridgeHandler bridge) {
super(bridge);
this.heosDynamicStateDescriptionProvider = heosDynamicStateDescriptionProvider;
}
@Override
public void handlePlayerCommand(Command command, String id, ThingUID uid) throws IOException, ReadException {
handleCommand(command, id, uid);
}
@Override
public void handleGroupCommand(Command command, @Nullable String id, ThingUID uid,
HeosGroupHandler heosGroupHandler) throws IOException, ReadException {
if (id == null) {
throw new HeosNotFoundException();
}
handleCommand(command, id, uid);
}
@Override
public void handleBridgeCommand(Command command, ThingUID uid) {
// not used on bridge
}
private void handleCommand(Command command, String id, ThingUID uid) throws IOException, ReadException {
ChannelUID channelUID = new ChannelUID(uid, CH_ID_QUEUE);
if (command instanceof RefreshType) {
heosDynamicStateDescriptionProvider.setQueue(channelUID, getApi().getQueue(id));
return;
}
String idCommand = heosDynamicStateDescriptionProvider.getValueByLabel(channelUID, command.toString());
getApi().playMediaFromQueue(id, idCommand);
}
}

View File

@@ -0,0 +1,66 @@
/**
* 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.heos.internal.handler;
import java.io.IOException;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.heos.internal.json.dto.HeosResponseObject;
import org.openhab.binding.heos.internal.resources.HeosEventListener;
import org.openhab.binding.heos.internal.resources.Telnet.ReadException;
import org.openhab.core.thing.ThingUID;
import org.openhab.core.types.Command;
import org.openhab.core.types.RefreshType;
import com.google.gson.JsonElement;
/**
*
* The {@link HeosChannelHandlerRawCommand} handles the RawCommand channel command
* from the implementing thing.
*
* @author Johannes Einig - Initial contribution
*/
@NonNullByDefault
public class HeosChannelHandlerRawCommand extends BaseHeosChannelHandler {
private final HeosEventListener eventListener;
public HeosChannelHandlerRawCommand(HeosEventListener eventListener, HeosBridgeHandler bridge) {
super(bridge);
this.eventListener = eventListener;
}
@Override
public void handlePlayerCommand(Command command, String id, ThingUID uid) {
// not used on player
}
@Override
public void handleGroupCommand(Command command, @Nullable String id, ThingUID uid,
HeosGroupHandler heosGroupHandler) {
// not used on group
}
@Override
public void handleBridgeCommand(Command command, ThingUID uid) throws IOException, ReadException {
if (command instanceof RefreshType) {
return;
}
HeosResponseObject<JsonElement> response = getApi().sendRawCommand(command.toString());
if (response.result) {
eventListener.playerStateChangeEvent(response);
}
}
}

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.heos.internal.handler;
import java.io.IOException;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.heos.internal.resources.Telnet.ReadException;
import org.openhab.core.library.types.OnOffType;
import org.openhab.core.thing.ThingUID;
import org.openhab.core.types.Command;
import org.openhab.core.types.RefreshType;
/**
* The {@link HeosChannelHandlerReboot} handles the Reboot channel command
* from the implementing thing.
*
* @author Johannes Einig - Initial contribution
*/
@NonNullByDefault
public class HeosChannelHandlerReboot extends BaseHeosChannelHandler {
public HeosChannelHandlerReboot(HeosBridgeHandler bridge) {
super(bridge);
}
@Override
public void handlePlayerCommand(Command command, String id, ThingUID uid) {
// not used on player
}
@Override
public void handleGroupCommand(Command command, @Nullable String id, ThingUID uid,
HeosGroupHandler heosGroupHandler) {
// Not used on group
}
@Override
public void handleBridgeCommand(Command command, ThingUID uid) throws IOException, ReadException {
if (command instanceof RefreshType) {
return;
}
if (command == OnOffType.ON) {
getApi().reboot();
}
}
}

View File

@@ -0,0 +1,76 @@
/**
* 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.heos.internal.handler;
import java.io.IOException;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.heos.internal.exception.HeosNotFoundException;
import org.openhab.binding.heos.internal.resources.HeosConstants;
import org.openhab.binding.heos.internal.resources.HeosEventListener;
import org.openhab.binding.heos.internal.resources.Telnet.ReadException;
import org.openhab.core.thing.ThingUID;
import org.openhab.core.types.Command;
import org.openhab.core.types.RefreshType;
/**
* The {@link HeosChannelHandlerRepeatMode} handles the RepeatMode channel command
* from the implementing thing.
*
* @author Johannes Einig - Initial contribution
*/
@NonNullByDefault
public class HeosChannelHandlerRepeatMode extends BaseHeosChannelHandler {
private final HeosEventListener eventListener;
public HeosChannelHandlerRepeatMode(HeosEventListener eventListener, HeosBridgeHandler bridge) {
super(bridge);
this.eventListener = eventListener;
}
@Override
public void handlePlayerCommand(Command command, String id, ThingUID uid) throws IOException, ReadException {
handleCommand(command, id);
}
@Override
public void handleGroupCommand(Command command, @Nullable String id, ThingUID uid,
HeosGroupHandler heosGroupHandler) throws IOException, ReadException {
if (id == null) {
throw new HeosNotFoundException();
}
handleCommand(command, id);
}
@Override
public void handleBridgeCommand(Command command, ThingUID uid) {
// Do nothing
}
private void handleCommand(Command command, String id) throws IOException, ReadException {
if (command instanceof RefreshType) {
eventListener.playerStateChangeEvent(getApi().getPlayMode(id));
return;
}
if (HeosConstants.HEOS_UI_ALL.equalsIgnoreCase(command.toString())) {
getApi().setRepeatMode(id, HeosConstants.REPEAT_ALL);
} else if (HeosConstants.HEOS_UI_ONE.equalsIgnoreCase(command.toString())) {
getApi().setRepeatMode(id, HeosConstants.REPEAT_ONE);
} else if (HeosConstants.HEOS_UI_OFF.equalsIgnoreCase(command.toString())) {
getApi().setRepeatMode(id, HeosConstants.OFF);
}
}
}

View File

@@ -0,0 +1,74 @@
/**
* 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.heos.internal.handler;
import java.io.IOException;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.heos.internal.exception.HeosNotFoundException;
import org.openhab.binding.heos.internal.resources.HeosConstants;
import org.openhab.binding.heos.internal.resources.HeosEventListener;
import org.openhab.binding.heos.internal.resources.Telnet.ReadException;
import org.openhab.core.library.types.OnOffType;
import org.openhab.core.thing.ThingUID;
import org.openhab.core.types.Command;
import org.openhab.core.types.RefreshType;
/**
* The {@link HeosChannelHandlerShuffleMode} handles the SchuffelModechannel command
* from the implementing thing.
*
* @author Johannes Einig - Initial contribution
*/
@NonNullByDefault
public class HeosChannelHandlerShuffleMode extends BaseHeosChannelHandler {
private final HeosEventListener eventListener;
public HeosChannelHandlerShuffleMode(HeosEventListener eventListener, HeosBridgeHandler bridge) {
super(bridge);
this.eventListener = eventListener;
}
@Override
public void handlePlayerCommand(Command command, String id, ThingUID uid) throws IOException, ReadException {
handleCommand(command, id);
}
@Override
public void handleGroupCommand(Command command, @Nullable String id, ThingUID uid,
HeosGroupHandler heosGroupHandler) throws IOException, ReadException {
if (id == null) {
throw new HeosNotFoundException();
}
handleCommand(command, id);
}
@Override
public void handleBridgeCommand(Command command, ThingUID uid) {
// Do nothing
}
private void handleCommand(Command command, String id) throws IOException, ReadException {
if (command instanceof RefreshType) {
eventListener.playerStateChangeEvent(getApi().getPlayMode(id));
return;
}
if (command == OnOffType.ON) {
getApi().setShuffleMode(id, HeosConstants.ON);
} else if (command == OnOffType.OFF) {
getApi().setShuffleMode(id, HeosConstants.OFF);
}
}
}

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.heos.internal.handler;
import java.io.IOException;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.heos.internal.exception.HeosNotFoundException;
import org.openhab.binding.heos.internal.resources.HeosEventListener;
import org.openhab.binding.heos.internal.resources.Telnet.ReadException;
import org.openhab.core.library.types.IncreaseDecreaseType;
import org.openhab.core.thing.ThingUID;
import org.openhab.core.types.Command;
import org.openhab.core.types.RefreshType;
/**
* The {@link HeosChannelHandlerVolume} handles the Volume channel command
* from the implementing thing.
*
* @author Johannes Einig - Initial contribution
*/
@NonNullByDefault
public class HeosChannelHandlerVolume extends BaseHeosChannelHandler {
private final HeosEventListener eventListener;
public HeosChannelHandlerVolume(HeosEventListener eventListener, HeosBridgeHandler bridge) {
super(bridge);
this.eventListener = eventListener;
}
@Override
public void handlePlayerCommand(Command command, String id, ThingUID uid) throws IOException, ReadException {
if (command instanceof RefreshType) {
eventListener.playerStateChangeEvent(getApi().getPlayerVolume(id));
return;
}
if (command instanceof IncreaseDecreaseType) {
if (IncreaseDecreaseType.INCREASE == command) {
getApi().increaseVolume(id);
} else {
getApi().decreaseVolume(id);
}
} else {
getApi().setVolume(command.toString(), id);
}
}
@Override
public void handleGroupCommand(Command command, @Nullable String id, ThingUID uid,
HeosGroupHandler heosGroupHandler) throws IOException, ReadException {
if (id == null) {
throw new HeosNotFoundException();
}
if (command instanceof RefreshType) {
eventListener.playerStateChangeEvent(getApi().getGroupVolume(id));
return;
}
if (command instanceof IncreaseDecreaseType) {
if (IncreaseDecreaseType.INCREASE == command) {
getApi().increaseGroupVolume(id);
} else {
getApi().decreaseGroupVolume(id);
}
} else {
getApi().volumeGroup(command.toString(), id);
}
}
@Override
public void handleBridgeCommand(Command command, ThingUID uid) {
// not used on bridge
}
}

View File

@@ -0,0 +1,80 @@
/**
* 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.heos.internal.handler;
import java.util.List;
import java.util.Optional;
import java.util.function.Function;
import java.util.stream.Collectors;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.heos.internal.json.payload.BrowseResult;
import org.openhab.binding.heos.internal.json.payload.Media;
import org.openhab.binding.heos.internal.json.payload.YesNoEnum;
import org.openhab.core.thing.ChannelUID;
import org.openhab.core.thing.binding.BaseDynamicStateDescriptionProvider;
import org.openhab.core.thing.type.DynamicStateDescriptionProvider;
import org.openhab.core.types.StateOption;
import org.osgi.service.component.annotations.Component;
/**
* Dynamically create the users list of favorites and playlists.
*
* @author Martin van Wingerden - Initial contribution
*/
@Component(service = { DynamicStateDescriptionProvider.class, HeosDynamicStateDescriptionProvider.class })
@NonNullByDefault
public class HeosDynamicStateDescriptionProvider extends BaseDynamicStateDescriptionProvider {
String getValueByLabel(ChannelUID channelUID, String input) {
Optional<String> optionalValueByLabel = channelOptionsMap.get(channelUID).stream()
.filter(o -> input.equals(o.getLabel())).map(StateOption::getValue).findFirst();
// if no match was found we assume that it already was a value and not a label
return optionalValueByLabel.orElse(input);
}
public void setFavorites(ChannelUID channelUID, List<BrowseResult> favorites) {
setBrowseResultList(channelUID, favorites, d -> d.mediaId);
}
public void setPlaylists(ChannelUID channelUID, List<BrowseResult> playLists) {
setBrowseResultList(channelUID, playLists, d -> d.containerId);
}
private void setBrowseResultList(ChannelUID channelUID, List<BrowseResult> playlists,
Function<BrowseResult, @Nullable String> function) {
setStateOptions(channelUID,
playlists.stream().filter(browseResult -> browseResult.playable == YesNoEnum.YES)
.map(browseResult -> getStateOption(function, browseResult)).filter(Optional::isPresent)
.map(Optional::get).collect(Collectors.toList()));
}
private Optional<StateOption> getStateOption(Function<BrowseResult, @Nullable String> function,
BrowseResult browseResult) {
@Nullable
String identifier = function.apply(browseResult);
if (identifier != null) {
return Optional.of(new StateOption(identifier, browseResult.name));
} else {
return Optional.empty();
}
}
public void setQueue(ChannelUID channelUID, List<Media> queue) {
setStateOptions(channelUID,
queue.stream().map(m -> new StateOption(String.valueOf(m.queueId), m.combinedSongArtist()))
.collect(Collectors.toList()));
}
}

View File

@@ -0,0 +1,317 @@
/**
* 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.heos.internal.handler;
import static org.openhab.binding.heos.internal.HeosBindingConstants.*;
import static org.openhab.binding.heos.internal.handler.FutureUtil.cancel;
import static org.openhab.binding.heos.internal.json.dto.HeosEvent.PLAYER_VOLUME_CHANGED;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.heos.internal.configuration.GroupConfiguration;
import org.openhab.binding.heos.internal.exception.HeosFunctionalException;
import org.openhab.binding.heos.internal.exception.HeosNotConnectedException;
import org.openhab.binding.heos.internal.exception.HeosNotFoundException;
import org.openhab.binding.heos.internal.json.dto.HeosCommunicationAttribute;
import org.openhab.binding.heos.internal.json.dto.HeosEventObject;
import org.openhab.binding.heos.internal.json.dto.HeosResponseObject;
import org.openhab.binding.heos.internal.json.payload.Group;
import org.openhab.binding.heos.internal.json.payload.Media;
import org.openhab.binding.heos.internal.resources.HeosGroup;
import org.openhab.binding.heos.internal.resources.Telnet.ReadException;
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.thing.ChannelUID;
import org.openhab.core.thing.Thing;
import org.openhab.core.thing.ThingStatus;
import org.openhab.core.thing.ThingStatusDetail;
import org.openhab.core.types.Command;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The {@link HeosGroupHandler} handles the actions for a HEOS group.
* Channel commands are received and send to the dedicated channels
*
* @author Johannes Einig - Initial contribution
*/
@NonNullByDefault
public class HeosGroupHandler extends HeosThingBaseHandler {
private final Logger logger = LoggerFactory.getLogger(HeosGroupHandler.class);
private @NonNullByDefault({}) GroupConfiguration configuration;
private @Nullable String gid;
private boolean blockInitialization;
private @Nullable Future<?> scheduledStartupFuture;
public HeosGroupHandler(Thing thing, HeosDynamicStateDescriptionProvider heosDynamicStateDescriptionProvider) {
super(thing, heosDynamicStateDescriptionProvider);
}
@Override
public void handleCommand(ChannelUID channelUID, Command command) {
// The GID is null if there is no group online with the groupMemberHash
// Only commands from the UNGROUP channel are passed through
// to activate the group if it is offline
if (gid != null || CH_ID_UNGROUP.equals(channelUID.getId())) {
@Nullable
HeosChannelHandler channelHandler = getHeosChannelHandler(channelUID);
if (channelHandler != null) {
try {
@Nullable
String id = getMaybeId(channelUID, command);
channelHandler.handleGroupCommand(command, id, thing.getUID(), this);
handleSuccess();
} catch (IOException | ReadException e) {
handleError(e);
}
}
}
}
@Nullable
private String getMaybeId(ChannelUID channelUID, Command command) throws HeosNotFoundException {
if (isCreateGroupRequest(channelUID, command)) {
return null;
} else {
return getId();
}
}
private boolean isCreateGroupRequest(ChannelUID channelUID, Command command) {
return CH_ID_UNGROUP.equals(channelUID.getId()) && OnOffType.ON == command;
}
/**
* Initialize the HEOS group. Starts an extra thread to avoid blocking
* during start up phase. Gathering all information can take longer
* than 5 seconds which can throw an error within the openHAB system.
*/
@Override
public synchronized void initialize() {
super.initialize();
configuration = thing.getConfiguration().as(GroupConfiguration.class);
// Prevents that initialize() is called multiple times if group goes online
blockInitialization = true;
scheduledStartUp();
}
@Override
public void dispose() {
cancel(scheduledStartupFuture);
super.dispose();
}
@Override
public String getId() throws HeosNotFoundException {
@Nullable
String localGroupId = this.gid;
if (localGroupId == null) {
throw new HeosNotFoundException();
}
return localGroupId;
}
public String getGroupMemberHash() {
return HeosGroup.calculateGroupMemberHash(configuration.members);
}
public String[] getGroupMemberPidList() {
return configuration.members.split(";");
}
@Override
public void setNotificationSoundVolume(PercentType volume) {
super.setNotificationSoundVolume(volume);
try {
getApiConnection().volumeGroup(volume.toString(), getId());
} catch (IOException | ReadException e) {
logger.warn("Failed to set notification volume", e);
}
}
@Override
public void playerStateChangeEvent(HeosEventObject eventObject) {
if (ThingStatus.UNINITIALIZED == getThing().getStatus()) {
logger.debug("Can't Handle Event. Group {} not initialized. Status is: {}", getConfig().get(PROP_NAME),
getThing().getStatus());
return;
}
@Nullable
String localGid = this.gid;
@Nullable
String eventGroupId = eventObject.getAttribute(HeosCommunicationAttribute.GROUP_ID);
@Nullable
String eventPlayerId = eventObject.getAttribute(HeosCommunicationAttribute.PLAYER_ID);
if (localGid == null || !(localGid.equals(eventGroupId) || localGid.equals(eventPlayerId))) {
return;
}
if (PLAYER_VOLUME_CHANGED.equals(eventObject.command)) {
logger.debug("Ignoring player-volume changes for groups");
return;
}
handleThingStateUpdate(eventObject);
}
@Override
public void playerStateChangeEvent(HeosResponseObject<?> responseObject) throws HeosFunctionalException {
if (ThingStatus.UNINITIALIZED == getThing().getStatus()) {
logger.debug("Can't Handle Event. Group {} not initialized. Status is: {}", getConfig().get(PROP_NAME),
getThing().getStatus());
return;
}
@Nullable
String localGid = this.gid;
if (localGid == null || !localGid.equals(responseObject.getAttribute(HeosCommunicationAttribute.GROUP_ID))) {
return;
}
handleThingStateUpdate(responseObject);
}
@Override
public void playerMediaChangeEvent(String pid, Media media) {
if (!pid.equals(gid)) {
return;
}
handleThingMediaUpdate(media);
}
/**
* Sets the status of the HEOS group to OFFLINE.
* Also sets the UNGROUP channel to OFF and the CONTROL
* channel to PAUSE
*/
@Override
public void setStatusOffline() {
logger.debug("Status was set offline");
try {
getApiConnection().unregisterForChangeEvents(this);
} catch (HeosNotConnectedException e) {
logger.debug("Not connected, failed to unregister");
}
updateState(CH_ID_UNGROUP, OnOffType.OFF);
updateState(CH_ID_CONTROL, PlayPauseType.PAUSE);
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.DISABLED, "Group is not available on HEOS system");
}
@Override
public void setStatusOnline() {
if (!blockInitialization) {
initialize();
} else {
logger.debug("Not initializing from setStatusOnline ({}, {})", thing.getStatus(), blockInitialization);
}
}
private void updateConfiguration(String groupId, Group group) {
Map<String, String> prop = new HashMap<>();
prop.put(PROP_NAME, group.name);
prop.put(PROP_GROUP_MEMBERS, group.getGroupMemberIds());
prop.put(PROP_GROUP_LEADER, group.getLeaderId());
prop.put(PROP_GROUP_HASH, HeosGroup.calculateGroupMemberHash(group));
prop.put(PROP_GID, groupId);
updateProperties(prop);
}
private void scheduledStartUp() {
cancel(scheduledStartupFuture);
scheduledStartupFuture = scheduler.submit(this::delayedInitialize);
}
private void delayedInitialize() {
@Nullable
HeosBridgeHandler bridgeHandler = this.bridgeHandler;
if (bridgeHandler == null) {
logger.debug("Bridge handler not found, rescheduling");
scheduledStartUp();
return;
}
if (bridgeHandler.isLoggedIn()) {
handleDynamicStatesSignedIn();
}
bridgeHandler.addGroupHandlerInformation(this);
// Checks if there is a group online with the same group member hash.
// If not setting the group offline.
@Nullable
String groupId = bridgeHandler.getActualGID(HeosGroup.calculateGroupMemberHash(configuration.members));
if (groupId == null) {
blockInitialization = false;
setStatusOffline();
} else {
try {
refreshPlayState(groupId);
HeosResponseObject<Group> response = getApiConnection().getGroupInfo(groupId);
@Nullable
Group group = response.payload;
if (group == null) {
throw new IllegalStateException("Invalid group response received");
}
assertSameGroup(group);
gid = groupId;
updateConfiguration(groupId, group);
updateStatus(ThingStatus.ONLINE);
updateState(CH_ID_UNGROUP, OnOffType.ON);
blockInitialization = false;
} catch (IOException | ReadException | IllegalStateException e) {
logger.debug("Failed initializing, will retry", e);
cancel(scheduledStartupFuture, false);
scheduledStartupFuture = scheduler.schedule(this::delayedInitialize, 30, TimeUnit.SECONDS);
}
}
}
/**
* Make sure the given group is group which this handler represents
*
* @param group retrieved from HEOS system
*/
private void assertSameGroup(Group group) {
String storedGroupHash = HeosGroup.calculateGroupMemberHash(configuration.members);
String retrievedGroupHash = HeosGroup.calculateGroupMemberHash(group);
if (!retrievedGroupHash.equals(storedGroupHash)) {
throw new IllegalStateException("Invalid group received, members / hash do not match.");
}
}
@Override
void refreshPlayState(String id) throws IOException, ReadException {
super.refreshPlayState(id);
handleThingStateUpdate(getApiConnection().getGroupMuteState(id));
handleThingStateUpdate(getApiConnection().getGroupVolume(id));
}
}

View File

@@ -0,0 +1,184 @@
/**
* 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.heos.internal.handler;
import static org.openhab.binding.heos.internal.HeosBindingConstants.*;
import static org.openhab.binding.heos.internal.handler.FutureUtil.cancel;
import static org.openhab.binding.heos.internal.json.dto.HeosCommunicationAttribute.PLAYER_ID;
import static org.openhab.binding.heos.internal.json.dto.HeosEvent.GROUP_VOLUME_CHANGED;
import java.io.IOException;
import java.util.Map;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.heos.internal.configuration.PlayerConfiguration;
import org.openhab.binding.heos.internal.exception.HeosFunctionalException;
import org.openhab.binding.heos.internal.json.dto.HeosErrorCode;
import org.openhab.binding.heos.internal.json.dto.HeosEventObject;
import org.openhab.binding.heos.internal.json.dto.HeosResponseObject;
import org.openhab.binding.heos.internal.json.payload.Media;
import org.openhab.binding.heos.internal.json.payload.Player;
import org.openhab.binding.heos.internal.resources.Telnet.ReadException;
import org.openhab.core.library.types.PercentType;
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.types.Command;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The {@link HeosPlayerHandler} handles the actions for a HEOS player.
* Channel commands are received and send to the dedicated channels
*
* @author Johannes Einig - Initial contribution
*/
@NonNullByDefault
public class HeosPlayerHandler extends HeosThingBaseHandler {
private final Logger logger = LoggerFactory.getLogger(HeosPlayerHandler.class);
private @NonNullByDefault({}) String pid;
private @Nullable Future<?> scheduledFuture;
public HeosPlayerHandler(Thing thing, HeosDynamicStateDescriptionProvider heosDynamicStateDescriptionProvider) {
super(thing, heosDynamicStateDescriptionProvider);
}
@Override
public void handleCommand(ChannelUID channelUID, Command command) {
@Nullable
HeosChannelHandler channelHandler = getHeosChannelHandler(channelUID);
if (channelHandler != null) {
try {
channelHandler.handlePlayerCommand(command, getId(), thing.getUID());
handleSuccess();
} catch (IOException | ReadException e) {
handleError(e);
}
}
}
@Override
public void initialize() {
super.initialize();
PlayerConfiguration configuration = thing.getConfiguration().as(PlayerConfiguration.class);
pid = configuration.pid;
cancel(scheduledFuture);
scheduledFuture = scheduler.submit(this::delayedInitialize);
}
private synchronized void delayedInitialize() {
try {
refreshPlayState(pid);
handleThingStateUpdate(getApiConnection().getPlayerInfo(pid));
updateStatus(ThingStatus.ONLINE);
} catch (HeosFunctionalException e) {
if (e.getCode() == HeosErrorCode.INVALID_ID) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.GONE, e.getCode().toString());
} else {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getCode().toString());
}
} catch (IOException | ReadException e) {
logger.debug("Failed to initialize, will try again", e);
cancel(scheduledFuture, false);
scheduledFuture = scheduler.schedule(this::delayedInitialize, 3, TimeUnit.SECONDS);
}
}
@Override
void refreshPlayState(String id) throws IOException, ReadException {
super.refreshPlayState(id);
handleThingStateUpdate(getApiConnection().getPlayerMuteState(id));
handleThingStateUpdate(getApiConnection().getPlayerVolume(id));
}
@Override
public void dispose() {
cancel(scheduledFuture);
super.dispose();
}
@Override
public String getId() {
return pid;
}
@Override
public void setNotificationSoundVolume(PercentType volume) {
}
@Override
public void playerStateChangeEvent(HeosEventObject eventObject) {
if (!pid.equals(eventObject.getAttribute(PLAYER_ID))) {
return;
}
if (GROUP_VOLUME_CHANGED == eventObject.command) {
logger.debug("Ignoring group-volume changes for players");
return;
}
handleThingStateUpdate(eventObject);
}
@Override
public void playerStateChangeEvent(HeosResponseObject<?> responseObject) throws HeosFunctionalException {
if (!pid.equals(responseObject.getAttribute(PLAYER_ID))) {
return;
}
handleThingStateUpdate(responseObject);
}
@Override
public void playerMediaChangeEvent(String eventPid, Media media) {
if (!pid.equals(eventPid)) {
return;
}
handleThingMediaUpdate(media);
}
@Override
public void setStatusOffline() {
updateStatus(ThingStatus.OFFLINE);
}
@Override
public void setStatusOnline() {
updateStatus(ThingStatus.ONLINE);
}
public static void propertiesFromPlayer(Map<String, ? super String> prop, Player player) {
prop.put(PROP_NAME, player.name);
prop.put(PROP_PID, String.valueOf(player.playerId));
prop.put(Thing.PROPERTY_MODEL_ID, player.model);
prop.put(Thing.PROPERTY_FIRMWARE_VERSION, player.version);
prop.put(PROP_NETWORK, player.network);
prop.put(PROP_IP, player.ip);
@Nullable
String serialNumber = player.serial;
if (serialNumber != null) {
prop.put(Thing.PROPERTY_SERIAL_NUMBER, serialNumber);
}
}
}

View File

@@ -0,0 +1,516 @@
/**
* 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.heos.internal.handler;
import static org.openhab.binding.heos.internal.HeosBindingConstants.*;
import static org.openhab.binding.heos.internal.handler.FutureUtil.cancel;
import static org.openhab.binding.heos.internal.json.dto.HeosCommandGroup.GROUP;
import static org.openhab.binding.heos.internal.json.dto.HeosCommandGroup.PLAYER;
import static org.openhab.binding.heos.internal.json.dto.HeosCommunicationAttribute.*;
import static org.openhab.core.thing.ThingStatus.*;
import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import javax.measure.quantity.Time;
import org.apache.commons.lang.StringUtils;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.heos.internal.HeosChannelHandlerFactory;
import org.openhab.binding.heos.internal.api.HeosFacade;
import org.openhab.binding.heos.internal.exception.HeosFunctionalException;
import org.openhab.binding.heos.internal.exception.HeosNotConnectedException;
import org.openhab.binding.heos.internal.exception.HeosNotFoundException;
import org.openhab.binding.heos.internal.json.dto.*;
import org.openhab.binding.heos.internal.json.payload.Media;
import org.openhab.binding.heos.internal.json.payload.Player;
import org.openhab.binding.heos.internal.resources.HeosEventListener;
import org.openhab.binding.heos.internal.resources.Telnet.ReadException;
import org.openhab.core.io.net.http.HttpUtil;
import org.openhab.core.library.types.*;
import org.openhab.core.library.unit.SmartHomeUnits;
import org.openhab.core.thing.*;
import org.openhab.core.thing.binding.BaseThingHandler;
import org.openhab.core.types.UnDefType;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The {@link HeosThingBaseHandler} class is the base Class all HEOS handler have to extend.
* It provides basic command handling and common needed methods.
*
* @author Johannes Einig - Initial contribution
*/
@NonNullByDefault
public abstract class HeosThingBaseHandler extends BaseThingHandler implements HeosEventListener {
private final Logger logger = LoggerFactory.getLogger(HeosThingBaseHandler.class);
private final HeosDynamicStateDescriptionProvider heosDynamicStateDescriptionProvider;
private final ChannelUID favoritesChannelUID;
private final ChannelUID playlistsChannelUID;
private final ChannelUID queueChannelUID;
private @Nullable HeosChannelHandlerFactory channelHandlerFactory;
protected @Nullable HeosBridgeHandler bridgeHandler;
private String notificationVolume = "0";
private int failureCount;
private @Nullable Future<?> scheduleQueueFetchFuture;
private @Nullable Future<?> handleDynamicStatesFuture;
HeosThingBaseHandler(Thing thing, HeosDynamicStateDescriptionProvider heosDynamicStateDescriptionProvider) {
super(thing);
this.heosDynamicStateDescriptionProvider = heosDynamicStateDescriptionProvider;
favoritesChannelUID = new ChannelUID(thing.getUID(), CH_ID_FAVORITES);
playlistsChannelUID = new ChannelUID(thing.getUID(), CH_ID_PLAYLISTS);
queueChannelUID = new ChannelUID(thing.getUID(), CH_ID_QUEUE);
}
@Override
public void initialize() {
@Nullable
Bridge bridge = getBridge();
@Nullable
HeosBridgeHandler localBridgeHandler;
if (bridge != null) {
localBridgeHandler = (HeosBridgeHandler) bridge.getHandler();
if (localBridgeHandler != null) {
bridgeHandler = localBridgeHandler;
channelHandlerFactory = localBridgeHandler.getChannelHandlerFactory();
} else {
updateStatus(OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE);
return;
}
} else {
logger.warn("No Bridge set within child handler");
updateStatus(OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE);
return;
}
try {
getApiConnection().registerForChangeEvents(this);
cancel(scheduleQueueFetchFuture);
scheduleQueueFetchFuture = scheduler.submit(this::fetchQueueFromPlayer);
if (localBridgeHandler.isLoggedIn()) {
scheduleImmediatelyHandleDynamicStatesSignedIn();
}
} catch (HeosNotConnectedException e) {
updateStatus(OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR);
}
}
void handleSuccess() {
failureCount = 0;
updateStatus(ONLINE);
}
void handleError(Exception e) {
logger.debug("Failed to handle player/group command", e);
failureCount++;
if (failureCount > FAILURE_COUNT_LIMIT) {
updateStatus(OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, "Failed to handle command: " + e.getMessage());
}
}
public HeosFacade getApiConnection() throws HeosNotConnectedException {
@Nullable
HeosBridgeHandler localBridge = bridgeHandler;
if (localBridge != null) {
return localBridge.getApiConnection();
}
throw new HeosNotConnectedException();
}
public abstract String getId() throws HeosNotFoundException;
public abstract void setStatusOffline();
public abstract void setStatusOnline();
public PercentType getNotificationSoundVolume() {
return PercentType.valueOf(notificationVolume);
}
public void setNotificationSoundVolume(PercentType volume) {
notificationVolume = volume.toString();
}
@Nullable
HeosChannelHandler getHeosChannelHandler(ChannelUID channelUID) {
@Nullable
HeosChannelHandlerFactory localChannelHandlerFactory = this.channelHandlerFactory;
return localChannelHandlerFactory != null ? localChannelHandlerFactory.getChannelHandler(channelUID, this, null)
: null;
}
@Override
public void bridgeChangeEvent(String event, boolean success, Object command) {
logger.debug("BridgeChangeEvent: {}", command);
if (HeosEvent.USER_CHANGED == command) {
handleDynamicStatesSignedIn();
}
if (EVENT_TYPE_EVENT.equals(event)) {
if (HeosEvent.GROUPS_CHANGED == command) {
fetchQueueFromPlayer();
} else if (CONNECTION_RESTORED.equals(command)) {
try {
refreshPlayState(getId());
} catch (IOException | ReadException e) {
logger.debug("Failed to refreshPlayState", e);
}
}
}
}
void scheduleImmediatelyHandleDynamicStatesSignedIn() {
cancel(handleDynamicStatesFuture);
handleDynamicStatesFuture = scheduler.submit(this::handleDynamicStatesSignedIn);
}
void handleDynamicStatesSignedIn() {
try {
heosDynamicStateDescriptionProvider.setFavorites(favoritesChannelUID, getApiConnection().getFavorites());
heosDynamicStateDescriptionProvider.setPlaylists(playlistsChannelUID, getApiConnection().getPlaylists());
} catch (IOException | ReadException e) {
logger.debug("Failed to set favorites / playlists, rescheduling", e);
cancel(handleDynamicStatesFuture, false);
handleDynamicStatesFuture = scheduler.schedule(this::handleDynamicStatesSignedIn, 30, TimeUnit.SECONDS);
}
}
@Override
public void bridgeStatusChanged(ThingStatusInfo bridgeStatusInfo) {
if (ThingStatus.OFFLINE.equals(bridgeStatusInfo.getStatus())) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE);
} else if (ThingStatus.ONLINE.equals(bridgeStatusInfo.getStatus())) {
updateStatus(ThingStatus.ONLINE);
} else if (ThingStatus.UNINITIALIZED.equals(bridgeStatusInfo.getStatus())) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_UNINITIALIZED);
}
}
/**
* Dispose the handler and unregister the handler
* form Change Events
*/
@Override
public void dispose() {
try {
logger.debug("Disposing this: {}", this);
getApiConnection().unregisterForChangeEvents(this);
} catch (HeosNotConnectedException e) {
logger.trace("No connection available while trying to unregister");
}
cancel(scheduleQueueFetchFuture);
cancel(handleDynamicStatesFuture);
}
/**
* Plays a media file from an external source. Can be
* used for audio sink function
*
* @param urlStr The external URL where the file is located
* @throws ReadException
* @throws IOException
*/
public void playURL(String urlStr) throws IOException, ReadException {
try {
URL url = new URL(urlStr);
getApiConnection().playURL(getId(), url);
} catch (MalformedURLException e) {
logger.debug("Command '{}' is not a proper URL. Error: {}", urlStr, e.getMessage());
}
}
/**
* Handles the updates send from the HEOS system to
* the binding. To receive updates the handler has
* to register itself via {@link HeosFacade} via the method:
* {@link HeosFacade#registerForChangeEvents(HeosEventListener)}
*
* @param eventObject containing information about the even which was sent to us by the HEOS device
*/
protected void handleThingStateUpdate(HeosEventObject eventObject) {
updateStatus(ONLINE, ThingStatusDetail.NONE, "Receiving events");
@Nullable
HeosEvent command = eventObject.command;
if (command == null) {
logger.debug("Ignoring event with null command");
return;
}
switch (command) {
case PLAYER_STATE_CHANGED:
playerStateChanged(eventObject);
break;
case PLAYER_VOLUME_CHANGED:
case GROUP_VOLUME_CHANGED:
@Nullable
String level = eventObject.getAttribute(LEVEL);
if (level != null) {
notificationVolume = level;
updateState(CH_ID_VOLUME, PercentType.valueOf(level));
updateState(CH_ID_MUTE, OnOffType.from(eventObject.getBooleanAttribute(MUTE)));
}
break;
case SHUFFLE_MODE_CHANGED:
handleShuffleMode(eventObject);
break;
case PLAYER_NOW_PLAYING_PROGRESS:
@Nullable
Long position = eventObject.getNumericAttribute(CURRENT_POSITION);
@Nullable
Long duration = eventObject.getNumericAttribute(DURATION);
if (position != null && duration != null) {
updateState(CH_ID_CUR_POS, quantityFromMilliSeconds(position));
updateState(CH_ID_DURATION, quantityFromMilliSeconds(duration));
}
break;
case REPEAT_MODE_CHANGED:
handleRepeatMode(eventObject);
break;
case PLAYER_PLAYBACK_ERROR:
updateStatus(UNKNOWN, ThingStatusDetail.NONE, eventObject.getAttribute(ERROR));
break;
case PLAYER_QUEUE_CHANGED:
fetchQueueFromPlayer();
break;
case SOURCES_CHANGED:
// we are not yet handling the actual sources, although we might want to do that in the future
logger.trace("Ignoring {}, support might be added in the future", command);
break;
case GROUPS_CHANGED:
case PLAYERS_CHANGED:
case PLAYER_NOW_PLAYING_CHANGED:
case USER_CHANGED:
logger.trace("Ignoring {}, will be handled inside HeosEventController", command);
break;
}
}
private QuantityType<Time> quantityFromMilliSeconds(long position) {
return new QuantityType<>(position / 1000, SmartHomeUnits.SECOND);
}
private void handleShuffleMode(HeosObject eventObject) {
updateState(CH_ID_SHUFFLE_MODE,
OnOffType.from(eventObject.getBooleanAttribute(HeosCommunicationAttribute.SHUFFLE)));
}
void refreshPlayState(String id) throws IOException, ReadException {
handleThingStateUpdate(getApiConnection().getPlayMode(id));
handleThingStateUpdate(getApiConnection().getPlayState(id));
handleThingStateUpdate(getApiConnection().getNowPlayingMedia(id));
}
protected <T> void handleThingStateUpdate(HeosResponseObject<T> responseObject) throws HeosFunctionalException {
handleResponseError(responseObject);
@Nullable
HeosCommandTuple cmd = responseObject.heosCommand;
if (cmd == null) {
logger.debug("Ignoring response with null command");
return;
}
if (cmd.commandGroup == PLAYER || cmd.commandGroup == GROUP) {
switch (cmd.command) {
case GET_PLAY_STATE:
playerStateChanged(responseObject);
break;
case GET_MUTE:
updateState(CH_ID_MUTE, OnOffType.from(responseObject.getBooleanAttribute(MUTE)));
break;
case GET_VOLUME:
@Nullable
String level = responseObject.getAttribute(LEVEL);
if (level != null) {
notificationVolume = level;
updateState(CH_ID_VOLUME, PercentType.valueOf(level));
}
break;
case GET_PLAY_MODE:
handleRepeatMode(responseObject);
handleShuffleMode(responseObject);
break;
case GET_NOW_PLAYING_MEDIA:
@Nullable
T mediaPayload = responseObject.payload;
if (mediaPayload instanceof Media) {
handleThingMediaUpdate((Media) mediaPayload);
}
break;
case GET_PLAYER_INFO:
@Nullable
T playerPayload = responseObject.payload;
if (playerPayload instanceof Player) {
handlePlayerInfo((Player) playerPayload);
}
break;
}
}
}
private <T> void handleResponseError(HeosResponseObject<T> responseObject) throws HeosFunctionalException {
@Nullable
HeosError error = responseObject.getError();
if (error != null) {
throw new HeosFunctionalException(error.code);
}
}
private void handleRepeatMode(HeosObject eventObject) {
@Nullable
String repeatMode = eventObject.getAttribute(REPEAT);
if (repeatMode == null) {
updateState(CH_ID_REPEAT_MODE, UnDefType.NULL);
return;
}
switch (repeatMode) {
case REPEAT_ALL:
updateState(CH_ID_REPEAT_MODE, StringType.valueOf(HEOS_UI_ALL));
break;
case REPEAT_ONE:
updateState(CH_ID_REPEAT_MODE, StringType.valueOf(HEOS_UI_ONE));
break;
case OFF:
updateState(CH_ID_REPEAT_MODE, StringType.valueOf(HEOS_UI_OFF));
break;
}
}
private void playerStateChanged(HeosObject eventObject) {
@Nullable
String attribute = eventObject.getAttribute(STATE);
if (attribute == null) {
updateState(CH_ID_CONTROL, UnDefType.NULL);
return;
}
switch (attribute) {
case PLAY:
updateState(CH_ID_CONTROL, PlayPauseType.PLAY);
break;
case PAUSE:
case STOP:
updateState(CH_ID_CONTROL, PlayPauseType.PAUSE);
break;
}
}
private synchronized void fetchQueueFromPlayer() {
try {
List<Media> queue = getApiConnection().getQueue(getId());
heosDynamicStateDescriptionProvider.setQueue(queueChannelUID, queue);
return;
} catch (HeosNotFoundException e) {
logger.debug("HEOS player/group is not found, rescheduling");
} catch (IOException | ReadException e) {
logger.debug("Failed to set queue, rescheduling", e);
}
cancel(scheduleQueueFetchFuture, false);
scheduleQueueFetchFuture = scheduler.schedule(this::fetchQueueFromPlayer, 30, TimeUnit.SECONDS);
}
protected void handleThingMediaUpdate(Media info) {
logger.debug("Received updated media state: {}", info);
updateState(CH_ID_SONG, StringType.valueOf(info.song));
updateState(CH_ID_ARTIST, StringType.valueOf(info.artist));
updateState(CH_ID_ALBUM, StringType.valueOf(info.album));
if (SONG.equals(info.type)) {
updateState(CH_ID_QUEUE, StringType.valueOf(String.valueOf(info.queueId)));
updateState(CH_ID_FAVORITES, UnDefType.UNDEF);
} else if (STATION.equals(info.type)) {
updateState(CH_ID_QUEUE, UnDefType.UNDEF);
updateState(CH_ID_FAVORITES, StringType.valueOf(info.albumId));
} else {
updateState(CH_ID_QUEUE, UnDefType.UNDEF);
updateState(CH_ID_FAVORITES, UnDefType.UNDEF);
}
handleImageUrl(info);
handleStation(info);
handleSourceId(info);
}
private void handleImageUrl(Media info) {
if (StringUtils.isNotBlank(info.imageUrl)) {
try {
URL url = new URL(info.imageUrl); // checks if String is proper URL
RawType cover = HttpUtil.downloadImage(url.toString());
if (cover != null) {
updateState(CH_ID_COVER, cover);
return;
}
} catch (MalformedURLException e) {
logger.debug("Cover can't be loaded. No proper URL: {}", info.imageUrl, e);
}
}
updateState(CH_ID_COVER, UnDefType.NULL);
}
private void handleStation(Media info) {
if (STATION.equals(info.type)) {
updateState(CH_ID_STATION, StringType.valueOf(info.station));
} else {
updateState(CH_ID_STATION, UnDefType.UNDEF);
}
}
private void handleSourceId(Media info) {
if (info.sourceId == INPUT_SID && info.mediaId != null) {
String inputName = info.mediaId.substring(info.mediaId.indexOf("/") + 1);
updateState(CH_ID_INPUTS, StringType.valueOf(inputName));
updateState(CH_ID_TYPE, StringType.valueOf(info.station));
} else {
updateState(CH_ID_TYPE, StringType.valueOf(info.type));
updateState(CH_ID_INPUTS, UnDefType.UNDEF);
}
}
private void handlePlayerInfo(Player player) {
Map<String, String> prop = new HashMap<>();
HeosPlayerHandler.propertiesFromPlayer(prop, player);
updateProperties(prop);
}
}

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.heos.internal.json;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
/**
* Object used for the initial JSON parsing of the result
*
* @author Martin van Wingerden - Initial contribution
*/
@NonNullByDefault
class HeosJsonObject {
String command = "";
@Nullable
String result;
@Nullable
String message;
}

View File

@@ -0,0 +1,98 @@
/**
* 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.heos.internal.json;
import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.Collections;
import java.util.Map;
import java.util.stream.Collectors;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.heos.internal.json.dto.HeosCommandTuple;
import org.openhab.binding.heos.internal.json.dto.HeosEvent;
import org.openhab.binding.heos.internal.json.dto.HeosEventObject;
import org.openhab.binding.heos.internal.json.dto.HeosResponseObject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.gson.FieldNamingPolicy;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
/**
* Parser used for parsing the responses of JSON cli
*
* @author Martin van Wingerden - Initial contribution
*/
@NonNullByDefault
public class HeosJsonParser {
private final Logger logger = LoggerFactory.getLogger(HeosJsonParser.class);
private final Gson gson = new GsonBuilder().setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES)
.create();
public HeosEventObject parseEvent(String jsonBody) {
HeosJsonWrapper wrapper = gson.fromJson(jsonBody, HeosJsonWrapper.class);
return postProcess(wrapper.heos);
}
private HeosEventObject postProcess(HeosJsonObject heos) {
return new HeosEventObject(HeosEvent.valueOfString(heos.command), heos.command, splitQuery(heos.message));
}
public <T> HeosResponseObject<T> parseResponse(String jsonBody, Class<T> clazz) {
HeosJsonWrapper wrapper = gson.fromJson(jsonBody, HeosJsonWrapper.class);
return postProcess(wrapper, clazz);
}
private <T> HeosResponseObject<T> postProcess(HeosJsonWrapper wrapper, Class<T> clazz) {
T payload = gson.fromJson(wrapper.payload, clazz);
return new HeosResponseObject<>(HeosCommandTuple.valueOf(wrapper.heos.command), wrapper.heos.command,
wrapper.heos.result, splitQuery(wrapper.heos.message), payload, wrapper.options);
}
private Map<String, String> splitQuery(@Nullable String url) {
if (url == null) {
return Collections.emptyMap();
}
return Arrays.stream(url.split("&")).map(p -> p.split("=", 2))
.collect(Collectors.toMap(v -> decode(v[0]), v -> v.length == 1 ? "" : decode(v[1]), this::merge));
}
/**
* for duplicates we ignore the first one
*
* @param v1 first occurrence
* @param v2 second occurrence
* @return second occurrence
*/
private String merge(String v1, String v2) {
logger.debug("Ignoring first occurrence '{}' in favor of '{}'", v1, v2);
return v2;
}
private static String decode(String encoded) {
try {
return URLDecoder.decode(encoded, StandardCharsets.UTF_8.name());
} catch (UnsupportedEncodingException e) {
throw new IllegalStateException("Impossible: UTF-8 is a required encoding", e);
}
}
}

View File

@@ -0,0 +1,35 @@
/**
* 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.heos.internal.json;
import java.util.List;
import java.util.Map;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import com.google.gson.JsonElement;
/**
* Wrapper used around HeosJsonObject used for the initial JSON parsing of the result
*
* @author Martin van Wingerden - Initial contribution
*/
@NonNullByDefault
class HeosJsonWrapper {
HeosJsonObject heos = new HeosJsonObject();
@Nullable
JsonElement payload;
@Nullable
List<Map<String, List<HeosOption>>> options;
}

View File

@@ -0,0 +1,31 @@
/**
* 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.heos.internal.json;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import com.google.gson.annotations.SerializedName;
/**
* Object used for the initial JSON parsing of the result
*
* @author Martin van Wingerden - Initial contribution
*/
@NonNullByDefault
public class HeosOption {
public int id;
@SerializedName("scid")
public @Nullable Integer criteriaId;
public @Nullable String name;
}

View File

@@ -0,0 +1,82 @@
/**
* 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.heos.internal.json.dto;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* Enum for the HEOS commands
*
* @author Martin van Wingerden - Initial contribution
*/
@NonNullByDefault
public enum HeosCommand {
BROWSE,
CHECK_ACCOUNT,
CHECK_UPDATE,
CLEAR_QUEUE,
DELETE_PLAYLIST,
GET_GROUPS,
GET_GROUP_INFO,
GET_MUSIC_SOURCES,
GET_NOW_PLAYING_MEDIA,
GET_PLAYERS,
GET_PLAYER_INFO,
GET_PLAY_MODE,
GET_PLAY_STATE,
GET_SEARCH_CRITERIA,
GET_SOURCE_INFO,
GET_VOLUME,
HEART_BEAT,
PLAY_INPUT,
PLAY_NEXT,
PLAY_PRESET,
PLAY_PREVIOUS,
ADD_TO_QUEUE,
GET_QUEUE,
MOVE_QUEUE_ITEM,
PLAY_QUEUE,
SAVE_QUEUE,
GET_QUICKSELECTS,
PLAY_QUICKSELECT,
SET_QUICKSELECT,
PLAY_STREAM,
PRETTIFY_JSON_RESPONSE,
REGISTER_FOR_CHANGE_EVENTS,
REMOVE_FROM_QUEUE,
RENAME_PLAYLIST,
RETRIEVE_METADATA,
SEARCH,
SET_GROUP,
SET_MUTE,
GET_MUTE,
TOGGLE_MUTE,
SET_PLAY_MODE,
SET_PLAY_STATE,
SIGN_IN,
SIGN_OUT,
SET_VOLUME,
VOLUME_DOWN,
VOLUME_UP;
@Override
public String toString() {
return name().toLowerCase();
}
}

View File

@@ -0,0 +1,33 @@
/**
* 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.heos.internal.json.dto;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* enum for the HEOS command groups, they appear as the first parts of the command
*
* @author Martin van Wingerden - Initial contribution
*/
@NonNullByDefault
public enum HeosCommandGroup {
BROWSE,
PLAYER,
GROUP,
SYSTEM;
@Override
public String toString() {
return name().toLowerCase();
}
}

View File

@@ -0,0 +1,60 @@
/**
* 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.heos.internal.json.dto;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Tuple to contain a command group and command enum, this represents the full command send to / received by the HEOS
* cli
*
* @author Martin van Wingerden - Initial contribution
*/
@NonNullByDefault
public class HeosCommandTuple {
private static final Logger LOGGER = LoggerFactory.getLogger(HeosCommandTuple.class);
public final HeosCommandGroup commandGroup;
public final HeosCommand command;
public HeosCommandTuple(HeosCommandGroup commandGroup, HeosCommand command) {
this.commandGroup = commandGroup;
this.command = command;
}
@Nullable
public static HeosCommandTuple valueOf(String commandString) {
String[] split = commandString.split("/");
if (split.length != 2) {
return null;
}
try {
HeosCommandGroup group = HeosCommandGroup.valueOf(split[0].toUpperCase());
HeosCommand cmd = HeosCommand.valueOf(split[1].toUpperCase());
return new HeosCommandTuple(group, cmd);
} catch (IllegalArgumentException e) {
LOGGER.debug("Unsupported command {}", commandString);
return null;
}
}
@Override
public String toString() {
return commandGroup + "/" + command;
}
}

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.heos.internal.json.dto;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* Enum to reference the attributes of the HEOS response
*
* @author Martin van Wingerden - Initial contribution
*/
@NonNullByDefault
public enum HeosCommunicationAttribute {
COMMAND_UNDER_PROCESS("command under process"),
COUNT("count"),
CURRENT_POSITION("cur_pos"),
DURATION("duration"),
ERROR_ID("eid"),
GROUP_ID("gid"),
LEVEL("level"),
MUTE("mute"),
PLAYER_ID("pid"),
REPEAT("repeat"),
RETURNED("returned"),
SHUFFLE("shuffle"),
SIGNED_IN("signed_in"),
SOURCE_ID("sid"),
STATE("state"),
SYSTEM_ERROR_NUMBER("syserrno"),
USERNAME("un"),
ERROR("error"),
TEXT("text");
private final String label;
HeosCommunicationAttribute(String label) {
this.label = label;
}
public String getLabel() {
return label;
}
}

View File

@@ -0,0 +1,40 @@
/**
* 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.heos.internal.json.dto;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
/**
* Error object for containing information about HEOS errors
*
* @author Martin van Wingerden - Initial contribution
*/
@NonNullByDefault
public class HeosError {
public final HeosErrorCode code;
private final @Nullable Long systemErrorNumber;
HeosError(@Nullable Long errorCode, @Nullable Long systemErrorNumber) {
if (errorCode == null) {
throw new IllegalArgumentException("Error code not given");
}
this.code = HeosErrorCode.of(errorCode);
this.systemErrorNumber = systemErrorNumber;
}
@Override
public String toString() {
return "HeosError{" + "code=" + code + ", systemErrorNumber=" + systemErrorNumber + '}';
}
}

View File

@@ -0,0 +1,61 @@
/**
* 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.heos.internal.json.dto;
import java.util.stream.Stream;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* Enum for the different documented error for HEOS responses
*
* @author Martin van Wingerden - Initial contribution
*/
@NonNullByDefault
public enum HeosErrorCode {
UNRECOGNIZED_COMMAND(1, "Unrecognized Command"),
INVALID_ID(2, "Invalid ID"),
WRONG_NUMBER_OF_COMMAND_ARGUMENTS(3, "Wrong Number of Command Arguments"),
REQUESTED_DATA_NOT_AVAILABLE(4, "Requested data not available"),
RESOURCE_CURRENTLY_NOT_AVAILABLE(5, "Resource currently not available"),
INVALID_CREDENTIALS(6, "Invalid Credentials"),
COMMAND_COULD_NOT_BE_EXECUTED(7, "Command Could Not Be Executed"),
USER_NOT_LOGGED_IN(8, "User not logged In"),
PARAMETER_OUT_OF_RANGE(9, "Parameter out of range"),
USER_NOT_FOUND(10, "User not found"),
INTERNAL_ERROR(11, "Internal Error"),
SYSTEM_ERROR(12, "System Error"),
PROCESSING_PREVIOUS_COMMAND(13, "Processing Previous Command"),
MEDIA_CANT_BE_PLAYED(14, "Media can't be played"),
OPTION_NO_SUPPORTED(15, "Option no supported"),
TOO_MANY_COMMANDS_IN_MESSAGE_QUEUE_TO_PROCESS(16, "Too many commands in message queue to process"),
REACHED_SKIP_LIMIT(17, "Reached skip limit");
private final int errorNumber;
private final String msg;
HeosErrorCode(int errorNumber, String msg) {
this.errorNumber = errorNumber;
this.msg = msg;
}
@Override
public String toString() {
return String.format("#%d: %s", errorNumber, msg);
}
public static HeosErrorCode of(long errorNumber) {
return Stream.of(values()).filter(v -> errorNumber == v.errorNumber).findAny()
.orElseThrow(() -> new IllegalArgumentException("An unknown error " + errorNumber + " occurred"));
}
}

View File

@@ -0,0 +1,56 @@
/**
* 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.heos.internal.json.dto;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Enum to reference the different HEOS events
*
* @author Martin van Wingerden - Initial contribution
*/
@NonNullByDefault
public enum HeosEvent {
SOURCES_CHANGED,
PLAYERS_CHANGED,
GROUPS_CHANGED,
PLAYER_STATE_CHANGED,
PLAYER_NOW_PLAYING_CHANGED,
PLAYER_NOW_PLAYING_PROGRESS,
PLAYER_PLAYBACK_ERROR,
PLAYER_QUEUE_CHANGED,
PLAYER_VOLUME_CHANGED,
REPEAT_MODE_CHANGED,
SHUFFLE_MODE_CHANGED,
GROUP_VOLUME_CHANGED,
USER_CHANGED;
private static final Logger LOGGER = LoggerFactory.getLogger(HeosEvent.class);
@Nullable
public static HeosEvent valueOfString(@Nullable String eventCommand) {
if (eventCommand == null) {
return null;
}
try {
String command = eventCommand.substring(6);
return HeosEvent.valueOf(command.toUpperCase());
} catch (IllegalArgumentException e) {
LOGGER.debug("Unsupported event {}", eventCommand);
return null;
}
}
}

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.heos.internal.json.dto;
import java.util.Map;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
/**
* Class for HEOS event objects
*
* @author Martin van Wingerden - Initial contribution
*/
@NonNullByDefault
public class HeosEventObject extends HeosObject {
public final @Nullable HeosEvent command;
public HeosEventObject(@Nullable HeosEvent command, String rawCommand, Map<String, String> attributes) {
super(rawCommand, attributes);
this.command = command;
}
@Override
public String toString() {
return "HeosEventObject{" + super.toString() + ", command=" + command + '}';
}
}

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.heos.internal.json.dto;
import java.util.Map;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Abstract parent class for the HEOS event/response objects
*
* @author Martin van Wingerden - Initial contribution
*/
@NonNullByDefault
public abstract class HeosObject {
private final Logger logger = LoggerFactory.getLogger(HeosObject.class);
public final String rawCommand;
private final Map<String, String> attributes;
HeosObject(String rawCommand, Map<String, String> attributes) {
this.rawCommand = rawCommand;
this.attributes = attributes;
}
public boolean getBooleanAttribute(HeosCommunicationAttribute attributeName) {
return "on".equals(attributes.get(attributeName.getLabel()));
}
public @Nullable Long getNumericAttribute(HeosCommunicationAttribute attributeName) {
@Nullable
String attribute = attributes.get(attributeName.getLabel());
if (attribute == null) {
return null;
}
try {
return Long.valueOf(attribute);
} catch (NumberFormatException e) {
logger.debug("Failed to parse number: {}, message: {}", attribute, e.getMessage());
return null;
}
}
public @Nullable String getAttribute(HeosCommunicationAttribute attributeName) {
return attributes.get(attributeName.getLabel());
}
public boolean hasAttribute(HeosCommunicationAttribute attribute) {
return attributes.containsKey(attribute.getLabel());
}
@Override
public String toString() {
return "HeosObject{" + "rawCommand='" + rawCommand + '\'' + ", attributes=" + attributes + '}';
}
}

View File

@@ -0,0 +1,69 @@
/**
* 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.heos.internal.json.dto;
import static org.openhab.binding.heos.internal.json.dto.HeosCommunicationAttribute.*;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.heos.internal.json.HeosOption;
/**
* Class for HEOS response objects
*
* @author Martin van Wingerden - Initial contribution
*/
@NonNullByDefault
public class HeosResponseObject<T> extends HeosObject {
public final @Nullable HeosCommandTuple heosCommand;
public final boolean result;
public final @Nullable T payload;
public final Map<String, HeosOption> options;
public HeosResponseObject(@Nullable HeosCommandTuple heosCommand, String rawCommand, @Nullable String result,
Map<String, String> attributes, @Nullable T payload,
@Nullable List<Map<String, List<HeosOption>>> options) {
super(rawCommand, attributes);
this.heosCommand = heosCommand;
this.result = "success".equals(result);
this.payload = payload;
this.options = processOptions(options);
}
private Map<String, HeosOption> processOptions(@Nullable List<Map<String, List<HeosOption>>> options) {
if (options == null) {
return Collections.emptyMap();
}
return options.stream().map(Map::entrySet).flatMap(Set::stream)
.collect(Collectors.toMap(Map.Entry::getKey, e -> e.getValue().get(0)));
}
public boolean isFinished() {
return (result || hasAttribute(ERROR_ID)) && !hasAttribute(COMMAND_UNDER_PROCESS);
}
public @Nullable HeosError getError() {
if (result || !hasAttribute(ERROR_ID)) {
return null;
}
return new HeosError(getNumericAttribute(ERROR_ID), getNumericAttribute(SYSTEM_ERROR_NUMBER));
}
}

View File

@@ -0,0 +1,44 @@
/**
* 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.heos.internal.json.payload;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import com.google.gson.annotations.SerializedName;
/**
* Data class for response payloads from browse commands
*
* @author Martin van Wingerden - Initial contribution
*/
@NonNullByDefault
public class BrowseResult {
public @Nullable YesNoEnum container;
@SerializedName("mid")
public @Nullable String mediaId;
public @Nullable YesNoEnum playable;
public @Nullable BrowseResultType type;
@SerializedName("cid")
public @Nullable String containerId;
public @Nullable String name;
@SerializedName("image_url")
public @Nullable String imageUrl;
@Override
public String toString() {
return "BrowseResult{" + "container=" + container + ", mediaId='" + mediaId + '\'' + ", playable=" + playable
+ ", type=" + type + ", containerId='" + containerId + '\'' + ", name='" + name + '\'' + ", imageUrl='"
+ imageUrl + '\'' + '}';
}
}

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.heos.internal.json.payload;
import org.eclipse.jdt.annotation.NonNullByDefault;
import com.google.gson.annotations.SerializedName;
/**
* Enum for browse result types from the HEOS cli
*
* @author Martin van Wingerden - Initial contribution
*/
@NonNullByDefault
public enum BrowseResultType {
@SerializedName("artist")
ARTIST,
@SerializedName("album")
ALBUM,
@SerializedName("song")
SONG,
@SerializedName("container")
CONTAINER,
@SerializedName("station")
STATION,
@SerializedName("playlist")
PLAYLIST
}

View File

@@ -0,0 +1,50 @@
/**
* 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.heos.internal.json.payload;
import java.util.Collections;
import java.util.List;
import java.util.stream.Collectors;
import org.eclipse.jdt.annotation.NonNullByDefault;
import com.google.gson.annotations.SerializedName;
/**
* Data class for response payloads when retrieving group (information)
*
* @author Martin van Wingerden - Initial contribution
*/
@NonNullByDefault
public class Group {
@SerializedName("gid")
public String id = "";
public String name = "";
public List<Player> players = Collections.emptyList();
public String getGroupMemberIds() {
return players.stream().map(p -> p.id).collect(Collectors.joining(";"));
}
public String getLeaderId() {
return players.stream().filter(p -> p.role == GroupPlayerRole.LEADER).map(p -> p.id).findFirst()
.orElseThrow(() -> new IllegalStateException("Every group should have a leader"));
}
public static class Player {
@SerializedName("pid")
public String id = "";
public String name = "";
public GroupPlayerRole role = GroupPlayerRole.MEMBER;
}
}

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.heos.internal.json.payload;
import org.eclipse.jdt.annotation.NonNullByDefault;
import com.google.gson.annotations.SerializedName;
/**
* Enum for the roles that players have in a HEOS group
*
* @author Martin van Wingerden - Initial contribution
*/
@NonNullByDefault
public enum GroupPlayerRole {
@SerializedName("member")
MEMBER,
@SerializedName("leader")
LEADER,
}

View File

@@ -0,0 +1,52 @@
/**
* 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.heos.internal.json.payload;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import com.google.gson.annotations.SerializedName;
/**
* Data class for response payloads from now_playing commands
*
* @author Martin van Wingerden - Initial contribution
*/
@NonNullByDefault
public class Media {
public @Nullable String type;
public @Nullable String song;
public @Nullable String station;
public @Nullable String album;
public @Nullable String artist;
public @Nullable String imageUrl;
public @Nullable String albumId;
@SerializedName("mid")
public @Nullable String mediaId;
@SerializedName("qid")
public int queueId;
@SerializedName("sid")
public int sourceId;
public String combinedSongArtist() {
return String.format("%s - %s", artist, song);
}
@Override
public String toString() {
return "Media{" + "type='" + type + '\'' + ", song='" + song + '\'' + ", station='" + station + '\''
+ ", album='" + album + '\'' + ", artist='" + artist + '\'' + ", imageUrl='" + imageUrl + '\''
+ ", albumId='" + albumId + '\'' + ", mediaId='" + mediaId + '\'' + ", queueId=" + queueId
+ ", sourceId=" + sourceId + '}';
}
}

View File

@@ -0,0 +1,45 @@
/**
* 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.heos.internal.json.payload;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import com.google.gson.annotations.SerializedName;
/**
* Data class for response payloads when retrieving players
*
* @author Martin van Wingerden - Initial contribution
*/
@NonNullByDefault
public class Player {
public String name = "";
@SerializedName("pid")
public int playerId;
@SerializedName("gid")
public @Nullable Integer playerIdOfGroupLeader;
public String model = "";
public String version = "";
public String ip = "";
public String network = "";
public int lineout;
public @Nullable String serial;
@Override
public String toString() {
return "Player{" + "name='" + name + '\'' + ", playerId=" + playerId + ", playerIdOfGroupLeader="
+ playerIdOfGroupLeader + ", model='" + model + '\'' + ", version='" + version + '\'' + ", ip='" + ip
+ '\'' + ", network='" + network + '\'' + ", lineout=" + lineout + ", serial='" + serial + '\'' + '}';
}
}

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.heos.internal.json.payload;
import org.eclipse.jdt.annotation.NonNullByDefault;
import com.google.gson.annotations.SerializedName;
/**
* Enum containing a yes/no values in HEOS responses
*
* @author Martin van Wingerden - Initial contribution
*/
@NonNullByDefault
public enum YesNoEnum {
@SerializedName("yes")
YES,
@SerializedName("no")
NO
}

View File

@@ -0,0 +1,333 @@
/**
* 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.heos.internal.resources;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
/**
* The {@link HeosCommands} provides the available commands for the HEOS network.
*
* @author Johannes Einig - Initial contribution
*/
@NonNullByDefault
public class HeosCommands {
// System Commands
private static final String REGISTER_CHANGE_EVENT_ON = "heos://system/register_for_change_events?enable=on";
private static final String REGISTER_CHANGE_EVENT_OFF = "heos://system/register_for_change_events?enable=off";
private static final String HEOS_ACCOUNT_CHECK = "heos://system/check_account";
private static final String prettifyJSONon = "heos://system/prettify_json_response?enable=on";
private static final String prettifyJSONoff = "heos://system/prettify_json_response?enable=off";
private static final String rebootSystem = "heos://system/reboot";
private static final String signOut = "heos://system/sign_out";
private static final String heartbeat = "heos://system/heart_beat";
// Player Commands Control
private static final String setPlayStatePlay = "heos://player/set_play_state?pid=";
private static final String setPlayStatePause = "heos://player/set_play_state?pid=";
private static final String setPlayStateStop = "heos://player/set_play_state?pid=";
private static final String setVolume = "heos://player/set_volume?pid=";
private static final String volumeUp = "heos://player/volume_up?pid=";
private static final String volumeDown = "heos://player/volume_down?pid=";
private static final String setMute = "heos://player/set_mute?pid=";
private static final String setMuteToggle = "heos://player/toggle_mute?pid=";
private static final String playNext = "heos://player/play_next?pid=";
private static final String playPrevious = "heos://player/play_previous?pid=";
private static final String playQueueItem = "heos://player/play_queue?pid=";
private static final String clearQueue = "heos://player/clear_queue?pid=";
private static final String deleteQueueItem = "heos://player/remove_from_queue?pid=";
private static final String setPlayMode = "heos://player/set_play_mode?pid=";
// Group Commands Control
private static final String getGroups = "heos://group/get_groups";
private static final String getGroupsInfo = "heos://group/get_group_info?gid=";
private static final String setGroup = "heos://group/set_group?pid=";
private static final String getGroupVolume = "heos://group/get_volume?gid=";
private static final String setGroupVolume = "heos://group/set_volume?gid=";
private static final String getGroupMute = "heos://group/get_mute?gid=";
private static final String setGroupMute = "heos://group/set_mute?gid=";
private static final String toggleGroupMute = "heos://group/toggle_mute?gid=";
private static final String groupVolumeUp = "heos://group/volume_up?gid=";
private static final String groupVolumeDown = "heos://group/volume_down?gid=";
// Player Commands get Information
private static final String getPlayers = "heos://player/get_players";
private static final String getPlayerInfo = "heos://player/get_player_info?pid=";
private static final String getPlayState = "heos://player/get_play_state?pid=";
private static final String getNowPlayingMedia = "heos://player/get_now_playing_media?pid=";
private static final String playerGetVolume = "heos://player/get_volume?pid=";
private static final String playerGetMute = "heos://player/get_mute?pid=";
private static final String getQueue = "heos://player/get_queue?pid=";
private static final String getPlayMode = "heos://player/get_play_mode?pid=";
// Browse Commands
private static final String getMusicSources = "heos://browse/get_music_sources";
private static final String browseSource = "heos://browse/browse?sid=";
private static final String playStream = "heos://browse/play_stream?pid=";
private static final String addToQueue = "heos://browse/add_to_queue?pid=";
private static final String playInputSource = "heos://browse/play_input?pid=";
private static final String playURL = "heos://browse/play_stream?pid=";
public static String registerChangeEventOn() {
return REGISTER_CHANGE_EVENT_ON;
}
public static String registerChangeEventOff() {
return REGISTER_CHANGE_EVENT_OFF;
}
public static String heosAccountCheck() {
return HEOS_ACCOUNT_CHECK;
}
public static String setPlayStatePlay(String pid) {
return setPlayStatePlay + pid + "&state=play";
}
public static String setPlayStatePause(String pid) {
return setPlayStatePause + pid + "&state=pause";
}
public static String setPlayStateStop(String pid) {
return setPlayStateStop + pid + "&state=stop";
}
public static String volumeUp(String pid) {
return volumeUp + pid + "&step=1";
}
public static String volumeDown(String pid) {
return volumeDown + pid + "&step=1";
}
public static String setMuteOn(String pid) {
return setMute + pid + "&state=on";
}
public static String setMuteOff(String pid) {
return setMute + pid + "&state=off";
}
public static String setMuteToggle(String pid) {
return setMuteToggle + pid + "&state=off";
}
public static String setShuffleMode(String pid, String shuffle) {
return setPlayMode + pid + "&shuffle=" + shuffle;
}
public static String setRepeatMode(String pid, String repeat) {
return setPlayMode + pid + "&repeat=" + repeat;
}
public static String getPlayMode(String pid) {
return getPlayMode + pid;
}
public static String playNext(String pid) {
return playNext + pid;
}
public static String playPrevious(String pid) {
return playPrevious + pid;
}
public static String setVolume(String vol, String pid) {
return setVolume + pid + "&level=" + vol;
}
public static String getPlayers() {
return getPlayers;
}
public static String getPlayerInfo(String pid) {
return getPlayerInfo + pid;
}
public static String getPlayState(String pid) {
return getPlayState + pid;
}
public static String getNowPlayingMedia(String pid) {
return getNowPlayingMedia + pid;
}
public static String getVolume(String pid) {
return playerGetVolume + pid;
}
public static String getMusicSources() {
return getMusicSources;
}
public static String prettifyJSONon() {
return prettifyJSONon;
}
public static String prettifyJSONoff() {
return prettifyJSONoff;
}
public static String getMute(String pid) {
return playerGetMute + pid;
}
public static String getQueue(String pid) {
return getQueue + pid;
}
public static String getQueue(String pid, int start, int end) {
return getQueue(pid) + "&range=" + start + "," + end;
}
public static String playQueueItem(String pid, String qid) {
return playQueueItem + pid + "&qid=" + qid;
}
public static String deleteQueueItem(String pid, String qid) {
return deleteQueueItem + pid + "&qid=" + qid;
}
public static String browseSource(String sid) {
return browseSource + sid;
}
public static String playStream(String pid) {
return playStream + pid;
}
public static String addToQueue(String pid) {
return addToQueue + pid;
}
public static String addContainerToQueuePlayNow(String pid, String sid, String cid) {
return addToQueue + pid + "&sid=" + sid + "&cid=" + cid + "&aid=1";
}
public static String clearQueue(String pid) {
return clearQueue + pid;
}
public static String rebootSystem() {
return rebootSystem;
}
public static String playStream(@Nullable String pid, @Nullable String sid, @Nullable String cid,
@Nullable String mid, @Nullable String name) {
String newCommand = playStream;
if (pid != null) {
newCommand = newCommand + pid;
}
if (sid != null) {
newCommand = newCommand + "&sid=" + sid;
}
if (cid != null) {
newCommand = newCommand + "&cid=" + cid;
}
if (mid != null) {
newCommand = newCommand + "&mid=" + mid;
}
if (name != null) {
newCommand = newCommand + "&name=" + name;
}
return newCommand;
}
public static String playStream(String pid, String sid, String mid) {
return playStream + pid + "&sid=" + sid + "&mid=" + mid;
}
public static String playInputSource(String des_pid, String source_pid, String input) {
return playInputSource + des_pid + "&spid=" + source_pid + "&input=inputs/" + input;
}
public static String playURL(String pid, String url) {
return playURL + pid + "&url=" + url;
}
public static String signIn(String username, String password) {
String encodedUsername = urlEncode(username);
String encodedPassword = urlEncode(password);
return "heos://system/sign_in?un=" + encodedUsername + "&pw=" + encodedPassword;
}
public static String signOut() {
return signOut;
}
public static String heartbeat() {
return heartbeat;
}
public static String getGroups() {
return getGroups;
}
public static String getGroupInfo(String gid) {
return getGroupsInfo + gid;
}
public static String setGroup(String[] gid) {
String players = String.join(",", gid);
return setGroup + players;
}
public static String getGroupVolume(String gid) {
return getGroupVolume + gid;
}
public static String setGroupVolume(String volume, String gid) {
return setGroupVolume + gid + "&level=" + volume;
}
public static String setGroupVolumeUp(String gid) {
return groupVolumeUp + gid + "&step=1";
}
public static String setGroupVolumeDown(String gid) {
return groupVolumeDown + gid + "&step=1";
}
public static String getGroupMute(String gid) {
return getGroupMute + gid;
}
public static String setGroupMuteOn(String gid) {
return setGroupMute + gid + "&state=on";
}
public static String setGroupMuteOff(String gid) {
return setGroupMute + gid + "&state=off";
}
public static String getToggleGroupMute(String gid) {
return toggleGroupMute + gid;
}
private static String urlEncode(String username) {
try {
String encoded = URLEncoder.encode(username, StandardCharsets.UTF_8.toString());
// however it cannot handle escaped @ signs
return encoded.replace("%40", "@");
} catch (UnsupportedEncodingException e) {
throw new IllegalStateException("UTF-8 is not supported, bailing out");
}
}
}

View File

@@ -0,0 +1,59 @@
/**
* 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.heos.internal.resources;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* The {@link HeosConstants} provides the constants used within the HEOS
* network
*
* @author Johannes Einig - Initial contribution
*/
@NonNullByDefault
public class HeosConstants {
public static final String HEOS = "heos";
public static final String CONNECTION_LOST = "connection_lost";
public static final String EVENT_STREAM_TIMEOUT = "event_stream_timeout";
public static final String CONNECTION_RESTORED = "connection_restored";
public static final String PID = "pid";
// Event Results
public static final String ON = "on";
public static final String OFF = "off";
public static final String REPEAT_ALL = "on_all";
public static final String REPEAT_ONE = "on_one";
// Event Types
public static final String EVENT_TYPE_SYSTEM = "system";
public static final String EVENT_TYPE_EVENT = "event";
// Browse Command
public static final String FAVORITE_SID = "1028";
public static final String PLAYLISTS_SID = "1025";
public static final int INPUT_SID = 1027;
public static final String PLAY = "play";
public static final String PAUSE = "pause";
public static final String STOP = "stop";
public static final String STATION = "station";
public static final String SONG = "song";
// UI Commands
public static final String HEOS_UI_ALL = "All";
public static final String HEOS_UI_ONE = "One";
public static final String HEOS_UI_OFF = "Off";
}

View File

@@ -0,0 +1,42 @@
/**
* 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.heos.internal.resources;
import java.util.EventListener;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.heos.internal.api.HeosEventController;
import org.openhab.binding.heos.internal.exception.HeosFunctionalException;
import org.openhab.binding.heos.internal.json.dto.HeosEventObject;
import org.openhab.binding.heos.internal.json.dto.HeosResponseObject;
import org.openhab.binding.heos.internal.json.payload.Media;
/**
* The {@link HeosEventListener } is an Event Listener
* for the HEOS network. Handler which wants the get informed
* by an HEOS event via the {@link HeosEventController} has to
* implement this class and register itself at the {@link HeosEventController}
*
* @author Johannes Einig - Initial contribution
*/
@NonNullByDefault
public interface HeosEventListener extends EventListener {
void playerStateChangeEvent(HeosEventObject eventObject);
void playerStateChangeEvent(HeosResponseObject<?> responseObject) throws HeosFunctionalException;
void playerMediaChangeEvent(String pid, Media media);
void bridgeChangeEvent(String event, boolean success, Object command);
}

View File

@@ -0,0 +1,46 @@
/**
* 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.heos.internal.resources;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.heos.internal.json.payload.Group;
/**
* The {@link HeosGroup} represents the group within the
* HEOS network
*
* @author Johannes Einig - Initial contribution
*/
@NonNullByDefault
public class HeosGroup {
public static String calculateGroupMemberHash(Group group) {
List<String> sortedPlayerIds = group.players.stream().map(player -> player.id).sorted()
.collect(Collectors.toList());
return sortedToString(sortedPlayerIds);
}
private static String sortedToString(List<String> sortedPlayerIds) {
return Integer.toUnsignedString(sortedPlayerIds.hashCode());
}
public static String calculateGroupMemberHash(String members) {
List<String> sortedPlayerIds = Arrays.stream(members.split(";")).sorted().collect(Collectors.toList());
return sortedToString(sortedPlayerIds);
}
}

View File

@@ -0,0 +1,115 @@
/**
* 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.heos.internal.resources;
import java.io.IOException;
import org.openhab.binding.heos.internal.json.HeosJsonParser;
import org.openhab.binding.heos.internal.json.dto.HeosResponseObject;
import org.openhab.binding.heos.internal.resources.Telnet.ReadException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The {@link HeosSendCommand} is responsible to send a command
* to the HEOS bridge
*
* @author Johannes Einig - Initial contribution
*/
public class HeosSendCommand {
private final Logger logger = LoggerFactory.getLogger(HeosSendCommand.class);
private final Telnet client;
private final HeosJsonParser parser = new HeosJsonParser();
public HeosSendCommand(Telnet client) {
this.client = client;
}
public <T> HeosResponseObject<T> send(String command, Class<T> clazz) throws IOException, ReadException {
HeosResponseObject<T> result;
int attempt = 0;
boolean send = client.send(command);
if (clazz == null) {
return null;
} else if (send) {
String line = client.readLine();
if (line == null) {
throw new IOException("No valid input was received");
}
result = parser.parseResponse(line, clazz);
while (!result.isFinished() && attempt < 3) {
attempt++;
logger.trace("Retrying \"{}\" (attempt {})", command, attempt);
line = client.readLine(15000);
if (line != null) {
result = parser.parseResponse(line, clazz);
}
}
if (attempt >= 3 && !result.isFinished()) {
throw new IOException("No valid input was received after multiple attempts");
}
return result;
} else {
throw new IOException("Not connected");
}
}
public boolean isHostReachable() {
return client.isHostReachable();
}
public boolean isConnected() {
return client.isConnected();
}
public void stopInputListener(String registerChangeEventOFF) {
logger.debug("Stopping HEOS event line listener");
client.stopInputListener();
if (client.isConnected()) {
try {
client.send(registerChangeEventOFF);
} catch (IOException e) {
logger.debug("Failure during closing connection to HEOS with message: {}", e.getMessage());
}
}
}
public void disconnect() {
if (client.isConnected()) {
return;
}
try {
logger.debug("Disconnecting HEOS command line");
client.disconnect();
} catch (IOException e) {
logger.debug("Failure during closing connection to HEOS with message: {}", e.getMessage());
}
logger.debug("Connection to HEOS system closed");
}
public void startInputListener(String command) throws IOException, ReadException {
HeosResponseObject<Void> response = send(command, Void.class);
if (response.result) {
client.startInputListener();
}
}
}

View File

@@ -0,0 +1,46 @@
/**
* 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.heos.internal.resources;
import java.beans.PropertyChangeListener;
import java.beans.PropertyChangeSupport;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The {@Link HeosStringPropertyChangeListener} provides the possibility
* to add a listener to an String and get informed about the new value.
*
* @author Johannes Einig - Initial contribution
*/
@NonNullByDefault
public class HeosStringPropertyChangeListener {
private final Logger logger = LoggerFactory.getLogger(HeosStringPropertyChangeListener.class);
private final PropertyChangeSupport pcs = new PropertyChangeSupport(this);
public void addPropertyChangeListener(PropertyChangeListener propertyChangeListener) {
pcs.addPropertyChangeListener(propertyChangeListener);
}
public void removePropertyChangeListener(PropertyChangeListener listener) {
pcs.removePropertyChangeListener(listener);
}
public void setValue(String newValue) {
logger.debug("Firing property change: {} {}", newValue, Thread.currentThread());
pcs.firePropertyChange("value", null, newValue);
}
}

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.heos.internal.resources;
import java.util.Set;
import java.util.concurrent.CopyOnWriteArraySet;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.heos.internal.json.dto.HeosEventObject;
import org.openhab.binding.heos.internal.json.payload.Media;
/**
* The {@link HeosSystemEventListener } is used for classes which
* wants to inform players or groups about change events
* from the HEOS system. Classes which wants to be informed
* has to implement the {@link HeosEventListener} and register at
* the class which extends this {@link HeosSystemEventListener}
*
* @author Johannes Einig - Initial contribution
*/
@NonNullByDefault
public class HeosSystemEventListener {
private Set<HeosEventListener> listenerList = new CopyOnWriteArraySet<>();
/**
* Register a listener from type {@link HeosEventListener} to be notified by
* a change event
*
* @param listener the lister from type {@link HeosEventListener} for change events
*/
public void addListener(HeosEventListener listener) {
listenerList.add(listener);
}
/**
* Removes the listener from the notification list
*
* @param listener the listener from type {@link HeosEventListener} to be removed
*/
public void removeListener(HeosEventListener listener) {
listenerList.remove(listener);
}
/**
* Notifies the registered listener of a changed state type event
*
* @param eventObject the command of the event
*/
public void fireStateEvent(HeosEventObject eventObject) {
listenerList.forEach(element -> element.playerStateChangeEvent(eventObject));
}
/**
* Notifies the registered listener of a changed media type event
*
* @param pid the ID of the player or group which has changed
* @param media the media information
*/
public void fireMediaEvent(String pid, Media media) {
listenerList.forEach(element -> element.playerMediaChangeEvent(pid, media));
}
/**
* Notifies the registered listener if a change of the bridge state
*
* @param event the event type
* @param success the result (success or fail)
* @param command the command of the event
*/
public void fireBridgeEvent(String event, boolean success, Object command) {
for (HeosEventListener heosEventListener : listenerList) {
heosEventListener.bridgeChangeEvent(event, success, command);
}
}
}

View File

@@ -0,0 +1,287 @@
/**
* 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.heos.internal.resources;
import java.io.BufferedInputStream;
import java.io.BufferedReader;
import java.io.DataOutputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.InetAddress;
import java.net.SocketException;
import java.net.UnknownHostException;
import java.nio.charset.StandardCharsets;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import org.apache.commons.net.io.CRLFLineReader;
import org.apache.commons.net.telnet.TelnetClient;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.core.common.NamedThreadFactory;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The {@link Telnet} is an Telnet Client which handles the connection
* to a network via the Telnet interface
*
* @author Johannes Einig - Initial contribution
*/
public class Telnet {
private final Logger logger = LoggerFactory.getLogger(Telnet.class);
private static final int READ_TIMEOUT = 3000;
private static final int IS_ALIVE_TIMEOUT = 10000;
private final HeosStringPropertyChangeListener eolNotifier = new HeosStringPropertyChangeListener();
private final TelnetClient client = new TelnetClient();
private ExecutorService timedReaderExecutor;
private String ip;
private int port;
private String readResult = "";
private InetAddress address;
private DataOutputStream outStream;
private BufferedInputStream bufferedStream;
/**
* Connects to a host with the specified IP address and port
*
* @param ip IP Address of the host
* @param port where to be connected
* @return True if connection was successful
* @throws SocketException
* @throws IOException
*/
public boolean connect(String ip, int port) throws SocketException, IOException {
this.ip = ip;
this.port = port;
try {
address = InetAddress.getByName(ip);
} catch (UnknownHostException e) {
logger.debug("Unknown Host Exception - Message: {}", e.getMessage());
}
timedReaderExecutor = Executors.newSingleThreadExecutor(new NamedThreadFactory("heos-telnet-reader", true));
return openConnection();
}
private boolean openConnection() throws IOException {
client.setConnectTimeout(5000);
client.connect(ip, port);
outStream = new DataOutputStream(client.getOutputStream());
bufferedStream = new BufferedInputStream(client.getInputStream());
return client.isConnected();
}
/**
* Appends \r\n to the command.
* For clear send use sendClear
*
* @param command The command to be send
* @return true after the command was send
* @throws IOException
*/
public boolean send(String command) throws IOException {
if (client.isConnected()) {
sendClear(command + "\r\n");
return true;
} else {
return false;
}
}
/**
* Send command without additional commands
*
* @param command The command to be send
* @throws IOException
*/
private void sendClear(String command) throws IOException {
if (!client.isConnected()) {
return;
}
outStream.writeBytes(command);
outStream.flush();
}
/**
* Read all commands till an End Of Line is detected
* I more than one line is read every line is an
* element in the returned {@code ArrayList<>}
* Reading timed out after 3000 milliseconds. For another timing
*
* @return A list with all read commands
* @throws ReadException
* @throws IOException
* @see Telnet.readLine(int timeOut).
*/
public String readLine() throws ReadException, IOException {
return readLine(READ_TIMEOUT);
}
/**
* Read all commands till an End Of Line is detected
* I more than one line is read every line is an
* element in the returned {@code ArrayList<>}
* Reading time out is defined by parameter in
* milliseconds.
*
* @param timeOut the time in millis after reading times out
* @return A list with all read commands
* @throws ReadException
* @throws IOException
*/
public @Nullable String readLine(int timeOut) throws ReadException, IOException {
if (client.isConnected()) {
try {
return timedCallable(() -> {
BufferedReader reader = new CRLFLineReader(
new InputStreamReader(bufferedStream, StandardCharsets.UTF_8));
String lastLine;
do {
lastLine = reader.readLine();
} while (reader.ready());
return lastLine;
}, timeOut);
} catch (InterruptedException | TimeoutException e) {
throw new ReadException(e);
} catch (ExecutionException e) {
Throwable cause = e.getCause();
if (cause instanceof IOException) {
throw (IOException) cause;
} else {
throw new ReadException(cause);
}
}
}
return null;
}
private String timedCallable(Callable<String> callable, int timeOut)
throws InterruptedException, ExecutionException, TimeoutException {
Future<String> future = timedReaderExecutor.submit(callable);
try {
return future.get(timeOut, TimeUnit.MILLISECONDS);
} catch (Exception e) {
future.cancel(true);
throw e;
}
}
/**
* Disconnect Telnet and close all Streams
*
* @throws IOException
*/
public void disconnect() throws IOException {
client.disconnect();
timedReaderExecutor.shutdown();
}
/**
* Input Listener which fires event if input is detected
*/
public void startInputListener() {
logger.debug("Starting input listener");
client.setReaderThread(true);
client.registerInputListener(this::inputAvailableRead);
}
public void stopInputListener() {
logger.debug("Stopping input listener");
client.unregisterInputListener();
}
/**
* Reader for InputListenerOnly which only reads the
* available data without any check
*/
private void inputAvailableRead() {
try {
int i = bufferedStream.available();
byte[] buffer = new byte[i];
bufferedStream.read(buffer);
String str = new String(buffer, StandardCharsets.UTF_8);
concatReadResult(str);
} catch (IOException e) {
logger.debug("IO Exception, message: {}", e.getMessage());
}
}
/**
* Read values until end of line is reached.
* Then fires event for change Listener.
*
* @return -1 to indicate that end of line is reached
* else returns 0
*/
private int concatReadResult(String value) {
readResult = readResult.concat(value);
if (readResult.contains("\r\n")) {
eolNotifier.setValue(readResult.trim());
readResult = "";
return -1;
}
return 0;
}
/**
* Checks if the HEOS system is reachable
* via the network. This does not check if
* a Telnet connection is open.
*
* @return true if HEOS is reachable
*/
public boolean isHostReachable() {
try {
return address != null && address.isReachable(IS_ALIVE_TIMEOUT);
} catch (IOException e) {
logger.debug("IO Exception- Message: {}", e.getMessage());
return false;
}
}
@Override
public String toString() {
return "Telnet{" + "ip='" + ip + '\'' + ", port=" + port + '}';
}
public HeosStringPropertyChangeListener getReadResultListener() {
return eolNotifier;
}
public boolean isConnected() {
return client.isConnected();
}
public static class ReadException extends Exception {
private static final long serialVersionUID = 1L;
public ReadException() {
super("Can not read from client");
}
public ReadException(Throwable cause) {
super("Can not read from client", cause);
}
}
}

View File

@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<binding:binding id="heos" 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>HEOS Binding</name>
<description>Binding for the Denon HEOS system.</description>
<author>Johannes Einig</author>
</binding:binding>

View File

@@ -0,0 +1,102 @@
<?xml version="1.0" encoding="UTF-8"?>
<thing:thing-descriptions bindingId="heos"
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">
<channel-type id="ungroup">
<item-type>Switch</item-type>
<label>Group</label>
</channel-type>
<channel-type id="album">
<item-type>String</item-type>
<label>Album</label>
<state readOnly="true"/>
</channel-type>
<channel-type id="cover" advanced="true">
<item-type>Image</item-type>
<label>Cover</label>
<state readOnly="true"/>
</channel-type>
<channel-type id="buildGroup">
<item-type>Switch</item-type>
<label>Make Group</label>
</channel-type>
<channel-type id="playlists" advanced="true">
<item-type>String</item-type>
<label>Playlists</label>
</channel-type>
<channel-type id="favorites" advanced="true">
<item-type>String</item-type>
<label>Favorites</label>
</channel-type>
<channel-type id="queue" advanced="true">
<item-type>String</item-type>
<label>Queue</label>
</channel-type>
<channel-type id="clearQueue" advanced="true">
<item-type>Switch</item-type>
<label>Clear Queue</label>
</channel-type>
<channel-type id="reboot">
<item-type>Switch</item-type>
<label>Reboot</label>
</channel-type>
<channel-type id="input" advanced="true">
<item-type>String</item-type>
<label>External Inputs</label>
</channel-type>
<channel-type id="currentPosition" advanced="true">
<item-type>Number:Time</item-type>
<label>Track Position</label>
<description>The current track position</description>
<category>Time</category>
<state readOnly="true" pattern="%d %unit%"/>
</channel-type>
<channel-type id="duration" advanced="true">
<item-type>Number:Time</item-type>
<label>Track Duration</label>
<description>The overall duration of the track</description>
<category>Time</category>
<state readOnly="true" pattern="%d %unit%"/>
</channel-type>
<channel-type id="rawCommand" advanced="true">
<item-type>String</item-type>
<label>Send RAW Command</label>
<description>Sending a HEOS command as specified within the HEOS CLI protocol</description>
</channel-type>
<channel-type id="type" advanced="true">
<item-type>String</item-type>
<label>Type</label>
<description>The media currently played type (station, song, ...)</description>
<state readOnly="true"/>
</channel-type>
<channel-type id="station" advanced="true">
<item-type>String</item-type>
<label>Station</label>
<description>The name of the station currently played</description>
<state readOnly="true"/>
</channel-type>
<channel-type id="playUrl" advanced="true">
<item-type>String</item-type>
<label>Play URL</label>
<description>Plays a media file from URL</description>
</channel-type>
<channel-type id="shuffleMode" advanced="true">
<item-type>Switch</item-type>
<label>Shuffle</label>
<description>Sets the shuffle mode</description>
</channel-type>
<channel-type id="repeatMode" advanced="true">
<item-type>String</item-type>
<label>Repeat Mode</label>
<description>Set the repeat mode</description>
<state readOnly="false">
<options>
<option value="One">One</option>
<option value="All">All</option>
<option value="Off">Off</option>
</options>
</state>
</channel-type>
</thing:thing-descriptions>

View File

@@ -0,0 +1,46 @@
<?xml version="1.0" encoding="UTF-8"?>
<thing:thing-descriptions bindingId="heos"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
<thing-type id="group">
<supported-bridge-type-refs>
<bridge-type-ref id="bridge"/>
</supported-bridge-type-refs>
<label>HEOS Group</label>
<description>A group of HEOS Player</description>
<channels>
<channel id="Control" typeId="system.media-control"/>
<channel id="Volume" typeId="system.volume"/>
<channel id="Mute" typeId="system.mute"/>
<channel id="Inputs" typeId="input"/>
<channel id="Title" typeId="system.media-title"/>
<channel id="Artist" typeId="system.media-artist"/>
<channel id="Album" typeId="album"/>
<channel id="Cover" typeId="cover"/>
<channel id="CurrentPosition" typeId="currentPosition"/>
<channel id="Duration" typeId="duration"/>
<channel id="Type" typeId="type"/>
<channel id="Station" typeId="station"/>
<channel id="PlayUrl" typeId="playUrl"/>
<channel id="Ungroup" typeId="ungroup"/>
<channel id="Shuffle" typeId="shuffleMode"/>
<channel id="RepeatMode" typeId="repeatMode"/>
<channel id="Playlists" typeId="playlists"/>
<channel id="Favorites" typeId="favorites"/>
<channel id="Queue" typeId="queue"/>
<channel id="ClearQueue" typeId="clearQueue"/>
</channels>
<properties>
<property name="vendor">Denon</property>
</properties>
<config-description>
<parameter name="members" type="text" readOnly="false">
<label>The Group Member Player IDs</label>
<description>Shows the player IDs of the members of this group</description>
<required>true</required>
</parameter>
</config-description>
</thing-type>
</thing:thing-descriptions>

View File

@@ -0,0 +1,48 @@
<?xml version="1.0" encoding="UTF-8"?>
<thing:thing-descriptions bindingId="heos"
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">
<!-- Heos Player Thing Type -->
<thing-type id="player">
<supported-bridge-type-refs>
<bridge-type-ref id="bridge"/>
</supported-bridge-type-refs>
<label>HEOS Player</label>
<description>A HEOS Player of the HEOS Network</description>
<channels>
<channel id="Control" typeId="system.media-control"/>
<channel id="Volume" typeId="system.volume"/>
<channel id="Mute" typeId="system.mute"/>
<channel id="Inputs" typeId="input"/>
<channel id="Title" typeId="system.media-title"/>
<channel id="Artist" typeId="system.media-artist"/>
<channel id="Album" typeId="album"/>
<channel id="Cover" typeId="cover"/>
<channel id="CurrentPosition" typeId="currentPosition"/>
<channel id="Duration" typeId="duration"/>
<channel id="Type" typeId="type"/>
<channel id="Station" typeId="station"/>
<channel id="PlayUrl" typeId="playUrl"/>
<channel id="Shuffle" typeId="shuffleMode"/>
<channel id="RepeatMode" typeId="repeatMode"/>
<channel id="Favorites" typeId="favorites"/>
<channel id="Playlists" typeId="playlists"/>
<channel id="Queue" typeId="queue"/>
<channel id="ClearQueue" typeId="clearQueue"/>
</channels>
<properties>
<property name="vendor">Denon</property>
</properties>
<representation-property>serialNumber</representation-property>
<config-description>
<parameter name="pid" type="text" readOnly="false">
<label>Player ID</label>
<description>The internal Player ID</description>
<required>true</required>
</parameter>
</config-description>
</thing-type>
</thing:thing-descriptions>

View File

@@ -0,0 +1,43 @@
<?xml version="1.0" encoding="UTF-8"?>
<thing:thing-descriptions bindingId="heos"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
<!-- Thing Type -->
<bridge-type id="bridge">
<label>HEOS Bridge</label>
<description>The HEOS System Bridge</description>
<channels>
<channel typeId="reboot" id="Reboot"></channel>
<channel typeId="buildGroup" id="BuildGroup"></channel>
</channels>
<representation-property>vendor</representation-property>
<config-description>
<parameter name="ipAddress" type="text">
<context>network-address</context>
<label>Network Address</label>
<description>Network address of the HEOS bridge.</description>
<required>true</required>
</parameter>
<parameter name="username" type="text">
<label>Username</label>
<description>Username for login to the HEOS account.</description>
<required>false</required>
</parameter>
<parameter name="password" type="text">
<context>password</context>
<label>Password</label>
<description>Password for login to the HEOS account</description>
<required>false</required>
</parameter>
<parameter name="heartbeat" type="integer" min="3" max="3600" unit="s">
<required>false</required>
<unitLabel>seconds</unitLabel>
<label>Heartbeat</label>
<description>The time in seconds for the HEOS Heartbeat (default = 60 s)</description>
<default>60</default>
<advanced>true</advanced>
</parameter>
</config-description>
</bridge-type>
</thing:thing-descriptions>

View File

@@ -0,0 +1,288 @@
/**
* 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.heos.internal.json;
import static java.lang.Long.valueOf;
import static org.junit.Assert.*;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.junit.Test;
import org.openhab.binding.heos.internal.json.dto.HeosCommunicationAttribute;
import org.openhab.binding.heos.internal.json.dto.HeosEvent;
import org.openhab.binding.heos.internal.json.dto.HeosEventObject;
/**
* Tests to validate the functioning of the HeosJsonParser specifically for event objects
*
* @author Martin van Wingerden - Initial Contribution
*/
@NonNullByDefault
public class HeosJsonParserEventTest {
private final HeosJsonParser subject = new HeosJsonParser();
@Test
public void event_now_playing_changed() {
HeosEventObject event = subject.parseEvent(
"{\"heos\": {\"command\": \"event/player_now_playing_changed\", \"message\": \"pid=1679855527\"}}");
assertEquals(HeosEvent.PLAYER_NOW_PLAYING_CHANGED, event.command);
assertEquals("event/player_now_playing_changed", event.rawCommand);
assertEquals(valueOf(1679855527), event.getNumericAttribute(HeosCommunicationAttribute.PLAYER_ID));
}
@Test
public void event_now_playing_progress() {
HeosEventObject event = subject.parseEvent(
"{\"heos\": {\"command\": \"event/player_now_playing_progress\", \"message\": \"pid=1679855527&cur_pos=224848000&duration=0\"}}");
assertEquals(HeosEvent.PLAYER_NOW_PLAYING_PROGRESS, event.command);
assertEquals("event/player_now_playing_progress", event.rawCommand);
assertEquals(valueOf(1679855527), event.getNumericAttribute(HeosCommunicationAttribute.PLAYER_ID));
assertEquals(valueOf(224848000), event.getNumericAttribute(HeosCommunicationAttribute.CURRENT_POSITION));
assertEquals(valueOf(0), event.getNumericAttribute(HeosCommunicationAttribute.DURATION));
}
@Test
public void event_state_changed() {
HeosEventObject event = subject.parseEvent(
"{\"heos\": {\"command\": \"event/player_state_changed\", \"message\": \"pid=1679855527&state=play\"}}");
assertEquals(HeosEvent.PLAYER_STATE_CHANGED, event.command);
assertEquals("event/player_state_changed", event.rawCommand);
assertEquals(valueOf(1679855527), event.getNumericAttribute(HeosCommunicationAttribute.PLAYER_ID));
assertEquals("play", event.getAttribute(HeosCommunicationAttribute.STATE));
}
@Test
public void event_playback_error() {
HeosEventObject event = subject.parseEvent(
"{\"heos\": {\"command\": \"event/player_playback_error\", \"message\": \"pid=1679855527&error=Could Not Download\"}}");
assertEquals(HeosEvent.PLAYER_PLAYBACK_ERROR, event.command);
assertEquals("event/player_playback_error", event.rawCommand);
assertEquals(valueOf(1679855527), event.getNumericAttribute(HeosCommunicationAttribute.PLAYER_ID));
assertEquals("Could Not Download", event.getAttribute(HeosCommunicationAttribute.ERROR));
}
@Test
public void event_volume_changed() {
HeosEventObject event = subject.parseEvent(
"{\"heos\": {\"command\": \"event/player_volume_changed\", \"message\": \"pid=1958912779&level=23&mute=off\"}}");
assertEquals(HeosEvent.PLAYER_VOLUME_CHANGED, event.command);
assertEquals("event/player_volume_changed", event.rawCommand);
assertEquals(valueOf(1958912779), event.getNumericAttribute(HeosCommunicationAttribute.PLAYER_ID));
assertEquals(valueOf(23), event.getNumericAttribute(HeosCommunicationAttribute.LEVEL));
assertFalse(event.getBooleanAttribute(HeosCommunicationAttribute.MUTE));
}
@Test
public void event_shuffle_mode_changed() {
HeosEventObject event = subject.parseEvent(
"{\"heos\": {\"command\": \"event/shuffle_mode_changed\", \"message\": \"pid=-831584083&shuffle=on\"}}");
assertEquals(HeosEvent.SHUFFLE_MODE_CHANGED, event.command);
assertEquals("event/shuffle_mode_changed", event.rawCommand);
assertEquals(valueOf(-831584083), event.getNumericAttribute(HeosCommunicationAttribute.PLAYER_ID));
assertTrue(event.getBooleanAttribute(HeosCommunicationAttribute.SHUFFLE));
}
@Test
public void event_sources_changed() {
HeosEventObject event = subject.parseEvent("{\"heos\": {\"command\": \"event/sources_changed\"}}");
assertEquals(HeosEvent.SOURCES_CHANGED, event.command);
assertEquals("event/sources_changed", event.rawCommand);
}
@Test
public void event_user_changed() {
HeosEventObject event = subject.parseEvent(
"{\"heos\": {\"command\": \"event/user_changed\", \"message\": \"signed_in&un=martinvw@mtin.nl\"}}");
assertEquals(HeosEvent.USER_CHANGED, event.command);
assertEquals("event/user_changed", event.rawCommand);
assertTrue(event.hasAttribute(HeosCommunicationAttribute.SIGNED_IN));
assertEquals("martinvw@mtin.nl", event.getAttribute(HeosCommunicationAttribute.USERNAME));
}
@Test
public void event_unknown_event() {
HeosEventObject event = subject.parseEvent("{\"heos\": {\"command\": \"event/does_not_exist\"}}");
assertNull(event.command);
assertEquals("event/does_not_exist", event.rawCommand);
}
@Test
public void event_duplicate_attributes() {
HeosEventObject event = subject.parseEvent(
"{\"heos\": {\"command\": \"event/does_not_exist\", \"message\": \"signed_in&un=test1&un=test2\"}}");
// the first one is ignored but it does not crash
assertEquals("test2", event.getAttribute(HeosCommunicationAttribute.USERNAME));
}
@Test
public void event_non_numeric() {
HeosEventObject event = subject
.parseEvent("{\"heos\": {\"command\": \"event/does_not_exist\", \"message\": \"pid=test\"}}");
// the first one is ignored but it does not crash
assertNull(event.getNumericAttribute(HeosCommunicationAttribute.PLAYER_ID));
}
@Test
public void event_numeric_missing() {
HeosEventObject event = subject.parseEvent("{\"heos\": {\"command\": \"event/does_not_exist\"}}");
// the first one is ignored but it does not crash
assertNull(event.getAttribute(HeosCommunicationAttribute.PLAYER_ID));
}
/*
*
* {"heos": {"command": "browse/browse", "result": "success", "message": "command under process&sid=1025"}}
* {"heos": {"command": "browse/browse", "result": "success", "message": "command under process&sid=1028"}}
* {"heos": {"command": "browse/browse", "result": "success", "message": "sid=1025&returned=6&count=6"}, "payload":
* [{"container": "yes", "type": "playlist", "cid": "132562", "playable": "yes", "name":
* "Maaike Ouboter - En hoe het dan ook weer dag wordt", "image_url": ""}, {"container": "yes", "type": "playlist",
* "cid": "132563", "playable": "yes", "name": "Maaike Ouboter - Vanaf nu is het van jou", "image_url": ""},
* {"container": "yes", "type": "playlist", "cid": "162887", "playable": "yes", "name": "Easy listening",
* "image_url": ""}, {"container": "yes", "type": "playlist", "cid": "174461", "playable": "yes", "name":
* "Nieuwe muziek 5-2019", "image_url": ""}, {"container": "yes", "type": "playlist", "cid": "194000", "playable":
* "yes", "name": "Nieuwe muziek 2019-05", "image_url": ""}, {"container": "yes", "type": "playlist", "cid":
* "194001", "playable": "yes", "name": "Clean Bandit", "image_url": ""}]}
* {"heos": {"command": "browse/browse", "result": "success", "message": "sid=1028&returned=3&count=3"}, "payload":
* [{"container": "no", "mid": "s6707", "type": "station", "playable": "yes", "name":
* "NPO 3FM 96.8 (Top 40 %26 Pop Music)", "image_url":
* "http://cdn-profiles.tunein.com/s6707/images/logoq.png?t=636268"}, {"container": "no", "mid": "s2967", "type":
* "station", "playable": "yes", "name": "Classic FM Nederland (Classical Music)", "image_url":
* "http://cdn-radiotime-logos.tunein.com/s2967q.png"}, {"container": "no", "mid": "s1993", "type": "station",
* "playable": "yes", "name": "BNR Nieuwsradio", "image_url": "http://cdn-radiotime-logos.tunein.com/s1993q.png"}],
* "options": [{"browse": [{"id": 20, "name": "Remove from HEOS Favorites"}]}]}
* {"heos": {"command": "event/user_changed", "message": "signed_in&un=martinvw@mtin.nl"}}
* {"heos": {"command": "group/get_groups", "result": "success", "message": ""}, "payload": []}
* {"heos": {"command": "player/get_mute", "result": "fail", "message": "eid=2&text=ID Not Valid&pid=null"}}
* {"heos": {"command": "player/get_mute", "result": "success", "message": "pid=1958912779&state=off"}}
* {"heos": {"command": "player/get_mute", "result": "success", "message": "pid=1958912779&state=on"}}
* {"heos": {"command": "player/get_mute", "result": "success", "message": "pid=-831584083&state=off"}}
* {"heos": {"command": "player/get_mute", "result": "success", "message": "pid=-831584083&state=on"}}
* {"heos": {"command": "player/get_now_playing_media", "result": "fail", "message":
* "eid=2&text=ID Not Valid&pid=null"}}
* {"heos": {"command": "player/get_now_playing_media", "result": "success", "message": "pid=1679855527"},
* "payload": {"type": "song", "song": "", "album": "", "artist": "", "image_url": "", "album_id": "1", "mid": "1",
* "qid": 1, "sid": 1024}, "options": []}
* {"heos": {"command": "player/get_now_playing_media", "result": "success", "message": "pid=1958912779"},
* "payload": {"type": "song", "song": "Solo (feat. Demi Lovato)", "album": "What Is Love? (Deluxe)", "artist":
* "Clean Bandit", "image_url":
* "http://192.168.1.230:8015//m-browsableMediaUri/getImageFromTag/mnt/326C72A3E307501E47DE2B0F47D90EB8/Clean%20Bandit/What%20Is%20Love_%20(Deluxe)/03%20Solo%20(feat.%20Demi%20Lovato).m4a",
* "album_id": "", "mid":
* "http://192.168.1.230:8015/m-1c176905-f6c7-d168-dc35-86b4735c5976/Clean+Bandit/What+Is+Love_+(Deluxe)/03+Solo+(feat.+Demi+Lovato).m4a",
* "qid": 1, "sid": 1024}, "options": []}
* {"heos": {"command": "player/get_now_playing_media", "result": "success", "message": "pid=1958912779"},
* "payload": {"type": "station", "song": "HEOS Bar - HDMI 2", "station": "HEOS Bar - HDMI 2", "album": "",
* "artist": "", "image_url": "", "album_id": "inputs", "mid": "inputs/hdmi_in_2", "qid": 1, "sid": 1027},
* "options": []}
* {"heos": {"command": "player/get_now_playing_media", "result": "success", "message": "pid=1958912779"},
* "payload": {"type": "station", "song": "HEOS Bar - HDMI 3", "station": "HEOS Bar - HDMI 3", "album": "",
* "artist": "", "image_url": "", "album_id": "inputs", "mid": "inputs/hdmi_in_3", "qid": 1, "sid": 1027},
* "options": []}
* {"heos": {"command": "player/get_now_playing_media", "result": "success", "message": "pid=-831584083"},
* "payload": {"type": "song", "song": "Applejack", "album":
* "The Real... Dolly Parton: The Ultimate Dolly Parton Collection", "artist": "Dolly Parton", "image_url":
* "http://192.168.1.230:8015/m-1c176905-f6c7-d168-dc35-86b4735c5976/getImageFromTag/Dolly%20Parton/The%20Real%20Dolly%20Parton%20%5bDisc%202%5d/2-07%20Applejack.m4a",
* "album_id": "m-1c176905-f6c7-d168-dc35-86b4735c5976/alb/a-418", "mid":
* "m-1c176905-f6c7-d168-dc35-86b4735c5976/alb/a-418/t-4150", "qid": 43, "sid": 1024}, "options": []}
* {"heos": {"command": "player/get_now_playing_media", "result": "success", "message": "pid=-831584083"},
* "payload": {"type": "song", "song": "Dancing Queen", "album": "ABBA Gold: Greatest Hits", "artist": "ABBA",
* "image_url":
* "http://192.168.1.230:8015/m-1c176905-f6c7-d168-dc35-86b4735c5976/getImageFromTag/ABBA/ABBA%20Gold_%20Greatest%20Hits/01%20Dancing%20Queen%201.m4a",
* "album_id": "m-1c176905-f6c7-d168-dc35-86b4735c5976/alb/a-398", "mid":
* "m-1c176905-f6c7-d168-dc35-86b4735c5976/alb/a-398/t-4237", "qid": 1, "sid": 1024}, "options": []}
* {"heos": {"command": "player/get_now_playing_media", "result": "success", "message": "pid=-831584083"},
* "payload": {"type": "song", "song": "D.I.V.O.R.C.E.", "album":
* "The Real... Dolly Parton: The Ultimate Dolly Parton Collection", "artist": "Dolly Parton", "image_url":
* "http://192.168.1.230:8015/m-1c176905-f6c7-d168-dc35-86b4735c5976/getImageFromTag/Dolly%20Parton/The%20Real%20Dolly%20Parton%20%5bDisc%201%5d/1-03%20D.I.V.O.R.C.E.m4a",
* "album_id": "m-1c176905-f6c7-d168-dc35-86b4735c5976/alb/a-417", "mid":
* "m-1c176905-f6c7-d168-dc35-86b4735c5976/alb/a-417/t-4138", "qid": 22, "sid": 1024}, "options": []}
* {"heos": {"command": "player/get_now_playing_media", "result": "success", "message": "pid=-831584083"},
* "payload": {"type": "song", "song": "Homeward Bound", "album": "The Very Best Of Art Garfunkel: Across America",
* "artist": "Art Garfunkel", "image_url":
* "http://192.168.1.230:8015/m-1c176905-f6c7-d168-dc35-86b4735c5976/getImageFromTag/Art%20Garfunkel/The%20Very%20Best%20Of%20Art%20Garfunkel_%20Across%20A/06%20-%20Art%20Garfunkel%20-%20Homeward%20Bound.mp3",
* "album_id": "m-1c176905-f6c7-d168-dc35-86b4735c5976/alb/a-127", "mid":
* "m-1c176905-f6c7-d168-dc35-86b4735c5976/alb/a-127/t-1385", "qid": 80, "sid": 1024}, "options": []}
* {"heos": {"command": "player/get_player_info", "result": "success", "message": "pid=1958912779"}, "payload":
* {"name": "HEOS Bar", "pid": 1958912779, "model": "HEOS Bar", "version": "1.520.200", "ip": "192.168.1.195",
* "network": "wired", "lineout": 0, "serial": "ADAG9180917029"}}
* {"heos": {"command": "player/get_player_info", "result": "success", "message": "pid=-831584083"}, "payload":
* {"name": "Kantoor HEOS 3", "pid": -831584083, "model": "HEOS 3", "version": "1.520.200", "ip": "192.168.1.230",
* "network": "wired", "lineout": 0, "serial": "ACNG9180110887"}}
* {"heos": {"command": "player/get_players", "result": "success", "message": ""}, "payload": [{"name": "HEOS Bar",
* "pid": 1958912779, "model": "HEOS Bar", "version": "1.520.200", "ip": "192.168.1.195", "network": "wired",
* "lineout": 0, "serial": "ADAG9180917029"}, {"name": "Kantoor HEOS 3", "pid": -831584083, "model": "HEOS 3",
* "version": "1.520.200", "ip": "192.168.1.230", "network": "wired", "lineout": 0, "serial": "ACNG9180110887"}]}
* {"heos": {"command": "player/get_players", "result": "success", "message": ""}, "payload": [{"name":
* "Kantoor HEOS 3", "pid": -831584083, "model": "HEOS 3", "version": "1.520.200", "ip": "192.168.1.230", "network":
* "wired", "lineout": 0, "serial": "ACNG9180110887"}, {"name": "HEOS Bar", "pid": 1958912779, "model": "HEOS Bar",
* "version": "1.520.200", "ip": "192.168.1.195", "network": "wired", "lineout": 0, "serial": "ADAG9180917029"}]}
* {"heos": {"command": "player/get_play_mode", "result": "fail", "message": "eid=2&text=ID Not Valid&pid=null"}}
* {"heos": {"command": "player/get_play_mode", "result": "success", "message":
* "pid=1958912779&repeat=off&shuffle=off"}}
* {"heos": {"command": "player/get_play_mode", "result": "success", "message":
* "pid=-831584083&repeat=off&shuffle=on"}}
* {"heos": {"command": "player/get_play_state", "result": "fail", "message": "eid=2&text=ID Not Valid&pid=null"}}
* {"heos": {"command": "player/get_play_state", "result": "success", "message": "pid=1958912779&state=stop"}}
* {"heos": {"command": "player/get_play_state", "result": "success", "message": "pid=-831584083&state=pause"}}
* {"heos": {"command": "player/get_play_state", "result": "success", "message": "pid=-831584083&state=play"}}
* {"heos": {"command": "player/get_play_state", "result": "success", "message": "pid=-831584083&state=stop"}}
* {"heos": {"command": "player/get_volume", "result": "fail", "message": "eid=2&text=ID Not Valid&pid=null"}}
* {"heos": {"command": "player/get_volume", "result": "success", "message": "pid=1958912779&level=14"}}
* {"heos": {"command": "player/get_volume", "result": "success", "message": "pid=1958912779&level=21"}}
* {"heos": {"command": "player/get_volume", "result": "success", "message": "pid=1958912779&level=23"}}
* {"heos": {"command": "player/get_volume", "result": "success", "message": "pid=-831584083&level=12"}}
* {"heos": {"command": "player/get_volume", "result": "success", "message": "pid=-831584083&level=15"}}
* {"heos": {"command": "player/play_next", "result": "success", "message": "pid=-831584083"}}
* {"heos": {"command": "player/play_previous", "result": "success", "message": "pid=-831584083"}}
* {"heos": {"command": "player/set_mute", "result": "success", "message": "pid=1958912779&state=off"}}
* {"heos": {"command": "player/set_mute", "result": "success", "message": "pid=1958912779&state=on"}}
* {"heos": {"command": "player/set_mute", "result": "success", "message": "pid=-831584083&state=off"}}
* {"heos": {"command": "player/set_mute", "result": "success", "message": "pid=-831584083&state=on"}}
* {"heos": {"command": "player/set_play_mode", "result": "success", "message": "pid=-831584083&shuffle=off"}}
* {"heos": {"command": "player/set_play_mode", "result": "success", "message": "pid=-831584083&shuffle=on"}}
* {"heos": {"command": "player/set_play_state", "result": "success", "message": "pid=-831584083&state=pause"}}
* {"heos": {"command": "player/set_play_state", "result": "success", "message": "pid=-831584083&state=play"}}
* {"heos": {"command": "player/set_volume", "result": "fail", "message":
* "eid=9&text=Out of range&pid=-831584083&level=OFF"}}
* {"heos": {"command": "player/set_volume", "result": "success", "message": "pid=1958912779&level=14"}}
* {"heos": {"command": "player/set_volume", "result": "success", "message": "pid=1958912779&level=17"}}
* {"heos": {"command": "player/set_volume", "result": "success", "message": "pid=-831584083&level=10"}}
* {"heos": {"command": "player/set_volume", "result": "success", "message": "pid=-831584083&level=12"}}
* {"heos": {"command": "player/set_volume", "result": "success", "message": "pid=-831584083&level=14"}}
* {"heos": {"command": "player/set_volume", "result": "success", "message": "pid=-831584083&level=15"}}
* {"heos": {"command": "player/set_volume", "result": "success", "message": "pid=-831584083&level=16"}}
* {"heos": {"command": "player/set_volume", "result": "success", "message": "pid=-831584083&level=18"}}
* {"heos": {"command": "player/set_volume", "result": "success", "message": "pid=-831584083&level=21"}}
* {"heos": {"command": "player/set_volume", "result": "success", "message": "pid=-831584083&level=4"}}
* {"heos": {"command": "player/volume_down", "result": "success", "message": "pid=-831584083&step=1"}}
* {"heos": {"command": "system/heart_beat", "result": "success", "message": ""}}
* {"heos": {"command": "system/register_for_change_events", "result": "success", "message": "enable=off"}}
* {"heos": {"command": "system/register_for_change_events", "result": "success", "message": "enable=on"}}
* {"heos": {"command": "system/register_for_change_events", "reult": "success", "message": "enable=on"}}
* {"heos": {"command": "system/sign_in", "result": "success", "message":
* "command under process&un=martinvw@mtin.nl&pw=Pl7WUFC61Q7zdQD5"}}
* {"heos": {"command": "system/sign_in", "result": "success", "message": "signed_in&un=martinvw@mtin.nl"}}
*
*/
}

View File

@@ -0,0 +1,311 @@
/**
* 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.heos.internal.json;
import static org.junit.Assert.*;
import static org.openhab.binding.heos.internal.json.dto.HeosCommunicationAttribute.*;
import java.util.List;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.junit.Test;
import org.openhab.binding.heos.internal.json.dto.HeosCommand;
import org.openhab.binding.heos.internal.json.dto.HeosCommandGroup;
import org.openhab.binding.heos.internal.json.dto.HeosErrorCode;
import org.openhab.binding.heos.internal.json.dto.HeosResponseObject;
import org.openhab.binding.heos.internal.json.payload.BrowseResult;
import org.openhab.binding.heos.internal.json.payload.BrowseResultType;
import org.openhab.binding.heos.internal.json.payload.Group;
import org.openhab.binding.heos.internal.json.payload.GroupPlayerRole;
import org.openhab.binding.heos.internal.json.payload.Media;
import org.openhab.binding.heos.internal.json.payload.Player;
import org.openhab.binding.heos.internal.json.payload.YesNoEnum;
/**
* Tests to validate the functioning of the HeosJsonParser specifically for response objects
*
* @author Martin van Wingerden - Initial Contribution
*/
@NonNullByDefault
public class HeosJsonParserResponseTest {
private final HeosJsonParser subject = new HeosJsonParser();
@Test
public void sign_in() {
HeosResponseObject<Void> response = subject.parseResponse(
"{\"heos\": {\"command\": \"system/sign_in\", \"result\": \"success\", \"message\": \"signed_in&un=test@example.org\"}}",
Void.class);
assertEquals(HeosCommandGroup.SYSTEM, response.heosCommand.commandGroup);
assertEquals(HeosCommand.SIGN_IN, response.heosCommand.command);
assertTrue(response.result);
assertEquals("test@example.org", response.getAttribute(USERNAME));
assertTrue(response.hasAttribute(SIGNED_IN));
}
@Test
public void sign_in_under_process() {
HeosResponseObject<Void> response = subject.parseResponse(
"{\"heos\": {\"command\": \"system/sign_in\", \"message\": \"command under process\"}}", Void.class);
assertEquals(HeosCommandGroup.SYSTEM, response.heosCommand.commandGroup);
assertEquals(HeosCommand.SIGN_IN, response.heosCommand.command);
assertFalse(response.result);
assertFalse(response.isFinished());
}
@Test
public void sign_in_failed() {
HeosResponseObject<Void> response = subject.parseResponse(
"{\"heos\": {\"command\": \"system/sign_in\", \"message\": \"eid=10&text=User not found\"}}",
Void.class);
assertEquals(HeosCommandGroup.SYSTEM, response.heosCommand.commandGroup);
assertEquals(HeosCommand.SIGN_IN, response.heosCommand.command);
assertFalse(response.result);
assertTrue(response.isFinished());
assertEquals(HeosErrorCode.USER_NOT_FOUND, response.getError().code);
}
@Test
public void get_mute() {
HeosResponseObject<Void> response = subject.parseResponse(
"{\"heos\": {\"command\": \"player/get_mute\", \"result\": \"success\", \"message\": \"pid=1958912779&state=on\"}}",
Void.class);
assertEquals(HeosCommandGroup.PLAYER, response.heosCommand.commandGroup);
assertEquals(HeosCommand.GET_MUTE, response.heosCommand.command);
assertTrue(response.result);
assertEquals(Long.valueOf(1958912779), response.getNumericAttribute(PLAYER_ID));
assertTrue(response.getBooleanAttribute(STATE));
}
@Test
public void get_mute_error() {
HeosResponseObject<Void> response = subject.parseResponse(
"{\"heos\": {\"command\": \"player/get_mute\", \"result\": \"fail\", \"message\": \"eid=2&text=ID Not Valid&pid=null\"}}",
Void.class);
assertEquals(HeosCommandGroup.PLAYER, response.heosCommand.commandGroup);
assertEquals(HeosCommand.GET_MUTE, response.heosCommand.command);
assertFalse(response.result);
assertEquals(HeosErrorCode.INVALID_ID, response.getError().code);
}
@Test
public void browse_browse_under_process() {
HeosResponseObject<Void> response = subject.parseResponse(
"{\"heos\": {\"command\": \"browse/browse\", \"result\": \"success\", \"message\": \"command under process&sid=1025\"}}",
Void.class);
assertEquals(HeosCommandGroup.BROWSE, response.heosCommand.commandGroup);
assertEquals(HeosCommand.BROWSE, response.heosCommand.command);
assertTrue(response.result);
assertEquals(Long.valueOf(1025), response.getNumericAttribute(SOURCE_ID));
assertFalse(response.isFinished());
}
@Test
public void incorrect_level() {
HeosResponseObject<Void> response = subject.parseResponse(
"{\"heos\": {\"command\": \"player/set_volume\", \"result\": \"fail\", \"message\": \"eid=9&text=Parameter out of range&pid=-831584083&level=OFF\"}}",
Void.class);
assertEquals(HeosCommandGroup.PLAYER, response.heosCommand.commandGroup);
assertEquals(HeosCommand.SET_VOLUME, response.heosCommand.command);
assertFalse(response.result);
assertEquals(HeosErrorCode.PARAMETER_OUT_OF_RANGE, response.getError().code);
assertEquals("#9: Parameter out of range", response.getError().code.toString());
}
@Test
public void get_players() {
HeosResponseObject<Player[]> response = subject.parseResponse(
"{\"heos\": {\"command\": \"player/get_players\", \"result\": \"success\", \"message\": \"\"}, \"payload\": ["
+ "{\"name\": \"Kantoor HEOS 3\", \"pid\": -831584083, \"model\": \"HEOS 3\", \"version\": \"1.520.200\", \"ip\": \"192.168.1.230\", \"network\": \"wired\", \"lineout\": 0, \"serial\": \"ACNG9180110887\"}, "
+ "{\"name\": \"HEOS Bar\", \"pid\": 1958912779, \"model\": \"HEOS Bar\", \"version\": \"1.520.200\", \"ip\": \"192.168.1.195\", \"network\": \"wired\", \"lineout\": 0, \"serial\": \"ADAG9180917029\"}]}",
Player[].class);
assertEquals(HeosCommandGroup.PLAYER, response.heosCommand.commandGroup);
assertEquals(HeosCommand.GET_PLAYERS, response.heosCommand.command);
assertTrue(response.result);
assertEquals(2, response.payload.length);
Player player0 = response.payload[0];
assertEquals("Kantoor HEOS 3", player0.name);
assertEquals(-831584083, player0.playerId);
assertEquals("HEOS 3", player0.model);
assertEquals("1.520.200", player0.version);
assertEquals("192.168.1.230", player0.ip);
assertEquals("wired", player0.network);
assertEquals(0, player0.lineout);
assertEquals("ACNG9180110887", player0.serial);
Player player1 = response.payload[1];
assertEquals("HEOS Bar", player1.name);
assertEquals(1958912779, player1.playerId);
assertEquals("HEOS Bar", player1.model);
assertEquals("1.520.200", player1.version);
assertEquals("192.168.1.195", player1.ip);
assertEquals("wired", player1.network);
assertEquals(0, player1.lineout);
assertEquals("ADAG9180917029", player1.serial);
}
@Test
public void get_player_info() {
HeosResponseObject<Player> response = subject.parseResponse(
"{\"heos\": {\"command\": \"player/get_player_info\", \"result\": \"success\", \"message\": \"pid=1958912779\"}, \"payload\": {\"name\": \"HEOS Bar\", \"pid\": 1958912779, \"model\": \"HEOS Bar\", \"version\": \"1.520.200\", \"ip\": \"192.168.1.195\", \"network\": \"wired\", \"lineout\": 0, \"serial\": \"ADAG9180917029\"}}",
Player.class);
assertEquals(HeosCommandGroup.PLAYER, response.heosCommand.commandGroup);
assertEquals(HeosCommand.GET_PLAYER_INFO, response.heosCommand.command);
assertTrue(response.result);
assertEquals("HEOS Bar", response.payload.name);
assertEquals(1958912779, response.payload.playerId);
assertEquals("HEOS Bar", response.payload.model);
assertEquals("1.520.200", response.payload.version);
assertEquals("192.168.1.195", response.payload.ip);
assertEquals("wired", response.payload.network);
assertEquals(0, response.payload.lineout);
assertEquals("ADAG9180917029", response.payload.serial);
}
@Test
public void get_now_playing_media() {
HeosResponseObject<Media> response = subject.parseResponse(
"{\"heos\": {\"command\": \"player/get_now_playing_media\", \"result\": \"success\", \"message\": \"pid=1958912779\"}, \"payload\": "
+ "{\"type\": \"song\", \"song\": \"Solo (feat. Demi Lovato)\", \"album\": \"What Is Love? (Deluxe)\", \"artist\": \"Clean Bandit\", \"image_url\": \"http://192.168.1.230:8015//m-browsableMediaUri/getImageFromTag/mnt/326C72A3E307501E47DE2B0F47D90EB8/Clean%20Bandit/What%20Is%20Love_%20(Deluxe)/03%20Solo%20(feat.%20Demi%20Lovato).m4a\", \"album_id\": \"\", \"mid\": \"http://192.168.1.230:8015/m-1c176905-f6c7-d168-dc35-86b4735c5976/Clean+Bandit/What+Is+Love_+(Deluxe)/03+Solo+(feat.+Demi+Lovato).m4a\", \"qid\": 1, \"sid\": 1024}, \"options\": []}\n",
Media.class);
assertEquals(HeosCommandGroup.PLAYER, response.heosCommand.commandGroup);
assertEquals(HeosCommand.GET_NOW_PLAYING_MEDIA, response.heosCommand.command);
assertTrue(response.result);
assertEquals(Long.valueOf(1958912779), response.getNumericAttribute(PLAYER_ID));
assertEquals("song", response.payload.type);
assertEquals("Solo (feat. Demi Lovato)", response.payload.song);
assertEquals("What Is Love? (Deluxe)", response.payload.album);
assertEquals("Clean Bandit", response.payload.artist);
assertEquals(
"http://192.168.1.230:8015//m-browsableMediaUri/getImageFromTag/mnt/326C72A3E307501E47DE2B0F47D90EB8/Clean%20Bandit/What%20Is%20Love_%20(Deluxe)/03%20Solo%20(feat.%20Demi%20Lovato).m4a",
response.payload.imageUrl);
assertEquals("", response.payload.albumId);
assertEquals(
"http://192.168.1.230:8015/m-1c176905-f6c7-d168-dc35-86b4735c5976/Clean+Bandit/What+Is+Love_+(Deluxe)/03+Solo+(feat.+Demi+Lovato).m4a",
response.payload.mediaId);
assertEquals(1, response.payload.queueId);
assertEquals(1024, response.payload.sourceId);
}
@Test
public void browse_playlist() {
HeosResponseObject<BrowseResult[]> response = subject.parseResponse(
"{\"heos\": {\"command\": \"browse/browse\", \"result\": \"success\", \"message\": \"sid=1025&returned=6&count=6\"}, \"payload\": ["
+ "{\"container\": \"yes\", \"type\": \"playlist\", \"cid\": \"132562\", \"playable\": \"yes\", \"name\": \"Maaike Ouboter - En hoe het dan ook weer dag wordt\", \"image_url\": \"\"}, "
+ "{\"container\": \"yes\", \"type\": \"playlist\", \"cid\": \"132563\", \"playable\": \"yes\", \"name\": \"Maaike Ouboter - Vanaf nu is het van jou\", \"image_url\": \"\"}, "
+ "{\"container\": \"yes\", \"type\": \"playlist\", \"cid\": \"162887\", \"playable\": \"yes\", \"name\": \"Easy listening\", \"image_url\": \"\"}, "
+ "{\"container\": \"yes\", \"type\": \"playlist\", \"cid\": \"174461\", \"playable\": \"yes\", \"name\": \"Nieuwe muziek 5-2019\", \"image_url\": \"\"}, "
+ "{\"container\": \"yes\", \"type\": \"playlist\", \"cid\": \"194000\", \"playable\": \"yes\", \"name\": \"Nieuwe muziek 2019-05\", \"image_url\": \"\"}, "
+ "{\"container\": \"yes\", \"type\": \"playlist\", \"cid\": \"194001\", \"playable\": \"yes\", \"name\": \"Clean Bandit\", \"image_url\": \"\"}]}",
BrowseResult[].class);
assertEquals(HeosCommandGroup.BROWSE, response.heosCommand.commandGroup);
assertEquals(HeosCommand.BROWSE, response.heosCommand.command);
assertTrue(response.result);
assertEquals(Long.valueOf(1025), response.getNumericAttribute(SOURCE_ID));
assertEquals(Long.valueOf(6), response.getNumericAttribute(RETURNED));
assertEquals(Long.valueOf(6), response.getNumericAttribute(COUNT));
BrowseResult result = response.payload[5];
assertEquals(YesNoEnum.YES, result.container);
assertEquals(BrowseResultType.PLAYLIST, result.type);
assertEquals(YesNoEnum.YES, result.playable);
assertEquals("194001", result.containerId);
assertEquals("Clean Bandit", result.name);
assertEquals("", result.imageUrl);
}
@Test
public void browse_favorites() {
HeosResponseObject<BrowseResult[]> response = subject.parseResponse(
"{\"heos\": {\"command\": \"browse/browse\", \"result\": \"success\", \"message\": \"sid=1028&returned=3&count=3\"}, \"payload\": ["
+ "{\"container\": \"no\", \"mid\": \"s6707\", \"type\": \"station\", \"playable\": \"yes\", \"name\": \"NPO 3FM 96.8 (Top 40 %26 Pop Music)\", \"image_url\": \"http://cdn-profiles.tunein.com/s6707/images/logoq.png?t=636268\"}, "
+ "{\"container\": \"no\", \"mid\": \"s2967\", \"type\": \"station\", \"playable\": \"yes\", \"name\": \"Classic FM Nederland (Classical Music)\", \"image_url\": \"http://cdn-radiotime-logos.tunein.com/s2967q.png\"}, "
+ "{\"container\": \"no\", \"mid\": \"s1993\", \"type\": \"station\", \"playable\": \"yes\", \"name\": \"BNR Nieuwsradio\", \"image_url\": \"http://cdn-radiotime-logos.tunein.com/s1993q.png\"}], "
+ "\"options\": [{\"browse\": [{\"id\": 20, \"name\": \"Remove from HEOS Favorites\"}]}]}",
BrowseResult[].class);
assertEquals(HeosCommandGroup.BROWSE, response.heosCommand.commandGroup);
assertEquals(HeosCommand.BROWSE, response.heosCommand.command);
assertTrue(response.result);
assertEquals(Long.valueOf(1028), response.getNumericAttribute(SOURCE_ID));
assertEquals(Long.valueOf(3), response.getNumericAttribute(RETURNED));
assertEquals(Long.valueOf(3), response.getNumericAttribute(COUNT));
BrowseResult result = response.payload[0];
assertEquals(YesNoEnum.NO, result.container);
assertEquals("s6707", result.mediaId);
assertEquals(BrowseResultType.STATION, result.type);
assertEquals(YesNoEnum.YES, result.playable);
assertEquals("NPO 3FM 96.8 (Top 40 %26 Pop Music)", result.name);
assertEquals("http://cdn-profiles.tunein.com/s6707/images/logoq.png?t=636268", result.imageUrl);
// TODO validate options
}
@Test
public void get_groups() {
HeosResponseObject<Group[]> response = subject.parseResponse(
"{\"heos\": {\"command\": \"group/get_groups\", \"result\": \"success\", \"message\": \"\"}, \"payload\": [ "
+ "{\"name\": \"Group 1\", \"gid\": \"214243242\", \"players\": [ {\"name\": \"HEOS 1\", \"pid\": \"2142443242\", \"role\": \"leader\"}, {\"name\": \"HEOS 3\", \"pid\": \"32432423432\", \"role\": \"member\"}, {\"name\": \"HEOS 5\", \"pid\": \"342423564\", \"role\": \"member\"}]}, "
+ "{\"name\": \"Group 2\", \"gid\": \"2142432342\", \"players\": [ {\"name\": \"HEOS 3\", \"pid\": \"32432423432\", \"role\": \"member\"}, {\"name\": \"HEOS 5\", \"pid\": \"342423564\", \"role\": \"member\"}]}]}",
Group[].class);
assertEquals(HeosCommandGroup.GROUP, response.heosCommand.commandGroup);
assertEquals(HeosCommand.GET_GROUPS, response.heosCommand.command);
assertTrue(response.result);
Group group = response.payload[0];
assertEquals("Group 1", group.name);
assertEquals("214243242", group.id);
List<Group.Player> players = group.players;
Group.Player player0 = players.get(0);
assertEquals("HEOS 1", player0.name);
assertEquals("2142443242", player0.id);
assertEquals(GroupPlayerRole.LEADER, player0.role);
Group.Player player1 = players.get(1);
assertEquals("HEOS 3", player1.name);
assertEquals("32432423432", player1.id);
assertEquals(GroupPlayerRole.MEMBER, player1.role);
}
}

View File

@@ -0,0 +1,130 @@
/**
* 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.heos.internal.json.dto;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.openhab.binding.heos.internal.json.dto.HeosCommand.*;
import static org.openhab.binding.heos.internal.json.dto.HeosCommandGroup.SYSTEM;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.junit.Test;
/**
* Tests to validate the functioning of the HeosCommandTuple
*
* @author Martin van Wingerden - Initial Contribution
*/
@NonNullByDefault
public class HeosCommandTupleTest {
@Test
public void system() {
assertMatches("system/check_account", SYSTEM, CHECK_ACCOUNT);
assertMatches("system/sign_in", SYSTEM, SIGN_IN);
assertMatches("system/sign_out", SYSTEM, SIGN_OUT);
assertMatches("system/register_for_change_events", SYSTEM, REGISTER_FOR_CHANGE_EVENTS);
assertMatches("system/heart_beat", SYSTEM, HEART_BEAT);
assertMatches("system/prettify_json_response", SYSTEM, PRETTIFY_JSON_RESPONSE);
}
@Test
public void browse() {
assertMatches("browse/browse", HeosCommandGroup.BROWSE, HeosCommand.BROWSE);
assertMatches("browse/get_music_sources", HeosCommandGroup.BROWSE, HeosCommand.GET_MUSIC_SOURCES);
assertMatches("browse/get_source_info", HeosCommandGroup.BROWSE, HeosCommand.GET_SOURCE_INFO);
assertMatches("browse/get_search_criteria", HeosCommandGroup.BROWSE, HeosCommand.GET_SEARCH_CRITERIA);
assertMatches("browse/search", HeosCommandGroup.BROWSE, HeosCommand.SEARCH);
assertMatches("browse/play_stream", HeosCommandGroup.BROWSE, HeosCommand.PLAY_STREAM);
assertMatches("browse/play_preset", HeosCommandGroup.BROWSE, HeosCommand.PLAY_PRESET);
assertMatches("browse/play_input", HeosCommandGroup.BROWSE, HeosCommand.PLAY_INPUT);
assertMatches("browse/play_stream", HeosCommandGroup.BROWSE, HeosCommand.PLAY_STREAM);
assertMatches("browse/add_to_queue", HeosCommandGroup.BROWSE, HeosCommand.ADD_TO_QUEUE);
assertMatches("browse/rename_playlist", HeosCommandGroup.BROWSE, HeosCommand.RENAME_PLAYLIST);
assertMatches("browse/delete_playlist", HeosCommandGroup.BROWSE, HeosCommand.DELETE_PLAYLIST);
assertMatches("browse/retrieve_metadata", HeosCommandGroup.BROWSE, HeosCommand.RETRIEVE_METADATA);
}
@Test
public void player() {
assertMatches("player/get_players", HeosCommandGroup.PLAYER, HeosCommand.GET_PLAYERS);
assertMatches("player/get_player_info", HeosCommandGroup.PLAYER, HeosCommand.GET_PLAYER_INFO);
assertMatches("player/get_play_state", HeosCommandGroup.PLAYER, HeosCommand.GET_PLAY_STATE);
assertMatches("player/set_play_state", HeosCommandGroup.PLAYER, HeosCommand.SET_PLAY_STATE);
assertMatches("player/get_now_playing_media", HeosCommandGroup.PLAYER, HeosCommand.GET_NOW_PLAYING_MEDIA);
assertMatches("player/get_volume", HeosCommandGroup.PLAYER, HeosCommand.GET_VOLUME);
assertMatches("player/set_volume", HeosCommandGroup.PLAYER, HeosCommand.SET_VOLUME);
assertMatches("player/volume_up", HeosCommandGroup.PLAYER, HeosCommand.VOLUME_UP);
assertMatches("player/volume_down", HeosCommandGroup.PLAYER, HeosCommand.VOLUME_DOWN);
assertMatches("player/get_mute", HeosCommandGroup.PLAYER, HeosCommand.GET_MUTE);
assertMatches("player/set_mute", HeosCommandGroup.PLAYER, HeosCommand.SET_MUTE);
assertMatches("player/toggle_mute", HeosCommandGroup.PLAYER, HeosCommand.TOGGLE_MUTE);
assertMatches("player/get_play_mode", HeosCommandGroup.PLAYER, HeosCommand.GET_PLAY_MODE);
assertMatches("player/set_play_mode", HeosCommandGroup.PLAYER, HeosCommand.SET_PLAY_MODE);
assertMatches("player/get_queue", HeosCommandGroup.PLAYER, HeosCommand.GET_QUEUE);
assertMatches("player/play_queue", HeosCommandGroup.PLAYER, HeosCommand.PLAY_QUEUE);
assertMatches("player/remove_from_queue", HeosCommandGroup.PLAYER, HeosCommand.REMOVE_FROM_QUEUE);
assertMatches("player/save_queue", HeosCommandGroup.PLAYER, HeosCommand.SAVE_QUEUE);
assertMatches("player/clear_queue", HeosCommandGroup.PLAYER, HeosCommand.CLEAR_QUEUE);
assertMatches("player/move_queue_item", HeosCommandGroup.PLAYER, HeosCommand.MOVE_QUEUE_ITEM);
assertMatches("player/play_next", HeosCommandGroup.PLAYER, HeosCommand.PLAY_NEXT);
assertMatches("player/play_previous", HeosCommandGroup.PLAYER, HeosCommand.PLAY_PREVIOUS);
assertMatches("player/set_quickselect", HeosCommandGroup.PLAYER, HeosCommand.SET_QUICKSELECT);
assertMatches("player/play_quickselect", HeosCommandGroup.PLAYER, HeosCommand.PLAY_QUICKSELECT);
assertMatches("player/get_quickselects", HeosCommandGroup.PLAYER, HeosCommand.GET_QUICKSELECTS);
assertMatches("player/check_update", HeosCommandGroup.PLAYER, HeosCommand.CHECK_UPDATE);
}
@Test
public void group() {
assertMatches("group/get_groups", HeosCommandGroup.GROUP, HeosCommand.GET_GROUPS);
assertMatches("group/get_group_info", HeosCommandGroup.GROUP, HeosCommand.GET_GROUP_INFO);
assertMatches("group/set_group", HeosCommandGroup.GROUP, HeosCommand.SET_GROUP);
assertMatches("group/get_volume", HeosCommandGroup.GROUP, HeosCommand.GET_VOLUME);
assertMatches("group/set_volume", HeosCommandGroup.GROUP, HeosCommand.SET_VOLUME);
assertMatches("group/volume_up", HeosCommandGroup.GROUP, HeosCommand.VOLUME_UP);
assertMatches("group/volume_down", HeosCommandGroup.GROUP, HeosCommand.VOLUME_DOWN);
assertMatches("group/get_mute", HeosCommandGroup.GROUP, HeosCommand.GET_MUTE);
assertMatches("group/set_mute", HeosCommandGroup.GROUP, HeosCommand.SET_MUTE);
assertMatches("group/toggle_mute", HeosCommandGroup.GROUP, HeosCommand.TOGGLE_MUTE);
}
private void assertMatches(String command, HeosCommandGroup commandGroup, HeosCommand heosCommand) {
HeosCommandTuple tuple = HeosCommandTuple.valueOf(command);
assertNotNull(tuple);
assertEquals(commandGroup, tuple.commandGroup);
assertEquals(heosCommand, tuple.command);
}
/**
* "browse/browse"
* "group/get_groups"
* "player/get_mute"
* "player/get_now_playing_media"
* "player/get_player_info"
* "player/get_players"
* "player/get_play_mode"
* "player/get_play_state"
* "player/get_volume"
* "player/play_next"
* "player/play_previous"
* "player/set_mute"
* "player/set_play_mode"
* "player/set_play_state"
* "player/set_volume"
* "player/volume_down"
* "system/heart_beat"
* "system/register_for_change_events"
* "system/sign_in"
*/
}