mirror of
https://github.com/openhab/openhab-addons.git
synced 2025-01-10 15:11:59 +01:00
[plex] Initial contribution (#15057)
* aronbeurskens plex baseline Signed-off-by: Michael Lobstein <michael.lobstein@gmail.com> * review changes Signed-off-by: Michael Lobstein <michael.lobstein@gmail.com> * Clean-up PlexApiConnector exception handling Signed-off-by: Michael Lobstein <michael.lobstein@gmail.com> * Additional clean-up Signed-off-by: Michael Lobstein <michael.lobstein@gmail.com> * review changes Signed-off-by: Michael Lobstein <michael.lobstein@gmail.com> * review changes Signed-off-by: Michael Lobstein <michael.lobstein@gmail.com> --------- Signed-off-by: Michael Lobstein <michael.lobstein@gmail.com>
This commit is contained in:
parent
ddffda3522
commit
44a78b99f5
@ -267,6 +267,7 @@
|
||||
/bundles/org.openhab.binding.pjlinkdevice/ @nils
|
||||
/bundles/org.openhab.binding.playstation/ @FluBBaOfWard
|
||||
/bundles/org.openhab.binding.plclogo/ @falkena
|
||||
/bundles/org.openhab.binding.plex/ @aronbeurskens
|
||||
/bundles/org.openhab.binding.plugwise/ @wborn
|
||||
/bundles/org.openhab.binding.plugwiseha/ @lsiepel
|
||||
/bundles/org.openhab.binding.powermax/ @lolodomo
|
||||
|
@ -1326,6 +1326,11 @@
|
||||
<artifactId>org.openhab.binding.plclogo</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.openhab.addons.bundles</groupId>
|
||||
<artifactId>org.openhab.binding.plex</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.openhab.addons.bundles</groupId>
|
||||
<artifactId>org.openhab.binding.plugwise</artifactId>
|
||||
|
13
bundles/org.openhab.binding.plex/NOTICE
Normal file
13
bundles/org.openhab.binding.plex/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
|
167
bundles/org.openhab.binding.plex/README.md
Normal file
167
bundles/org.openhab.binding.plex/README.md
Normal file
@ -0,0 +1,167 @@
|
||||
# PLEX Binding
|
||||
|
||||
This binding can read information from multiple PLEX players connected to a PLEX server.
|
||||
|
||||
It can be used for multiple scenarios:
|
||||
|
||||
* Drive light changes based on player status. For instances turn off the lights when movie starts playing and turn them back on when movie is stopped/paused
|
||||
* Create a page that displays currently played media of one or more player connected to the server.
|
||||
* Send social media messages when player plays new media
|
||||
* Inform what the end time of the currently played media is
|
||||
|
||||
The binding can also control `PLAY/PAUSE/NEXT/PREVIOUS` the players which can be used for:
|
||||
|
||||
* Start playing some music when someone enters a room
|
||||
* Pause the movie when motion is detected
|
||||
|
||||
## Supported Things
|
||||
|
||||
This binding supports 2 things.
|
||||
|
||||
- `server`: The PLEX server will act as a bridge to read out the information from all connected players
|
||||
- `player`: A PLEX client of any type / os connected to the server.
|
||||
|
||||
## Discovery
|
||||
|
||||
For the auto discovery to work correctly you first need to configure and add the `PLEX Server` Thing.
|
||||
Next step is to *PLAY* something on the desired player. Only when media is played on the player it will show up in the auto discovery!
|
||||
|
||||
## Thing Configuration
|
||||
|
||||
The PLEX Server needs to be configured first. The hostname of the PLEX server is mandatory and the either the PLEX token (recommended) or the username/password of the PLEX server (not recommended).
|
||||
|
||||
Then find the PLEX token please follow the instructions from the PLEX support forum:
|
||||
|
||||
1. Sign in to your Plex account in Plex Web App
|
||||
2. Browse to a library item and view the XML for it
|
||||
3. Look in the URL and find the token as the X-Plex-Token value
|
||||
|
||||
### `PLEX Server` Thing Configuration
|
||||
|
||||
| Name | Type | Description | Default | Required | Advanced |
|
||||
|-------------|---------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|---------|----------|---------|
|
||||
| host | text | PLEX host name or IP address | N/A | yes | no |
|
||||
| portNumber | integer | Port Number (leave blank if PLEX installed on default port) | 32400 | no | no |
|
||||
| refreshRate | integer | Interval in seconds at which PLEX server status is polled | 5 | no | no |
|
||||
| username | text | If you're using Plex Home you need to supply the username and password of your Plex account here. If you don't want to enter your credentials you can also directly set your account token below instead. | N/A | no | no |
|
||||
| password | text | If you're using Plex Home you need to supply the username and password of your Plex account here. If you don't want to enter your credentials you can also directly set your account token below instead. | N/A | no | no |
|
||||
| token | text | The authentication token when username/password is left blank | N/A | no | no |
|
||||
|
||||
### `PLEX Player` Thing Configuration
|
||||
|
||||
You can add multiple PLEX players. You can choose to find the player by autodiscovery or add them manually.
|
||||
|
||||
#### Autodiscovery
|
||||
|
||||
Turn on the player you want to add and *play* some media on it. Navigate to `/settings/things/add/plex` and start the auto discover.
|
||||
The player will be found and you can add it.
|
||||
|
||||
#### Manual adding a player Thing
|
||||
|
||||
When you want to add them manually go to the following url [https://plex.tv/devices.xml] and login when needed.
|
||||
|
||||
It will display the following XML file.
|
||||
|
||||
```xml
|
||||
<MediaContainer publicAddress="XXX.XXX.XXX.XXX">
|
||||
<Device name="iPhone" publicAddress="XXX.XXX.XXX.XXX" product="Plex for iOS" productVersion="8.4" platform="iOS" platformVersion="15.5" device="iPhone" model="14,5" vendor="Apple" provides="client,controller,sync-target,player,pubsub-player,provider-playback" clientIdentifier="B03466F7-BEEB-405F-A315-C7BBAA2D3FAE" version="8.4" id="547394701" token="XXX" createdAt="1633194400" lastSeenAt="1655715607" screenResolution="1170x2532" screenDensity="3">
|
||||
<SyncList itemsCompleteCount="0" totalSize="0" version="2"/>
|
||||
<Connection uri="http://192.168.1.194:32500"/>
|
||||
</Device>
|
||||
<Device name="Chrome" publicAddress="XXX.XXX.XXX.XXX" product="Plex Web" productVersion="4.83.2" platform="Chrome" platformVersion="102.0" device="Linux" model="hosted" vendor="" provides="" clientIdentifier="e29nk766fd48skpm8uuu1x9l" version="4.83.2" id="660497510" token="XXX" createdAt="1655714525" lastSeenAt="1655714526" screenResolution="1920x975,1920x1080" screenDensity=""> </Device>
|
||||
<Device name="MY PLEX SERVER" publicAddress="XXX.XXX.XXX.XXX" product="Plex Media Server" productVersion="1.27.0.5897-3940636f2" platform="Linux" platformVersion="20.04.4 LTS (Focal Fossa)" device="PC" model="x86_64" vendor="Ubuntu" provides="server" clientIdentifier="906a992fc4c5722595f36732838bcc330700b2af" version="1.27.0.5897-3940636f2" id="282416069" token="XXX" createdAt="1560609688" lastSeenAt="1655709241" screenResolution="" screenDensity="">
|
||||
<Connection uri="http://[2001:1c05:380f:5700:642:1aff:fe08:1c22]:32400"/>
|
||||
<Connection uri="http://192.168.1.2:32400"/>
|
||||
</Device>
|
||||
<Device name="BRAVIA 4K UR2" publicAddress="XXX.XXX.XXX.XXX" product="Plex for Android (TV)" productVersion="8.26.2.29389" platform="Android" platformVersion="9" device="BRAVIA 4K UR2" model="BRAVIA_UR2_4K" vendor="Sony" provides="player,pubsub-player,controller" clientIdentifier="97d2510bd3942159-com-plexapp-android" version="8.26.2.29389" id="403947762" token="XXX" createdAt="1601489892" lastSeenAt="1655701261" screenResolution="1920x1080" screenDensity="320">
|
||||
<Connection uri="http://192.168.1.19:32500"/>
|
||||
</Device>
|
||||
<Device name="SHIELD Android TV" publicAddress="XXX.XXX.XXX.XXX" product="Plex for Android (TV)" productVersion="8.26.2.29389" platform="Android" platformVersion="11" device="SHIELD Android TV" model="mdarcy" vendor="NVIDIA" provides="player,pubsub-player,controller" clientIdentifier="2c098c67afd0ca79-com-plexapp-android" version="8.26.2.29389" id="508114660" token="XXX" createdAt="1625317867" lastSeenAt="1655693424" screenResolution="1920x1080" screenDensity="320">
|
||||
<Connection uri="http://192.168.1.6:32500"/>
|
||||
</Device>
|
||||
<Device name="iPad" publicAddress="XXX.XXX.XXX.XXX" product="Plex for iOS" productVersion="8.0" platform="iOS" platformVersion="14.7.1" device="iPad" model="5,3" vendor="Apple" provides="client,controller,sync-target,player,pubsub-player,provider-playback" clientIdentifier="D9629798-4B91-4375-8844-C62400573E42" version="8.0" id="617442968" token="XXX" createdAt="1646760020" lastSeenAt="1647973844" screenResolution="2048x1536" screenDensity="2">
|
||||
<SyncList itemsCompleteCount="0" totalSize="0" version="2"/>
|
||||
<Connection uri="http://192.168.1.220:32500"/>
|
||||
</Device>
|
||||
<Device name="MacBook-Pro.local" publicAddress="XXX.XXX.XXX.XXX" product="Plex for Mac" productVersion="1.41.0.2876-e960c9ca" platform="osx" platformVersion="12.2" device="" model="standalone" vendor="" provides="client,player,pubsub-player" clientIdentifier="5ehipgz2ca60ikqnv9jrgojx" version="1.41.0.2876-e960c9ca" id="507110703" token="XXX" createdAt="1625081186" lastSeenAt="1647973509" screenResolution="1680x1050,1680x1050" screenDensity=""> </Device>
|
||||
</MediaContainer>
|
||||
```
|
||||
|
||||
Find the `Device` block of the player you want to add and fill in the `clientIdentifier` as `playerID`
|
||||
|
||||
| Name | Type | Description | Default | Required | Advanced |
|
||||
|-------------|---------|--------------------------------------------------------------------------------------------|---------|----------|---------|
|
||||
| playerID | text | The unique identifier of the player. `clientIdentifier` from [https://plex.tv/devices.xml] | N/A | yes | no |
|
||||
|
||||
## Channels
|
||||
|
||||
The PLEX Player supports the following channels:
|
||||
|
||||
| Channel | Type | Read/Write | Description |
|
||||
|----------------------|----------|------------|-----------------------------------------------------------------------|
|
||||
| currentPlayers | Number | RO | The number of players currently configured to watch on PLEX |
|
||||
| currentPlayersActive | Number | RO | The number of players currently being used on PLEX |
|
||||
| state | String | RO | The current state of the Player (BUFFERING, PLAYING, PAUSED, STOPPED) |
|
||||
| power | Switch | RO | The power status of the player |
|
||||
| title | String | RO | The title of media that is playing |
|
||||
| type | String | RO | The current type of playing media |
|
||||
| endtime | DateTime | RO | Time at which the media that is playing will end |
|
||||
| progress | Dimmer | RO | The current progress of playing media |
|
||||
| art | String | RO | The URL of the background art for currently playing media |
|
||||
| thumb | String | RO | The URL of the cover art for currently playing media |
|
||||
| player | Player | RW | The control channel for the player `PLAY/PAUSE/NEXT/PREVIOUS` |
|
||||
|
||||
## Full Example
|
||||
|
||||
`.things` file:
|
||||
|
||||
```java
|
||||
Bridge plex:server:plexrServer "Bridge Plex : Plex" [host="IP.Address.Or.Hostname", token="SadhjsajjA3AG", refreshRate=5]
|
||||
{
|
||||
Thing plex:player:MyViewerName01 "My Viewer Name 01" [playerID="ClientIdentifierFromDevices.XML1"]
|
||||
Thing plex:player:MyViewerName02 "My Viewer Name 02" [playerID="ClientIdentifierFromDevices.XML2"]
|
||||
}
|
||||
```
|
||||
|
||||
`.items` file
|
||||
|
||||
```java
|
||||
String BridgePlexCurrent "Current players" {channel="plex:server:plexrServer:currentPlayers"}
|
||||
String BridgePlexCurrentActive "Current players active" {channel="plex:server:plexrServer:currentPlayersActive"}
|
||||
Switch PlexTVPower01 "Power" {channel="plex:player:MyViewerName01:power"}
|
||||
String PlexTVStatus01 "Status [%s]" {channel="plex:player:MyViewerName01:state"}
|
||||
String PlexTVTitle01 "Title [%s]" {channel="plex:player:MyViewerName01:title"}
|
||||
String PlexTVType01 "Type [%s]" {channel="plex:player:MyViewerName01:type"}
|
||||
String PlexTVEndTime01 "End time" {channel="plex:player:MyViewerName01:endtime"}
|
||||
Dimmer PlexTVProgress01 "Progress [%.1f%%]" {channel="plex:player:MyViewerName01:progress"}
|
||||
String PlexTVCover1 "Cover" {channel="plex:player:MyViewerName01:thumb"}
|
||||
String ShellArt01 "Background art" {channel="plex:player:MyViewerName01:art"}
|
||||
Switch PlexTVPower02 "Power" {channel="plex:player:MyViewerName02:power"}
|
||||
String PlexTVStatus02 "Status [%s]" {channel="plex:player:MyViewerName02:state"}
|
||||
String PlexTVTitle02 "Title [%s]" {channel="plex:player:MyViewerName02:title"}
|
||||
String PlexTVType02 "Type [%s]" {channel="plex:player:MyViewerName02:type"}
|
||||
String PlexTVEndTime02 "End time" {channel="plex:player:MyViewerName02:endtime"}
|
||||
Dimmer PlexTVProgress02 "Progress [%.1f%%]" {channel="plex:player:MyViewerName02:progress"}
|
||||
String PlexTVCover2 "Cover" {channel="plex:player:MyViewerName02:thumb"}
|
||||
String ShellArt02 "Background art" {channel="plex:player:MyViewerName02:art"}
|
||||
```
|
||||
|
||||
`.rules` file
|
||||
|
||||
```java
|
||||
rule "Send telegram with title for My Viewer Name 01"
|
||||
when
|
||||
Item PlexTVTitle01 changed
|
||||
then
|
||||
val telegramActionPlexBot = getActions("telegram","telegram:telegramBot:PlexBot")
|
||||
telegramActionPlexBot.sendTelegram("Bedroom Roku is watching %s", PlexTVTitle01.state.toString)
|
||||
end
|
||||
|
||||
rule "Send telegram with title for My Viewer Name 02"
|
||||
when
|
||||
Item PlexTVTitle02 changed
|
||||
then
|
||||
val telegramActionPlexBot = getActions("telegram","telegram:telegramBot:PlexBot")
|
||||
telegramActionPlexBot.sendTelegram("Bedroom Roku is watching %s", PlexTVTitle02.state.toString)
|
||||
end
|
||||
```
|
17
bundles/org.openhab.binding.plex/pom.xml
Normal file
17
bundles/org.openhab.binding.plex/pom.xml
Normal file
@ -0,0 +1,17 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://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>4.0.0-SNAPSHOT</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>org.openhab.binding.plex</artifactId>
|
||||
|
||||
<name>openHAB Add-ons :: Bundles :: Plex Binding</name>
|
||||
|
||||
</project>
|
@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<features name="org.openhab.binding.plex-${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-plex" description="Plex Binding" version="${project.version}">
|
||||
<feature>openhab-runtime-base</feature>
|
||||
<feature dependency="true">openhab.tp-jaxb</feature>
|
||||
<bundle start-level="80">mvn:org.openhab.addons.bundles/org.openhab.binding.plex/${project.version}</bundle>
|
||||
</feature>
|
||||
</features>
|
@ -0,0 +1,58 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2023 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.plex.discovery;
|
||||
|
||||
import static org.openhab.binding.plex.internal.PlexBindingConstants.*;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.openhab.binding.plex.internal.handler.PlexServerHandler;
|
||||
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.ThingTypeUID;
|
||||
import org.openhab.core.thing.ThingUID;
|
||||
|
||||
/**
|
||||
* @author Brian Homeyer - Initial contribution
|
||||
* @author Aron Beurskens - Binding development
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class PlexDiscoveryService extends AbstractDiscoveryService {
|
||||
private final PlexServerHandler bridgeHandler;
|
||||
|
||||
public PlexDiscoveryService(PlexServerHandler bridgeHandler) {
|
||||
super(SUPPORTED_THING_TYPES_UIDS, 10, false);
|
||||
this.bridgeHandler = bridgeHandler;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void startScan() {
|
||||
for (String machineId : bridgeHandler.getAvailablePlayers()) {
|
||||
ThingUID bridgeUID = bridgeHandler.getThing().getUID();
|
||||
ThingTypeUID thingTypeUID = UID_PLAYER;
|
||||
ThingUID playerThingUid = new ThingUID(UID_PLAYER, bridgeUID, machineId);
|
||||
|
||||
Map<String, Object> properties = new HashMap<>();
|
||||
properties.put(CONFIG_PLAYER_ID, machineId);
|
||||
|
||||
DiscoveryResult discoveryResult = DiscoveryResultBuilder.create(playerThingUid).withThingType(thingTypeUID)
|
||||
.withProperties(properties).withBridge(bridgeUID).withRepresentationProperty(CONFIG_PLAYER_ID)
|
||||
.withLabel("PLEX Player (" + machineId + ")").build();
|
||||
|
||||
thingDiscovered(discoveryResult);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,77 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2023 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.plex.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.core.thing.ThingTypeUID;
|
||||
|
||||
/**
|
||||
* The {@link PlexBindingConstants} class defines common constants, which are
|
||||
* used across the whole binding.
|
||||
*
|
||||
* @author Brian Homeyer - Initial contribution
|
||||
* @author Aron Beurskens - Binding development
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class PlexBindingConstants {
|
||||
private static final String BINDING_ID = "plex";
|
||||
|
||||
// Bridge thing
|
||||
public static final String THING_TYPE_SERVER = "server";
|
||||
public static final ThingTypeUID UID_SERVER = new ThingTypeUID(BINDING_ID, THING_TYPE_SERVER);
|
||||
public static final Set<ThingTypeUID> SUPPORTED_SERVER_THING_TYPES_UIDS = Set.of(UID_SERVER);
|
||||
|
||||
// Monitor things
|
||||
public static final String THING_TYPE_PLAYER = "player";
|
||||
public static final ThingTypeUID UID_PLAYER = new ThingTypeUID(BINDING_ID, THING_TYPE_PLAYER);
|
||||
|
||||
// Collection of monitor thing types
|
||||
public static final Set<ThingTypeUID> SUPPORTED_PLAYER_THING_TYPES_UIDS = Set.of(UID_PLAYER);
|
||||
|
||||
// Collection of all supported thing types
|
||||
public static final Set<ThingTypeUID> SUPPORTED_THING_TYPES_UIDS = Collections.unmodifiableSet(
|
||||
Stream.concat(SUPPORTED_PLAYER_THING_TYPES_UIDS.stream(), SUPPORTED_SERVER_THING_TYPES_UIDS.stream())
|
||||
.collect(Collectors.toSet()));
|
||||
// General purpose stuff
|
||||
public static final int DEFAULT_REFRESH_PERIOD_SEC = 5;
|
||||
// Config parameters
|
||||
// Server
|
||||
public static final String CONFIG_HOST = "host";
|
||||
public static final String CONFIG_PORT_NUMBER = "portNumber";
|
||||
public static final String CONFIG_TOKEN = "token";
|
||||
public static final String CONFIG_REFRESH_RATE = "refreshRate";
|
||||
// Player parameters
|
||||
public static final String CONFIG_PLAYER_ID = "playerID";
|
||||
public static final String CONFIG_PLAYER_NAME = "playerName";
|
||||
|
||||
// List of all Channel ids
|
||||
// Server
|
||||
public static final String CHANNEL_SERVER_COUNT = "currentPlayers";
|
||||
public static final String CHANNEL_SERVER_COUNTACTIVE = "currentPlayersActive";
|
||||
// Player
|
||||
public static final String CHANNEL_PLAYER_STATE = "state";
|
||||
public static final String CHANNEL_PLAYER_TITLE = "title";
|
||||
public static final String CHANNEL_PLAYER_TYPE = "type";
|
||||
public static final String CHANNEL_PLAYER_POWER = "power";
|
||||
public static final String CHANNEL_PLAYER_ART = "art";
|
||||
public static final String CHANNEL_PLAYER_THUMB = "thumb";
|
||||
public static final String CHANNEL_PLAYER_PROGRESS = "progress";
|
||||
public static final String CHANNEL_PLAYER_ENDTIME = "endtime";
|
||||
public static final String CHANNEL_PLAYER_CONTROL = "player";
|
||||
public static final String CHANNEL_PLAYER_USER = "user";
|
||||
}
|
@ -0,0 +1,91 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2023 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.plex.internal;
|
||||
|
||||
import static org.openhab.binding.plex.internal.PlexBindingConstants.*;
|
||||
|
||||
import java.util.Hashtable;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.openhab.binding.plex.discovery.PlexDiscoveryService;
|
||||
import org.openhab.binding.plex.internal.handler.PlexPlayerHandler;
|
||||
import org.openhab.binding.plex.internal.handler.PlexServerHandler;
|
||||
import org.openhab.core.config.discovery.DiscoveryService;
|
||||
import org.openhab.core.io.net.http.HttpClientFactory;
|
||||
import org.openhab.core.thing.Bridge;
|
||||
import org.openhab.core.thing.Thing;
|
||||
import org.openhab.core.thing.ThingTypeUID;
|
||||
import org.openhab.core.thing.binding.BaseThingHandlerFactory;
|
||||
import org.openhab.core.thing.binding.ThingHandler;
|
||||
import org.openhab.core.thing.binding.ThingHandlerFactory;
|
||||
import org.osgi.framework.ServiceRegistration;
|
||||
import org.osgi.service.component.annotations.Activate;
|
||||
import org.osgi.service.component.annotations.Component;
|
||||
import org.osgi.service.component.annotations.Reference;
|
||||
|
||||
/**
|
||||
* The {@link PlexHandlerFactory} is responsible for creating things and thing
|
||||
* handlers.
|
||||
*
|
||||
* @author Brian Homeyer - Initial contribution
|
||||
* @author Aron Beurskens - Binding development
|
||||
*/
|
||||
@NonNullByDefault
|
||||
@Component(configurationPid = "binding.plex", service = ThingHandlerFactory.class)
|
||||
public class PlexHandlerFactory extends BaseThingHandlerFactory {
|
||||
private final HttpClientFactory httpClientFactory;
|
||||
private @Nullable ServiceRegistration<?> plexDiscoveryServiceRegistration;
|
||||
|
||||
@Activate
|
||||
public PlexHandlerFactory(final @Reference HttpClientFactory httpClientFactory) {
|
||||
this.httpClientFactory = httpClientFactory;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsThingType(ThingTypeUID thingTypeUID) {
|
||||
return SUPPORTED_THING_TYPES_UIDS.contains(thingTypeUID);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected @Nullable ThingHandler createHandler(Thing thing) {
|
||||
ThingTypeUID thingTypeUID = thing.getThingTypeUID();
|
||||
if (SUPPORTED_SERVER_THING_TYPES_UIDS.contains(thingTypeUID)) {
|
||||
PlexServerHandler handler = new PlexServerHandler((Bridge) thing, httpClientFactory);
|
||||
registerPlexDiscoveryService(handler);
|
||||
return handler;
|
||||
} else if (SUPPORTED_PLAYER_THING_TYPES_UIDS.contains(thingTypeUID)) {
|
||||
return new PlexPlayerHandler(thing);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected synchronized void removeHandler(ThingHandler thingHandler) {
|
||||
if (thingHandler instanceof PlexServerHandler) {
|
||||
ServiceRegistration<?> plexDiscoveryServiceRegistration = this.plexDiscoveryServiceRegistration;
|
||||
if (plexDiscoveryServiceRegistration != null) {
|
||||
// remove discovery service, if bridge handler is removed
|
||||
plexDiscoveryServiceRegistration.unregister();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void registerPlexDiscoveryService(PlexServerHandler handler) {
|
||||
PlexDiscoveryService discoveryService = new PlexDiscoveryService(handler);
|
||||
if (bundleContext != null) {
|
||||
this.plexDiscoveryServiceRegistration = bundleContext.registerService(DiscoveryService.class.getName(),
|
||||
discoveryService, new Hashtable<>());
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,41 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2023 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.plex.internal;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.openhab.core.thing.binding.BaseDynamicStateDescriptionProvider;
|
||||
import org.openhab.core.thing.i18n.ChannelTypeI18nLocalizationService;
|
||||
import org.openhab.core.thing.type.DynamicStateDescriptionProvider;
|
||||
import org.osgi.service.component.annotations.Component;
|
||||
import org.osgi.service.component.annotations.Reference;
|
||||
|
||||
/**
|
||||
* Dynamic provider of state options while leaving other state description fields as original.
|
||||
*
|
||||
* @author Brian Homeyer - Initial contribution
|
||||
* @author Aron Beurskens - Binding development
|
||||
*/
|
||||
@Component(service = { DynamicStateDescriptionProvider.class, PlexStateDescriptionOptionProvider.class })
|
||||
@NonNullByDefault
|
||||
public class PlexStateDescriptionOptionProvider extends BaseDynamicStateDescriptionProvider {
|
||||
@Reference
|
||||
protected void setChannelTypeI18nLocalizationService(
|
||||
final ChannelTypeI18nLocalizationService channelTypeI18nLocalizationService) {
|
||||
this.channelTypeI18nLocalizationService = channelTypeI18nLocalizationService;
|
||||
}
|
||||
|
||||
protected void unsetChannelTypeI18nLocalizationService(
|
||||
final ChannelTypeI18nLocalizationService channelTypeI18nLocalizationService) {
|
||||
this.channelTypeI18nLocalizationService = null;
|
||||
}
|
||||
}
|
@ -0,0 +1,27 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2023 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.plex.internal.config;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
|
||||
/**
|
||||
* The {@link PlexPlayerConfiguration} is the class used to match the
|
||||
* thing configuration.
|
||||
*
|
||||
* @author Brian Homeyer - Initial contribution
|
||||
* @author Aron Beurskens - Binding development
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class PlexPlayerConfiguration {
|
||||
public String playerID = "";
|
||||
}
|
@ -0,0 +1,33 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2023 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.plex.internal.config;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
|
||||
/**
|
||||
* The {@link PlexServerConfiguration} is the class used to match the
|
||||
* bridge configuration.
|
||||
*
|
||||
* @author Brian Homeyer - Initial contribution
|
||||
* @author Aron Beurskens - Binding development
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class PlexServerConfiguration {
|
||||
public String host = "";
|
||||
public Integer portNumber = 32400;
|
||||
public String token = "";
|
||||
public Integer refreshRate = 5;
|
||||
public String username = "";
|
||||
public String password = "";
|
||||
public String scheme = "";
|
||||
}
|
@ -0,0 +1,277 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2023 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.plex.internal.dto;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import com.thoughtworks.xstream.annotations.XStreamAlias;
|
||||
import com.thoughtworks.xstream.annotations.XStreamAsAttribute;
|
||||
import com.thoughtworks.xstream.annotations.XStreamImplicit;
|
||||
|
||||
/**
|
||||
*
|
||||
* @author Brian Homeyer - Initial contribution
|
||||
* @author Aron Beurskens - Binding development
|
||||
*/
|
||||
@XStreamAlias("MediaContainer")
|
||||
public class MediaContainer {
|
||||
@XStreamAsAttribute
|
||||
private Integer size;
|
||||
@XStreamImplicit
|
||||
@XStreamAsAttribute
|
||||
private List<Video> video = null;
|
||||
|
||||
@XStreamImplicit
|
||||
@XStreamAsAttribute
|
||||
private List<Track> track = null;
|
||||
|
||||
@XStreamImplicit
|
||||
@XStreamAsAttribute
|
||||
private List<Device> device = null;
|
||||
|
||||
public List<Device> getDevice() {
|
||||
return device;
|
||||
}
|
||||
|
||||
public Integer getSize() {
|
||||
return size;
|
||||
}
|
||||
|
||||
public List<Video> getVideo() {
|
||||
return video;
|
||||
}
|
||||
|
||||
public List<Track> getTrack() {
|
||||
return track;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a list of video or track objects, depends on what is playing
|
||||
*/
|
||||
public List<? extends MediaType> getMediaTypes() {
|
||||
if (video != null) {
|
||||
return video;
|
||||
}
|
||||
if (track != null) {
|
||||
return track;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public class MediaType {
|
||||
@XStreamAsAttribute
|
||||
private String title;
|
||||
@XStreamAsAttribute
|
||||
private String thumb;
|
||||
@XStreamAsAttribute
|
||||
private String art;
|
||||
@XStreamAsAttribute
|
||||
private String grandparentThumb;
|
||||
@XStreamAsAttribute
|
||||
private String grandparentTitle;
|
||||
@XStreamAsAttribute
|
||||
private String parentThumb;
|
||||
@XStreamAsAttribute
|
||||
private String parentTitle;
|
||||
@XStreamAsAttribute
|
||||
private long viewOffset;
|
||||
@XStreamAsAttribute
|
||||
private String type;
|
||||
@XStreamAsAttribute
|
||||
private String sessionKey;
|
||||
@XStreamAlias("Media")
|
||||
private Media media;
|
||||
@XStreamAlias("Player")
|
||||
private Player player;
|
||||
@XStreamAlias("User")
|
||||
private User user;
|
||||
|
||||
public String getGrandparentThumb() {
|
||||
return this.grandparentThumb;
|
||||
}
|
||||
|
||||
public void setGrandparentThumb(String grandparentThumb) {
|
||||
this.grandparentThumb = grandparentThumb;
|
||||
}
|
||||
|
||||
public String getGrandparentTitle() {
|
||||
return this.grandparentTitle;
|
||||
}
|
||||
|
||||
public void setGrandparentTitle(String grandparentTitle) {
|
||||
this.grandparentTitle = grandparentTitle;
|
||||
}
|
||||
|
||||
public String getParentThumb() {
|
||||
return this.parentThumb;
|
||||
}
|
||||
|
||||
public void setParentThumb(String parentThumb) {
|
||||
this.parentThumb = parentThumb;
|
||||
}
|
||||
|
||||
public String getParentTitle() {
|
||||
return this.parentTitle;
|
||||
}
|
||||
|
||||
public void setParentTitle(String parentTitle) {
|
||||
this.parentTitle = parentTitle;
|
||||
}
|
||||
|
||||
public Media getMedia() {
|
||||
return this.media;
|
||||
}
|
||||
|
||||
public Player getPlayer() {
|
||||
return this.player;
|
||||
}
|
||||
|
||||
public String getTitle() {
|
||||
return title;
|
||||
}
|
||||
|
||||
public void setTitle(String title) {
|
||||
this.title = title;
|
||||
}
|
||||
|
||||
public long getViewOffset() {
|
||||
return this.viewOffset;
|
||||
}
|
||||
|
||||
public String getType() {
|
||||
return this.type;
|
||||
}
|
||||
|
||||
public String getThumb() {
|
||||
return this.thumb;
|
||||
}
|
||||
|
||||
public void setThumb(String art) {
|
||||
this.thumb = art;
|
||||
}
|
||||
|
||||
public String getArt() {
|
||||
return art;
|
||||
}
|
||||
|
||||
public void setArt(String art) {
|
||||
this.art = art;
|
||||
}
|
||||
|
||||
public String getSessionKey() {
|
||||
return sessionKey;
|
||||
}
|
||||
|
||||
public void setSessionKey(String sessionKey) {
|
||||
this.sessionKey = sessionKey;
|
||||
}
|
||||
|
||||
public User getUser() {
|
||||
return user;
|
||||
}
|
||||
|
||||
public class Player {
|
||||
@XStreamAsAttribute
|
||||
private String machineIdentifier;
|
||||
@XStreamAsAttribute
|
||||
private String state;
|
||||
@XStreamAsAttribute
|
||||
private String local;
|
||||
|
||||
public String getMachineIdentifier() {
|
||||
return this.machineIdentifier;
|
||||
}
|
||||
|
||||
public String getState() {
|
||||
return this.state;
|
||||
}
|
||||
|
||||
public void setState(String state) {
|
||||
this.state = state;
|
||||
}
|
||||
|
||||
public String getLocal() {
|
||||
return this.local;
|
||||
}
|
||||
}
|
||||
|
||||
public class Media {
|
||||
|
||||
@XStreamAsAttribute
|
||||
private long duration;
|
||||
|
||||
public long getDuration() {
|
||||
return this.duration;
|
||||
}
|
||||
}
|
||||
|
||||
@XStreamAlias("User")
|
||||
public class User {
|
||||
@XStreamAsAttribute
|
||||
private Integer id;
|
||||
|
||||
public Integer getId() {
|
||||
return this.id;
|
||||
}
|
||||
|
||||
@XStreamAsAttribute
|
||||
private String title;
|
||||
|
||||
public String getTitle() {
|
||||
return this.title;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@XStreamAlias("Video")
|
||||
public class Video extends MediaType {
|
||||
}
|
||||
|
||||
@XStreamAlias("Track")
|
||||
public class Track extends MediaType {
|
||||
}
|
||||
|
||||
@XStreamAlias("Device")
|
||||
public class Device {
|
||||
@XStreamAsAttribute
|
||||
private String name;
|
||||
|
||||
public String getName() {
|
||||
return this.name;
|
||||
}
|
||||
|
||||
@XStreamAlias("Connection")
|
||||
@XStreamImplicit
|
||||
private List<Connection> connection = null;
|
||||
|
||||
public List<Connection> getConnection() {
|
||||
return connection;
|
||||
}
|
||||
|
||||
public class Connection {
|
||||
@XStreamAsAttribute
|
||||
private String protocol;
|
||||
@XStreamAsAttribute
|
||||
private String address;
|
||||
|
||||
public String getProtocol() {
|
||||
return this.protocol;
|
||||
}
|
||||
|
||||
public String getAddress() {
|
||||
return this.address;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,166 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2023 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.plex.internal.dto;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import com.google.gson.annotations.Expose;
|
||||
import com.google.gson.annotations.SerializedName;
|
||||
|
||||
/**
|
||||
* @author Brian Homeyer - Initial contribution
|
||||
* @author Aron Beurskens - Binding development
|
||||
*/
|
||||
public class NotificationContainer {
|
||||
|
||||
@SerializedName("NotificationContainer")
|
||||
@Expose
|
||||
private InnerNotificationContainer notificationContainer;
|
||||
|
||||
public InnerNotificationContainer getNotificationContainer() {
|
||||
return notificationContainer;
|
||||
}
|
||||
|
||||
public void setNotificationContainer(InnerNotificationContainer notificationContainer) {
|
||||
this.notificationContainer = notificationContainer;
|
||||
}
|
||||
|
||||
public class InnerNotificationContainer {
|
||||
|
||||
@SerializedName("type")
|
||||
@Expose
|
||||
private String type;
|
||||
@SerializedName("size")
|
||||
@Expose
|
||||
private Integer size;
|
||||
@SerializedName("PlaySessionStateNotification")
|
||||
@Expose
|
||||
private List<PlaySessionStateNotification> playSessionStateNotification = null;
|
||||
|
||||
public String getType() {
|
||||
return type;
|
||||
}
|
||||
|
||||
public void setType(String type) {
|
||||
this.type = type;
|
||||
}
|
||||
|
||||
public Integer getSize() {
|
||||
return size;
|
||||
}
|
||||
|
||||
public void setSize(Integer size) {
|
||||
this.size = size;
|
||||
}
|
||||
|
||||
public List<PlaySessionStateNotification> getPlaySessionStateNotification() {
|
||||
return playSessionStateNotification;
|
||||
}
|
||||
|
||||
public void setPlaySessionStateNotification(List<PlaySessionStateNotification> playSessionStateNotification) {
|
||||
this.playSessionStateNotification = playSessionStateNotification;
|
||||
}
|
||||
|
||||
public class PlaySessionStateNotification {
|
||||
|
||||
@SerializedName("sessionKey")
|
||||
@Expose
|
||||
private String sessionKey;
|
||||
@SerializedName("guid")
|
||||
@Expose
|
||||
private String guid;
|
||||
@SerializedName("ratingKey")
|
||||
@Expose
|
||||
private String ratingKey;
|
||||
@SerializedName("url")
|
||||
@Expose
|
||||
private String url;
|
||||
@SerializedName("key")
|
||||
@Expose
|
||||
private String key;
|
||||
@SerializedName("viewOffset")
|
||||
@Expose
|
||||
private Integer viewOffset;
|
||||
@SerializedName("playQueueItemID")
|
||||
@Expose
|
||||
private Integer playQueueItemID;
|
||||
@SerializedName("state")
|
||||
@Expose
|
||||
private String state;
|
||||
|
||||
public String getSessionKey() {
|
||||
return sessionKey;
|
||||
}
|
||||
|
||||
public void setSessionKey(String sessionKey) {
|
||||
this.sessionKey = sessionKey;
|
||||
}
|
||||
|
||||
public String getGuid() {
|
||||
return guid;
|
||||
}
|
||||
|
||||
public void setGuid(String guid) {
|
||||
this.guid = guid;
|
||||
}
|
||||
|
||||
public String getRatingKey() {
|
||||
return ratingKey;
|
||||
}
|
||||
|
||||
public void setRatingKey(String ratingKey) {
|
||||
this.ratingKey = ratingKey;
|
||||
}
|
||||
|
||||
public String getUrl() {
|
||||
return url;
|
||||
}
|
||||
|
||||
public void setUrl(String url) {
|
||||
this.url = url;
|
||||
}
|
||||
|
||||
public String getKey() {
|
||||
return key;
|
||||
}
|
||||
|
||||
public void setKey(String key) {
|
||||
this.key = key;
|
||||
}
|
||||
|
||||
public Integer getViewOffset() {
|
||||
return viewOffset;
|
||||
}
|
||||
|
||||
public void setViewOffset(Integer viewOffset) {
|
||||
this.viewOffset = viewOffset;
|
||||
}
|
||||
|
||||
public Integer getPlayQueueItemID() {
|
||||
return playQueueItemID;
|
||||
}
|
||||
|
||||
public void setPlayQueueItemID(Integer playQueueItemID) {
|
||||
this.playQueueItemID = playQueueItemID;
|
||||
}
|
||||
|
||||
public String getState() {
|
||||
return state;
|
||||
}
|
||||
|
||||
public void setState(String state) {
|
||||
this.state = state;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,40 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2023 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.plex.internal.dto;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
|
||||
/**
|
||||
* The {@link PlexPlayerState} is the class used to map the
|
||||
* player states for the player things.
|
||||
*
|
||||
* @author Brian Homeyer - Initial contribution
|
||||
* @author Aron Beurskens - Binding development
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public enum PlexPlayerState {
|
||||
STOPPED,
|
||||
BUFFERING,
|
||||
PLAYING,
|
||||
PAUSED;
|
||||
|
||||
public static @Nullable PlexPlayerState of(String state) {
|
||||
for (PlexPlayerState playerState : values()) {
|
||||
if (playerState.toString().toLowerCase().equals(state)) {
|
||||
return playerState;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
@ -0,0 +1,177 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2023 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.plex.internal.dto;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.math.MathContext;
|
||||
import java.math.RoundingMode;
|
||||
import java.util.Date;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* The {@link PlexSession} is the class used internally by the PlexPlayer things
|
||||
* to keep state of updates from the Bridge.
|
||||
*
|
||||
* @author Brian Homeyer - Initial contribution
|
||||
* @author Aron Beurskens - Binding development
|
||||
*/
|
||||
public class PlexSession {
|
||||
private String title;
|
||||
private String thumb;
|
||||
private String art;
|
||||
private long viewOffset;
|
||||
private String type;
|
||||
private BigDecimal progress;
|
||||
private String machineIdentifier;
|
||||
private PlexPlayerState state;
|
||||
private String local;
|
||||
private long duration;
|
||||
private Date endTime;
|
||||
private String sessionKey = "";
|
||||
private Integer userId;
|
||||
private String userTitle = "";
|
||||
|
||||
private final Logger logger = LoggerFactory.getLogger(PlexSession.class);
|
||||
|
||||
public String getSessionKey() {
|
||||
return sessionKey;
|
||||
}
|
||||
|
||||
public void setSessionKey(String sessionKey) {
|
||||
this.sessionKey = sessionKey;
|
||||
}
|
||||
|
||||
public String getTitle() {
|
||||
return title;
|
||||
}
|
||||
|
||||
public Date getEndTime() {
|
||||
return endTime;
|
||||
}
|
||||
|
||||
public void setEndTime(Date endTime) {
|
||||
this.endTime = endTime;
|
||||
}
|
||||
|
||||
public void setTitle(String title) {
|
||||
this.title = title;
|
||||
}
|
||||
|
||||
public String getThumb() {
|
||||
return thumb;
|
||||
}
|
||||
|
||||
public void setThumb(String thumb) {
|
||||
this.thumb = thumb;
|
||||
}
|
||||
|
||||
public long getViewOffset() {
|
||||
return viewOffset;
|
||||
}
|
||||
|
||||
public void setViewOffset(long viewOffset) {
|
||||
this.viewOffset = viewOffset;
|
||||
updateProgress();
|
||||
}
|
||||
|
||||
public String getType() {
|
||||
return type;
|
||||
}
|
||||
|
||||
public void setType(String type) {
|
||||
this.type = type;
|
||||
}
|
||||
|
||||
public BigDecimal getProgress() {
|
||||
return progress;
|
||||
}
|
||||
|
||||
public void setProgress(BigDecimal progress) {
|
||||
this.progress = progress;
|
||||
}
|
||||
|
||||
public String getMachineIdentifier() {
|
||||
return machineIdentifier;
|
||||
}
|
||||
|
||||
public void setMachineIdentifier(String machineIdentifier) {
|
||||
this.machineIdentifier = machineIdentifier;
|
||||
}
|
||||
|
||||
public PlexPlayerState getState() {
|
||||
return state;
|
||||
}
|
||||
|
||||
public void setState(PlexPlayerState state) {
|
||||
this.state = state;
|
||||
}
|
||||
|
||||
public String getLocal() {
|
||||
return local;
|
||||
}
|
||||
|
||||
public void setLocal(String local) {
|
||||
this.local = local;
|
||||
}
|
||||
|
||||
public long getDuration() {
|
||||
return duration;
|
||||
}
|
||||
|
||||
public void setDuration(long duration) {
|
||||
this.duration = duration;
|
||||
}
|
||||
|
||||
public String getArt() {
|
||||
return art;
|
||||
}
|
||||
|
||||
public void setArt(String art) {
|
||||
this.art = art;
|
||||
}
|
||||
|
||||
public Integer getUserId() {
|
||||
return userId;
|
||||
}
|
||||
|
||||
public void setUserId(Integer userId) {
|
||||
this.userId = userId;
|
||||
}
|
||||
|
||||
public String getUserTitle() {
|
||||
return userTitle;
|
||||
}
|
||||
|
||||
public void setUserTitle(String userTitle) {
|
||||
this.userTitle = userTitle;
|
||||
}
|
||||
|
||||
private void updateProgress() {
|
||||
try {
|
||||
if (this.duration > 0) {
|
||||
BigDecimal progress = new BigDecimal("100")
|
||||
.divide(new BigDecimal(this.duration), new MathContext(100, RoundingMode.HALF_UP))
|
||||
.multiply(new BigDecimal(this.viewOffset)).setScale(2, RoundingMode.HALF_UP);
|
||||
progress = BigDecimal.ZERO.max(progress);
|
||||
progress = new BigDecimal("100").min(progress);
|
||||
|
||||
this.endTime = new Date(System.currentTimeMillis() + (this.duration - this.viewOffset));
|
||||
this.progress = progress;
|
||||
}
|
||||
} catch (Exception e) {
|
||||
logger.debug("An exception occurred while polling the updating Progress: '{}'", e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,53 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2023 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.plex.internal.dto;
|
||||
|
||||
import com.thoughtworks.xstream.annotations.XStreamAlias;
|
||||
|
||||
/**
|
||||
* The {@link User} Maps the XML response from the PLEX API
|
||||
*
|
||||
* @author Brian Homeyer - Initial contribution
|
||||
* @author Aron Beurskens - Binding development
|
||||
*/
|
||||
@XStreamAlias("user")
|
||||
public class User {
|
||||
private String username;
|
||||
private String email;
|
||||
@XStreamAlias("authentication-token")
|
||||
private String authenticationToken;
|
||||
|
||||
public String getUsername() {
|
||||
return username;
|
||||
}
|
||||
|
||||
public void setUsername(String username) {
|
||||
this.username = username;
|
||||
}
|
||||
|
||||
public String getEmail() {
|
||||
return email;
|
||||
}
|
||||
|
||||
public void setEmail(String email) {
|
||||
this.email = email;
|
||||
}
|
||||
|
||||
public String getAuthenticationToken() {
|
||||
return authenticationToken;
|
||||
}
|
||||
|
||||
public void setAuthenticationToken(String authenticationToken) {
|
||||
this.authenticationToken = authenticationToken;
|
||||
}
|
||||
}
|
@ -0,0 +1,392 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2023 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.plex.internal.handler;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.URI;
|
||||
import java.net.URISyntaxException;
|
||||
import java.util.Base64;
|
||||
import java.util.Properties;
|
||||
import java.util.concurrent.ScheduledExecutorService;
|
||||
import java.util.concurrent.ScheduledFuture;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.eclipse.jetty.client.HttpClient;
|
||||
import org.eclipse.jetty.http.HttpHeader;
|
||||
import org.eclipse.jetty.websocket.api.Session;
|
||||
import org.eclipse.jetty.websocket.api.annotations.OnWebSocketClose;
|
||||
import org.eclipse.jetty.websocket.api.annotations.OnWebSocketConnect;
|
||||
import org.eclipse.jetty.websocket.api.annotations.OnWebSocketError;
|
||||
import org.eclipse.jetty.websocket.api.annotations.OnWebSocketMessage;
|
||||
import org.eclipse.jetty.websocket.api.annotations.WebSocket;
|
||||
import org.eclipse.jetty.websocket.client.ClientUpgradeRequest;
|
||||
import org.eclipse.jetty.websocket.client.WebSocketClient;
|
||||
import org.openhab.binding.plex.internal.config.PlexServerConfiguration;
|
||||
import org.openhab.binding.plex.internal.dto.MediaContainer;
|
||||
import org.openhab.binding.plex.internal.dto.MediaContainer.Device;
|
||||
import org.openhab.binding.plex.internal.dto.MediaContainer.Device.Connection;
|
||||
import org.openhab.binding.plex.internal.dto.NotificationContainer;
|
||||
import org.openhab.binding.plex.internal.dto.User;
|
||||
import org.openhab.core.i18n.ConfigurationException;
|
||||
import org.openhab.core.io.net.http.HttpUtil;
|
||||
import org.openhab.core.library.types.NextPreviousType;
|
||||
import org.openhab.core.library.types.PlayPauseType;
|
||||
import org.openhab.core.types.Command;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import com.google.gson.Gson;
|
||||
import com.thoughtworks.xstream.XStream;
|
||||
import com.thoughtworks.xstream.io.xml.StaxDriver;
|
||||
|
||||
/**
|
||||
* The {@link PlexApiConnector} is responsible for communications with the PLEX server
|
||||
*
|
||||
* @author Brian Homeyer - Initial contribution
|
||||
* @author Aron Beurskens - Binding development
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class PlexApiConnector {
|
||||
private static final int REQUEST_TIMEOUT_MS = 2000;
|
||||
private static final String TOKEN_HEADER = "X-Plex-Token";
|
||||
private static final String SIGNIN_URL = "https://plex.tv/users/sign_in.xml";
|
||||
private static final String CLIENT_ID = "928dcjhd-91ka-la91-md7a-0msnan214563";
|
||||
private static final String API_URL = "https://plex.tv/api/resources?includeHttps=1";
|
||||
private final HttpClient httpClient;
|
||||
private WebSocketClient wsClient = new WebSocketClient();
|
||||
private PlexSocket plexSocket = new PlexSocket();
|
||||
|
||||
private final Logger logger = LoggerFactory.getLogger(PlexApiConnector.class);
|
||||
private @Nullable PlexUpdateListener listener;
|
||||
|
||||
private final XStream xStream = new XStream(new StaxDriver());
|
||||
private Gson gson = new Gson();
|
||||
private boolean isShutDown = false;
|
||||
|
||||
private @Nullable ScheduledFuture<?> socketReconnect;
|
||||
private ScheduledExecutorService scheduler;
|
||||
private @Nullable URI uri;
|
||||
|
||||
private String username = "";
|
||||
private String password = "";
|
||||
private String token = "";
|
||||
private String host = "";
|
||||
private int port = 0;
|
||||
private static String scheme = "";
|
||||
|
||||
public PlexApiConnector(ScheduledExecutorService scheduler, HttpClient httpClient) {
|
||||
this.scheduler = scheduler;
|
||||
this.httpClient = httpClient;
|
||||
setupXstream();
|
||||
}
|
||||
|
||||
public void setParameters(PlexServerConfiguration connProps) {
|
||||
username = connProps.username;
|
||||
password = connProps.password;
|
||||
token = connProps.token;
|
||||
host = connProps.host;
|
||||
port = connProps.portNumber;
|
||||
wsClient = new WebSocketClient();
|
||||
plexSocket = new PlexSocket();
|
||||
}
|
||||
|
||||
private String getSchemeWS() {
|
||||
return "http".equals(scheme) ? "ws" : "wss";
|
||||
}
|
||||
|
||||
public boolean hasToken() {
|
||||
return !token.isBlank();
|
||||
}
|
||||
|
||||
/**
|
||||
* Base configuration for XStream
|
||||
*/
|
||||
private void setupXstream() {
|
||||
xStream.allowTypesByWildcard(
|
||||
new String[] { User.class.getPackageName() + ".**", MediaContainer.class.getPackageName() + ".**" });
|
||||
xStream.setClassLoader(PlexApiConnector.class.getClassLoader());
|
||||
xStream.ignoreUnknownElements();
|
||||
xStream.processAnnotations(User.class);
|
||||
xStream.processAnnotations(MediaContainer.class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch the XML data and parse it through xStream to get a MediaContainer object
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
public @Nullable MediaContainer getSessionData() {
|
||||
try {
|
||||
String url = "http://" + host + ":" + String.valueOf(port) + "/status/sessions" + "?X-Plex-Token=" + token;
|
||||
logger.debug("Getting session data '{}'", url);
|
||||
MediaContainer mediaContainer = doHttpRequest("GET", url, getClientHeaders(), MediaContainer.class);
|
||||
return mediaContainer;
|
||||
} catch (IOException e) {
|
||||
logger.debug("An exception occurred while polling the PLEX Server: '{}'", e.getMessage());
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Assemble the URL to include the Token
|
||||
*
|
||||
* @param url The url portion that is returned from the sessions call
|
||||
* @return the completed url that will be usable
|
||||
*/
|
||||
public String getURL(String url) {
|
||||
String artURL = scheme + "://" + host + ":" + String.valueOf(port + url + "?X-Plex-Token=" + token);
|
||||
return artURL;
|
||||
}
|
||||
|
||||
/**
|
||||
* This method will get an X-Token from the PLEX server if one is not provided in the bridge config
|
||||
* and use this in the communication with the plex server
|
||||
*/
|
||||
public void getToken() {
|
||||
User user;
|
||||
String authString = Base64.getEncoder().encodeToString((username + ":" + password).getBytes());
|
||||
Properties headers = getClientHeaders();
|
||||
headers.put("Authorization", "Basic " + authString);
|
||||
|
||||
try {
|
||||
user = doHttpRequest("POST", SIGNIN_URL, headers, User.class);
|
||||
} catch (IOException e) {
|
||||
logger.debug("An exception occurred while fetching PLEX user token :'{}'", e.getMessage(), e);
|
||||
throw new ConfigurationException("Error occurred while fetching PLEX user token, please check config");
|
||||
}
|
||||
|
||||
if (user.getAuthenticationToken() != null) {
|
||||
token = user.getAuthenticationToken();
|
||||
logger.debug("PLEX login successful using username/password");
|
||||
} else {
|
||||
throw new ConfigurationException("Invalid credentials for PLEX account, please check config");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This method will get the Api information from the PLEX servers.
|
||||
*/
|
||||
public boolean getApi() {
|
||||
try {
|
||||
MediaContainer api = doHttpRequest("GET", API_URL, getClientHeaders(), MediaContainer.class);
|
||||
logger.debug("MediaContainer {}", api.getSize());
|
||||
if (api.getDevice() != null) {
|
||||
for (Device tmpDevice : api.getDevice()) {
|
||||
if (tmpDevice.getConnection() != null) {
|
||||
for (Connection tmpConn : tmpDevice.getConnection()) {
|
||||
if (host.equals(tmpConn.getAddress())) {
|
||||
scheme = tmpConn.getProtocol();
|
||||
logger.debug(
|
||||
"PLEX Api fetched. Found configured PLEX server in Api request, applied. Protocol used : {}",
|
||||
scheme);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
} catch (IOException e) {
|
||||
logger.debug("An exception occurred while fetching API :'{}'", e.getMessage(), e);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Make an HTTP request and return the class object that was used when calling.
|
||||
*
|
||||
* @param <T> Class being used(dto)
|
||||
* @param method GET/POST
|
||||
* @param url What URL to call
|
||||
* @param headers Additional headers that will be used
|
||||
* @param type class type for the XML parsing
|
||||
* @return Returns a class object from the data returned by the call
|
||||
* @throws IOException
|
||||
*/
|
||||
private <T> T doHttpRequest(String method, String url, Properties headers, Class<T> type) throws IOException {
|
||||
String response = HttpUtil.executeUrl(method, url, headers, null, null, REQUEST_TIMEOUT_MS);
|
||||
@SuppressWarnings("unchecked")
|
||||
T obj = (T) xStream.fromXML(response);
|
||||
logger.debug("HTTP response {}", response);
|
||||
return obj;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fills in the header information for any calls to PLEX services
|
||||
*
|
||||
* @return Property headers
|
||||
*/
|
||||
private Properties getClientHeaders() {
|
||||
Properties headers = new Properties();
|
||||
headers.put(HttpHeader.USER_AGENT, "openHAB / PLEX binding "); // + VERSION);
|
||||
headers.put("X-Plex-Client-Identifier", CLIENT_ID);
|
||||
headers.put("X-Plex-Product", "openHAB");
|
||||
headers.put("X-Plex-Version", "");
|
||||
headers.put("X-Plex-Device", "JRE11");
|
||||
headers.put("X-Plex-Device-Name", "openHAB");
|
||||
headers.put("X-Plex-Provides", "controller");
|
||||
headers.put("X-Plex-Platform", "Java");
|
||||
headers.put("X-Plex-Platform-Version", "JRE11");
|
||||
if (hasToken()) {
|
||||
headers.put(TOKEN_HEADER, token);
|
||||
}
|
||||
return headers;
|
||||
}
|
||||
|
||||
/**
|
||||
* Register callback to PlexServerHandler
|
||||
*
|
||||
* @param listener function to call
|
||||
*/
|
||||
public void registerListener(PlexUpdateListener listener) {
|
||||
this.listener = listener;
|
||||
}
|
||||
|
||||
/**
|
||||
* Dispose method, cleans up the websocket starts the reconnect logic
|
||||
*/
|
||||
public void dispose() {
|
||||
isShutDown = true;
|
||||
try {
|
||||
wsClient.stop();
|
||||
ScheduledFuture<?> socketReconnect = this.socketReconnect;
|
||||
if (socketReconnect != null) {
|
||||
socketReconnect.cancel(true);
|
||||
this.socketReconnect = null;
|
||||
}
|
||||
httpClient.stop();
|
||||
} catch (Exception e) {
|
||||
logger.debug("Could not stop webSocketClient, message {}", e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Connect to the websocket
|
||||
*/
|
||||
public void connect() {
|
||||
logger.debug("Connecting to WebSocket");
|
||||
try {
|
||||
wsClient = new WebSocketClient(httpClient);
|
||||
uri = new URI(getSchemeWS() + "://" + host + ":32400/:/websockets/notifications?X-Plex-Token=" + token); // WS_ENDPOINT_TOUCHWAND);
|
||||
} catch (URISyntaxException e) {
|
||||
logger.debug("URI not valid {} message {}", uri, e.getMessage());
|
||||
return;
|
||||
}
|
||||
wsClient.setConnectTimeout(2000);
|
||||
ClientUpgradeRequest request = new ClientUpgradeRequest();
|
||||
try {
|
||||
isShutDown = false;
|
||||
wsClient.start();
|
||||
wsClient.connect(plexSocket, uri, request);
|
||||
} catch (Exception e) {
|
||||
logger.debug("Could not connect webSocket URI {} message {}", uri, e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* PlexSocket class to handle the websocket connection to the PLEX server
|
||||
*/
|
||||
@WebSocket(maxIdleTime = 360000) // WEBSOCKET_IDLE_TIMEOUT_MS)
|
||||
public class PlexSocket {
|
||||
@OnWebSocketClose
|
||||
public void onClose(int statusCode, String reason) {
|
||||
logger.debug("Connection closed: {} - {}", statusCode, reason);
|
||||
if (!isShutDown) {
|
||||
logger.debug("PLEX websocket closed - reconnecting");
|
||||
asyncWeb();
|
||||
}
|
||||
}
|
||||
|
||||
@OnWebSocketConnect
|
||||
public void onConnect(Session session) {
|
||||
logger.debug("PLEX Socket connected to {}", session.getRemoteAddress().getAddress());
|
||||
}
|
||||
|
||||
@OnWebSocketMessage
|
||||
public void onMessage(String msg) {
|
||||
NotificationContainer notification = gson.fromJson(msg, NotificationContainer.class);
|
||||
if (notification != null) {
|
||||
PlexUpdateListener listenerLocal = listener;
|
||||
if (listenerLocal != null && notification.getNotificationContainer().getType().equals("playing")) {
|
||||
listenerLocal.onItemStatusUpdate(
|
||||
notification.getNotificationContainer().getPlaySessionStateNotification().get(0)
|
||||
.getSessionKey(),
|
||||
notification.getNotificationContainer().getPlaySessionStateNotification().get(0)
|
||||
.getState());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@OnWebSocketError
|
||||
public void onError(Throwable cause) {
|
||||
if (!isShutDown) {
|
||||
logger.debug("WebSocket onError - reconnecting");
|
||||
asyncWeb();
|
||||
}
|
||||
}
|
||||
|
||||
private void asyncWeb() {
|
||||
ScheduledFuture<?> mySocketReconnect = socketReconnect;
|
||||
if (mySocketReconnect == null || mySocketReconnect.isDone()) {
|
||||
socketReconnect = scheduler.schedule(PlexApiConnector.this::connect, 5, TimeUnit.SECONDS); // WEBSOCKET_RECONNECT_INTERVAL_SEC,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles control commands to the plex player.
|
||||
*
|
||||
* Supports:
|
||||
* - Play / Pause
|
||||
* - Previous / Next
|
||||
*
|
||||
* @param command The control command
|
||||
* @param playerID The ID of the PLEX player
|
||||
*/
|
||||
public void controlPlayer(Command command, String playerID) {
|
||||
String commandPath = null;
|
||||
if (command instanceof PlayPauseType) {
|
||||
if (command.equals(PlayPauseType.PLAY)) {
|
||||
commandPath = "/player/playback/play";
|
||||
}
|
||||
if (command.equals(PlayPauseType.PAUSE)) {
|
||||
commandPath = "/player/playback/pause";
|
||||
}
|
||||
}
|
||||
|
||||
if (command instanceof NextPreviousType) {
|
||||
if (command.equals(NextPreviousType.PREVIOUS)) {
|
||||
commandPath = "/player/playback/skipPrevious";
|
||||
}
|
||||
if (command.equals(NextPreviousType.NEXT)) {
|
||||
commandPath = "/player/playback/skipNext";
|
||||
}
|
||||
}
|
||||
|
||||
if (commandPath != null) {
|
||||
try {
|
||||
String url = "http://" + host + ":" + String.valueOf(port) + commandPath;
|
||||
Properties headers = getClientHeaders();
|
||||
headers.put("X-Plex-Target-Client-Identifier", playerID);
|
||||
HttpUtil.executeUrl("GET", url, headers, null, null, REQUEST_TIMEOUT_MS);
|
||||
} catch (IOException e) {
|
||||
logger.debug("An exception occurred trying to send command '{}' to the player: {}", commandPath,
|
||||
e.getMessage());
|
||||
}
|
||||
} else {
|
||||
logger.warn("Could not match command '{}' to an action", command);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,192 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2023 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.plex.internal.handler;
|
||||
|
||||
import static org.openhab.binding.plex.internal.PlexBindingConstants.*;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.openhab.binding.plex.internal.config.PlexPlayerConfiguration;
|
||||
import org.openhab.binding.plex.internal.dto.MediaContainer.MediaType;
|
||||
import org.openhab.binding.plex.internal.dto.PlexPlayerState;
|
||||
import org.openhab.binding.plex.internal.dto.PlexSession;
|
||||
import org.openhab.core.library.types.PlayPauseType;
|
||||
import org.openhab.core.library.types.StringType;
|
||||
import org.openhab.core.thing.Bridge;
|
||||
import org.openhab.core.thing.ChannelUID;
|
||||
import org.openhab.core.thing.Thing;
|
||||
import org.openhab.core.thing.ThingStatus;
|
||||
import org.openhab.core.thing.ThingStatusDetail;
|
||||
import org.openhab.core.thing.binding.BaseThingHandler;
|
||||
import org.openhab.core.types.Command;
|
||||
import org.openhab.core.types.RefreshType;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* The {@link PlexBindingConstants} class defines common constants, which are
|
||||
* used across the whole binding.
|
||||
*
|
||||
* @author Brian Homeyer - Initial contribution
|
||||
* @author Aron Beurskens - Binding development
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class PlexPlayerHandler extends BaseThingHandler {
|
||||
private @NonNullByDefault({}) String playerID;
|
||||
|
||||
private PlexSession currentSessionData;
|
||||
private boolean foundInSession;
|
||||
|
||||
private final Logger logger = LoggerFactory.getLogger(PlexPlayerHandler.class);
|
||||
|
||||
public PlexPlayerHandler(Thing thing) {
|
||||
super(thing);
|
||||
currentSessionData = new PlexSession();
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize the player thing, check the bridge status and hang out waiting
|
||||
* for the session data to get polled.
|
||||
*/
|
||||
@Override
|
||||
public void initialize() {
|
||||
PlexPlayerConfiguration config = getConfigAs(PlexPlayerConfiguration.class);
|
||||
foundInSession = false;
|
||||
playerID = config.playerID;
|
||||
logger.debug("Initializing PLEX player : {}", playerID);
|
||||
updateStatus(ThingStatus.UNKNOWN);
|
||||
}
|
||||
|
||||
/**
|
||||
* Currently only the 'player' channel accepts commands, all others are read-only
|
||||
*/
|
||||
@Override
|
||||
public void handleCommand(ChannelUID channelUID, Command command) {
|
||||
if (command instanceof RefreshType) {
|
||||
logger.debug("REFRESH not implemented");
|
||||
return;
|
||||
}
|
||||
|
||||
Bridge bridge = getBridge();
|
||||
PlexServerHandler bridgeHandler = bridge == null ? null : (PlexServerHandler) bridge.getHandler();
|
||||
|
||||
if (bridgeHandler == null) {
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.HANDLER_MISSING_ERROR, "No bridge associated");
|
||||
} else {
|
||||
switch (channelUID.getId()) {
|
||||
case CHANNEL_PLAYER_CONTROL:
|
||||
bridgeHandler.getPlexAPIConnector().controlPlayer(command, playerID);
|
||||
break;
|
||||
default:
|
||||
logger.debug("Channel {} not implemented/supported to control player {}", channelUID.getId(),
|
||||
this.thing.getUID());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This is really just to set these all back to false so when we refresh the data it's
|
||||
* updated for Power On/Off. This is only called from the Server Bridge.
|
||||
*
|
||||
* @param foundInSession Will always be false, so this can probably be changed.
|
||||
*/
|
||||
public void setFoundInSession(boolean foundInSession) {
|
||||
this.foundInSession = foundInSession;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the session key from the current player
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
public String getSessionKey() {
|
||||
return currentSessionData.getSessionKey();
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when this thing gets its configuration changed.
|
||||
*/
|
||||
@Override
|
||||
public void thingUpdated(Thing thing) {
|
||||
dispose();
|
||||
this.thing = thing;
|
||||
initialize();
|
||||
}
|
||||
|
||||
/**
|
||||
* Refreshes all the data from the session XML call. This is called from the bridge
|
||||
*
|
||||
* @param sessionData The Video section of the XML(which is what pertains to the player)
|
||||
*/
|
||||
public void refreshSessionData(MediaType sessionData) {
|
||||
currentSessionData.setState(PlexPlayerState.of(sessionData.getPlayer().getState()));
|
||||
currentSessionData.setDuration(sessionData.getMedia().getDuration());
|
||||
currentSessionData.setMachineIdentifier(sessionData.getPlayer().getMachineIdentifier());
|
||||
currentSessionData.setViewOffset(sessionData.getViewOffset());
|
||||
currentSessionData.setTitle(sessionData.getTitle());
|
||||
currentSessionData.setType(sessionData.getType());
|
||||
currentSessionData.setThumb(sessionData.getThumb());
|
||||
currentSessionData.setArt(sessionData.getArt());
|
||||
currentSessionData.setLocal(sessionData.getPlayer().getLocal());
|
||||
currentSessionData.setSessionKey(sessionData.getSessionKey());
|
||||
currentSessionData.setUserId(sessionData.getUser().getId());
|
||||
currentSessionData.setUserTitle(sessionData.getUser().getTitle());
|
||||
|
||||
foundInSession = true;
|
||||
updateStatus(ThingStatus.ONLINE);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update just the state, this status comes from the websocket.
|
||||
*
|
||||
* @param state - The state to update it to.
|
||||
*/
|
||||
public synchronized void updateStateChannel(String state) {
|
||||
currentSessionData.setState(PlexPlayerState.of(state));
|
||||
updateState(new ChannelUID(getThing().getUID(), CHANNEL_PLAYER_STATE),
|
||||
new StringType(String.valueOf(foundInSession ? currentSessionData.getState() : "Stopped")));
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the channel states to match reality.
|
||||
*/
|
||||
public synchronized void updateChannels() {
|
||||
updateState(new ChannelUID(getThing().getUID(), CHANNEL_PLAYER_STATE),
|
||||
new StringType(String.valueOf(foundInSession ? currentSessionData.getState() : "Stopped")));
|
||||
updateState(new ChannelUID(getThing().getUID(), CHANNEL_PLAYER_POWER),
|
||||
new StringType(String.valueOf(foundInSession ? "ON" : "OFF")));
|
||||
updateState(new ChannelUID(getThing().getUID(), CHANNEL_PLAYER_TITLE),
|
||||
new StringType(String.valueOf(foundInSession ? currentSessionData.getTitle() : "")));
|
||||
updateState(new ChannelUID(getThing().getUID(), CHANNEL_PLAYER_TYPE),
|
||||
new StringType(String.valueOf(foundInSession ? currentSessionData.getType() : "")));
|
||||
updateState(new ChannelUID(getThing().getUID(), CHANNEL_PLAYER_ART),
|
||||
new StringType(String.valueOf(foundInSession ? currentSessionData.getArt() : "")));
|
||||
updateState(new ChannelUID(getThing().getUID(), CHANNEL_PLAYER_THUMB),
|
||||
new StringType(String.valueOf(foundInSession ? currentSessionData.getThumb() : "")));
|
||||
updateState(new ChannelUID(getThing().getUID(), CHANNEL_PLAYER_PROGRESS),
|
||||
new StringType(String.valueOf(foundInSession ? currentSessionData.getProgress() : "0")));
|
||||
updateState(new ChannelUID(getThing().getUID(), CHANNEL_PLAYER_ENDTIME),
|
||||
new StringType(String.valueOf(foundInSession ? currentSessionData.getEndTime() : "")));
|
||||
updateState(new ChannelUID(getThing().getUID(), CHANNEL_PLAYER_ENDTIME),
|
||||
new StringType(String.valueOf(foundInSession ? currentSessionData.getEndTime() : "")));
|
||||
updateState(new ChannelUID(getThing().getUID(), CHANNEL_PLAYER_USER),
|
||||
new StringType(String.valueOf(foundInSession ? currentSessionData.getUserTitle() : "")));
|
||||
|
||||
// Make sure player control is in sync with the play state
|
||||
if (currentSessionData.getState() == PlexPlayerState.PLAYING) {
|
||||
updateState(new ChannelUID(getThing().getUID(), CHANNEL_PLAYER_CONTROL), PlayPauseType.PLAY);
|
||||
}
|
||||
if (currentSessionData.getState() == PlexPlayerState.PAUSED) {
|
||||
updateState(new ChannelUID(getThing().getUID(), CHANNEL_PLAYER_CONTROL), PlayPauseType.PAUSE);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,323 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2023 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.plex.internal.handler;
|
||||
|
||||
import static org.openhab.binding.plex.internal.PlexBindingConstants.*;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.ScheduledFuture;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.eclipse.jetty.client.HttpClient;
|
||||
import org.eclipse.jetty.util.ssl.SslContextFactory;
|
||||
import org.openhab.binding.plex.internal.config.PlexServerConfiguration;
|
||||
import org.openhab.binding.plex.internal.dto.MediaContainer;
|
||||
import org.openhab.binding.plex.internal.dto.MediaContainer.MediaType;
|
||||
import org.openhab.core.i18n.ConfigurationException;
|
||||
import org.openhab.core.io.net.http.HttpClientFactory;
|
||||
import org.openhab.core.library.types.StringType;
|
||||
import org.openhab.core.thing.Bridge;
|
||||
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.util.ThingWebClientUtil;
|
||||
import org.openhab.core.types.Command;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* The {@link PlexServerHandler} is responsible for creating the
|
||||
* Bridge Thing for a PLEX Server.
|
||||
*
|
||||
* @author Brian Homeyer - Initial contribution
|
||||
* @author Aron Beurskens - Binding development
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class PlexServerHandler extends BaseBridgeHandler implements PlexUpdateListener {
|
||||
private final Logger logger = LoggerFactory.getLogger(PlexServerHandler.class);
|
||||
|
||||
private final HttpClientFactory httpClientFactory;
|
||||
private @Nullable HttpClient httpClient;
|
||||
|
||||
// Maintain mapping of handler and players
|
||||
private final Map<String, PlexPlayerHandler> playerHandlers = new ConcurrentHashMap<>();
|
||||
|
||||
private PlexServerConfiguration config = new PlexServerConfiguration();
|
||||
private PlexApiConnector plexAPIConnector;
|
||||
|
||||
private @Nullable ScheduledFuture<?> pollingJob;
|
||||
|
||||
private volatile boolean isRunning = false;
|
||||
|
||||
public PlexServerHandler(Bridge bridge, HttpClientFactory httpClientFactory) {
|
||||
super(bridge);
|
||||
this.httpClientFactory = httpClientFactory;
|
||||
plexAPIConnector = new PlexApiConnector(scheduler, httpClientFactory.getCommonHttpClient());
|
||||
logger.debug("Initializing server handler");
|
||||
}
|
||||
|
||||
public PlexApiConnector getPlexAPIConnector() {
|
||||
return plexAPIConnector;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize the Bridge set the config paramaters for the PLEX Server and
|
||||
* start the refresh Job.
|
||||
*/
|
||||
@Override
|
||||
public void initialize() {
|
||||
final String httpClientName = ThingWebClientUtil.buildWebClientConsumerName(thing.getUID(), null);
|
||||
try {
|
||||
SslContextFactory.Server sslContextFactory = new SslContextFactory.Server();
|
||||
sslContextFactory.setEndpointIdentificationAlgorithm(null);
|
||||
HttpClient localHttpClient = httpClient = httpClientFactory.createHttpClient(httpClientName,
|
||||
sslContextFactory);
|
||||
localHttpClient.start();
|
||||
plexAPIConnector = new PlexApiConnector(scheduler, localHttpClient);
|
||||
} catch (Exception e) {
|
||||
logger.error(
|
||||
"Long running HttpClient for PlexServerHandler {} cannot be started. Creating Handler failed. Exception: {}",
|
||||
httpClientName, e.getMessage(), e);
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR);
|
||||
return;
|
||||
}
|
||||
|
||||
config = getConfigAs(PlexServerConfiguration.class);
|
||||
if (!config.host.isEmpty()) { // Check if a hostname is set
|
||||
plexAPIConnector.setParameters(config);
|
||||
} else {
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
|
||||
"Host must be specified, check configuration");
|
||||
return;
|
||||
}
|
||||
if (!plexAPIConnector.hasToken()) {
|
||||
// No token is set by config, let's see if we can fetch one from username/password
|
||||
logger.debug("Token is not set, trying to fetch one");
|
||||
if (config.username.isEmpty() || config.password.isEmpty()) {
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
|
||||
"Username, password and Token is not set, unable to connect to PLEX without. ");
|
||||
return;
|
||||
} else {
|
||||
try {
|
||||
plexAPIConnector.getToken();
|
||||
} catch (ConfigurationException e) {
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, e.getMessage());
|
||||
return;
|
||||
} catch (Exception e) {
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
logger.debug("Fetch API with config, {}", config.toString());
|
||||
if (!plexAPIConnector.getApi()) {
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
|
||||
"Unable to fetch API, token may be wrong?");
|
||||
return;
|
||||
}
|
||||
isRunning = true;
|
||||
onUpdate(); // Start the session refresh
|
||||
scheduler.execute(() -> { // Start the web socket
|
||||
synchronized (this) {
|
||||
if (isRunning) {
|
||||
final HttpClient localHttpClient = this.httpClient;
|
||||
if (localHttpClient != null) {
|
||||
PlexApiConnector localSockets = plexAPIConnector = new PlexApiConnector(scheduler,
|
||||
localHttpClient);
|
||||
localSockets.setParameters(config);
|
||||
localSockets.registerListener(this);
|
||||
localSockets.connect();
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Not currently used, all channels in this thing are read-only.
|
||||
*/
|
||||
@Override
|
||||
public void handleCommand(ChannelUID channelUID, Command command) {
|
||||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a list of all the players currently being used w/ a status of local. This
|
||||
* is used for discovery only.
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
public List<String> getAvailablePlayers() {
|
||||
List<String> availablePlayers = new ArrayList<String>();
|
||||
MediaContainer sessionData = plexAPIConnector.getSessionData();
|
||||
|
||||
if (sessionData != null && sessionData.getSize() > 0) {
|
||||
for (MediaType tmpMeta : sessionData.getMediaTypes()) {
|
||||
if (tmpMeta != null && playerHandlers.get(tmpMeta.getPlayer().getMachineIdentifier()) == null) {
|
||||
if ("1".equals(tmpMeta.getPlayer().getLocal())) {
|
||||
availablePlayers.add(tmpMeta.getPlayer().getMachineIdentifier());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return availablePlayers;
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when a new player thing has been added. We add it to the hash map so we can
|
||||
* keep track of things.
|
||||
*/
|
||||
@Override
|
||||
public synchronized void childHandlerInitialized(ThingHandler childHandler, Thing childThing) {
|
||||
String playerID = (String) childThing.getConfiguration().get(CONFIG_PLAYER_ID);
|
||||
playerHandlers.put(playerID, (PlexPlayerHandler) childHandler);
|
||||
logger.debug("Bridge: Monitor handler was initialized for {} with id {}", childThing.getUID(), playerID);
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when a player has been removed from the system.
|
||||
*/
|
||||
@Override
|
||||
public synchronized void childHandlerDisposed(ThingHandler childHandler, Thing childThing) {
|
||||
String playerID = (String) childThing.getConfiguration().get(CONFIG_PLAYER_ID);
|
||||
playerHandlers.remove(playerID);
|
||||
logger.debug("Bridge: Monitor handler was disposed for {} with id {}", childThing.getUID(), playerID);
|
||||
}
|
||||
|
||||
/**
|
||||
* Basically a callback method for the websocket handling
|
||||
*/
|
||||
@Override
|
||||
public void onItemStatusUpdate(String sessionKey, String state) {
|
||||
try {
|
||||
for (Map.Entry<String, PlexPlayerHandler> entry : playerHandlers.entrySet()) {
|
||||
if (entry.getValue().getSessionKey().equals(sessionKey)) {
|
||||
entry.getValue().updateStateChannel(state);
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
logger.debug("Failed setting item status : {}", e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears the foundInSession field for the configured players, then it sets the
|
||||
* data for the machineIds that are found in the session data set. This allows
|
||||
* us to determine if a device is on or off.
|
||||
*
|
||||
* @param sessionData The MediaContainer object that is pulled from the XML result of
|
||||
* a call to the session data on PLEX.
|
||||
*/
|
||||
@SuppressWarnings("null")
|
||||
private void refreshStates(MediaContainer sessionData) {
|
||||
int playerCount = 0;
|
||||
int playerActiveCount = 0;
|
||||
Iterator<PlexPlayerHandler> valueIterator = playerHandlers.values().iterator();
|
||||
while (valueIterator.hasNext()) {
|
||||
playerCount++;
|
||||
valueIterator.next().setFoundInSession(false);
|
||||
}
|
||||
if (sessionData != null && sessionData.getSize() > 0) { // Cover condition where nothing is playing
|
||||
for (MediaContainer.MediaType tmpMeta : sessionData.getMediaTypes()) { // Roll through mediaType objects
|
||||
// looking for machineID
|
||||
if (playerHandlers.get(tmpMeta.getPlayer().getMachineIdentifier()) != null) { // if we have a player
|
||||
// configured, update
|
||||
// it
|
||||
tmpMeta.setArt(plexAPIConnector.getURL(tmpMeta.getArt()));
|
||||
if (tmpMeta.getType().equals("episode")) {
|
||||
tmpMeta.setThumb(plexAPIConnector.getURL(tmpMeta.getGrandparentThumb()));
|
||||
tmpMeta.setTitle(tmpMeta.getGrandparentTitle() + " : " + tmpMeta.getTitle());
|
||||
} else if (tmpMeta.getType().equals("track")) {
|
||||
tmpMeta.setThumb(plexAPIConnector.getURL(tmpMeta.getThumb()));
|
||||
tmpMeta.setTitle(tmpMeta.getGrandparentTitle() + " - " + tmpMeta.getParentTitle() + " - "
|
||||
+ tmpMeta.getTitle());
|
||||
} else {
|
||||
tmpMeta.setThumb(plexAPIConnector.getURL(tmpMeta.getThumb()));
|
||||
}
|
||||
playerHandlers.get(tmpMeta.getPlayer().getMachineIdentifier()).refreshSessionData(tmpMeta);
|
||||
playerActiveCount++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
updateState(new ChannelUID(getThing().getUID(), CHANNEL_SERVER_COUNT),
|
||||
new StringType(String.valueOf(playerCount)));
|
||||
updateState(new ChannelUID(getThing().getUID(), CHANNEL_SERVER_COUNTACTIVE),
|
||||
new StringType(String.valueOf(playerActiveCount)));
|
||||
}
|
||||
|
||||
/**
|
||||
* Refresh all the configured players
|
||||
*/
|
||||
private void refreshAllPlayers() {
|
||||
Iterator<PlexPlayerHandler> valueIterator = playerHandlers.values().iterator();
|
||||
while (valueIterator.hasNext()) {
|
||||
valueIterator.next().updateChannels();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This is called to start the refresh job and also to reset that refresh job when a config change is done.
|
||||
*/
|
||||
private synchronized void onUpdate() {
|
||||
ScheduledFuture<?> pollingJob = this.pollingJob;
|
||||
if (pollingJob == null || pollingJob.isCancelled()) {
|
||||
int pollingInterval = ((BigDecimal) getConfig().get(CONFIG_REFRESH_RATE)).intValue();
|
||||
this.pollingJob = scheduler.scheduleWithFixedDelay(pollingRunnable, 1, pollingInterval, TimeUnit.SECONDS);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The refresh job, pulls the session data and then calls refreshAllPlayers which will have them send
|
||||
* out their current status.
|
||||
*/
|
||||
private Runnable pollingRunnable = () -> {
|
||||
try {
|
||||
MediaContainer plexSessionData = plexAPIConnector.getSessionData();
|
||||
if (plexSessionData != null) {
|
||||
refreshStates(plexSessionData);
|
||||
updateStatus(ThingStatus.ONLINE);
|
||||
} else {
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
|
||||
"PLEX is not returning valid session data");
|
||||
}
|
||||
refreshAllPlayers();
|
||||
} catch (Exception e) {
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, String
|
||||
.format("An exception occurred while polling the PLEX Server: '%s'", e.getMessage()).toString());
|
||||
}
|
||||
};
|
||||
|
||||
@Override
|
||||
public void dispose() {
|
||||
logger.debug("Disposing PLEX Bridge Handler.");
|
||||
isRunning = false;
|
||||
plexAPIConnector.dispose();
|
||||
|
||||
ScheduledFuture<?> pollingJob = this.pollingJob;
|
||||
if (pollingJob != null && !pollingJob.isCancelled()) {
|
||||
pollingJob.cancel(true);
|
||||
this.pollingJob = null;
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,24 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2023 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.plex.internal.handler;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
|
||||
/**
|
||||
* @author Brian Homeyer - Initial contribution
|
||||
* @author Aron Beurskens - Binding development
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public interface PlexUpdateListener {
|
||||
void onItemStatusUpdate(String sessionKey, String state);
|
||||
}
|
@ -0,0 +1,11 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<addon:addon id="plex" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xmlns:addon="https://openhab.org/schemas/addon/v1.0.0"
|
||||
xsi:schemaLocation="https://openhab.org/schemas/addon/v1.0.0 https://openhab.org/schemas/addon-1.0.0.xsd">
|
||||
|
||||
<type>binding</type>
|
||||
<name>Plex Binding</name>
|
||||
<description>Monitor your PLEX server and players</description>
|
||||
<connection>hybrid</connection>
|
||||
|
||||
</addon:addon>
|
@ -0,0 +1,52 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<config-description:config-descriptions
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xmlns:config-description="https://openhab.org/schemas/config-description/v1.0.0"
|
||||
xsi:schemaLocation="https://openhab.org/schemas/config-description/v1.0.0 https://openhab.org/schemas/config-description-1.0.0.xsd">
|
||||
|
||||
<config-description uri="thing-type:plex:server">
|
||||
<parameter-group name="config-info">
|
||||
<label>PLEX Bridge Configuration</label>
|
||||
</parameter-group>
|
||||
<parameter-group name="auth-info">
|
||||
<label>Authentication Information</label>
|
||||
</parameter-group>
|
||||
<parameter name="host" type="text" required="true" groupName="config-info">
|
||||
<label>Server</label>
|
||||
<description>PLEX host name or IP address</description>
|
||||
<context>network-address</context>
|
||||
</parameter>
|
||||
<parameter name="portNumber" type="integer" min="1" max="65535" required="false" groupName="config-info">
|
||||
<label>Port Number</label>
|
||||
<description>Port Number (leave blank if PLEX installed on default port)</description>
|
||||
<default>32400</default>
|
||||
</parameter>
|
||||
<parameter name="refreshRate" type="integer" min="2" unit="s" required="true" groupName="config-info">
|
||||
<label>Refresh Interval</label>
|
||||
<description>Interval in seconds at which PLEX server status is polled</description>
|
||||
<default>5</default>
|
||||
</parameter>
|
||||
<parameter name="token" type="text" required="false" groupName="auth-info">
|
||||
<label>X-Token</label>
|
||||
<description>X-Token</description>
|
||||
</parameter>
|
||||
<parameter name="username" type="text" groupName="auth-info">
|
||||
<label>Username</label>
|
||||
<description>If you're using PLEX Home you need to supply the username and password of your PLEX account here. If you
|
||||
don't want to enter your credentials you can also directly set your account token below instead.</description>
|
||||
</parameter>
|
||||
<parameter name="password" type="text" groupName="auth-info">
|
||||
<context>password</context>
|
||||
<label>Password</label>
|
||||
<description>Password</description>
|
||||
</parameter>
|
||||
</config-description>
|
||||
|
||||
<config-description uri="thing-type:plex:player">
|
||||
<parameter name="playerID" type="text" required="true">
|
||||
<label>Player ID</label>
|
||||
<description>The unique identifier of the player</description>
|
||||
</parameter>
|
||||
</config-description>
|
||||
|
||||
</config-description:config-descriptions>
|
@ -0,0 +1,57 @@
|
||||
# add-on
|
||||
|
||||
addon.plex.name = Plex Binding
|
||||
addon.plex.description = Monitor your PLEX server and players
|
||||
|
||||
# thing types
|
||||
|
||||
thing-type.plex.player.label = PLEX Player
|
||||
thing-type.plex.player.description = Represents a PLEX player
|
||||
thing-type.plex.server.label = PLEX Server
|
||||
thing-type.plex.server.description = Represents a PLEX Server
|
||||
|
||||
# thing types config
|
||||
|
||||
thing-type.config.plex.player.playerID.label = Player ID
|
||||
thing-type.config.plex.player.playerID.description = The unique identifier of the player
|
||||
thing-type.config.plex.server.group.auth-info.label = Authentication Information
|
||||
thing-type.config.plex.server.group.config-info.label = PLEX Bridge Configuration
|
||||
thing-type.config.plex.server.host.label = Server
|
||||
thing-type.config.plex.server.host.description = PLEX host name or IP address
|
||||
thing-type.config.plex.server.password.label = Password
|
||||
thing-type.config.plex.server.password.description = Password
|
||||
thing-type.config.plex.server.portNumber.label = Port Number
|
||||
thing-type.config.plex.server.portNumber.description = Port Number (leave blank if PLEX installed on default port)
|
||||
thing-type.config.plex.server.refreshRate.label = Refresh Interval
|
||||
thing-type.config.plex.server.refreshRate.description = Interval in seconds at which PLEX server status is polled
|
||||
thing-type.config.plex.server.token.label = X-Token
|
||||
thing-type.config.plex.server.token.description = X-Token
|
||||
thing-type.config.plex.server.username.label = Username
|
||||
thing-type.config.plex.server.username.description = If you're using PLEX Home you need to supply the username and password of your PLEX account here. If you don't want to enter your credentials you can also directly set your account token below instead.
|
||||
|
||||
# channel types
|
||||
|
||||
channel-type.plex.art.label = Background Art
|
||||
channel-type.plex.art.description = The URL of the background art for currently playing media
|
||||
channel-type.plex.currentPlayers.label = Current Players
|
||||
channel-type.plex.currentPlayers.description = The number of players currently configured to watch on PLEX
|
||||
channel-type.plex.currentPlayersActive.label = Current Players Active
|
||||
channel-type.plex.currentPlayersActive.description = The number of players currently being used on PLEX
|
||||
channel-type.plex.endtime.label = End Time
|
||||
channel-type.plex.endtime.description = Time at which the media that is playing will end
|
||||
channel-type.plex.player.label = Player Control
|
||||
channel-type.plex.player.description = The control channel for the player `PLAY/PAUSE/NEXT/PREVIOUS`
|
||||
channel-type.plex.power.label = Player Power State
|
||||
channel-type.plex.power.description = The power status of the player
|
||||
channel-type.plex.progress.label = Media Progress
|
||||
channel-type.plex.progress.description = The current progress of playing media
|
||||
channel-type.plex.state.label = Player State
|
||||
channel-type.plex.state.description = The current state of the Player
|
||||
channel-type.plex.thumb.label = Cover Art
|
||||
channel-type.plex.thumb.description = The URL of the cover art for currently playing media
|
||||
channel-type.plex.title.label = Player Title
|
||||
channel-type.plex.title.description = The title of media that is playing
|
||||
channel-type.plex.type.label = Media Type
|
||||
channel-type.plex.type.description = The current type of playing media
|
||||
channel-type.plex.user.label = Player User
|
||||
channel-type.plex.user.description = The title of the user controlling the player
|
@ -0,0 +1,108 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<thing:thing-descriptions bindingId="plex"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
|
||||
xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
|
||||
|
||||
<bridge-type id="server">
|
||||
<label>PLEX Server</label>
|
||||
<description>Represents a PLEX Server</description>
|
||||
<channels>
|
||||
<channel id="currentPlayer" typeId="currentPlayers"/>
|
||||
<channel id="currentPlayersActive" typeId="currentPlayersActive"/>
|
||||
</channels>
|
||||
<config-description-ref uri="thing-type:plex:server"/>
|
||||
</bridge-type>
|
||||
|
||||
<thing-type id="player">
|
||||
<supported-bridge-type-refs>
|
||||
<bridge-type-ref id="server"/>
|
||||
</supported-bridge-type-refs>
|
||||
<label>PLEX Player</label>
|
||||
<description>Represents a PLEX player</description>
|
||||
<channels>
|
||||
<channel id="state" typeId="state"/>
|
||||
<channel id="power" typeId="power"/>
|
||||
<channel id="title" typeId="title"/>
|
||||
<channel id="type" typeId="type"/>
|
||||
<channel id="endtime" typeId="endtime"/>
|
||||
<channel id="progress" typeId="progress"/>
|
||||
<channel id="art" typeId="art"/>
|
||||
<channel id="thumb" typeId="thumb"/>
|
||||
<channel id="player" typeId="player"/>
|
||||
<channel id="user" typeId="user"/>
|
||||
</channels>
|
||||
<config-description-ref uri="thing-type:plex:player"/>
|
||||
</thing-type>
|
||||
|
||||
<channel-type id="currentPlayers">
|
||||
<item-type>Number</item-type>
|
||||
<label>Current Players</label>
|
||||
<description>The number of players currently configured to watch on PLEX</description>
|
||||
<state readOnly="true"/>
|
||||
</channel-type>
|
||||
<channel-type id="currentPlayersActive">
|
||||
<item-type>Number</item-type>
|
||||
<label>Current Players Active</label>
|
||||
<description>The number of players currently being used on PLEX</description>
|
||||
<state readOnly="true"/>
|
||||
</channel-type>
|
||||
<channel-type id="state">
|
||||
<item-type>String</item-type>
|
||||
<label>Player State</label>
|
||||
<description>The current state of the Player</description>
|
||||
<state readOnly="true"/>
|
||||
</channel-type>
|
||||
<channel-type id="power">
|
||||
<item-type>Switch</item-type>
|
||||
<label>Player Power State</label>
|
||||
<description>The power status of the player</description>
|
||||
<state readOnly="true"/>
|
||||
</channel-type>
|
||||
<channel-type id="title">
|
||||
<item-type>String</item-type>
|
||||
<label>Player Title</label>
|
||||
<description>The title of media that is playing</description>
|
||||
<state readOnly="true"/>
|
||||
</channel-type>
|
||||
<channel-type id="type">
|
||||
<item-type>String</item-type>
|
||||
<label>Media Type</label>
|
||||
<description>The current type of playing media</description>
|
||||
<state readOnly="true"/>
|
||||
</channel-type>
|
||||
<channel-type id="endtime">
|
||||
<item-type>DateTime</item-type>
|
||||
<label>End Time</label>
|
||||
<description>Time at which the media that is playing will end</description>
|
||||
<state readOnly="true"/>
|
||||
</channel-type>
|
||||
<channel-type id="progress">
|
||||
<item-type>Dimmer</item-type>
|
||||
<label>Media Progress</label>
|
||||
<description>The current progress of playing media</description>
|
||||
</channel-type>
|
||||
<channel-type id="art">
|
||||
<item-type>String</item-type>
|
||||
<label>Background Art</label>
|
||||
<description>The URL of the background art for currently playing media</description>
|
||||
<state readOnly="true"/>
|
||||
</channel-type>
|
||||
<channel-type id="thumb">
|
||||
<item-type>String</item-type>
|
||||
<label>Cover Art</label>
|
||||
<description>The URL of the cover art for currently playing media</description>
|
||||
<state readOnly="true"/>
|
||||
</channel-type>
|
||||
<channel-type id="player">
|
||||
<item-type>Player</item-type>
|
||||
<label>Player Control</label>
|
||||
<description>The control channel for the player `PLAY/PAUSE/NEXT/PREVIOUS`</description>
|
||||
</channel-type>
|
||||
<channel-type id="user">
|
||||
<item-type>String</item-type>
|
||||
<label>Player User</label>
|
||||
<description>The title of the user controlling the player</description>
|
||||
<state readOnly="true"/>
|
||||
</channel-type>
|
||||
</thing:thing-descriptions>
|
@ -0,0 +1,64 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2023 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.plex.internal.handler;
|
||||
|
||||
import static org.hamcrest.CoreMatchers.*;
|
||||
import static org.hamcrest.MatcherAssert.assertThat;
|
||||
|
||||
import java.util.concurrent.ScheduledExecutorService;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jetty.client.HttpClient;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.params.ParameterizedTest;
|
||||
import org.junit.jupiter.params.provider.Arguments;
|
||||
import org.junit.jupiter.params.provider.MethodSource;
|
||||
import org.mockito.Mock;
|
||||
import org.openhab.binding.plex.internal.config.PlexServerConfiguration;
|
||||
|
||||
/**
|
||||
* Tests cases for {@link org.openhab.binding.plex.internal.handler.PlexApiConnector}.
|
||||
*
|
||||
* @author Brian Homeyer - Initial contribution
|
||||
* @author Aron Beurskens - Added test
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class PlexApiConnectorTest {
|
||||
private @NonNullByDefault({}) @Mock ScheduledExecutorService scheduler;
|
||||
private @NonNullByDefault({}) @Mock HttpClient httpClient;
|
||||
|
||||
private @NonNullByDefault({}) PlexApiConnector plexApiConnector;
|
||||
|
||||
@BeforeEach
|
||||
public void setUp() {
|
||||
plexApiConnector = new PlexApiConnector(scheduler, httpClient);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test that the .hasToken check return the correct values.
|
||||
*/
|
||||
@ParameterizedTest(name = "{index} => token={0}, result={1}")
|
||||
@MethodSource("tokenProvider")
|
||||
public void testHasToken(String token, Boolean result) {
|
||||
PlexServerConfiguration config = new PlexServerConfiguration();
|
||||
config.token = token;
|
||||
plexApiConnector.setParameters(config);
|
||||
assertThat(plexApiConnector.hasToken(), is(result));
|
||||
}
|
||||
|
||||
private static Stream<Arguments> tokenProvider() {
|
||||
return Stream.of(Arguments.of("123", true), Arguments.of(" ", false),
|
||||
Arguments.of("fdsjkghdf-dsjfhs-dsafkshj", true), Arguments.of("", false));
|
||||
}
|
||||
}
|
@ -300,6 +300,7 @@
|
||||
<module>org.openhab.binding.pjlinkdevice</module>
|
||||
<module>org.openhab.binding.playstation</module>
|
||||
<module>org.openhab.binding.plclogo</module>
|
||||
<module>org.openhab.binding.plex</module>
|
||||
<module>org.openhab.binding.plugwise</module>
|
||||
<module>org.openhab.binding.plugwiseha</module>
|
||||
<module>org.openhab.binding.powermax</module>
|
||||
|
Loading…
Reference in New Issue
Block a user