[playstation] Initial contribution (#8585)

* [playstation] Initial contribution.

Signed-off-by: Fredrik Ahlström <fredrikahlstrom75@gmail.com>
This commit is contained in:
Fredrik Ahlström 2020-11-26 02:39:27 +01:00 committed by GitHub
parent 72c441e422
commit fde30fbada
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
24 changed files with 3175 additions and 0 deletions

View File

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

View File

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

View File

@ -0,0 +1,13 @@
This content is produced and maintained by the openHAB project.
* Project home: https://www.openhab.org
== Declared Project Licenses
This program and the accompanying materials are made available under the terms
of the Eclipse Public License 2.0 which is available at
https://www.eclipse.org/legal/epl-2.0/.
== Source Code
https://github.com/openhab/openhab-addons

View File

@ -0,0 +1,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.

View File

@ -0,0 +1,17 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/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>

View File

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

View File

@ -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 + ".";
}
}

View File

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

View File

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

View File

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

View File

@ -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 + ".";
}
}

View File

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

View File

@ -0,0 +1,72 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.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);
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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