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