mirror of
https://github.com/openhab/openhab-addons.git
synced 2025-01-10 15:11:59 +01:00
[playstation] Initial contribution (#8585)
* [playstation] Initial contribution. Signed-off-by: Fredrik Ahlström <fredrikahlstrom75@gmail.com>
This commit is contained in:
parent
72c441e422
commit
fde30fbada
@ -195,6 +195,7 @@
|
||||
/bundles/org.openhab.binding.pioneeravr/ @Stratehm
|
||||
/bundles/org.openhab.binding.pixometer/ @Confectrician
|
||||
/bundles/org.openhab.binding.pjlinkdevice/ @nils
|
||||
/bundles/org.openhab.binding.playstation/ @FluBBaOfWard
|
||||
/bundles/org.openhab.binding.plclogo/ @falkena
|
||||
/bundles/org.openhab.binding.plugwise/ @wborn
|
||||
/bundles/org.openhab.binding.powermax/ @lolodomo
|
||||
|
@ -966,6 +966,11 @@
|
||||
<artifactId>org.openhab.binding.pjlinkdevice</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.openhab.addons.bundles</groupId>
|
||||
<artifactId>org.openhab.binding.playstation</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.openhab.addons.bundles</groupId>
|
||||
<artifactId>org.openhab.binding.plclogo</artifactId>
|
||||
|
13
bundles/org.openhab.binding.playstation/NOTICE
Executable file
13
bundles/org.openhab.binding.playstation/NOTICE
Executable 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
|
126
bundles/org.openhab.binding.playstation/README.md
Executable file
126
bundles/org.openhab.binding.playstation/README.md
Executable file
@ -0,0 +1,126 @@
|
||||
# Sony PlayStation Binding
|
||||
|
||||
This binding allows you to monitor the on/off status and which application that is currently running on your PlayStation 4.
|
||||
By providing your user-credentials you can also change the power, which application that is running and more.
|
||||
|
||||
## Supported Things
|
||||
|
||||
This binding should support all PS4 variants.
|
||||
It can also tell if your PS3 is ON or OFF/not present.
|
||||
|
||||
## Discovery
|
||||
|
||||
Discovery should find all your PS4s within a few seconds as long as they are in standby mode and not completely turned off.
|
||||
To be able to discover your PS3 you need to turn on "Connect PS Vita System Using Network" in
|
||||
Settings -> System Settings -> Connect PS Vita System Using Network.
|
||||
|
||||
## Thing Configuration
|
||||
|
||||
**playstation4** parameters:
|
||||
|
||||
| Property | Default | Required | Description |
|
||||
|---------------------|---------|:--------:|--------------------------------------------------------------------------|
|
||||
| ipAddress | | Yes | The IP address of the PlayStation 4 |
|
||||
| userCredential | | Yes | A key used for authentication, get via PS4-waker. |
|
||||
| pairingCode | | Yes | This is shown on the PlayStation 4 during pairing, only needed once. |
|
||||
| passCode | | (Yes) | If you use a code to log in your user on the PS4, set this. |
|
||||
| connectionTimeout | 60 | No | How long the connection to the PS4 is kept up, seconds. |
|
||||
| autoConnect | false | No | If a connection should be establish to the PS4 when it's turned on. |
|
||||
| artworkSize | 320 | No | Width and height of downloaded artwork. |
|
||||
| outboundIP | | No | Use this if your PS4 is not on the normal openHAB network. |
|
||||
| ipPort | 997 | No | The port to probe the PS4 on, no need to change normally. |
|
||||
|
||||
If you want to control your PS4 the first thing you need is your user-credentials, this is a 64 characters HEX string that is easiest obtained by using PS4-waker https://github.com/dhleong/ps4-waker.
|
||||
The result file is called ".ps4-wake.credentials.json" in your home directory.
|
||||
|
||||
Then you need to pair your openHAB device with the PS4.
|
||||
This can be done by saving the Thing while the pairing screen is open on the PS4. The code is only needed during pairing.
|
||||
|
||||
Then, if you have a pass code when you log in to your PS4 you have to specify that as well.
|
||||
|
||||
**playstation3** parameters:
|
||||
|
||||
| Property | Default | Required | Description |
|
||||
|---------------------|---------|:--------:|--------------------------------------------------------------------------|
|
||||
| ipAddress | | Yes | The IP address of the PlayStation 3 |
|
||||
|
||||
|
||||
## Channels
|
||||
|
||||
| Channel Type ID | Item Type | Description | Read/Write |
|
||||
|------------------|-----------|-------------------------------------------------------------------------|------------|
|
||||
| power | Switch | Shows if PlayStation is ON or in standby. | RW |
|
||||
| applicationName | String | Name of the currently running application. | R |
|
||||
| applicationId | String | Id of the currently running application. | RW |
|
||||
| applicationImage | Image | Application artwork. | R |
|
||||
| oskText | String | The text from the OnScreenKeyboard. | RW |
|
||||
| sendKey | String | Send a key/button push to PS4. | W |
|
||||
| secondScreen | String | HTTP link to the second screen. | R |
|
||||
| connect | Switch | Connect/disconnect to/from PS4. | RW |
|
||||
|
||||
## Full Example
|
||||
|
||||
Example of how to configure a thing.
|
||||
|
||||
demo.thing
|
||||
|
||||
```
|
||||
Thing playstation:PS4:123456789ABC "PlayStation4" @ "Living Room" [ ipAddress="192.168.0.2", userCredential="0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF", passCode="1234", pairingCode="12345678",
|
||||
connectionTimeout="60", autoConnect="false", artworkSize="320", outboundIP="192.168.0.3", ipPort="997" ]
|
||||
|
||||
Thing playstation:PS3:123456789ABC "PlayStation3" @ "Living Room" [ ipAddress="192.168.0.2" ]
|
||||
```
|
||||
|
||||
Here are some examples on how to map the channels to items.
|
||||
|
||||
demo.items:
|
||||
|
||||
```
|
||||
Switch PS4_Power "Power" { channel="playstation:PS4:123456789ABC:power" }
|
||||
String PS4_Application "Application [%s]" { channel="playstation:PS4:123456789ABC:applicationName" }
|
||||
String PS4_ApplicationId "Application id [%s]" { channel="playstation:PS4:123456789ABC:applicationId" }
|
||||
Image PS4_ArtWork "Artwork" { channel="playstation:PS4:123456789ABC:applicationImage" }
|
||||
String PS4_OSKText "OSK Text" { channel="playstation:PS4:123456789ABC:oskText" }
|
||||
String PS4_SendKey "SendKey" { channel="playstation:PS4:123456789ABC:sendKey" }
|
||||
String PS4_2ndScr "2ndScreen" { channel="playstation:PS4:123456789ABC:secondScreen" }
|
||||
Switch PS4_Connect "Connect" { channel="playstation:PS4:123456789ABC:connect" }
|
||||
|
||||
Switch PS3_Power "Power" { channel="playstation:PS3:123456789ABC:power" }
|
||||
```
|
||||
|
||||
demo.sitemap:
|
||||
|
||||
```
|
||||
sitemap demo label="Main Menu"
|
||||
{
|
||||
Frame label="PlayStation 4" {
|
||||
Switch item=PS4_Power
|
||||
Text item=PS4_Application
|
||||
Text item=PS4_ApplicationId
|
||||
Selection item=PS4_ApplicationId mappings=[
|
||||
"CUSA00127"="Netflix",
|
||||
"CUSA01116"="Youtube",
|
||||
"CUSA02827"="HBO",
|
||||
"CUSA01780"="Spotify",
|
||||
"CUSA11993"="Marvel's Spider-Man" ]
|
||||
Image item=PS4_Artwork
|
||||
Text item=PS4_OSKText
|
||||
Switch item=PS4_Connect
|
||||
String item=PS4_SendKey
|
||||
Selection item=PS4_SendKey mappings=[
|
||||
"keyUp"="Up",
|
||||
"keyDown"="Down",
|
||||
"keyRight"="Right",
|
||||
"keyLeft"="Left",
|
||||
"keySelect"="Select",
|
||||
"keyBack"="Back",
|
||||
"keyOption"="Option",
|
||||
"keyPS"="PS" ]
|
||||
Text item=PS4_2ndScr
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Caveat and Limitations!
|
||||
|
||||
I tried my hardest to figure out how to turn on the PS3 through WakeOnLan but it looks like Sony never got it to work properly, the only way I've seen it turn on is via WiFi, but if you hook up your PS3 through WiFi to your router and enable WakeOnLan it turns itself on randomly.
|
17
bundles/org.openhab.binding.playstation/pom.xml
Executable file
17
bundles/org.openhab.binding.playstation/pom.xml
Executable file
@ -0,0 +1,17 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
|
||||
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<parent>
|
||||
<groupId>org.openhab.addons.bundles</groupId>
|
||||
<artifactId>org.openhab.addons.reactor.bundles</artifactId>
|
||||
<version>3.0.0-SNAPSHOT</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>org.openhab.binding.playstation</artifactId>
|
||||
|
||||
<name>openHAB Add-ons :: Bundles :: Sony PlayStation Binding</name>
|
||||
|
||||
</project>
|
@ -0,0 +1,9 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<features name="org.openhab.binding.playstation-${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-playstation" description="PlayStation Binding" version="${project.version}">
|
||||
<feature>openhab-runtime-base</feature>
|
||||
<bundle start-level="80">mvn:org.openhab.addons.bundles/org.openhab.binding.playstation/${project.version}</bundle>
|
||||
</feature>
|
||||
</features>
|
@ -0,0 +1,34 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.playstation.internal;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
|
||||
/**
|
||||
* The {@link PS3Configuration} class contains fields mapping thing configuration parameters.
|
||||
*
|
||||
* @author Fredrik Ahlström - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class PS3Configuration {
|
||||
|
||||
/**
|
||||
* IP-address of PS3.
|
||||
*/
|
||||
public String ipAddress = "";
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "IP" + ipAddress + ".";
|
||||
}
|
||||
}
|
@ -0,0 +1,197 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.playstation.internal;
|
||||
|
||||
import static org.openhab.binding.playstation.internal.PlayStationBindingConstants.*;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.DatagramPacket;
|
||||
import java.net.DatagramSocket;
|
||||
import java.net.InetAddress;
|
||||
import java.net.InetSocketAddress;
|
||||
import java.net.Socket;
|
||||
import java.net.SocketTimeoutException;
|
||||
import java.nio.channels.SocketChannel;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.concurrent.ScheduledFuture;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.openhab.core.library.types.OnOffType;
|
||||
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.openhab.core.util.HexUtils;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* The {@link PS3Handler} is responsible for handling commands, which are
|
||||
* sent to one of the channels.
|
||||
*
|
||||
* @author Fredrik Ahlström - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class PS3Handler extends BaseThingHandler {
|
||||
|
||||
private final Logger logger = LoggerFactory.getLogger(PS3Handler.class);
|
||||
private static final int SOCKET_TIMEOUT_SECONDS = 2;
|
||||
private boolean isDisposed = false;
|
||||
|
||||
private PS3Configuration config = new PS3Configuration();
|
||||
|
||||
private @Nullable ScheduledFuture<?> refreshTimer;
|
||||
|
||||
public PS3Handler(Thing thing) {
|
||||
super(thing);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleCommand(ChannelUID channelUID, Command command) {
|
||||
if (!(command instanceof RefreshType)) {
|
||||
if (CHANNEL_POWER.equals(channelUID.getId()) && command instanceof OnOffType) {
|
||||
if (command.equals(OnOffType.ON)) {
|
||||
turnOnPS3();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void initialize() {
|
||||
config = getConfigAs(PS3Configuration.class);
|
||||
isDisposed = false;
|
||||
|
||||
updateStatus(ThingStatus.ONLINE);
|
||||
setupRefreshTimer();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void dispose() {
|
||||
isDisposed = true;
|
||||
final ScheduledFuture<?> timer = refreshTimer;
|
||||
if (timer != null) {
|
||||
timer.cancel(false);
|
||||
refreshTimer = null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets up a timer for querying the PS3 (using the scheduler) every 10 seconds.
|
||||
*/
|
||||
private void setupRefreshTimer() {
|
||||
final ScheduledFuture<?> timer = refreshTimer;
|
||||
if (timer != null) {
|
||||
timer.cancel(false);
|
||||
}
|
||||
refreshTimer = scheduler.scheduleWithFixedDelay(this::updateAllChannels, 0, 10, TimeUnit.SECONDS);
|
||||
}
|
||||
|
||||
/**
|
||||
* This tries to connect to port 5223 on the PS3,
|
||||
* if the connection times out the PS3 is OFF, if connection is refused the PS3 is ON.
|
||||
*/
|
||||
private void updateAllChannels() {
|
||||
try (SocketChannel channel = SocketChannel.open()) {
|
||||
Socket socket = channel.socket();
|
||||
socket.setSoTimeout(SOCKET_TIMEOUT_SECONDS * 1000);
|
||||
channel.configureBlocking(true);
|
||||
channel.connect(new InetSocketAddress(config.ipAddress, DEFAULT_PS3_WAKE_ON_LAN_PORT));
|
||||
} catch (IOException e) {
|
||||
String message = e.getMessage();
|
||||
if (message.contains("refused")) {
|
||||
updateState(CHANNEL_POWER, OnOffType.ON);
|
||||
updateStatus(ThingStatus.ONLINE);
|
||||
} else if (message.contains("timed out") || message.contains("is down")) {
|
||||
updateState(CHANNEL_POWER, OnOffType.OFF);
|
||||
} else {
|
||||
logger.debug("PS3 read power, IOException: {}", e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void turnOnPS3() {
|
||||
String macAdr = thing.getProperties().get(Thing.PROPERTY_MAC_ADDRESS);
|
||||
if (macAdr == null) {
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "No MAC address configured.");
|
||||
return;
|
||||
}
|
||||
try {
|
||||
// send WOL magic packet
|
||||
byte[] magicPacket = makeWOLMagicPacket(macAdr);
|
||||
logger.debug("PS3 wol packet: {}", magicPacket);
|
||||
InetAddress bcAddress = InetAddress.getByName("255.255.255.255");
|
||||
DatagramPacket wakePacket = new DatagramPacket(magicPacket, magicPacket.length, bcAddress,
|
||||
DEFAULT_PS3_WAKE_ON_LAN_PORT);
|
||||
// send discover
|
||||
byte[] discover = "SRCH".getBytes(StandardCharsets.US_ASCII);
|
||||
DatagramPacket srchPacket = new DatagramPacket(discover, discover.length, bcAddress,
|
||||
DEFAULT_PS3_WAKE_ON_LAN_PORT);
|
||||
logger.debug("Search message: '{}'", discover);
|
||||
|
||||
// wait for responses
|
||||
byte[] rxbuf = new byte[256];
|
||||
DatagramPacket receivePacket = new DatagramPacket(rxbuf, rxbuf.length);
|
||||
scheduler.execute(() -> wakeMethod(srchPacket, receivePacket, wakePacket, 34));
|
||||
} catch (IOException e) {
|
||||
logger.debug("No PS3 device found. Diagnostic: {}", e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private void wakeMethod(DatagramPacket srchPacket, DatagramPacket receivePacket, DatagramPacket wakePacket,
|
||||
int triesLeft) {
|
||||
try (DatagramSocket searchSocket = new DatagramSocket(); DatagramSocket wakeSocket = new DatagramSocket();) {
|
||||
wakeSocket.setBroadcast(true);
|
||||
searchSocket.setBroadcast(true);
|
||||
searchSocket.setSoTimeout(1000);
|
||||
|
||||
searchSocket.send(srchPacket);
|
||||
try {
|
||||
searchSocket.receive(receivePacket);
|
||||
logger.debug("PS3 started?: '{}'", receivePacket);
|
||||
return;
|
||||
} catch (SocketTimeoutException e) {
|
||||
// try again
|
||||
}
|
||||
wakeSocket.send(wakePacket);
|
||||
if (triesLeft <= 0 || isDisposed) {
|
||||
logger.debug("PS3 not started!");
|
||||
} else {
|
||||
scheduler.execute(() -> wakeMethod(srchPacket, receivePacket, wakePacket, triesLeft - 1));
|
||||
}
|
||||
} catch (IOException e) {
|
||||
logger.debug("No PS3 device found. Diagnostic: {}", e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private byte[] makeWOLMagicPacket(String macAddress) {
|
||||
byte[] wolPacket = new byte[6 * 17];
|
||||
if (macAddress.length() < 17) {
|
||||
return wolPacket;
|
||||
}
|
||||
int pos = 0;
|
||||
for (int i = 0; i < 6; i++) {
|
||||
wolPacket[pos++] = -1; // 0xFF
|
||||
}
|
||||
byte[] macBytes = HexUtils.hexToBytes(macAddress, ":");
|
||||
for (int j = 0; j < 16; j++) {
|
||||
System.arraycopy(macBytes, 0, wolPacket, 6 + j * 6, 6);
|
||||
}
|
||||
return wolPacket;
|
||||
}
|
||||
}
|
@ -0,0 +1,160 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.playstation.internal;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.util.Locale;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.openhab.core.OpenHAB;
|
||||
import org.openhab.core.io.net.http.HttpUtil;
|
||||
import org.openhab.core.library.types.RawType;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* The {@link PS4ArtworkHandler} is responsible for fetching and caching
|
||||
* application artwork.
|
||||
*
|
||||
* @author Fredrik Ahlström - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class PS4ArtworkHandler {
|
||||
|
||||
private static final Logger LOGGER = LoggerFactory.getLogger(PS4ArtworkHandler.class);
|
||||
private static final File ARTWORK_CACHE_FOLDER;
|
||||
|
||||
/** Service pid */
|
||||
private static final String SERVICE_PID = "org.openhab.binding.playstation";
|
||||
|
||||
/** Cache folder under $userdata */
|
||||
private static final String CACHE_FOLDER_NAME = "cache";
|
||||
|
||||
/** Some countries use EN as language in the PS Store, this is to minimize requests */
|
||||
private static boolean useLanguageEn = false;
|
||||
|
||||
private PS4ArtworkHandler() {
|
||||
// No need to instantiate
|
||||
}
|
||||
|
||||
static {
|
||||
// create cache folder
|
||||
File userData = new File(OpenHAB.getUserDataFolder());
|
||||
File homeFolder = new File(userData, CACHE_FOLDER_NAME);
|
||||
|
||||
if (!homeFolder.exists()) {
|
||||
homeFolder.mkdirs();
|
||||
}
|
||||
LOGGER.debug("Using home folder: {}", homeFolder.getAbsolutePath());
|
||||
|
||||
// create binding folder
|
||||
File cacheFolder = new File(homeFolder, SERVICE_PID);
|
||||
if (!cacheFolder.exists()) {
|
||||
cacheFolder.mkdirs();
|
||||
}
|
||||
LOGGER.debug("Using cache folder {}", cacheFolder.getAbsolutePath());
|
||||
ARTWORK_CACHE_FOLDER = cacheFolder;
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds a artwork request string for the specified TitleId, also takes into account if the language should be from
|
||||
* the specified locale or just "en".
|
||||
*
|
||||
* @param locale The country and language to use for the store look up.
|
||||
* @param titleId The Title ID of the Application/game.
|
||||
* @param size The size of the artwork.
|
||||
* @return A https request as a String.
|
||||
*/
|
||||
private static String buildArtworkRequest(Locale locale, String titleId, Integer size) {
|
||||
String language = useLanguageEn ? "en" : locale.getLanguage();
|
||||
return "https://store.playstation.com/store/api/chihiro/00_09_000/titlecontainer/" + locale.getCountry() + "/"
|
||||
+ language + "/999/" + titleId + "_00/image?w=" + size.toString() + "&h=" + size.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch artwork for PS4 application. First looks for the file on disc, if the file is not on the disc it checks
|
||||
* PlayStation store
|
||||
*
|
||||
* @param titleid Title ID of application.
|
||||
* @param size Size (width & height) of art work in pixels , max 1024.
|
||||
* @param locale Locale used on PlayStation store to find art work.
|
||||
* @return A JPEG image as a RawType if an art work file is found otherwise null.
|
||||
*/
|
||||
public static @Nullable RawType fetchArtworkForTitleid(String titleId, Integer size, Locale locale) {
|
||||
return fetchArtworkForTitleid(titleId, size, locale, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch artwork for PS4 application. First looks for the file on disc, if the file is not on the disc it checks
|
||||
* PlayStation store
|
||||
*
|
||||
* @param titleid Title ID of application.
|
||||
* @param size Size (width & height) of art work in pixels , max 1024.
|
||||
* @param locale Locale used on PlayStation store to find art work.
|
||||
* @param forceRefetch When true, tries to re-fetch art work from PlayStation store, sometimes the art work is
|
||||
* updated along with the game.
|
||||
* @return A JPEG image as a RawType if an art work file is found otherwise null.
|
||||
*/
|
||||
public static @Nullable RawType fetchArtworkForTitleid(String titleId, Integer size, Locale locale,
|
||||
boolean forceRefetch) {
|
||||
// Try to find the image in the cache first, then try to download it from PlayStation Store.
|
||||
RawType artwork = null;
|
||||
if (titleId.isEmpty()) {
|
||||
return artwork;
|
||||
}
|
||||
String artworkFilename = titleId + "_" + size.toString() + ".jpg";
|
||||
File artworkFileInCache = new File(ARTWORK_CACHE_FOLDER, artworkFilename);
|
||||
if (artworkFileInCache.exists() && !forceRefetch) {
|
||||
LOGGER.trace("Artwork file {} was found in cache.", artworkFileInCache.getName());
|
||||
int length = (int) artworkFileInCache.length();
|
||||
byte[] fileBuffer = new byte[length];
|
||||
try (FileInputStream fis = new FileInputStream(artworkFileInCache)) {
|
||||
fis.read(fileBuffer, 0, length);
|
||||
artwork = new RawType(fileBuffer, "image/jpeg");
|
||||
} catch (FileNotFoundException ex) {
|
||||
LOGGER.debug("Could not find {} in cache. {}", artworkFileInCache, ex.getMessage());
|
||||
} catch (IOException ex) {
|
||||
LOGGER.debug("Could not read {} from cache. {}", artworkFileInCache, ex.getMessage());
|
||||
}
|
||||
if (artwork != null) {
|
||||
return artwork;
|
||||
}
|
||||
}
|
||||
String request = buildArtworkRequest(locale, titleId, size);
|
||||
artwork = HttpUtil.downloadImage(request);
|
||||
if (artwork == null) {
|
||||
// If artwork is not found for specified language/"en", try the other way around.
|
||||
useLanguageEn = !useLanguageEn;
|
||||
request = buildArtworkRequest(locale, titleId, size);
|
||||
artwork = HttpUtil.downloadImage(request);
|
||||
}
|
||||
if (artwork != null) {
|
||||
try (FileOutputStream fos = new FileOutputStream(artworkFileInCache)) {
|
||||
LOGGER.debug("Caching artwork file {}", artworkFileInCache.getName());
|
||||
fos.write(artwork.getBytes(), 0, artwork.getBytes().length);
|
||||
} catch (FileNotFoundException ex) {
|
||||
LOGGER.debug("Could not create {} in cache. {}", artworkFileInCache, ex.getMessage());
|
||||
} catch (IOException ex) {
|
||||
LOGGER.debug("Could not write {} to cache. {}", artworkFileInCache, ex.getMessage());
|
||||
}
|
||||
} else {
|
||||
LOGGER.debug("Could not download artwork file from {}", request);
|
||||
}
|
||||
return artwork;
|
||||
}
|
||||
}
|
@ -0,0 +1,81 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.playstation.internal;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
|
||||
/**
|
||||
* Enum of the possible commands.
|
||||
*
|
||||
* @author Fredrik Ahlström - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
enum PS4Command {
|
||||
UNKNOWN1_REQ(0x02),
|
||||
BUFFER_SIZE_RSP(0x03),
|
||||
BYEBYE_REQ(0x04),
|
||||
LOGIN_RSP(0x07),
|
||||
SCREEN_SHOT_REQ(0x08),
|
||||
SCREEN_SHOT_RSP(0x09),
|
||||
APP_START_REQ(0x0a),
|
||||
APP_START_RSP(0x0b),
|
||||
OSK_START_REQ(0x0c),
|
||||
OSK_START_RSP(0x0d),
|
||||
OSK_CHANGE_STRING_REQ(0x0e),
|
||||
OSK_CONTROL_REQ(0x10),
|
||||
SERVER_STATUS_RSP(0x12),
|
||||
STATUS_REQ(0x14),
|
||||
HTTPD_STATUS_RSP(0x16),
|
||||
SCREEN_STATUS_RSP(0x18),
|
||||
STANDBY_REQ(0x1a),
|
||||
STANDBY_RSP(0x1b),
|
||||
REMOTE_CONTROL_REQ(0x1c),
|
||||
LOGIN_REQ(0x1e),
|
||||
HANDSHAKE_REQ(0x20),
|
||||
LOGOUT_REQ(0x22),
|
||||
LOGOUT_RSP(0x23),
|
||||
APP_START2_REQ(0x24),
|
||||
APP_START2_RSP(0x25),
|
||||
CLIENT_IDENTITY_REQ(0x26),
|
||||
COMMENT_VIEWER_START_REQ(0x2a),
|
||||
COMMENT_VIEWER_START_RESULT(0x2b),
|
||||
COMMENT_VIEWER_NEW_COMMENT(0x2c),
|
||||
COMMENT_VIEWER_NEW_COMMENT2(0x2e),
|
||||
COMMENT_VIEWER_EVENT(0x30),
|
||||
COMMENT_VIEWER_SEND(0x32),
|
||||
HELLO_REQ(0x6f636370);
|
||||
|
||||
private static final Map<Integer, PS4Command> TAG_MAP = Arrays.stream(PS4Command.values())
|
||||
.collect(Collectors.toMap(command -> command.value, command -> command));
|
||||
|
||||
public final int value;
|
||||
|
||||
private PS4Command(int value) {
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get command from value
|
||||
*
|
||||
* @param tag the tag string
|
||||
* @return accessoryType or null if not found
|
||||
*/
|
||||
public static @Nullable PS4Command valueOfTag(int value) {
|
||||
return TAG_MAP.get(value);
|
||||
}
|
||||
}
|
@ -0,0 +1,81 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.playstation.internal;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
|
||||
/**
|
||||
* The {@link PS4Configuration} class contains fields mapping thing configuration parameters.
|
||||
*
|
||||
* @author Fredrik Ahlström - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class PS4Configuration {
|
||||
|
||||
/**
|
||||
* User-credential for the PS4.
|
||||
*/
|
||||
public String userCredential = "";
|
||||
|
||||
/**
|
||||
* pass code for user (4 digits).
|
||||
*/
|
||||
public String passCode = "";
|
||||
|
||||
/**
|
||||
* pairing code for this device (8 digits).
|
||||
*/
|
||||
public String pairingCode = "";
|
||||
|
||||
/**
|
||||
* Timeout of connection in seconds.
|
||||
*/
|
||||
public int connectionTimeout = 60;
|
||||
|
||||
/**
|
||||
* Automatic connection as soon as PS4 is turned on.
|
||||
*/
|
||||
public boolean autoConnect = false;
|
||||
|
||||
/**
|
||||
* Size of artwork for applications.
|
||||
*/
|
||||
public int artworkSize = 320;
|
||||
|
||||
/**
|
||||
* IP-address of OpenHABs network interface.
|
||||
* This should only be used if the PS4 is on a sub-net
|
||||
* different from the one configured in OpenHAB.
|
||||
*/
|
||||
public String outboundIP = "";
|
||||
|
||||
/**
|
||||
* IP-address of PS4.
|
||||
*/
|
||||
public String ipAddress = "";
|
||||
|
||||
/**
|
||||
* IP-port of PS4.
|
||||
*/
|
||||
public int ipPort = PlayStationBindingConstants.DEFAULT_COMMUNICATION_PORT;
|
||||
|
||||
/**
|
||||
* host-id of PS4.
|
||||
*/
|
||||
public String hostId = "";
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "IP" + ipAddress + ", User-credential" + userCredential + ", Port" + ipPort + ", HostId" + hostId + ".";
|
||||
}
|
||||
}
|
@ -0,0 +1,182 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.playstation.internal;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.ByteOrder;
|
||||
import java.security.InvalidAlgorithmParameterException;
|
||||
import java.security.InvalidKeyException;
|
||||
import java.security.KeyFactory;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.security.PublicKey;
|
||||
import java.security.SecureRandom;
|
||||
import java.security.spec.InvalidKeySpecException;
|
||||
import java.security.spec.X509EncodedKeySpec;
|
||||
import java.util.Base64;
|
||||
|
||||
import javax.crypto.BadPaddingException;
|
||||
import javax.crypto.Cipher;
|
||||
import javax.crypto.IllegalBlockSizeException;
|
||||
import javax.crypto.NoSuchPaddingException;
|
||||
import javax.crypto.spec.IvParameterSpec;
|
||||
import javax.crypto.spec.SecretKeySpec;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* The {@link PS4Crypto} is responsible for encryption and decryption of
|
||||
* packets to / from the PS4.
|
||||
*
|
||||
* @author Fredrik Ahlström - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class PS4Crypto {
|
||||
|
||||
private final Logger logger = LoggerFactory.getLogger(PS4Crypto.class);
|
||||
|
||||
// Public key is from ps4-waker (https://github.com/dhleong/ps4-waker)
|
||||
private static final String PUBLIC_KEY = "-----BEGIN PUBLIC KEY-----"
|
||||
+ "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAxfAO/MDk5ovZpp7xlG9J"
|
||||
+ "JKc4Sg4ztAz+BbOt6Gbhub02tF9bryklpTIyzM0v817pwQ3TCoigpxEcWdTykhDL"
|
||||
+ "cGhAbcp6E7Xh8aHEsqgtQ/c+wY1zIl3fU//uddlB1XuipXthDv6emXsyyU/tJWqc"
|
||||
+ "zy9HCJncLJeYo7MJvf2TE9nnlVm1x4flmD0k1zrvb3MONqoZbKb/TQVuVhBv7SM+"
|
||||
+ "U5PSi3diXIx1Nnj4vQ8clRNUJ5X1tT9XfVmKQS1J513XNZ0uYHYRDzQYujpLWucu"
|
||||
+ "ob7v50wCpUm3iKP1fYCixMP6xFm0jPYz1YQaMV35VkYwc40qgk3av0PDS+1G0dCm" + "swIDAQAB"
|
||||
+ "-----END PUBLIC KEY-----";
|
||||
|
||||
private final byte[] remoteSeed = new byte[16];
|
||||
private final byte[] randomSeed = new byte[16];
|
||||
private @Nullable Cipher ps4Cipher;
|
||||
private @Nullable Cipher aesEncryptCipher;
|
||||
private @Nullable Cipher aesDecryptCipher;
|
||||
|
||||
PS4Crypto() {
|
||||
ps4Cipher = getRsaCipher(PUBLIC_KEY);
|
||||
}
|
||||
|
||||
void clearCiphers() {
|
||||
aesEncryptCipher = null;
|
||||
aesDecryptCipher = null;
|
||||
}
|
||||
|
||||
void initCiphers() {
|
||||
new SecureRandom().nextBytes(randomSeed);
|
||||
SecretKeySpec keySpec = new SecretKeySpec(randomSeed, "AES");
|
||||
IvParameterSpec ivSpec = new IvParameterSpec(remoteSeed);
|
||||
try {
|
||||
Cipher encCipher = Cipher.getInstance("AES/CBC/NoPadding");
|
||||
encCipher.init(Cipher.ENCRYPT_MODE, keySpec, ivSpec);
|
||||
Cipher decCipher = Cipher.getInstance("AES/CBC/NoPadding");
|
||||
decCipher.init(Cipher.DECRYPT_MODE, keySpec, ivSpec);
|
||||
logger.debug("Ciphers initialized.");
|
||||
aesEncryptCipher = encCipher;
|
||||
aesDecryptCipher = decCipher;
|
||||
} catch (NoSuchAlgorithmException | NoSuchPaddingException | InvalidKeyException
|
||||
| InvalidAlgorithmParameterException e) {
|
||||
logger.warn("Can not initialize ciphers.", e);
|
||||
}
|
||||
}
|
||||
|
||||
int parseHelloResponsePacket(ByteBuffer rBuffer) {
|
||||
int result = -1;
|
||||
rBuffer.rewind();
|
||||
final int buffSize = rBuffer.remaining();
|
||||
final int size = rBuffer.getInt();
|
||||
if (size > buffSize || size < 12) {
|
||||
logger.warn("Response size ({}) not good, buffer size ({}).", size, buffSize);
|
||||
return result;
|
||||
}
|
||||
int cmdValue = rBuffer.getInt();
|
||||
int statusValue = rBuffer.getInt();
|
||||
PS4Command command = PS4Command.valueOfTag(cmdValue);
|
||||
byte[] respBuff = new byte[size];
|
||||
rBuffer.rewind();
|
||||
rBuffer.get(respBuff);
|
||||
if (command == PS4Command.HELLO_REQ) {
|
||||
if (statusValue == PS4PacketHandler.REQ_VERSION) {
|
||||
rBuffer.position(20);
|
||||
rBuffer.get(remoteSeed, 0, 16);
|
||||
initCiphers();
|
||||
result = 0;
|
||||
}
|
||||
} else {
|
||||
logger.debug("Unknown resp-cmd, size:{}, command:{}, status:{}, data:{}.", size, cmdValue, statusValue,
|
||||
respBuff);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
ByteBuffer makeHandshakePacket() {
|
||||
byte[] msg = null;
|
||||
Cipher hsCipher = ps4Cipher;
|
||||
if (hsCipher != null) {
|
||||
try {
|
||||
msg = hsCipher.doFinal(randomSeed);
|
||||
} catch (IllegalBlockSizeException | BadPaddingException e) {
|
||||
logger.debug("Cipher exception: {}", e.getMessage());
|
||||
}
|
||||
}
|
||||
if (msg == null || msg.length != 256) {
|
||||
return ByteBuffer.allocate(0);
|
||||
}
|
||||
ByteBuffer packet = PS4PacketHandler.newPacketOfSize(8 + 256 + 16, PS4Command.HANDSHAKE_REQ);
|
||||
packet.put(msg);
|
||||
packet.put(remoteSeed); // Seed = 16 bytes
|
||||
packet.rewind();
|
||||
return packet;
|
||||
}
|
||||
|
||||
ByteBuffer encryptPacket(ByteBuffer packet) {
|
||||
Cipher encCipher = aesEncryptCipher;
|
||||
if (encCipher != null) {
|
||||
return ByteBuffer.wrap(encCipher.update(packet.array()));
|
||||
}
|
||||
logger.debug("Not encrypting packet.");
|
||||
return ByteBuffer.allocate(0);
|
||||
}
|
||||
|
||||
ByteBuffer decryptPacket(ByteBuffer encBuffer) {
|
||||
Cipher decCipher = aesDecryptCipher;
|
||||
if (decCipher != null) {
|
||||
byte[] respBuff = new byte[encBuffer.position()];
|
||||
encBuffer.position(0);
|
||||
encBuffer.get(respBuff, 0, respBuff.length);
|
||||
byte[] data = decCipher.update(respBuff);
|
||||
if (data != null) {
|
||||
return ByteBuffer.wrap(data).order(ByteOrder.LITTLE_ENDIAN);
|
||||
}
|
||||
}
|
||||
logger.debug("Not decrypting response.");
|
||||
return ByteBuffer.allocate(0);
|
||||
}
|
||||
|
||||
private @Nullable Cipher getRsaCipher(String key) {
|
||||
try {
|
||||
String keyString = key.replace("-----BEGIN PUBLIC KEY-----", "").replace("-----END PUBLIC KEY-----", "");
|
||||
byte[] keyData = Base64.getDecoder().decode(keyString);
|
||||
KeyFactory keyFactory = KeyFactory.getInstance("RSA");
|
||||
X509EncodedKeySpec x509keySpec = new X509EncodedKeySpec(keyData);
|
||||
PublicKey publicKey = keyFactory.generatePublic(x509keySpec);
|
||||
Cipher cipher = Cipher.getInstance("RSA/ECB/OAEPWithSHA-1AndMGF1Padding");
|
||||
cipher.init(Cipher.ENCRYPT_MODE, publicKey);
|
||||
logger.debug("Initialized RSA public key cipher");
|
||||
return cipher;
|
||||
} catch (InvalidKeyException | NoSuchAlgorithmException | NoSuchPaddingException | InvalidKeySpecException e) {
|
||||
logger.warn("Exception enabling RSA cipher.", e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,72 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.playstation.internal;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
|
||||
/**
|
||||
* Enum of response error status.
|
||||
*
|
||||
* @author Fredrik Ahlström - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
enum PS4ErrorStatus {
|
||||
STATUS_OK(0x00, "Status OK."),
|
||||
STATUS_UPDATE_APP(0x02, "Plugin needs to be updated."),
|
||||
STATUS_UPDATE_PS4(0x03, "PS4 needs to update."),
|
||||
STATUS_DO_LOGIN(0x06, "Log in on PS4."),
|
||||
STATUS_MAX_USERS(0x07, "Max users logged in on PS4."),
|
||||
STATUS_RESTART_APP(0x08, "Can not log in, restart plugin."),
|
||||
STATUS_COMMAND_NOT_GOOD(0x0b, "Command not good!"),
|
||||
STATUS_GAME_NOT_STARTED(0x0c, "Game not started!"), // Game/app not installed or other game running.
|
||||
STATUS_NOT_PAIRED(0x0e, "Not paired to PS4!"), // Not allowed?
|
||||
STATUS_OSK_NOT_OPENED(0x0f, "OSK not open right now."),
|
||||
STATUS_CLOSE_OTHER_APP(0x11, "Close the other app connected to PS4!"),
|
||||
STATUS_SOMEONE_ELSE_USING(0x12, "Someone else is using the PS4!"),
|
||||
STATUS_OSK_NOT_SUPPORTED(0x13, "Can't control OSK now!"),
|
||||
STATUS_MISSING_PAIRING_CODE(0x14, "Missing pairing-code!"), // ??
|
||||
STATUS_WRONG_USER_CREDENTIAL(0x15, "Wrong user-credential!"),
|
||||
STATUS_MISSING_PASS_CODE(0x16, "Missing pass-code!"),
|
||||
STATUS_WRONG_PAIRING_CODE(0x17, "Wrong pairing-code!"),
|
||||
STATUS_WRONG_PASS_CODE(0x18, "Wrong pass-code!"),
|
||||
STATUS_REGISTER_DEVICE_OVER(0x1a, "To many devices registered!"),
|
||||
STATUS_COULD_NOT_LOG_IN(0x1e, "Someone else is logging in now."),
|
||||
STATUS_CAN_NOT_PLAY_NOW(0x21, "You can not log in right now."),
|
||||
STATUS_ERROR_IN_COMMUNICATION(-1, "Error in comunication with PS4!");
|
||||
|
||||
private static final Map<Integer, PS4ErrorStatus> TAG_MAP = Arrays.stream(PS4ErrorStatus.values())
|
||||
.collect(Collectors.toMap(status -> status.value, status -> status));
|
||||
|
||||
public final int value;
|
||||
public final String message;
|
||||
|
||||
private PS4ErrorStatus(int value, String message) {
|
||||
this.value = value;
|
||||
this.message = message;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get error status from value
|
||||
*
|
||||
* @param value the integer value of the status
|
||||
* @return error status or null if unknown
|
||||
*/
|
||||
public static @Nullable PS4ErrorStatus valueOfTag(int value) {
|
||||
return TAG_MAP.get(value);
|
||||
}
|
||||
}
|
@ -0,0 +1,866 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.playstation.internal;
|
||||
|
||||
import static org.openhab.binding.playstation.internal.PlayStationBindingConstants.*;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.DatagramPacket;
|
||||
import java.net.DatagramSocket;
|
||||
import java.net.InetAddress;
|
||||
import java.net.InetSocketAddress;
|
||||
import java.net.UnknownHostException;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.ByteOrder;
|
||||
import java.nio.channels.SocketChannel;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ScheduledFuture;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.openhab.binding.playstation.internal.discovery.PlayStationDiscovery;
|
||||
import org.openhab.core.config.core.Configuration;
|
||||
import org.openhab.core.i18n.LocaleProvider;
|
||||
import org.openhab.core.library.types.OnOffType;
|
||||
import org.openhab.core.library.types.RawType;
|
||||
import org.openhab.core.library.types.StringType;
|
||||
import org.openhab.core.net.NetworkAddressService;
|
||||
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.openhab.core.types.State;
|
||||
import org.openhab.core.types.UnDefType;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* The {@link PS4Handler} is responsible for handling commands, which are
|
||||
* sent to one of the channels.
|
||||
*
|
||||
* @author Fredrik Ahlström - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class PS4Handler extends BaseThingHandler {
|
||||
|
||||
private final Logger logger = LoggerFactory.getLogger(PS4Handler.class);
|
||||
private final PS4Crypto ps4Crypto = new PS4Crypto();
|
||||
private static final int SOCKET_TIMEOUT_SECONDS = 5;
|
||||
/** Time after connect that we can start to send key events, milli seconds */
|
||||
private static final int POST_CONNECT_SENDKEY_DELAY_MS = 500;
|
||||
/** Minimum delay between sendKey sends, milli seconds */
|
||||
private static final int MIN_SENDKEY_DELAY_MS = 210;
|
||||
/** Minimum delay after Key set, milli seconds */
|
||||
private static final int MIN_HOLDKEY_DELAY_MS = 300;
|
||||
|
||||
private PS4Configuration config = new PS4Configuration();
|
||||
|
||||
private final @Nullable LocaleProvider localeProvider;
|
||||
private final @Nullable NetworkAddressService networkAS;
|
||||
private List<ScheduledFuture<?>> scheduledFutures = Collections.synchronizedList(new ArrayList<>());
|
||||
private @Nullable ScheduledFuture<?> refreshTimer;
|
||||
private @Nullable ScheduledFuture<?> timeoutTimer;
|
||||
private @Nullable SocketChannelHandler socketChannelHandler;
|
||||
private @Nullable InetAddress localAddress;
|
||||
|
||||
// State of PS4
|
||||
private String currentApplication = "";
|
||||
private String currentApplicationId = "";
|
||||
private OnOffType currentPower = OnOffType.OFF;
|
||||
private State currentArtwork = UnDefType.UNDEF;
|
||||
private int currentComPort = DEFAULT_COMMUNICATION_PORT;
|
||||
|
||||
boolean loggedIn = false;
|
||||
boolean oskOpen = false;
|
||||
|
||||
public PS4Handler(Thing thing, LocaleProvider locProvider, NetworkAddressService network) {
|
||||
super(thing);
|
||||
localeProvider = locProvider;
|
||||
networkAS = network;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleConfigurationUpdate(Map<String, Object> configurationParameters) {
|
||||
super.handleConfigurationUpdate(configurationParameters);
|
||||
figureOutLocalIP();
|
||||
SocketChannelHandler scHandler = socketChannelHandler;
|
||||
if (!config.pairingCode.isEmpty() && (scHandler == null || !loggedIn)) {
|
||||
// Try to log in then remove pairing code as it's one use only.
|
||||
scheduler.execute(() -> {
|
||||
login();
|
||||
Configuration editedConfig = editConfiguration();
|
||||
editedConfig.put(PAIRING_CODE, "");
|
||||
updateConfiguration(editedConfig);
|
||||
});
|
||||
}
|
||||
setupConnectionTimeout(config.connectionTimeout);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleCommand(ChannelUID channelUID, Command command) {
|
||||
if (command instanceof RefreshType) {
|
||||
refreshFromState(channelUID);
|
||||
} else {
|
||||
if (command instanceof StringType) {
|
||||
switch (channelUID.getId()) {
|
||||
case CHANNEL_APPLICATION_ID:
|
||||
if (!currentApplicationId.equals(((StringType) command).toString())) {
|
||||
updateApplicationTitleid(((StringType) command).toString());
|
||||
startApplication(currentApplicationId);
|
||||
}
|
||||
break;
|
||||
case CHANNEL_OSK_TEXT:
|
||||
setOSKText(((StringType) command).toString());
|
||||
break;
|
||||
case CHANNEL_SEND_KEY:
|
||||
int ps4Key = 0;
|
||||
switch (((StringType) command).toString()) {
|
||||
case SEND_KEY_UP:
|
||||
ps4Key = PS4_KEY_UP;
|
||||
break;
|
||||
case SEND_KEY_DOWN:
|
||||
ps4Key = PS4_KEY_DOWN;
|
||||
break;
|
||||
case SEND_KEY_RIGHT:
|
||||
ps4Key = PS4_KEY_RIGHT;
|
||||
break;
|
||||
case SEND_KEY_LEFT:
|
||||
ps4Key = PS4_KEY_LEFT;
|
||||
break;
|
||||
case SEND_KEY_ENTER:
|
||||
ps4Key = PS4_KEY_ENTER;
|
||||
break;
|
||||
case SEND_KEY_BACK:
|
||||
ps4Key = PS4_KEY_BACK;
|
||||
break;
|
||||
case SEND_KEY_OPTION:
|
||||
ps4Key = PS4_KEY_OPTION;
|
||||
break;
|
||||
case SEND_KEY_PS:
|
||||
ps4Key = PS4_KEY_PS;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
if (ps4Key != 0) {
|
||||
sendRemoteKey(ps4Key);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
} else if (command instanceof OnOffType) {
|
||||
OnOffType onOff = (OnOffType) command;
|
||||
switch (channelUID.getId()) {
|
||||
case CHANNEL_POWER:
|
||||
if (currentPower != onOff) {
|
||||
currentPower = onOff;
|
||||
if (currentPower.equals(OnOffType.ON)) {
|
||||
turnOnPS4();
|
||||
} else if (currentPower.equals(OnOffType.OFF)) {
|
||||
sendStandby();
|
||||
}
|
||||
}
|
||||
break;
|
||||
case CHANNEL_CONNECT:
|
||||
boolean connected = socketChannelHandler != null && socketChannelHandler.isChannelOpen();
|
||||
if (connected && onOff.equals(OnOffType.OFF)) {
|
||||
sendByeBye();
|
||||
} else if (!connected && onOff.equals(OnOffType.ON)) {
|
||||
scheduler.execute(() -> login());
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void initialize() {
|
||||
config = getConfigAs(PS4Configuration.class);
|
||||
|
||||
figureOutLocalIP();
|
||||
updateStatus(ThingStatus.UNKNOWN);
|
||||
setupRefreshTimer();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void dispose() {
|
||||
stopConnection();
|
||||
ScheduledFuture<?> timer = refreshTimer;
|
||||
if (timer != null) {
|
||||
timer.cancel(false);
|
||||
refreshTimer = null;
|
||||
}
|
||||
timer = timeoutTimer;
|
||||
if (timer != null) {
|
||||
timer.cancel(false);
|
||||
timeoutTimer = null;
|
||||
}
|
||||
scheduledFutures.forEach(f -> f.cancel(false));
|
||||
scheduledFutures.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* Tries to figure out a local IP that can communicate with the PS4.
|
||||
*/
|
||||
private void figureOutLocalIP() {
|
||||
if (!config.outboundIP.trim().isEmpty()) {
|
||||
try {
|
||||
localAddress = InetAddress.getByName(config.outboundIP);
|
||||
logger.debug("Outbound local IP.\"{}\"", localAddress);
|
||||
return;
|
||||
} catch (UnknownHostException e) {
|
||||
// This is expected
|
||||
}
|
||||
}
|
||||
NetworkAddressService network = networkAS;
|
||||
String adr = (network != null) ? network.getPrimaryIpv4HostAddress() : null;
|
||||
if (adr != null) {
|
||||
try {
|
||||
localAddress = InetAddress.getByName(adr);
|
||||
} catch (UnknownHostException e) {
|
||||
// Ignore, just let the socket use whatever.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets up a timer for querying the PS4 (using the scheduler) every 10 seconds.
|
||||
*/
|
||||
private void setupRefreshTimer() {
|
||||
final ScheduledFuture<?> timer = refreshTimer;
|
||||
if (timer != null) {
|
||||
timer.cancel(false);
|
||||
}
|
||||
refreshTimer = scheduler.scheduleWithFixedDelay(this::updateAllChannels, 0, 10, TimeUnit.SECONDS);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets up a timer for stopping the connection to the PS4 (using the scheduler) with the given time.
|
||||
*
|
||||
* @param waitTime The time in seconds before the connection is stopped.
|
||||
*/
|
||||
private void setupConnectionTimeout(int waitTime) {
|
||||
final ScheduledFuture<?> timer = timeoutTimer;
|
||||
if (timer != null) {
|
||||
timer.cancel(false);
|
||||
}
|
||||
if (waitTime > 0) {
|
||||
timeoutTimer = scheduler.schedule(this::stopConnection, waitTime, TimeUnit.SECONDS);
|
||||
}
|
||||
}
|
||||
|
||||
private void refreshFromState(ChannelUID channelUID) {
|
||||
switch (channelUID.getId()) {
|
||||
case CHANNEL_POWER:
|
||||
updateState(channelUID, currentPower);
|
||||
break;
|
||||
case CHANNEL_APPLICATION_NAME:
|
||||
updateState(channelUID, StringType.valueOf(currentApplication));
|
||||
break;
|
||||
case CHANNEL_APPLICATION_ID:
|
||||
updateState(channelUID, StringType.valueOf(currentApplicationId));
|
||||
break;
|
||||
case CHANNEL_APPLICATION_IMAGE:
|
||||
updateApplicationTitleid(currentApplicationId);
|
||||
updateState(channelUID, currentArtwork);
|
||||
break;
|
||||
case CHANNEL_OSK_TEXT:
|
||||
case CHANNEL_2ND_SCREEN:
|
||||
updateState(channelUID, UnDefType.UNDEF);
|
||||
break;
|
||||
case CHANNEL_CONNECT:
|
||||
boolean connected = socketChannelHandler != null && socketChannelHandler.isChannelOpen();
|
||||
updateState(channelUID, OnOffType.from(connected));
|
||||
break;
|
||||
case CHANNEL_SEND_KEY:
|
||||
break;
|
||||
default:
|
||||
logger.warn("Channel refresh for {} not implemented!", channelUID.getId());
|
||||
}
|
||||
}
|
||||
|
||||
private void updateAllChannels() {
|
||||
try (DatagramSocket socket = new DatagramSocket(0, localAddress)) {
|
||||
socket.setBroadcast(false);
|
||||
socket.setSoTimeout(SOCKET_TIMEOUT_SECONDS * 1000);
|
||||
InetAddress inetAddress = InetAddress.getByName(config.ipAddress);
|
||||
|
||||
// send discover
|
||||
byte[] discover = PS4PacketHandler.makeSearchPacket();
|
||||
DatagramPacket packet = new DatagramPacket(discover, discover.length, inetAddress, DEFAULT_BROADCAST_PORT);
|
||||
socket.send(packet);
|
||||
|
||||
// wait for response
|
||||
byte[] rxbuf = new byte[256];
|
||||
packet = new DatagramPacket(rxbuf, rxbuf.length);
|
||||
socket.receive(packet);
|
||||
parseSearchResponse(packet);
|
||||
} catch (IOException e) {
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private void stopConnection() {
|
||||
SocketChannelHandler handler = socketChannelHandler;
|
||||
if (handler != null && handler.isChannelOpen()) {
|
||||
sendByeBye();
|
||||
}
|
||||
}
|
||||
|
||||
private void wakeUpPS4() {
|
||||
logger.debug("Waking up PS4...");
|
||||
try (DatagramSocket socket = new DatagramSocket(0, localAddress)) {
|
||||
socket.setBroadcast(false);
|
||||
InetAddress inetAddress = InetAddress.getByName(config.ipAddress);
|
||||
// send wake-up
|
||||
byte[] wakeup = PS4PacketHandler.makeWakeupPacket(config.userCredential);
|
||||
DatagramPacket packet = new DatagramPacket(wakeup, wakeup.length, inetAddress, DEFAULT_BROADCAST_PORT);
|
||||
socket.send(packet);
|
||||
} catch (IOException e) {
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private boolean openComs() {
|
||||
try (DatagramSocket socket = new DatagramSocket(0, localAddress)) {
|
||||
socket.setBroadcast(false);
|
||||
InetAddress inetAddress = InetAddress.getByName(config.ipAddress);
|
||||
// send launch
|
||||
byte[] launch = PS4PacketHandler.makeLaunchPacket(config.userCredential);
|
||||
DatagramPacket packet = new DatagramPacket(launch, launch.length, inetAddress, DEFAULT_BROADCAST_PORT);
|
||||
socket.send(packet);
|
||||
Thread.sleep(100);
|
||||
return true;
|
||||
} catch (IOException e) {
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
|
||||
} catch (InterruptedException e) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private boolean setupConnection(SocketChannel channel) throws IOException {
|
||||
logger.debug("TCP connecting");
|
||||
|
||||
channel.socket().setSoTimeout(2000);
|
||||
channel.configureBlocking(true);
|
||||
channel.connect(new InetSocketAddress(config.ipAddress, currentComPort));
|
||||
|
||||
ByteBuffer outPacket = PS4PacketHandler.makeHelloPacket();
|
||||
sendPacketToPS4(outPacket, channel, false, false);
|
||||
|
||||
// Read hello response
|
||||
final ByteBuffer readBuffer = ByteBuffer.allocate(512).order(ByteOrder.LITTLE_ENDIAN);
|
||||
|
||||
int responseLength = channel.read(readBuffer);
|
||||
if (responseLength > 0) {
|
||||
ps4Crypto.parseHelloResponsePacket(readBuffer);
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
|
||||
outPacket = ps4Crypto.makeHandshakePacket();
|
||||
sendPacketToPS4(outPacket, channel, false, false);
|
||||
return true;
|
||||
}
|
||||
|
||||
private class SocketChannelHandler extends Thread {
|
||||
private SocketChannel socketChannel;
|
||||
|
||||
public SocketChannelHandler() throws IOException {
|
||||
socketChannel = setupChannel();
|
||||
loggedIn = false;
|
||||
oskOpen = false;
|
||||
start();
|
||||
}
|
||||
|
||||
public SocketChannel getChannel() {
|
||||
if (!socketChannel.isOpen()) {
|
||||
try {
|
||||
socketChannel = setupChannel();
|
||||
} catch (IOException e) {
|
||||
logger.debug("Couldn't open SocketChannel.{}", e.getMessage());
|
||||
}
|
||||
}
|
||||
return socketChannel;
|
||||
}
|
||||
|
||||
public boolean isChannelOpen() {
|
||||
return socketChannel.isOpen();
|
||||
}
|
||||
|
||||
private SocketChannel setupChannel() throws IOException {
|
||||
SocketChannel channel = SocketChannel.open();
|
||||
if (!openComs()) {
|
||||
throw new IOException("Open coms failed");
|
||||
}
|
||||
if (!setupConnection(channel)) {
|
||||
throw new IOException("Setup connection failed");
|
||||
}
|
||||
updateState(CHANNEL_CONNECT, OnOffType.ON);
|
||||
return channel;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
SocketChannel channel = socketChannel;
|
||||
final ByteBuffer readBuffer = ByteBuffer.allocate(512).order(ByteOrder.LITTLE_ENDIAN);
|
||||
try {
|
||||
while (channel.read(readBuffer) > 0) {
|
||||
ByteBuffer messBuffer = ps4Crypto.decryptPacket(readBuffer);
|
||||
readBuffer.position(0);
|
||||
PS4Command lastCommand = parseResponsePacket(messBuffer);
|
||||
|
||||
if (lastCommand == PS4Command.SERVER_STATUS_RSP) {
|
||||
if (oskOpen && isLinked(CHANNEL_OSK_TEXT)) {
|
||||
sendOSKStart();
|
||||
} else {
|
||||
sendStatus();
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (IOException e) {
|
||||
logger.debug("Connection read exception: {}", e.getMessage());
|
||||
} finally {
|
||||
try {
|
||||
channel.close();
|
||||
} catch (IOException e) {
|
||||
logger.debug("Connection close exception: {}", e.getMessage());
|
||||
}
|
||||
}
|
||||
updateState(CHANNEL_CONNECT, OnOffType.OFF);
|
||||
logger.debug("SocketHandler done.");
|
||||
ps4Crypto.clearCiphers();
|
||||
loggedIn = false;
|
||||
}
|
||||
}
|
||||
|
||||
private @Nullable PS4Command parseResponsePacket(ByteBuffer rBuffer) {
|
||||
rBuffer.rewind();
|
||||
final int buffSize = rBuffer.remaining();
|
||||
final int size = rBuffer.getInt();
|
||||
if (size > buffSize || size < 12) {
|
||||
logger.debug("Response size ({}) not good, buffer size ({}).", size, buffSize);
|
||||
return null;
|
||||
}
|
||||
int cmdValue = rBuffer.getInt();
|
||||
int statValue = rBuffer.getInt();
|
||||
PS4ErrorStatus status = PS4ErrorStatus.valueOfTag(statValue);
|
||||
PS4Command command = PS4Command.valueOfTag(cmdValue);
|
||||
byte[] respBuff = new byte[size];
|
||||
rBuffer.rewind();
|
||||
rBuffer.get(respBuff);
|
||||
if (command != null) {
|
||||
if (status == null) {
|
||||
logger.debug("Resp; size:{}, command:{}, statValue:{}, data:{}.", size, command, statValue, respBuff);
|
||||
} else {
|
||||
logger.debug("Resp; size:{}, command:{}, status:{}, data:{}.", size, command, status, respBuff);
|
||||
}
|
||||
switch (command) {
|
||||
case LOGIN_RSP:
|
||||
if (status == null) {
|
||||
logger.debug("Unhandled Login status value: {}", statValue);
|
||||
return command;
|
||||
}
|
||||
// Read login response
|
||||
switch (status) {
|
||||
case STATUS_OK:
|
||||
updateStatus(ThingStatus.ONLINE, ThingStatusDetail.NONE, status.message);
|
||||
loggedIn = true;
|
||||
if (isLinked(CHANNEL_2ND_SCREEN)) {
|
||||
scheduler.execute(() -> {
|
||||
ByteBuffer outPacket = PS4PacketHandler
|
||||
.makeClientIDPacket("com.playstation.mobile2ndscreen", "18.9.3");
|
||||
sendPacketEncrypted(outPacket, false);
|
||||
});
|
||||
}
|
||||
break;
|
||||
case STATUS_NOT_PAIRED:
|
||||
updateStatus(ThingStatus.ONLINE, ThingStatusDetail.CONFIGURATION_PENDING, status.message);
|
||||
loggedIn = false;
|
||||
break;
|
||||
case STATUS_MISSING_PAIRING_CODE:
|
||||
case STATUS_MISSING_PASS_CODE:
|
||||
case STATUS_WRONG_PAIRING_CODE:
|
||||
case STATUS_WRONG_PASS_CODE:
|
||||
case STATUS_WRONG_USER_CREDENTIAL:
|
||||
updateStatus(ThingStatus.ONLINE, ThingStatusDetail.CONFIGURATION_ERROR, status.message);
|
||||
loggedIn = false;
|
||||
logger.debug("Not logged in: {}", status.message);
|
||||
break;
|
||||
case STATUS_CAN_NOT_PLAY_NOW:
|
||||
case STATUS_CLOSE_OTHER_APP:
|
||||
case STATUS_COMMAND_NOT_GOOD:
|
||||
case STATUS_COULD_NOT_LOG_IN:
|
||||
case STATUS_DO_LOGIN:
|
||||
case STATUS_MAX_USERS:
|
||||
case STATUS_REGISTER_DEVICE_OVER:
|
||||
case STATUS_RESTART_APP:
|
||||
case STATUS_SOMEONE_ELSE_USING:
|
||||
case STATUS_UPDATE_APP:
|
||||
case STATUS_UPDATE_PS4:
|
||||
updateStatus(ThingStatus.ONLINE, ThingStatusDetail.NONE, status.message);
|
||||
loggedIn = false;
|
||||
logger.debug("Not logged in: {}", status.message);
|
||||
break;
|
||||
default:
|
||||
logger.debug("Unhandled Login response status:{}, message:{}", status, status.message);
|
||||
break;
|
||||
}
|
||||
break;
|
||||
case APP_START_RSP:
|
||||
if (status != null && status != PS4ErrorStatus.STATUS_OK) {
|
||||
logger.debug("App start response: {}", status.message);
|
||||
}
|
||||
break;
|
||||
case STANDBY_RSP:
|
||||
if (status != null && status != PS4ErrorStatus.STATUS_OK) {
|
||||
logger.debug("Standby response: {}", status.message);
|
||||
}
|
||||
break;
|
||||
case SERVER_STATUS_RSP:
|
||||
if ((statValue & 4) != 0) {
|
||||
oskOpen = true;
|
||||
} else {
|
||||
if (oskOpen) {
|
||||
updateState(CHANNEL_OSK_TEXT, StringType.valueOf(""));
|
||||
}
|
||||
oskOpen = false;
|
||||
}
|
||||
logger.debug("Server status value:{}", statValue);
|
||||
break;
|
||||
case HTTPD_STATUS_RSP:
|
||||
String httpdStat = PS4PacketHandler.parseHTTPdPacket(rBuffer);
|
||||
logger.debug("HTTPd Response; {}", httpdStat);
|
||||
String secondScrStr = "";
|
||||
int httpStatus = rBuffer.getInt(8);
|
||||
int port = rBuffer.getInt(12);
|
||||
if (httpStatus != 0 && port != 0) {
|
||||
secondScrStr = "http://" + config.ipAddress + ":" + port;
|
||||
}
|
||||
updateState(CHANNEL_2ND_SCREEN, StringType.valueOf(secondScrStr));
|
||||
break;
|
||||
case OSK_CHANGE_STRING_REQ:
|
||||
String oskText = PS4PacketHandler.parseOSKStringChangePacket(rBuffer);
|
||||
updateState(CHANNEL_OSK_TEXT, StringType.valueOf(oskText));
|
||||
break;
|
||||
case OSK_START_RSP:
|
||||
case OSK_CONTROL_REQ:
|
||||
case COMMENT_VIEWER_START_RESULT:
|
||||
case SCREEN_SHOT_RSP:
|
||||
case APP_START2_RSP:
|
||||
case LOGOUT_RSP:
|
||||
break;
|
||||
default:
|
||||
logger.debug("Unknown response, command:{}. Missing case.", command);
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
logger.debug("Unknown resp-cmd, size:{}, command:{}, status:{}, data:{}.", size, cmdValue, statValue,
|
||||
respBuff);
|
||||
}
|
||||
return command;
|
||||
}
|
||||
|
||||
private SocketChannel getConnection() throws IOException {
|
||||
return getConnection(true);
|
||||
}
|
||||
|
||||
private SocketChannel getConnection(boolean requiresLogin) throws IOException {
|
||||
SocketChannel channel = null;
|
||||
SocketChannelHandler handler = socketChannelHandler;
|
||||
if (handler == null || !handler.isChannelOpen()) {
|
||||
try {
|
||||
handler = new SocketChannelHandler();
|
||||
socketChannelHandler = handler;
|
||||
} catch (IOException e) {
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
channel = handler.getChannel();
|
||||
if (!loggedIn && requiresLogin) {
|
||||
login(channel);
|
||||
}
|
||||
return channel;
|
||||
}
|
||||
|
||||
private void sendPacketToPS4(ByteBuffer packet, SocketChannel channel, boolean encrypted, boolean restartTimeout) {
|
||||
PS4Command cmd = PS4Command.valueOfTag(packet.getInt(4));
|
||||
logger.debug("Sending {} packet.", cmd);
|
||||
try {
|
||||
if (encrypted) {
|
||||
ByteBuffer outPacket = ps4Crypto.encryptPacket(packet);
|
||||
channel.write(outPacket);
|
||||
} else {
|
||||
channel.write(packet);
|
||||
}
|
||||
if (restartTimeout) {
|
||||
setupConnectionTimeout(config.connectionTimeout);
|
||||
}
|
||||
} catch (IOException e) {
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private void sendPacketEncrypted(ByteBuffer packet, SocketChannel channel) {
|
||||
sendPacketToPS4(packet, channel, true, true);
|
||||
}
|
||||
|
||||
private void sendPacketEncrypted(ByteBuffer packet) {
|
||||
sendPacketEncrypted(packet, true);
|
||||
}
|
||||
|
||||
private void sendPacketEncrypted(ByteBuffer packet, boolean requiresLogin) {
|
||||
try {
|
||||
SocketChannel channel = getConnection(requiresLogin);
|
||||
if (requiresLogin && !loggedIn) {
|
||||
ScheduledFuture<?> future = scheduler.schedule(
|
||||
() -> sendPacketToPS4(packet, channel, true, requiresLogin), 250, TimeUnit.MILLISECONDS);
|
||||
scheduledFutures.add(future);
|
||||
scheduledFutures.removeIf(ScheduledFuture::isDone);
|
||||
} else {
|
||||
sendPacketToPS4(packet, channel, true, requiresLogin);
|
||||
}
|
||||
} catch (IOException e) {
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This is used as a heart beat to let the PS4 know that we are still listening.
|
||||
*/
|
||||
private void sendStatus() {
|
||||
ByteBuffer outPacket = PS4PacketHandler.makeStatusPacket(0);
|
||||
sendPacketEncrypted(outPacket, false);
|
||||
}
|
||||
|
||||
private void login(SocketChannel channel) {
|
||||
// Send login request
|
||||
ByteBuffer outPacket = PS4PacketHandler.makeLoginPacket(config.userCredential, config.passCode,
|
||||
config.pairingCode);
|
||||
sendPacketEncrypted(outPacket, channel);
|
||||
}
|
||||
|
||||
private void login() {
|
||||
try {
|
||||
SocketChannel channel = getConnection(false);
|
||||
login(channel);
|
||||
} catch (IOException e) {
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This closes the connection with the PS4.
|
||||
*/
|
||||
private void sendByeBye() {
|
||||
ByteBuffer outPacket = PS4PacketHandler.makeByebyePacket();
|
||||
sendPacketEncrypted(outPacket, false);
|
||||
}
|
||||
|
||||
private void turnOnPS4() {
|
||||
wakeUpPS4();
|
||||
ScheduledFuture<?> future = scheduler.schedule(this::waitAndConnectToPS4, 17, TimeUnit.SECONDS);
|
||||
scheduledFutures.add(future);
|
||||
scheduledFutures.removeIf(ScheduledFuture::isDone);
|
||||
}
|
||||
|
||||
private void waitAndConnectToPS4() {
|
||||
try {
|
||||
getConnection();
|
||||
} catch (IOException e) {
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private void sendStandby() {
|
||||
ByteBuffer outPacket = PS4PacketHandler.makeStandbyPacket();
|
||||
sendPacketEncrypted(outPacket);
|
||||
}
|
||||
|
||||
/**
|
||||
* Ask PS4 if the OSK is open so we can get and set text.
|
||||
*/
|
||||
private void sendOSKStart() {
|
||||
ByteBuffer outPacket = PS4PacketHandler.makeOSKStartPacket();
|
||||
sendPacketEncrypted(outPacket);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the entire OSK string on the PS4.
|
||||
*
|
||||
* @param text The text to set in the OSK.
|
||||
*/
|
||||
private void setOSKText(String text) {
|
||||
logger.debug("Sending osk text packet,\"{}\"", text);
|
||||
ByteBuffer outPacket = PS4PacketHandler.makeOSKStringChangePacket(text);
|
||||
sendPacketEncrypted(outPacket);
|
||||
}
|
||||
|
||||
/**
|
||||
* Tries to start an application on the PS4.
|
||||
*
|
||||
* @param applicationId The unique id for the application (CUSAxxxxx).
|
||||
*/
|
||||
private void startApplication(String applicationId) {
|
||||
ByteBuffer outPacket = PS4PacketHandler.makeApplicationPacket(applicationId);
|
||||
sendPacketEncrypted(outPacket);
|
||||
}
|
||||
|
||||
private void sendRemoteKey(int pushedKey) {
|
||||
try {
|
||||
SocketChannelHandler scHandler = socketChannelHandler;
|
||||
int preWait = (scHandler == null || !loggedIn) ? POST_CONNECT_SENDKEY_DELAY_MS : 0;
|
||||
SocketChannel channel = getConnection();
|
||||
|
||||
ScheduledFuture<?> future = scheduler.schedule(() -> {
|
||||
ByteBuffer outPacket = PS4PacketHandler.makeRemoteControlPacket(PS4_KEY_OPEN_RC);
|
||||
sendPacketEncrypted(outPacket, channel);
|
||||
}, preWait, TimeUnit.MILLISECONDS);
|
||||
scheduledFutures.add(future);
|
||||
|
||||
future = scheduler.schedule(() -> {
|
||||
// Send remote key
|
||||
ByteBuffer keyPacket = PS4PacketHandler.makeRemoteControlPacket(pushedKey);
|
||||
sendPacketEncrypted(keyPacket, channel);
|
||||
}, preWait + MIN_SENDKEY_DELAY_MS, TimeUnit.MILLISECONDS);
|
||||
scheduledFutures.add(future);
|
||||
|
||||
future = scheduler.schedule(() -> {
|
||||
ByteBuffer offPacket = PS4PacketHandler.makeRemoteControlPacket(PS4_KEY_OFF);
|
||||
sendPacketEncrypted(offPacket, channel);
|
||||
}, preWait + MIN_SENDKEY_DELAY_MS + MIN_HOLDKEY_DELAY_MS, TimeUnit.MILLISECONDS);
|
||||
scheduledFutures.add(future);
|
||||
|
||||
future = scheduler.schedule(() -> {
|
||||
ByteBuffer closePacket = PS4PacketHandler.makeRemoteControlPacket(PS4_KEY_CLOSE_RC);
|
||||
sendPacketEncrypted(closePacket, channel);
|
||||
}, preWait + MIN_SENDKEY_DELAY_MS * 2 + MIN_HOLDKEY_DELAY_MS, TimeUnit.MILLISECONDS);
|
||||
scheduledFutures.add(future);
|
||||
|
||||
} catch (IOException e) {
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
|
||||
}
|
||||
scheduledFutures.removeIf(ScheduledFuture::isDone);
|
||||
}
|
||||
|
||||
private void parseSearchResponse(DatagramPacket packet) {
|
||||
byte[] data = packet.getData();
|
||||
String message = new String(data, StandardCharsets.UTF_8);
|
||||
String applicationName = "";
|
||||
String applicationId = "";
|
||||
|
||||
String[] rowStrings = message.trim().split("\\r?\\n");
|
||||
for (String row : rowStrings) {
|
||||
int index = row.indexOf(':');
|
||||
if (index == -1) {
|
||||
OnOffType power = null;
|
||||
if (row.contains("200")) {
|
||||
power = OnOffType.ON;
|
||||
} else if (row.contains("620")) {
|
||||
power = OnOffType.OFF;
|
||||
}
|
||||
if (power != null) {
|
||||
updateState(CHANNEL_POWER, power);
|
||||
if (!currentPower.equals(power)) {
|
||||
currentPower = power;
|
||||
if (power.equals(OnOffType.ON) && config.autoConnect) {
|
||||
SocketChannelHandler scHandler = socketChannelHandler;
|
||||
if (scHandler == null || !loggedIn) {
|
||||
logger.debug("Trying to login after power on.");
|
||||
ScheduledFuture<?> future = scheduler.schedule(() -> login(), 20, TimeUnit.SECONDS);
|
||||
scheduledFutures.add(future);
|
||||
scheduledFutures.removeIf(ScheduledFuture::isDone);
|
||||
}
|
||||
}
|
||||
}
|
||||
updateStatus(ThingStatus.ONLINE);
|
||||
} else {
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
|
||||
"Could not determine power status.");
|
||||
}
|
||||
continue;
|
||||
}
|
||||
String key = row.substring(0, index);
|
||||
String value = row.substring(index + 1);
|
||||
switch (key) {
|
||||
case RESPONSE_RUNNING_APP_NAME:
|
||||
applicationName = value;
|
||||
break;
|
||||
case RESPONSE_RUNNING_APP_TITLEID:
|
||||
applicationId = value;
|
||||
break;
|
||||
case RESPONSE_HOST_REQUEST_PORT:
|
||||
int port = Integer.parseInt(value);
|
||||
if (currentComPort != port) {
|
||||
currentComPort = port;
|
||||
logger.debug("Host request port: {}", port);
|
||||
}
|
||||
break;
|
||||
case RESPONSE_SYSTEM_VERSION:
|
||||
updateProperty(Thing.PROPERTY_FIRMWARE_VERSION, PlayStationDiscovery.formatPS4Version(value));
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!currentApplication.equals(applicationName)) {
|
||||
currentApplication = applicationName;
|
||||
updateState(CHANNEL_APPLICATION_NAME, StringType.valueOf(applicationName));
|
||||
logger.debug("Current application: {}", applicationName);
|
||||
}
|
||||
if (!currentApplicationId.equals(applicationId)) {
|
||||
updateApplicationTitleid(applicationId);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the cached TitleId and tries to download artwork
|
||||
* for application if CHANNEL_APPLICATION_IMAGE is linked.
|
||||
*
|
||||
* @param titleId Id of application.
|
||||
*/
|
||||
private void updateApplicationTitleid(String titleId) {
|
||||
currentApplicationId = titleId;
|
||||
updateState(CHANNEL_APPLICATION_ID, StringType.valueOf(titleId));
|
||||
logger.debug("Current application title id: {}", titleId);
|
||||
if (!isLinked(CHANNEL_APPLICATION_IMAGE)) {
|
||||
return;
|
||||
}
|
||||
LocaleProvider lProvider = localeProvider;
|
||||
Locale locale = (lProvider != null) ? lProvider.getLocale() : Locale.US;
|
||||
|
||||
RawType artWork = PS4ArtworkHandler.fetchArtworkForTitleid(titleId, config.artworkSize, locale);
|
||||
if (artWork != null) {
|
||||
currentArtwork = artWork;
|
||||
updateState(CHANNEL_APPLICATION_IMAGE, artWork);
|
||||
} else if (!titleId.isEmpty()) {
|
||||
logger.debug("Couldn't fetch artwork for title id: {}", titleId);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,325 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.playstation.internal;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.ByteOrder;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
|
||||
/**
|
||||
* The {@link PS4PacketHandler} is responsible for creating and parsing
|
||||
* packets to / from the PS4.
|
||||
*
|
||||
* @author Fredrik Ahlström - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class PS4PacketHandler {
|
||||
|
||||
private static final String APPLICATION_NAME = "openHAB PlayStation 4 Binding";
|
||||
private static final String DEVICE_NAME = "openHAB Server";
|
||||
|
||||
private static final String OS_VERSION = "8.1.0";
|
||||
private static final String DDP_VERSION = "device-discovery-protocol-version:00020020\n";
|
||||
static final int REQ_VERSION = 0x20000;
|
||||
|
||||
private PS4PacketHandler() {
|
||||
// Don't instantiate
|
||||
}
|
||||
|
||||
/**
|
||||
* Allocates a new ByteBuffer of exactly size.
|
||||
*
|
||||
* @param size The size of the packet.
|
||||
* @param cmd The command to add to the packet.
|
||||
* @return A ByteBuffer of exactly size number of bytes.
|
||||
*/
|
||||
static ByteBuffer newPacketOfSize(int size, PS4Command cmd) {
|
||||
ByteBuffer packet = ByteBuffer.allocate(size).order(ByteOrder.LITTLE_ENDIAN);
|
||||
packet.putInt(size).putInt(cmd.value);
|
||||
return packet;
|
||||
}
|
||||
|
||||
/**
|
||||
* Allocates a new ByteBuffer of size aligned to be a multiple of 16 bytes.
|
||||
*
|
||||
* @param size The size of the data in the packet.
|
||||
* @param cmd The command to add to the packet.
|
||||
* @return A ByteBuffer aligned to 16 byte size.
|
||||
*/
|
||||
private static ByteBuffer newPacketForEncryption(int size, PS4Command cmd) {
|
||||
int realSize = (((size + 15) >> 4) << 4);
|
||||
ByteBuffer packet = ByteBuffer.allocate(realSize).order(ByteOrder.LITTLE_ENDIAN);
|
||||
packet.putInt(size).putInt(cmd.value);
|
||||
return packet;
|
||||
}
|
||||
|
||||
static byte[] makeSearchPacket() {
|
||||
StringBuilder packet = new StringBuilder("SRCH * HTTP/1.1\n");
|
||||
packet.append(DDP_VERSION);
|
||||
return packet.toString().getBytes(StandardCharsets.UTF_8);
|
||||
}
|
||||
|
||||
/**
|
||||
* A packet to start up the PS4 from standby mode.
|
||||
*
|
||||
* @param userCredential A 64 character long hex string.
|
||||
* @return A wake-up packet.
|
||||
*/
|
||||
static byte[] makeWakeupPacket(String userCredential) {
|
||||
StringBuilder packet = new StringBuilder("WAKEUP * HTTP/1.1\n");
|
||||
packet.append("client-type:a\n"); // i or a
|
||||
packet.append("auth-type:C\n");
|
||||
packet.append("model:a\n");
|
||||
packet.append("app-type:g\n"); // c or g
|
||||
packet.append("user-credential:" + userCredential + "\n");
|
||||
packet.append(DDP_VERSION);
|
||||
return packet.toString().getBytes(StandardCharsets.UTF_8);
|
||||
}
|
||||
|
||||
/**
|
||||
* A packet to start up communication with the PS4.
|
||||
*
|
||||
* @param userCredential A 64 character long hex string
|
||||
* @return A launch packet.
|
||||
*/
|
||||
static byte[] makeLaunchPacket(String userCredential) {
|
||||
StringBuilder packet = new StringBuilder("LAUNCH * HTTP/1.1\n");
|
||||
packet.append("user-credential:" + userCredential + "\n");
|
||||
packet.append(DDP_VERSION);
|
||||
return packet.toString().getBytes(StandardCharsets.UTF_8);
|
||||
}
|
||||
|
||||
static ByteBuffer makeHelloPacket() {
|
||||
ByteBuffer packet = newPacketOfSize(28, PS4Command.HELLO_REQ);
|
||||
packet.putInt(REQ_VERSION);
|
||||
packet.put(new byte[16]); // Seed = 16 bytes
|
||||
packet.rewind();
|
||||
return packet;
|
||||
}
|
||||
|
||||
/**
|
||||
* Make a login packet, also used when pairing the device to the PS4.
|
||||
*
|
||||
* @param userCredential
|
||||
* @param passCode
|
||||
* @param pairingCode
|
||||
* @return
|
||||
*/
|
||||
static ByteBuffer makeLoginPacket(String userCredential, String passCode, String pairingCode) {
|
||||
ByteBuffer packet = newPacketForEncryption(16 + 64 + 256 + 16 + 16 + 16, PS4Command.LOGIN_REQ);
|
||||
if (passCode.length() == 4) {
|
||||
packet.put(passCode.getBytes(), 0, 4); // Pass-code
|
||||
}
|
||||
packet.position(12);
|
||||
packet.putInt(0x0F00); // Magic number (was 0x0201 before).
|
||||
if (userCredential.length() == 64) {
|
||||
packet.put(userCredential.getBytes(StandardCharsets.US_ASCII), 0, 64);
|
||||
}
|
||||
packet.position(16 + 64);
|
||||
packet.put(APPLICATION_NAME.getBytes(StandardCharsets.UTF_8)); // app_label
|
||||
packet.position(16 + 64 + 256);
|
||||
packet.put(OS_VERSION.getBytes()); // os_version
|
||||
packet.position(16 + 64 + 256 + 16);
|
||||
packet.put(DEVICE_NAME.getBytes(StandardCharsets.UTF_8)); // Model, name of paired unit, shown on the PS4
|
||||
// in the settings view.
|
||||
packet.position(16 + 64 + 256 + 16 + 16);
|
||||
if (pairingCode.length() == 8) {
|
||||
packet.put(pairingCode.getBytes(), 0, 8); // Pairing-code
|
||||
}
|
||||
return packet;
|
||||
}
|
||||
|
||||
/**
|
||||
* Required for getting HPPTd status. Tell the PS4 who we are?
|
||||
*
|
||||
* @param clientID Example: "com.playstation.mobile2ndscreen".
|
||||
* @param clientVersion Example: "18.9.3"
|
||||
* @return A ClientID packet.
|
||||
*/
|
||||
static ByteBuffer makeClientIDPacket(String clientID, String clientVersion) {
|
||||
ByteBuffer packet = newPacketForEncryption(8 + 128 + 32, PS4Command.CLIENT_IDENTITY_REQ);
|
||||
int length = clientID.length();
|
||||
if (length < 128) {
|
||||
packet.put(clientID.getBytes(StandardCharsets.UTF_8));
|
||||
}
|
||||
packet.position(8 + 128);
|
||||
length = clientVersion.length();
|
||||
if (length < 32) {
|
||||
packet.put(clientVersion.getBytes(StandardCharsets.UTF_8));
|
||||
}
|
||||
return packet;
|
||||
}
|
||||
|
||||
/**
|
||||
* Ask for PS4 status.
|
||||
*
|
||||
* @param status Can be one of 0 or 1?
|
||||
* @return A ServerStatus packet.
|
||||
*/
|
||||
static ByteBuffer makeStatusPacket(int status) {
|
||||
ByteBuffer packet = newPacketForEncryption(12, PS4Command.STATUS_REQ);
|
||||
packet.putInt(status); // status
|
||||
return packet;
|
||||
}
|
||||
|
||||
/**
|
||||
* Makes a packet that puts the PS4 in standby mode.
|
||||
*
|
||||
* @return A standby-packet.
|
||||
*/
|
||||
static ByteBuffer makeStandbyPacket() {
|
||||
return newPacketForEncryption(8, PS4Command.STANDBY_REQ);
|
||||
}
|
||||
|
||||
/**
|
||||
* Tries to start an application on the PS4.
|
||||
*
|
||||
* @param applicationId The ID of the application.
|
||||
* @return A appStart-packet
|
||||
*/
|
||||
static ByteBuffer makeApplicationPacket(String applicationId) {
|
||||
ByteBuffer packet = newPacketForEncryption(8 + 16, PS4Command.APP_START_REQ);
|
||||
packet.put(applicationId.getBytes(StandardCharsets.UTF_8)); // Application Id (CUSAxxxxx)
|
||||
return packet;
|
||||
}
|
||||
|
||||
/**
|
||||
* Makes a packet that closes down the connection with the PS4.
|
||||
*
|
||||
* @return A ByeBye-packet.
|
||||
*/
|
||||
static ByteBuffer makeByebyePacket() {
|
||||
return newPacketForEncryption(8, PS4Command.BYEBYE_REQ);
|
||||
}
|
||||
|
||||
/**
|
||||
* This doesn't seem to do anything?
|
||||
*
|
||||
* @return A logout-packet.
|
||||
*/
|
||||
static ByteBuffer makeLogoutPacket() {
|
||||
return newPacketForEncryption(8, PS4Command.LOGOUT_REQ);
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @return A screenshot-packet?
|
||||
*/
|
||||
static ByteBuffer makeScreenShotPacket() {
|
||||
ByteBuffer packet = newPacketForEncryption(12, PS4Command.SCREEN_SHOT_REQ);
|
||||
packet.putInt(1);
|
||||
return packet;
|
||||
}
|
||||
|
||||
static String parseHTTPdPacket(ByteBuffer buffer) {
|
||||
buffer.position(0);
|
||||
int status = buffer.getInt(8);
|
||||
int port = buffer.getInt(12);
|
||||
int option = buffer.getInt(16);
|
||||
return String.format("status:%d, port:%d, option:%08x.", status, port, option);
|
||||
}
|
||||
|
||||
/**
|
||||
* Tell the PS4 that we want to get info about the OnScreenKeyboard.
|
||||
*
|
||||
* @return A OSKStartPacket.
|
||||
*/
|
||||
static ByteBuffer makeOSKStartPacket() {
|
||||
return newPacketForEncryption(8, PS4Command.OSK_START_REQ);
|
||||
}
|
||||
|
||||
/**
|
||||
* Send text to the OSK on the PS4. Replaces all the text as it is now.
|
||||
*
|
||||
* @param text
|
||||
* @return A OSKStringChangePacket.
|
||||
*/
|
||||
static ByteBuffer makeOSKStringChangePacket(String text) {
|
||||
byte[] chars = text.getBytes(StandardCharsets.UTF_16LE);
|
||||
ByteBuffer packet = newPacketForEncryption(28 + chars.length, PS4Command.OSK_CHANGE_STRING_REQ);
|
||||
packet.putInt(text.length()); // preEditIndex
|
||||
packet.putInt(0); // preEditLength
|
||||
packet.putInt(text.length()); // caretIndex
|
||||
packet.putInt(0); // editIndex
|
||||
packet.putInt(0); // editLength
|
||||
packet.put(chars);
|
||||
return packet;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses out the text from a OSKStringChange-packet.
|
||||
*
|
||||
* @param buffer The received packet from the PS4.
|
||||
* @return The text in the packet.
|
||||
*/
|
||||
static String parseOSKStringChangePacket(ByteBuffer buffer) {
|
||||
buffer.position(0);
|
||||
int length = buffer.getInt() - 28;
|
||||
byte[] chars = new byte[length];
|
||||
buffer.position(28);
|
||||
buffer.get(chars);
|
||||
return new String(chars, StandardCharsets.UTF_16LE);
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param command 0 = return, 1 = close.
|
||||
* @return
|
||||
*/
|
||||
static ByteBuffer makeOSKControlPacket(int command) {
|
||||
ByteBuffer packet = newPacketForEncryption(12, PS4Command.OSK_CONTROL_REQ);
|
||||
packet.putInt(command);
|
||||
return packet;
|
||||
}
|
||||
|
||||
static ByteBuffer makeRemoteControlPacket(int pushedKey) {
|
||||
ByteBuffer packet = newPacketForEncryption(16, PS4Command.REMOTE_CONTROL_REQ);
|
||||
packet.putInt(pushedKey);
|
||||
packet.putInt(0); // HoldTime in milliseconds
|
||||
return packet;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param i only 0?
|
||||
* @return
|
||||
*/
|
||||
static ByteBuffer makeCommentViewerStart(int i) {
|
||||
ByteBuffer packet = newPacketForEncryption(12, PS4Command.COMMENT_VIEWER_START_REQ);
|
||||
packet.putInt(i);
|
||||
return packet;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param type Can be 5?
|
||||
* @param info If type is 5 only check bit 0.
|
||||
* @return
|
||||
*/
|
||||
static ByteBuffer makeCommentViewerEvent(int type, int info) {
|
||||
ByteBuffer packet = newPacketForEncryption(16, PS4Command.COMMENT_VIEWER_EVENT);
|
||||
packet.putInt(type);
|
||||
packet.putInt(info);
|
||||
return packet;
|
||||
}
|
||||
|
||||
static ByteBuffer makeCommentViewerSendPacket(int i, String text) {
|
||||
byte[] chars = (text.length() > 60 ? text.substring(0, 60) : text).getBytes(StandardCharsets.UTF_8);
|
||||
ByteBuffer packet = newPacketForEncryption(12 + chars.length, PS4Command.OSK_CHANGE_STRING_REQ);
|
||||
packet.putInt(i);
|
||||
packet.put(chars);
|
||||
return packet;
|
||||
}
|
||||
}
|
@ -0,0 +1,160 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.playstation.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 PlayStationBindingConstants} class defines common constants, which are
|
||||
* used across the whole binding.
|
||||
*
|
||||
* @author Fredrik Ahlström - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class PlayStationBindingConstants {
|
||||
|
||||
private static final String BINDING_ID = "playstation";
|
||||
|
||||
// List of all Thing Type UIDs
|
||||
public static final ThingTypeUID THING_TYPE_PS3 = new ThingTypeUID(BINDING_ID, "PS3");
|
||||
public static final ThingTypeUID THING_TYPE_PS4 = new ThingTypeUID(BINDING_ID, "PS4");
|
||||
public static final ThingTypeUID THING_TYPE_PS5 = new ThingTypeUID(BINDING_ID, "PS5");
|
||||
|
||||
public static final Set<ThingTypeUID> SUPPORTED_THING_TYPES_UIDS = Collections
|
||||
.unmodifiableSet(Stream.of(THING_TYPE_PS3, THING_TYPE_PS4, THING_TYPE_PS5).collect(Collectors.toSet()));
|
||||
|
||||
// List of all Channel ids
|
||||
static final String CHANNEL_POWER = "power";
|
||||
static final String CHANNEL_APPLICATION_NAME = "applicationName";
|
||||
static final String CHANNEL_APPLICATION_ID = "applicationId";
|
||||
static final String CHANNEL_APPLICATION_IMAGE = "applicationImage";
|
||||
static final String CHANNEL_OSK_TEXT = "oskText";
|
||||
static final String CHANNEL_SEND_KEY = "sendKey";
|
||||
static final String CHANNEL_2ND_SCREEN = "secondScreen";
|
||||
static final String CHANNEL_CONNECT = "connect";
|
||||
|
||||
// List of sendKey commands
|
||||
static final String SEND_KEY_UP = "keyUp";
|
||||
static final String SEND_KEY_DOWN = "keyDown";
|
||||
static final String SEND_KEY_RIGHT = "keyRight";
|
||||
static final String SEND_KEY_LEFT = "keyLeft";
|
||||
static final String SEND_KEY_ENTER = "keyEnter";
|
||||
static final String SEND_KEY_BACK = "keyBack";
|
||||
static final String SEND_KEY_OPTION = "keyOption";
|
||||
static final String SEND_KEY_PS = "keyPS";
|
||||
|
||||
// List of all known properties in the response from the PS3/PS4
|
||||
public static final String RESPONSE_HOST_ID = "host-id";
|
||||
public static final String RESPONSE_HOST_TYPE = "host-type";
|
||||
public static final String RESPONSE_HOST_NAME = "host-name";
|
||||
public static final String RESPONSE_HOST_MTP_PROTOCOL_VERSION = "host-mtp-protocol-version";
|
||||
public static final String RESPONSE_HOST_REQUEST_PORT = "host-request-port";
|
||||
public static final String RESPONSE_HOST_WIRELESS_PROTOCOL_VERSION = "host-wireless-protocol-version";
|
||||
public static final String RESPONSE_HOST_MAC_ADDRESS = "host-mac-address";
|
||||
public static final String RESPONSE_HOST_SUPPORTED_DEVICE = "host-supported-device";
|
||||
public static final String RESPONSE_DEVICE_DISCOVERY_PROTOCOL_VERSION = "device_discovery_protocol-version";
|
||||
public static final String RESPONSE_SYSTEM_VERSION = "system-version";
|
||||
public static final String RESPONSE_RUNNING_APP_NAME = "running-app-name";
|
||||
public static final String RESPONSE_RUNNING_APP_TITLEID = "running-app-titleid";
|
||||
|
||||
// Constant field used in PlayStationDiscovery to set the configuration properties during discovery.
|
||||
public static final String USER_CREDENTIAL = "userCredential";
|
||||
public static final String PAIRING_CODE = "pairingCode";
|
||||
public static final String IP_ADDRESS = "ipAddress";
|
||||
public static final String IP_PORT = "ipPort";
|
||||
|
||||
// PlayStation Vita HW versions
|
||||
public static final String PSVHW_PCHXXXX = "PCHXXXX";
|
||||
public static final String PSVHW_PCH1000 = "PCH1000";
|
||||
public static final String PSVHW_PCH1100 = "PCH1100";
|
||||
public static final String PSVHW_PCH2000 = "PCH2000";
|
||||
|
||||
// PlayStation Vita TV HW versions
|
||||
public static final String PSVTVHW_VTE1000 = "VTE1000";
|
||||
|
||||
// PlayStation 3 HW versions
|
||||
public static final String PS3HW_CECHXXXX = "CECHXXXX";
|
||||
public static final String PS3HW_CECHA00 = "CECHA00";
|
||||
public static final String PS3HW_CECHB00 = "CECHB00";
|
||||
public static final String PS3HW_CECHC00 = "CECHC00";
|
||||
public static final String PS3HW_CECHE00 = "CECHE00";
|
||||
public static final String PS3HW_CECHG00 = "CECHG00";
|
||||
public static final String PS3HW_CECHH00 = "CECHH00";
|
||||
public static final String PS3HW_CECHJ00 = "CECHJ00";
|
||||
public static final String PS3HW_CECHK00 = "CECHK00";
|
||||
public static final String PS3HW_CECHL00 = "CECHL00";
|
||||
public static final String PS3HW_CECHM00 = "CECHM00";
|
||||
public static final String PS3HW_CECHP00 = "CECHP00";
|
||||
public static final String PS3HW_CECHQ00 = "CECHQ00";
|
||||
public static final String PS3HW_CECH2000 = "CECH-2000";
|
||||
public static final String PS3HW_CECH2100 = "CECH-2100";
|
||||
public static final String PS3HW_CECH2500 = "CECH-2500";
|
||||
public static final String PS3HW_CECH3000 = "CECH-3000";
|
||||
public static final String PS3HW_CECH4000 = "CECH-4000";
|
||||
public static final String PS3HW_CECH4200 = "CECH-4200";
|
||||
public static final String PS3HW_CECH4300 = "CECH-4300";
|
||||
|
||||
// PlayStation 4 HW versions
|
||||
public static final String PS4HW_CUHXXXX = "CUH-XXXX";
|
||||
public static final String PS4HW_CUH1000 = "CUH-1000";
|
||||
public static final String PS4HW_CUH1100 = "CUH-1100";
|
||||
public static final String PS4HW_CUH1200 = "CUH-1200";
|
||||
public static final String PS4HW_CUH2000 = "CUH-2000";
|
||||
public static final String PS4HW_CUH2100 = "CUH-2100";
|
||||
public static final String PS4HW_CUH2200 = "CUH-2200";
|
||||
public static final String PS4HW_CUH7000 = "CUH-7000";
|
||||
public static final String PS4HW_CUH7100 = "CUH-7100";
|
||||
|
||||
// PlayStation 5 HW versions
|
||||
public static final String PS5HW_CFIXXXX = "CFI-XXXX";
|
||||
public static final String PS5HW_CFI1000A = "CFI-1000A";
|
||||
public static final String PS5HW_CFI1000B = "CFI-1000B";
|
||||
|
||||
static final int PS4_KEY_UP = 1 << 0;
|
||||
static final int PS4_KEY_DOWN = 1 << 1;
|
||||
static final int PS4_KEY_RIGHT = 1 << 2;
|
||||
static final int PS4_KEY_LEFT = 1 << 3;
|
||||
static final int PS4_KEY_ENTER = 1 << 4;
|
||||
static final int PS4_KEY_BACK = 1 << 5;
|
||||
static final int PS4_KEY_OPTION = 1 << 6;
|
||||
static final int PS4_KEY_PS = 1 << 7;
|
||||
static final int PS4_KEY_OFF = 1 << 8;
|
||||
static final int PS4_KEY_CANCEL = 1 << 9;
|
||||
static final int PS4_KEY_OPEN_RC = 1 << 10;
|
||||
static final int PS4_KEY_CLOSE_RC = 1 << 11;
|
||||
|
||||
/** Default port for PS3. */
|
||||
public static final int DEFAULT_PS3_WAKE_ON_LAN_PORT = 5223;
|
||||
public static final int DEFAULT_PS3_REMOTE_PLAY_PORT = 9293;
|
||||
public static final int DEFAULT_PS3_MEDIA_MANAGER_PORT = 9309;
|
||||
public static final int DEFAULT_PS3_DLNA_PORT1 = 56235;
|
||||
public static final int DEFAULT_PS3_DLNA_PORT2 = 56259;
|
||||
|
||||
// Default port numbers PS4 uses.
|
||||
public static final int DEFAULT_BROADCAST_PORT = 987;
|
||||
public static final int DEFAULT_COMMUNICATION_PORT = 997;
|
||||
public static final int DEFAULT_REMOTE_PLAY_PORT = 9295;
|
||||
|
||||
// Open ports on the PS5.
|
||||
public static final int DEFAULT_PS5_HTTP_PORT = 41800;
|
||||
|
||||
private PlayStationBindingConstants() {
|
||||
// Don't instantiate this class.
|
||||
}
|
||||
}
|
@ -0,0 +1,67 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.playstation.internal;
|
||||
|
||||
import static org.openhab.binding.playstation.internal.PlayStationBindingConstants.*;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.openhab.core.i18n.LocaleProvider;
|
||||
import org.openhab.core.net.NetworkAddressService;
|
||||
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.service.component.annotations.Activate;
|
||||
import org.osgi.service.component.annotations.Component;
|
||||
import org.osgi.service.component.annotations.Reference;
|
||||
|
||||
/**
|
||||
* The {@link PlayStationHandlerFactory} is responsible for creating things and thing
|
||||
* handlers.
|
||||
*
|
||||
* @author Fredrik Ahlström - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
@Component(configurationPid = "binding.playstation", service = ThingHandlerFactory.class)
|
||||
public class PlayStationHandlerFactory extends BaseThingHandlerFactory {
|
||||
|
||||
private final LocaleProvider localeProvider;
|
||||
private final NetworkAddressService networkAS;
|
||||
|
||||
@Activate
|
||||
public PlayStationHandlerFactory(@Reference LocaleProvider provider, @Reference NetworkAddressService network) {
|
||||
localeProvider = provider;
|
||||
networkAS = network;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsThingType(ThingTypeUID thingTypeUID) {
|
||||
return PlayStationBindingConstants.SUPPORTED_THING_TYPES_UIDS.contains(thingTypeUID);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected @Nullable ThingHandler createHandler(Thing thing) {
|
||||
ThingTypeUID thingTypeUID = thing.getThingTypeUID();
|
||||
|
||||
if (THING_TYPE_PS4.equals(thingTypeUID) || THING_TYPE_PS5.equals(thingTypeUID)) {
|
||||
return new PS4Handler(thing, localeProvider, networkAS);
|
||||
}
|
||||
if (THING_TYPE_PS3.equals(thingTypeUID)) {
|
||||
return new PS3Handler(thing);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
@ -0,0 +1,479 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.playstation.internal.discovery;
|
||||
|
||||
import static org.openhab.binding.playstation.internal.PlayStationBindingConstants.*;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.DatagramPacket;
|
||||
import java.net.DatagramSocket;
|
||||
import java.net.InetAddress;
|
||||
import java.net.NetworkInterface;
|
||||
import java.net.SocketTimeoutException;
|
||||
import java.net.UnknownHostException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
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.config.discovery.DiscoveryService;
|
||||
import org.openhab.core.net.NetworkAddressService;
|
||||
import org.openhab.core.thing.Thing;
|
||||
import org.openhab.core.thing.ThingUID;
|
||||
import org.openhab.core.util.HexUtils;
|
||||
import org.osgi.service.component.annotations.Component;
|
||||
import org.osgi.service.component.annotations.Reference;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* The {@link PlayStationDiscovery} is responsible for discovering
|
||||
* all PS4 devices
|
||||
*
|
||||
* @author Fredrik Ahlström - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
@Component(service = { DiscoveryService.class, PlayStationDiscovery.class }, configurationPid = "discovery.playstation")
|
||||
public class PlayStationDiscovery extends AbstractDiscoveryService {
|
||||
|
||||
private final Logger logger = LoggerFactory.getLogger(PlayStationDiscovery.class);
|
||||
|
||||
private static final int DISCOVERY_TIMEOUT_SECONDS = 2;
|
||||
|
||||
private @Nullable NetworkAddressService networkAS;
|
||||
|
||||
public PlayStationDiscovery() {
|
||||
super(SUPPORTED_THING_TYPES_UIDS, DISCOVERY_TIMEOUT_SECONDS * 2, true);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void startScan() {
|
||||
logger.debug("Updating discovered things (new scan)");
|
||||
discoverPS4();
|
||||
discoverPS3();
|
||||
}
|
||||
|
||||
@Reference
|
||||
public void bindNetworkAddressService(NetworkAddressService network) {
|
||||
networkAS = network;
|
||||
}
|
||||
|
||||
private @Nullable InetAddress getBroadcastAdress() {
|
||||
NetworkAddressService nwService = networkAS;
|
||||
if (nwService != null) {
|
||||
try {
|
||||
String address = nwService.getConfiguredBroadcastAddress();
|
||||
if (address != null) {
|
||||
return InetAddress.getByName(address);
|
||||
} else {
|
||||
return InetAddress.getByName("255.255.255.255");
|
||||
}
|
||||
} catch (UnknownHostException e) {
|
||||
// We catch errors later.
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private @Nullable InetAddress getIPv4Adress() {
|
||||
NetworkAddressService nwService = networkAS;
|
||||
if (nwService != null) {
|
||||
try {
|
||||
String address = nwService.getPrimaryIpv4HostAddress();
|
||||
if (address != null) {
|
||||
return InetAddress.getByName(address);
|
||||
}
|
||||
} catch (UnknownHostException e) {
|
||||
// We catch errors later.
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private synchronized void discoverPS4() {
|
||||
logger.debug("Trying to discover all PS4 devices");
|
||||
|
||||
try (DatagramSocket socket = new DatagramSocket(0, getIPv4Adress())) {
|
||||
socket.setBroadcast(true);
|
||||
socket.setSoTimeout(DISCOVERY_TIMEOUT_SECONDS * 1000);
|
||||
|
||||
InetAddress bcAddress = getBroadcastAdress();
|
||||
|
||||
// send discover
|
||||
byte[] discover = "SRCH * HTTP/1.1\ndevice-discovery-protocol-version:00020020\n".getBytes();
|
||||
DatagramPacket packet = new DatagramPacket(discover, discover.length, bcAddress, DEFAULT_BROADCAST_PORT);
|
||||
socket.send(packet);
|
||||
logger.debug("Discover message sent: '{}'", discover);
|
||||
|
||||
// wait for responses
|
||||
while (true) {
|
||||
byte[] rxbuf = new byte[256];
|
||||
packet = new DatagramPacket(rxbuf, rxbuf.length);
|
||||
try {
|
||||
socket.receive(packet);
|
||||
parsePS4Packet(packet);
|
||||
} catch (SocketTimeoutException e) {
|
||||
break; // leave the endless loop
|
||||
}
|
||||
}
|
||||
} catch (IOException e) {
|
||||
logger.debug("No PS4 device found. Diagnostic: {}", e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private synchronized void discoverPS3() {
|
||||
logger.trace("Trying to discover all PS3 devices that have \"Connect PS Vita System Using Network\" on.");
|
||||
|
||||
InetAddress bcAddress = getBroadcastAdress();
|
||||
InetAddress localAddress = getIPv4Adress();
|
||||
|
||||
if (localAddress == null || bcAddress == null) {
|
||||
logger.warn("No IP/Broadcast address found. Make sure OpenHab is configured!");
|
||||
return;
|
||||
}
|
||||
try (DatagramSocket socket = new DatagramSocket(0, getIPv4Adress())) {
|
||||
socket.setBroadcast(true);
|
||||
socket.setSoTimeout(DISCOVERY_TIMEOUT_SECONDS * 1000);
|
||||
|
||||
NetworkInterface nic = NetworkInterface.getByInetAddress(localAddress);
|
||||
byte[] macAdr = nic.getHardwareAddress();
|
||||
String macString = HexUtils.bytesToHex(macAdr);
|
||||
// send discover
|
||||
StringBuilder srchBuilder = new StringBuilder("SRCH3 * HTTP/1.1\n");
|
||||
srchBuilder.append("device-id:");
|
||||
srchBuilder.append(macString);
|
||||
srchBuilder.append("01010101010101010101\n");
|
||||
srchBuilder.append("device-type:PS Vita\n");
|
||||
srchBuilder.append("device-class:0\n");
|
||||
srchBuilder.append("device-mac-address:");
|
||||
srchBuilder.append(macString);
|
||||
srchBuilder.append("\n");
|
||||
srchBuilder.append("device-wireless-protocol-version:01000000\n\n");
|
||||
byte[] discover = srchBuilder.toString().getBytes();
|
||||
DatagramPacket packet = new DatagramPacket(discover, discover.length, bcAddress,
|
||||
DEFAULT_PS3_MEDIA_MANAGER_PORT);
|
||||
socket.send(packet);
|
||||
|
||||
// wait for responses
|
||||
while (true) {
|
||||
byte[] rxbuf = new byte[512];
|
||||
packet = new DatagramPacket(rxbuf, rxbuf.length);
|
||||
try {
|
||||
socket.receive(packet);
|
||||
parsePS3Packet(packet);
|
||||
} catch (SocketTimeoutException e) {
|
||||
break; // leave the endless loop
|
||||
}
|
||||
}
|
||||
} catch (IOException e) {
|
||||
logger.debug("No PS3 device found. Diagnostic: {}", e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The response from the PS4 looks something like this:
|
||||
*
|
||||
* HTTP/1.1 200 Ok
|
||||
* host-id:0123456789AB
|
||||
* host-type:PS4
|
||||
* host-name:MyPS4
|
||||
* host-request-port:997
|
||||
* device-discovery-protocol-version:00020020
|
||||
* system-version:07020001
|
||||
* running-app-name:Youtube
|
||||
* running-app-titleid:CUSA01116
|
||||
*
|
||||
* @param packet
|
||||
* @return
|
||||
*/
|
||||
private boolean parsePS4Packet(DatagramPacket packet) {
|
||||
byte[] data = packet.getData();
|
||||
String message = new String(data, StandardCharsets.UTF_8);
|
||||
logger.debug("PS4 data '{}', length:{}", message, packet.getLength());
|
||||
|
||||
String ipAddress = packet.getAddress().toString().split("/")[1];
|
||||
String hostId = "";
|
||||
String hostType = "";
|
||||
String hostName = "";
|
||||
String hostPort = "";
|
||||
String protocolVersion = "";
|
||||
String systemVersion = "";
|
||||
|
||||
String[] rowStrings = message.trim().split("\\r?\\n");
|
||||
for (String row : rowStrings) {
|
||||
int index = row.indexOf(':');
|
||||
index = index != -1 ? index : 0;
|
||||
String key = row.substring(0, index);
|
||||
String value = row.substring(index + 1);
|
||||
switch (key) {
|
||||
case RESPONSE_HOST_ID:
|
||||
hostId = value;
|
||||
break;
|
||||
case RESPONSE_HOST_TYPE:
|
||||
hostType = value;
|
||||
break;
|
||||
case RESPONSE_HOST_NAME:
|
||||
hostName = value;
|
||||
break;
|
||||
case RESPONSE_HOST_REQUEST_PORT:
|
||||
hostPort = value;
|
||||
break;
|
||||
case RESPONSE_DEVICE_DISCOVERY_PROTOCOL_VERSION:
|
||||
protocolVersion = value;
|
||||
if (!"00020020".equals(protocolVersion)) {
|
||||
logger.debug("Different protocol version: '{}'", protocolVersion);
|
||||
}
|
||||
break;
|
||||
case RESPONSE_SYSTEM_VERSION:
|
||||
systemVersion = value;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
String hwVersion = hwVersionFromHostId(hostId);
|
||||
String modelID = modelNameFromHostTypeAndHWVersion(hostType, hwVersion);
|
||||
Map<String, Object> properties = new HashMap<>();
|
||||
properties.put(IP_ADDRESS, ipAddress);
|
||||
properties.put(IP_PORT, Integer.valueOf(hostPort));
|
||||
properties.put(Thing.PROPERTY_MODEL_ID, modelID);
|
||||
properties.put(Thing.PROPERTY_HARDWARE_VERSION, hwVersion);
|
||||
properties.put(Thing.PROPERTY_FIRMWARE_VERSION, formatPS4Version(systemVersion));
|
||||
properties.put(Thing.PROPERTY_MAC_ADDRESS, hostIdToMacAddress(hostId));
|
||||
ThingUID uid = hostType.equalsIgnoreCase("PS5") ? new ThingUID(THING_TYPE_PS5, hostId)
|
||||
: new ThingUID(THING_TYPE_PS4, hostId);
|
||||
|
||||
DiscoveryResult result = DiscoveryResultBuilder.create(uid).withProperties(properties).withLabel(hostName)
|
||||
.withRepresentationProperty(Thing.PROPERTY_MAC_ADDRESS).build();
|
||||
thingDiscovered(result);
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* The response from the PS3 looks like this:
|
||||
*
|
||||
* HTTP/1.1 200 OK
|
||||
* host-id:00000000-0000-0000-0000-123456789abc
|
||||
* host-type:ps3
|
||||
* host-name:MyPS3
|
||||
* host-mtp-protocol-version:1800010
|
||||
* host-request-port:9309
|
||||
* host-wireless-protocol-version:1000000
|
||||
* host-mac-address:123456789abc
|
||||
* host-supported-device:PS Vita, PS Vita TV
|
||||
*
|
||||
* @param packet
|
||||
* @return
|
||||
*/
|
||||
private boolean parsePS3Packet(DatagramPacket packet) {
|
||||
byte[] data = packet.getData();
|
||||
String message = new String(data, StandardCharsets.UTF_8);
|
||||
logger.debug("PS3 data '{}', length:{}", message, packet.getLength());
|
||||
|
||||
String ipAddress = packet.getAddress().toString().split("/")[1];
|
||||
String hostId = "";
|
||||
String hostType = "";
|
||||
String hostName = "";
|
||||
String hostPort = "";
|
||||
String protocolVersion = "";
|
||||
|
||||
String[] rowStrings = message.trim().split("\\r?\\n");
|
||||
for (String row : rowStrings) {
|
||||
int index = row.indexOf(':');
|
||||
index = index != -1 ? index : 0;
|
||||
String key = row.substring(0, index);
|
||||
String value = row.substring(index + 1);
|
||||
switch (key) {
|
||||
case RESPONSE_HOST_ID:
|
||||
hostId = value;
|
||||
break;
|
||||
case RESPONSE_HOST_TYPE:
|
||||
hostType = value;
|
||||
break;
|
||||
case RESPONSE_HOST_NAME:
|
||||
hostName = value;
|
||||
break;
|
||||
case RESPONSE_HOST_REQUEST_PORT:
|
||||
hostPort = value;
|
||||
if (!Integer.toString(DEFAULT_PS3_MEDIA_MANAGER_PORT).equals(hostPort)) {
|
||||
logger.debug("Different host request port: '{}'", hostPort);
|
||||
}
|
||||
break;
|
||||
case RESPONSE_HOST_WIRELESS_PROTOCOL_VERSION:
|
||||
protocolVersion = value;
|
||||
if (!"1000000".equals(protocolVersion)) {
|
||||
logger.debug("Different protocol version: '{}'", protocolVersion);
|
||||
}
|
||||
break;
|
||||
case RESPONSE_HOST_MAC_ADDRESS:
|
||||
hostId = value;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
String hwVersion = hwVersionFromHostId(hostId);
|
||||
String modelID = modelNameFromHostTypeAndHWVersion(hostType, hwVersion);
|
||||
Map<String, Object> properties = new HashMap<>();
|
||||
properties.put(IP_ADDRESS, ipAddress);
|
||||
properties.put(Thing.PROPERTY_MODEL_ID, modelID);
|
||||
properties.put(Thing.PROPERTY_HARDWARE_VERSION, hwVersion);
|
||||
properties.put(Thing.PROPERTY_MAC_ADDRESS, hostIdToMacAddress(hostId));
|
||||
ThingUID uid = new ThingUID(THING_TYPE_PS3, hostId);
|
||||
|
||||
DiscoveryResult result = DiscoveryResultBuilder.create(uid).withProperties(properties).withLabel(hostName)
|
||||
.withRepresentationProperty(Thing.PROPERTY_MAC_ADDRESS).build();
|
||||
thingDiscovered(result);
|
||||
return true;
|
||||
}
|
||||
|
||||
private static String hostIdToMacAddress(String hostId) {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
if (hostId.length() >= 12) {
|
||||
for (int i = 0; i < 6; i++) {
|
||||
sb.append(hostId.substring(i * 2, i * 2 + 2).toLowerCase());
|
||||
if (i < 5) {
|
||||
sb.append(':');
|
||||
}
|
||||
}
|
||||
}
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
public static String formatPS4Version(String fwVersion) {
|
||||
String resultV = fwVersion;
|
||||
int len = fwVersion.length();
|
||||
for (Character c : fwVersion.toCharArray()) {
|
||||
if (!Character.isDigit(c)) {
|
||||
return resultV;
|
||||
}
|
||||
}
|
||||
if (len > 4) {
|
||||
resultV = resultV.substring(0, 4) + "." + resultV.substring(4, len);
|
||||
len++;
|
||||
}
|
||||
if (len > 2) {
|
||||
resultV = resultV.substring(0, 2) + "." + resultV.substring(2, len);
|
||||
}
|
||||
|
||||
if (resultV.charAt(0) == '0') {
|
||||
resultV = resultV.substring(1);
|
||||
}
|
||||
return resultV;
|
||||
}
|
||||
|
||||
private static String hwVersionFromHostId(String hostId) {
|
||||
String hwVersion = PS4HW_CUHXXXX;
|
||||
if (hostId.length() >= 12) {
|
||||
final String manufacturer = hostId.substring(0, 6).toLowerCase();
|
||||
final String ethId = hostId.substring(6, 8).toLowerCase();
|
||||
switch (manufacturer) {
|
||||
case "d44b5e":
|
||||
hwVersion = PSVHW_PCHXXXX;
|
||||
break;
|
||||
case "001315":
|
||||
case "001fa7":
|
||||
case "a8e3ee":
|
||||
case "fc0fe6":
|
||||
case "00248d":
|
||||
case "280dfc":
|
||||
case "0015c1":
|
||||
case "0019c5":
|
||||
case "0022a6":
|
||||
case "0cfe45":
|
||||
case "f8d0ac":
|
||||
case "00041f":
|
||||
case "001d0d":
|
||||
hwVersion = PS3HW_CECHXXXX;
|
||||
break;
|
||||
case "00d9d1":
|
||||
hwVersion = PS3HW_CECH4000;
|
||||
break;
|
||||
case "709e29": // Ethernet
|
||||
case "b00594": // WiFi
|
||||
hwVersion = PS4HW_CUH1000;
|
||||
break;
|
||||
case "bc60a7": // Ethernet
|
||||
if (ethId.equals("7b")) {
|
||||
hwVersion = PS4HW_CUH2000;
|
||||
}
|
||||
if (ethId.equals("8f")) {
|
||||
hwVersion = PS4HW_CUH7000;
|
||||
}
|
||||
break;
|
||||
case "c863f1": // Ethernet
|
||||
case "f8461c": // Ethernet
|
||||
case "5cea1d": // WiFi
|
||||
case "f8da0c": // WiFi
|
||||
hwVersion = PS4HW_CUH2000;
|
||||
break;
|
||||
case "40490f": // WiFi
|
||||
case "5c9656": // WiFi
|
||||
if (ethId.equals("07")) {
|
||||
hwVersion = PS4HW_CUH2000;
|
||||
}
|
||||
if (ethId.equals("da")) {
|
||||
hwVersion = PS4HW_CUH7000;
|
||||
}
|
||||
break;
|
||||
case "2ccc44": // Ethernet
|
||||
case "dca266": // WiFi
|
||||
hwVersion = PS4HW_CUH7100;
|
||||
break;
|
||||
case "78c881": // Ethernet
|
||||
case "1c98c1": // WiFi
|
||||
hwVersion = PS5HW_CFI1000B;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return hwVersion;
|
||||
}
|
||||
|
||||
private static String modelNameFromHostTypeAndHWVersion(String hostType, String hwVersion) {
|
||||
String modelName = "PlayStation 4";
|
||||
switch (hostType.toUpperCase()) {
|
||||
case "PS3":
|
||||
modelName = "PlayStation 3";
|
||||
if (hwVersion.startsWith("CECH-2") || hwVersion.startsWith("CECH-3")) {
|
||||
modelName += " Slim";
|
||||
} else if (hwVersion.startsWith("CECH-4")) {
|
||||
modelName += " Super Slim";
|
||||
}
|
||||
break;
|
||||
case "PS4":
|
||||
modelName = "PlayStation 4";
|
||||
if (hwVersion.startsWith("CUH-2")) {
|
||||
modelName += " Slim";
|
||||
} else if (hwVersion.startsWith("CUH-7")) {
|
||||
modelName += " Pro";
|
||||
}
|
||||
break;
|
||||
case "PS5":
|
||||
modelName = "PlayStation 5";
|
||||
if (hwVersion.endsWith("B")) {
|
||||
modelName += " Digital Edition";
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
return modelName;
|
||||
}
|
||||
}
|
@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<binding:binding id="playstation" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xmlns:binding="https://openhab.org/schemas/binding/v1.0.0"
|
||||
xsi:schemaLocation="https://openhab.org/schemas/binding/v1.0.0 https://openhab.org/schemas/binding-1.0.0.xsd">
|
||||
|
||||
<name>Sony PlayStation Binding</name>
|
||||
<description>Monitor and control your Sony PlayStation.</description>
|
||||
<author>Fredrik Ahlström</author>
|
||||
|
||||
</binding:binding>
|
@ -0,0 +1,69 @@
|
||||
<?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:playstation:PS3">
|
||||
<parameter name="ipAddress" type="text" required="true">
|
||||
<context>network-address</context>
|
||||
<label>IP Address</label>
|
||||
<description>The IP Address of the PlayStation 3.</description>
|
||||
<advanced>true</advanced>
|
||||
</parameter>
|
||||
</config-description>
|
||||
|
||||
<config-description uri="thing-type:playstation:PS4">
|
||||
<parameter name="userCredential" type="text" required="true" min="64" max="64" pattern="[0-9A-Fa-f]*">
|
||||
<label>User Credential</label>
|
||||
<description>User Credential to Communicate with the PlayStation 4.</description>
|
||||
</parameter>
|
||||
<parameter name="passCode" type="text" min="4" max="4" pattern="[0-9]*">
|
||||
<context>password</context>
|
||||
<label>Pass Code</label>
|
||||
<description>Pass Code to Log in to PlayStation 4.</description>
|
||||
</parameter>
|
||||
<parameter name="pairingCode" type="text" min="8" max="8" pattern="[0-9]*">
|
||||
<label>Pairing Code</label>
|
||||
<description>Code to Pair openHAB Device to PlayStation 4. Only Needed During Pairing.</description>
|
||||
</parameter>
|
||||
<parameter name="connectionTimeout" type="integer" min="0" unit="s">
|
||||
<label>Connection Timeout</label>
|
||||
<description>How Many Seconds After the Last Command the Connection to the PS4 Closes Down. Use 0 to Never Close
|
||||
Connection.</description>
|
||||
<default>60</default>
|
||||
<advanced>true</advanced>
|
||||
</parameter>
|
||||
<parameter name="autoConnect" type="boolean">
|
||||
<label>Auto Connect</label>
|
||||
<description>Should the Binding Try to Connect to the PS4 as Soon as it's Turned On.</description>
|
||||
<default>false</default>
|
||||
<advanced>true</advanced>
|
||||
</parameter>
|
||||
<parameter name="artworkSize" type="integer" min="128" max="1024">
|
||||
<label>Artwork Size</label>
|
||||
<description>The Width and Height of the Downloaded Artwork.</description>
|
||||
<default>320</default>
|
||||
<advanced>true</advanced>
|
||||
</parameter>
|
||||
<parameter name="ipAddress" type="text" required="true">
|
||||
<context>network-address</context>
|
||||
<label>IP Address</label>
|
||||
<description>The IP Address of the PlayStation 4.</description>
|
||||
<advanced>true</advanced>
|
||||
</parameter>
|
||||
<parameter name="ipPort" type="integer">
|
||||
<label>IP Port</label>
|
||||
<description>The IP Port Used to Communicate with the PlayStation 4.</description>
|
||||
<default>997</default>
|
||||
<advanced>true</advanced>
|
||||
</parameter>
|
||||
<parameter name="outboundIP" type="text">
|
||||
<context>network-address</context>
|
||||
<label>Outbound IP</label>
|
||||
<description>IP Address of the Network Interface to Use.</description>
|
||||
<advanced>true</advanced>
|
||||
</parameter>
|
||||
</config-description>
|
||||
|
||||
</config-description:config-descriptions>
|
@ -0,0 +1,52 @@
|
||||
|
||||
# binding
|
||||
binding.playstation.name = Sony PlayStation Binding
|
||||
binding.playstation.description = Monitor and control your Sony PlayStation.
|
||||
|
||||
# thing types
|
||||
thing-type.playstation.PS3.label = PlayStation 3
|
||||
thing-type.playstation.PS3.description = Sony PlayStation 3 console.
|
||||
thing-type.playstation.PS4.label = PlayStation 4
|
||||
thing-type.playstation.PS4.description = Sony PlayStation 4 console.
|
||||
|
||||
# thing type configuration
|
||||
thing-type.config.playstation.PS3.ipAddress.label = IP Address
|
||||
thing-type.config.playstation.PS3.ipAddress.description = The IP Address of the PlayStation 3.
|
||||
thing-type.config.playstation.PS4.userCredential.label = User Credential
|
||||
thing-type.config.playstation.PS4.userCredential.description = User Credential to Communicate with the PlayStation 4, 64 Hex Characters.
|
||||
thing-type.config.playstation.PS4.passCode.label = Pass Code
|
||||
thing-type.config.playstation.PS4.passCode.description = Pass Code to Log in to PlayStation 4, Optional, 4 Digits.
|
||||
thing-type.config.playstation.PS4.pairingCode.label = Pairing Code
|
||||
thing-type.config.playstation.PS4.pairingCode.description = Pairing Code to Pair openHAB Device to PlayStation 4, only Needed During Pairing, 8 Digits.
|
||||
thing-type.config.playstation.PS4.connectionTimeout.label = Connection Timeout
|
||||
thing-type.config.playstation.PS4.connectionTimeout.description = How Many Seconds After the Last Command the Connection to the PS4 Closes Down. Use 0 to Never Close Connection.
|
||||
thing-type.config.playstation.PS4.autoConnect.label = Auto Connect
|
||||
thing-type.config.playstation.PS4.autoConnect.description = Should the Binding Try to Connect to the PS4 as Soon as it's Turned On.
|
||||
thing-type.config.playstation.PS4.artworkSize.label = Artwork Size
|
||||
thing-type.config.playstation.PS4.artworkSize.description = The Width and Height of the Downloaded Artwork.
|
||||
thing-type.config.playstation.PS4.outboundIP.label = Outbound IP
|
||||
thing-type.config.playstation.PS4.outboundIP.description = IP Address of the Network Interface to use. Only use if your PS4 is on a Sub-Net Different from the Standard openHAB.
|
||||
thing-type.config.playstation.PS4.ipAddress.label = IP Address
|
||||
thing-type.config.playstation.PS4.ipAddress.description = The IP Address of the PlayStation 4.
|
||||
thing-type.config.playstation.PS4.ipPort.label = IP Port
|
||||
thing-type.config.playstation.PS4.ipPort.description = The IP Port Used to Communicate with the PlayStation 4.
|
||||
|
||||
# channel types
|
||||
channel-type.playstation.power-ps3-channel.label = PlayStation 3 Power
|
||||
channel-type.playstation.power-ps3-channel.description = Shows if PlayStation 3 is On or Off.
|
||||
channel-type.playstation.power-channel.label = PlayStation 4 Power
|
||||
channel-type.playstation.power-channel.description = Shows if PlayStation 4 is On or in Standby/Off.
|
||||
channel-type.playstation.application-channel.label = Application
|
||||
channel-type.playstation.application-channel.description = Name of the Currently Running Application.
|
||||
channel-type.playstation.applicationId-channel.label = Application ID
|
||||
channel-type.playstation.applicationId-channel.description = ID of the Currently Running Application.
|
||||
channel-type.playstation.applicationImage-channel.label = Artwork
|
||||
channel-type.playstation.applicationImage-channel.description = Application Artwork.
|
||||
channel-type.playstation.oskText-channel.label = OSK Text
|
||||
channel-type.playstation.oskText-channel.description = The Text from the OnScreenKeyboard.
|
||||
channel-type.playstation.sendKey-channel.label = Send a Key Press
|
||||
channel-type.playstation.sendKey-channel.description = Send a Key Press to the PS4 UI.
|
||||
channel-type.playstation.secondScreen-channel.label = Second screen
|
||||
channel-type.playstation.secondScreen-channel.description = Link to 2ndScreen HTTP server.
|
||||
channel-type.playstation.connect-channel.label = Connect
|
||||
channel-type.playstation.connect-channel.description = Connect/Disconnect to/from PS4 Without Sending a Command.
|
@ -0,0 +1,52 @@
|
||||
|
||||
# binding
|
||||
binding.playstation.name = Sony PlayStation Binding
|
||||
binding.playstation.description = Övervaka och styr ditt Sony PlayStation.
|
||||
|
||||
# thing types
|
||||
thing-type.playstation.PS3.label = PlayStation 3
|
||||
thing-type.playstation.PS3.description = Sony PlayStation 3 console.
|
||||
thing-type.playstation.PS4.label = PlayStation 4
|
||||
thing-type.playstation.PS4.description = Sony PlayStation 4 console.
|
||||
|
||||
# thing type configuration
|
||||
thing-type.config.playstation.PS3.ipAddress.label = Nätverksadress
|
||||
thing-type.config.playstation.PS3.ipAddress.description = IP-adress på PlayStation 3.
|
||||
thing-type.config.playstation.PS4.userCredential.label = Användar credential
|
||||
thing-type.config.playstation.PS4.userCredential.description = Användar credential för att kommunicera med PlayStation 4, 64 hex tecken.
|
||||
thing-type.config.playstation.PS4.passCode.label = Passkod
|
||||
thing-type.config.playstation.PS4.passCode.description = Passkod för att logga in på PlayStation 4, valfritt, 4 siffror.
|
||||
thing-type.config.playstation.PS4.pairingCode.label = Parningskod
|
||||
thing-type.config.playstation.PS4.pairingCode.description = Kod för att para openHAB-enheten med PlayStation 4, behövs bara under parning, 8 siffror.
|
||||
thing-type.config.playstation.PS4.connectionTimeout.label = Uppkopplings timeout
|
||||
thing-type.config.playstation.PS4.connectionTimeout.description = Hur många sekunder efter det senaste kommandot som uppkopplingen stängs ner. Använd 0 för att aldrig stänga ner.
|
||||
thing-type.config.playstation.PS4.autoConnect.label = Auto uppkoppling
|
||||
thing-type.config.playstation.PS4.autoConnect.description = Skall bindingen försöka koppla upp sig mot PS4:an så fort den sätts på.
|
||||
thing-type.config.playstation.PS4.artworkSize.label = Omslagsbild storlek
|
||||
thing-type.config.playstation.PS4.artworkSize.description = Bredden och höjden på nerladdad omslagsbild.
|
||||
thing-type.config.playstation.PS4.outboundIP.label = Utgående IP
|
||||
thing-type.config.playstation.PS4.outboundIP.description = IP adress på nätverkskortet som ska användas. Använd bara om din PS4 är på ett sub-net som skiljer sig från standard openHAB.
|
||||
thing-type.config.playstation.PS4.ipAddress.label = Nätverksadress
|
||||
thing-type.config.playstation.PS4.ipAddress.description = IP-adress på PlayStation 4.
|
||||
thing-type.config.playstation.PS4.ipPort.label = IP port
|
||||
thing-type.config.playstation.PS4.ipPort.description = IP porten som används för att kommunicera med PlayStation 4.
|
||||
|
||||
# channel types
|
||||
channel-type.playstation.power-ps3-channel.label = PlayStation 3 Ström
|
||||
channel-type.playstation.power-ps3-channel.description = Om PlayStation 3 är på eller av.
|
||||
channel-type.playstation.power-channel.label = PlayStation 4 Ström
|
||||
channel-type.playstation.power-channel.description = Om PlayStation 4 är på eller i vänteläge/av.
|
||||
channel-type.playstation.application-channel.label = Applikation
|
||||
channel-type.playstation.application-channel.description = Namn på nuvarande applikation som körs.
|
||||
channel-type.playstation.applicationId-channel.label = Applikations-ID
|
||||
channel-type.playstation.applicationId-channel.description = ID på nuvarande applikation som körs.
|
||||
channel-type.playstation.applicationImage-channel.label = Omslagsbild
|
||||
channel-type.playstation.applicationImage-channel.description = Omslagsbild för applikationen.
|
||||
channel-type.playstation.oskText-channel.label = OSK Text
|
||||
channel-type.playstation.oskText-channel.description = The text from the OnScreenKeyboard.
|
||||
channel-type.playstation.sendKey-channel.label = Skicka en tangent tryckning
|
||||
channel-type.playstation.sendKey-channel.description = Skicka en tangent tryckning till PS4 UI.
|
||||
channel-type.playstation.secondScreen-channel.label = Second screen
|
||||
channel-type.playstation.secondScreen-channel.description = Länk till 2ndScreen HTTP server.
|
||||
channel-type.playstation.connect-channel.label = Koppla upp
|
||||
channel-type.playstation.connect-channel.description = Koppla upp/från PS4 utan att skicka kommando.
|
@ -0,0 +1,116 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<thing:thing-descriptions bindingId="playstation"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
|
||||
xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
|
||||
|
||||
<thing-type id="PS3">
|
||||
<label>PlayStation 3</label>
|
||||
<description>Sony PlayStation 3 console.</description>
|
||||
|
||||
<channels>
|
||||
<channel id="power" typeId="power-channel"/>
|
||||
</channels>
|
||||
|
||||
<properties>
|
||||
<property name="vendor">Sony</property>
|
||||
<property name="modelId">PlayStation 3</property>
|
||||
</properties>
|
||||
<config-description-ref uri="thing-type:playstation:PS3"/>
|
||||
|
||||
</thing-type>
|
||||
|
||||
<thing-type id="PS4">
|
||||
<label>PlayStation 4</label>
|
||||
<description>Sony PlayStation 4 console.</description>
|
||||
|
||||
<channels>
|
||||
<channel id="power" typeId="power-channel"/>
|
||||
<channel id="applicationName" typeId="application-channel"/>
|
||||
<channel id="applicationId" typeId="applicationId-channel"/>
|
||||
<channel id="applicationImage" typeId="applicationImage-channel"/>
|
||||
<channel id="oskText" typeId="oskText-channel"/>
|
||||
<channel id="connect" typeId="connect-channel"/>
|
||||
<channel id="sendKey" typeId="sendKey-channel"/>
|
||||
<channel id="secondScreen" typeId="secondScreen-channel"/>
|
||||
</channels>
|
||||
|
||||
<properties>
|
||||
<property name="vendor">Sony</property>
|
||||
<property name="modelId">PlayStation 4</property>
|
||||
</properties>
|
||||
<config-description-ref uri="thing-type:playstation:PS4"/>
|
||||
|
||||
</thing-type>
|
||||
|
||||
<channel-type id="power-ps3-channel">
|
||||
<item-type>Switch</item-type>
|
||||
<label>PlayStation 3 Power</label>
|
||||
<description>Shows if PlayStation 3 is On or Off.</description>
|
||||
<tags>
|
||||
<tag>Switchable</tag>
|
||||
</tags>
|
||||
<state readOnly="true"/>
|
||||
</channel-type>
|
||||
<channel-type id="power-channel">
|
||||
<item-type>Switch</item-type>
|
||||
<label>PlayStation 4 Power</label>
|
||||
<description>Shows if PlayStation 4 is On or in Standby/Off.</description>
|
||||
<tags>
|
||||
<tag>Switchable</tag>
|
||||
</tags>
|
||||
</channel-type>
|
||||
<channel-type id="application-channel">
|
||||
<item-type>String</item-type>
|
||||
<label>Application</label>
|
||||
<description>Name of the Currently Running Application.</description>
|
||||
<state pattern="%s" readOnly="true"/>
|
||||
</channel-type>
|
||||
<channel-type id="applicationId-channel">
|
||||
<item-type>String</item-type>
|
||||
<label>Application ID</label>
|
||||
<description>ID of the Currently running application.</description>
|
||||
<category>MediaControl</category>
|
||||
</channel-type>
|
||||
<channel-type id="applicationImage-channel">
|
||||
<item-type>Image</item-type>
|
||||
<label>Artwork</label>
|
||||
<description>Application Artwork.</description>
|
||||
<state readOnly="true"/>
|
||||
</channel-type>
|
||||
<channel-type id="oskText-channel" advanced="true">
|
||||
<item-type>String</item-type>
|
||||
<label>OSK Text</label>
|
||||
<description>The Text from the OnScreenKeyboard.</description>
|
||||
</channel-type>
|
||||
<channel-type id="sendKey-channel" advanced="true">
|
||||
<item-type>String</item-type>
|
||||
<label>Send a Key Press</label>
|
||||
<description>Send a Key Press to the PS4 UI.</description>
|
||||
<category>MoveControl</category>
|
||||
<command>
|
||||
<options>
|
||||
<option value="keyUp">Up</option>
|
||||
<option value="keyDown">Down</option>
|
||||
<option value="keyRight">Right</option>
|
||||
<option value="keyLeft">Left</option>
|
||||
<option value="keyEnter">Enter</option>
|
||||
<option value="keyBack">Back</option>
|
||||
<option value="keyOption">Option</option>
|
||||
<option value="keyPS">PS</option>
|
||||
</options>
|
||||
</command>
|
||||
</channel-type>
|
||||
<channel-type id="secondScreen-channel" advanced="true">
|
||||
<item-type>String</item-type>
|
||||
<label>Second screen</label>
|
||||
<description>Link to 2ndScreen HTTP Server.</description>
|
||||
<state pattern="%s" readOnly="true"/>
|
||||
</channel-type>
|
||||
<channel-type id="connect-channel" advanced="true">
|
||||
<item-type>Switch</item-type>
|
||||
<label>Connected</label>
|
||||
<description>Connect/Disconnect to/from PS4 without Sending Command.</description>
|
||||
</channel-type>
|
||||
|
||||
</thing:thing-descriptions>
|
@ -227,6 +227,7 @@
|
||||
<module>org.openhab.binding.pioneeravr</module>
|
||||
<module>org.openhab.binding.pixometer</module>
|
||||
<module>org.openhab.binding.pjlinkdevice</module>
|
||||
<module>org.openhab.binding.playstation</module>
|
||||
<module>org.openhab.binding.plclogo</module>
|
||||
<module>org.openhab.binding.plugwise</module>
|
||||
<module>org.openhab.binding.powermax</module>
|
||||
|
Loading…
Reference in New Issue
Block a user