[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
24 changed files with 3175 additions and 0 deletions

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>