added migrated 2.x add-ons
Signed-off-by: Kai Kreuzer <kai@openhab.org>
This commit is contained in:
32
bundles/org.openhab.binding.heos/.classpath
Normal file
32
bundles/org.openhab.binding.heos/.classpath
Normal 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>
|
||||
23
bundles/org.openhab.binding.heos/.project
Normal file
23
bundles/org.openhab.binding.heos/.project
Normal 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>
|
||||
13
bundles/org.openhab.binding.heos/NOTICE
Normal file
13
bundles/org.openhab.binding.heos/NOTICE
Normal 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
|
||||
410
bundles/org.openhab.binding.heos/README.md
Normal file
410
bundles/org.openhab.binding.heos/README.md
Normal 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.
|
||||
17
bundles/org.openhab.binding.heos/pom.xml
Normal file
17
bundles/org.openhab.binding.heos/pom.xml
Normal 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>
|
||||
@@ -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>
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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 = "";
|
||||
}
|
||||
@@ -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 = "";
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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()));
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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 + '}';
|
||||
}
|
||||
}
|
||||
@@ -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"));
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 + '}';
|
||||
}
|
||||
}
|
||||
@@ -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 + '}';
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -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 + '\'' + '}';
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
@@ -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 + '}';
|
||||
}
|
||||
}
|
||||
@@ -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 + '\'' + '}';
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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";
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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"}}
|
||||
*
|
||||
*/
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
*/
|
||||
}
|
||||
Reference in New Issue
Block a user