2 * Copyright (c) 2010-2023 Contributors to the openHAB project
4 * See the NOTICE file(s) distributed with this work for additional
7 * This program and the accompanying materials are made available under the
8 * terms of the Eclipse Public License 2.0 which is available at
9 * http://www.eclipse.org/legal/epl-2.0
11 * SPDX-License-Identifier: EPL-2.0
13 package org.openhab.binding.playstation.internal;
15 import static org.openhab.binding.playstation.internal.PlayStationBindingConstants.*;
17 import java.io.IOException;
18 import java.net.DatagramPacket;
19 import java.net.DatagramSocket;
20 import java.net.InetAddress;
21 import java.net.InetSocketAddress;
22 import java.net.UnknownHostException;
23 import java.nio.ByteBuffer;
24 import java.nio.ByteOrder;
25 import java.nio.channels.SocketChannel;
26 import java.nio.charset.StandardCharsets;
27 import java.util.ArrayList;
28 import java.util.Collections;
29 import java.util.List;
30 import java.util.Locale;
32 import java.util.concurrent.ScheduledFuture;
33 import java.util.concurrent.TimeUnit;
35 import org.eclipse.jdt.annotation.NonNullByDefault;
36 import org.eclipse.jdt.annotation.Nullable;
37 import org.openhab.binding.playstation.internal.discovery.PlayStationDiscovery;
38 import org.openhab.core.config.core.Configuration;
39 import org.openhab.core.i18n.LocaleProvider;
40 import org.openhab.core.library.types.OnOffType;
41 import org.openhab.core.library.types.RawType;
42 import org.openhab.core.library.types.StringType;
43 import org.openhab.core.net.NetworkAddressService;
44 import org.openhab.core.thing.ChannelUID;
45 import org.openhab.core.thing.Thing;
46 import org.openhab.core.thing.ThingStatus;
47 import org.openhab.core.thing.ThingStatusDetail;
48 import org.openhab.core.thing.binding.BaseThingHandler;
49 import org.openhab.core.types.Command;
50 import org.openhab.core.types.RefreshType;
51 import org.openhab.core.types.State;
52 import org.openhab.core.types.UnDefType;
53 import org.slf4j.Logger;
54 import org.slf4j.LoggerFactory;
57 * The {@link PS4Handler} is responsible for handling commands, which are
58 * sent to one of the channels.
60 * @author Fredrik Ahlström - Initial contribution
63 public class PS4Handler extends BaseThingHandler {
65 private final Logger logger = LoggerFactory.getLogger(PS4Handler.class);
66 private final PS4Crypto ps4Crypto = new PS4Crypto();
67 private static final int SOCKET_TIMEOUT_SECONDS = 5;
68 /** Time after connect that we can start to send key events, milli seconds */
69 private static final int POST_CONNECT_SENDKEY_DELAY_MS = 500;
70 /** Minimum delay between sendKey sends, milli seconds */
71 private static final int MIN_SENDKEY_DELAY_MS = 210;
72 /** Minimum delay after Key set, milli seconds */
73 private static final int MIN_HOLDKEY_DELAY_MS = 300;
75 private PS4Configuration config = new PS4Configuration();
77 private final @Nullable LocaleProvider localeProvider;
78 private final @Nullable NetworkAddressService networkAS;
79 private List<ScheduledFuture<?>> scheduledFutures = Collections.synchronizedList(new ArrayList<>());
80 private @Nullable ScheduledFuture<?> refreshTimer;
81 private @Nullable ScheduledFuture<?> timeoutTimer;
82 private @Nullable SocketChannelHandler socketChannelHandler;
83 private @Nullable InetAddress localAddress;
86 private String currentApplication = "";
87 private String currentApplicationId = "";
88 private OnOffType currentPower = OnOffType.OFF;
89 private State currentArtwork = UnDefType.UNDEF;
90 private int currentComPort = DEFAULT_COMMUNICATION_PORT;
92 boolean loggedIn = false;
93 boolean oskOpen = false;
95 public PS4Handler(Thing thing, LocaleProvider locProvider, NetworkAddressService network) {
97 localeProvider = locProvider;
102 public void handleConfigurationUpdate(Map<String, Object> configurationParameters) {
103 super.handleConfigurationUpdate(configurationParameters);
105 SocketChannelHandler scHandler = socketChannelHandler;
106 if (!config.pairingCode.isEmpty() && (scHandler == null || !loggedIn)) {
107 // Try to log in then remove pairing code as it's one use only.
108 scheduler.execute(() -> {
110 Configuration editedConfig = editConfiguration();
111 editedConfig.put(PAIRING_CODE, "");
112 updateConfiguration(editedConfig);
115 setupConnectionTimeout(config.connectionTimeout);
119 public void handleCommand(ChannelUID channelUID, Command command) {
120 if (command instanceof RefreshType) {
121 refreshFromState(channelUID);
123 if (command instanceof StringType) {
124 switch (channelUID.getId()) {
125 case CHANNEL_APPLICATION_ID:
126 if (!currentApplicationId.equals(((StringType) command).toString())) {
127 updateApplicationTitleid(((StringType) command).toString());
128 startApplication(currentApplicationId);
131 case CHANNEL_OSK_TEXT:
132 setOSKText(((StringType) command).toString());
134 case CHANNEL_SEND_KEY:
136 switch (((StringType) command).toString()) {
141 ps4Key = PS4_KEY_DOWN;
144 ps4Key = PS4_KEY_RIGHT;
147 ps4Key = PS4_KEY_LEFT;
150 ps4Key = PS4_KEY_ENTER;
153 ps4Key = PS4_KEY_BACK;
155 case SEND_KEY_OPTION:
156 ps4Key = PS4_KEY_OPTION;
165 sendRemoteKey(ps4Key);
171 } else if (command instanceof OnOffType) {
172 OnOffType onOff = (OnOffType) command;
173 switch (channelUID.getId()) {
175 if (currentPower != onOff) {
176 currentPower = onOff;
177 if (currentPower.equals(OnOffType.ON)) {
179 } else if (currentPower.equals(OnOffType.OFF)) {
184 case CHANNEL_CONNECT:
185 boolean connected = socketChannelHandler != null && socketChannelHandler.isChannelOpen();
186 if (connected && onOff.equals(OnOffType.OFF)) {
188 } else if (!connected && onOff.equals(OnOffType.ON)) {
189 scheduler.execute(() -> login());
200 public void initialize() {
201 config = getConfigAs(PS4Configuration.class);
204 updateStatus(ThingStatus.UNKNOWN);
209 public void dispose() {
211 ScheduledFuture<?> timer = refreshTimer;
216 timer = timeoutTimer;
221 scheduledFutures.forEach(f -> f.cancel(false));
222 scheduledFutures.clear();
226 * Tries to figure out a local IP that can communicate with the PS4.
228 private void figureOutLocalIP() {
229 if (!config.outboundIP.trim().isEmpty()) {
231 localAddress = InetAddress.getByName(config.outboundIP);
232 logger.debug("Outbound local IP.\"{}\"", localAddress);
234 } catch (UnknownHostException e) {
238 NetworkAddressService network = networkAS;
239 String adr = (network != null) ? network.getPrimaryIpv4HostAddress() : null;
242 localAddress = InetAddress.getByName(adr);
243 } catch (UnknownHostException e) {
244 // Ignore, just let the socket use whatever.
250 * Sets up a timer for querying the PS4 (using the scheduler) every 10 seconds.
252 private void setupRefreshTimer() {
253 final ScheduledFuture<?> timer = refreshTimer;
257 refreshTimer = scheduler.scheduleWithFixedDelay(this::updateAllChannels, 0, 10, TimeUnit.SECONDS);
261 * Sets up a timer for stopping the connection to the PS4 (using the scheduler) with the given time.
263 * @param waitTime The time in seconds before the connection is stopped.
265 private void setupConnectionTimeout(int waitTime) {
266 final ScheduledFuture<?> timer = timeoutTimer;
271 timeoutTimer = scheduler.schedule(this::stopConnection, waitTime, TimeUnit.SECONDS);
275 private void refreshFromState(ChannelUID channelUID) {
276 switch (channelUID.getId()) {
278 updateState(channelUID, currentPower);
280 case CHANNEL_APPLICATION_NAME:
281 updateState(channelUID, StringType.valueOf(currentApplication));
283 case CHANNEL_APPLICATION_ID:
284 updateState(channelUID, StringType.valueOf(currentApplicationId));
286 case CHANNEL_APPLICATION_IMAGE:
287 updateApplicationTitleid(currentApplicationId);
288 updateState(channelUID, currentArtwork);
290 case CHANNEL_OSK_TEXT:
291 case CHANNEL_2ND_SCREEN:
292 updateState(channelUID, UnDefType.UNDEF);
294 case CHANNEL_CONNECT:
295 boolean connected = socketChannelHandler != null && socketChannelHandler.isChannelOpen();
296 updateState(channelUID, OnOffType.from(connected));
298 case CHANNEL_SEND_KEY:
301 logger.warn("Channel refresh for {} not implemented!", channelUID.getId());
305 private void updateAllChannels() {
306 try (DatagramSocket socket = new DatagramSocket(0, localAddress)) {
307 socket.setBroadcast(false);
308 socket.setSoTimeout(SOCKET_TIMEOUT_SECONDS * 1000);
309 InetAddress inetAddress = InetAddress.getByName(config.ipAddress);
312 byte[] discover = PS4PacketHandler.makeSearchPacket();
313 DatagramPacket packet = new DatagramPacket(discover, discover.length, inetAddress, DEFAULT_BROADCAST_PORT);
317 byte[] rxbuf = new byte[256];
318 packet = new DatagramPacket(rxbuf, rxbuf.length);
319 socket.receive(packet);
320 parseSearchResponse(packet);
321 } catch (IOException e) {
322 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
326 private void stopConnection() {
327 SocketChannelHandler handler = socketChannelHandler;
328 if (handler != null && handler.isChannelOpen()) {
333 private void wakeUpPS4() {
334 logger.debug("Waking up PS4...");
335 try (DatagramSocket socket = new DatagramSocket(0, localAddress)) {
336 socket.setBroadcast(false);
337 InetAddress inetAddress = InetAddress.getByName(config.ipAddress);
339 byte[] wakeup = PS4PacketHandler.makeWakeupPacket(config.userCredential);
340 DatagramPacket packet = new DatagramPacket(wakeup, wakeup.length, inetAddress, DEFAULT_BROADCAST_PORT);
342 } catch (IOException e) {
343 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
347 private boolean openComs() {
348 try (DatagramSocket socket = new DatagramSocket(0, localAddress)) {
349 socket.setBroadcast(false);
350 InetAddress inetAddress = InetAddress.getByName(config.ipAddress);
352 byte[] launch = PS4PacketHandler.makeLaunchPacket(config.userCredential);
353 DatagramPacket packet = new DatagramPacket(launch, launch.length, inetAddress, DEFAULT_BROADCAST_PORT);
357 } catch (IOException e) {
358 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
359 } catch (InterruptedException e) {
365 private boolean setupConnection(SocketChannel channel) throws IOException {
366 logger.debug("TCP connecting");
368 channel.socket().setSoTimeout(2000);
369 channel.configureBlocking(true);
370 channel.connect(new InetSocketAddress(config.ipAddress, currentComPort));
372 ByteBuffer outPacket = PS4PacketHandler.makeHelloPacket();
373 sendPacketToPS4(outPacket, channel, false, false);
375 // Read hello response
376 final ByteBuffer readBuffer = ByteBuffer.allocate(512).order(ByteOrder.LITTLE_ENDIAN);
378 int responseLength = channel.read(readBuffer);
379 if (responseLength > 0) {
380 ps4Crypto.parseHelloResponsePacket(readBuffer);
385 outPacket = ps4Crypto.makeHandshakePacket();
386 sendPacketToPS4(outPacket, channel, false, false);
390 private class SocketChannelHandler extends Thread {
391 private SocketChannel socketChannel;
393 public SocketChannelHandler() throws IOException {
394 socketChannel = setupChannel();
400 public SocketChannel getChannel() {
401 if (!socketChannel.isOpen()) {
403 socketChannel = setupChannel();
404 } catch (IOException e) {
405 logger.debug("Couldn't open SocketChannel.{}", e.getMessage());
408 return socketChannel;
411 public boolean isChannelOpen() {
412 return socketChannel.isOpen();
415 private SocketChannel setupChannel() throws IOException {
416 SocketChannel channel = SocketChannel.open();
418 throw new IOException("Open coms failed");
420 if (!setupConnection(channel)) {
421 throw new IOException("Setup connection failed");
423 updateState(CHANNEL_CONNECT, OnOffType.ON);
429 SocketChannel channel = socketChannel;
430 final ByteBuffer readBuffer = ByteBuffer.allocate(512).order(ByteOrder.LITTLE_ENDIAN);
432 while (channel.read(readBuffer) > 0) {
433 ByteBuffer messBuffer = ps4Crypto.decryptPacket(readBuffer);
434 readBuffer.position(0);
435 PS4Command lastCommand = parseResponsePacket(messBuffer);
437 if (lastCommand == PS4Command.SERVER_STATUS_RSP) {
438 if (oskOpen && isLinked(CHANNEL_OSK_TEXT)) {
445 } catch (IOException e) {
446 logger.debug("Connection read exception: {}", e.getMessage());
450 } catch (IOException e) {
451 logger.debug("Connection close exception: {}", e.getMessage());
454 updateState(CHANNEL_CONNECT, OnOffType.OFF);
455 logger.debug("SocketHandler done.");
456 ps4Crypto.clearCiphers();
461 private @Nullable PS4Command parseResponsePacket(ByteBuffer rBuffer) {
463 final int buffSize = rBuffer.remaining();
464 final int size = rBuffer.getInt();
465 if (size > buffSize || size < 12) {
466 logger.debug("Response size ({}) not good, buffer size ({}).", size, buffSize);
469 int cmdValue = rBuffer.getInt();
470 int statValue = rBuffer.getInt();
471 PS4ErrorStatus status = PS4ErrorStatus.valueOfTag(statValue);
472 PS4Command command = PS4Command.valueOfTag(cmdValue);
473 byte[] respBuff = new byte[size];
475 rBuffer.get(respBuff);
476 if (command != null) {
477 if (status == null) {
478 logger.debug("Resp; size:{}, command:{}, statValue:{}, data:{}.", size, command, statValue, respBuff);
480 logger.debug("Resp; size:{}, command:{}, status:{}, data:{}.", size, command, status, respBuff);
484 if (status == null) {
485 logger.debug("Unhandled Login status value: {}", statValue);
488 // Read login response
491 updateStatus(ThingStatus.ONLINE, ThingStatusDetail.NONE, status.message);
493 if (isLinked(CHANNEL_2ND_SCREEN)) {
494 scheduler.execute(() -> {
495 ByteBuffer outPacket = PS4PacketHandler
496 .makeClientIDPacket("com.playstation.mobile2ndscreen", "18.9.3");
497 sendPacketEncrypted(outPacket, false);
501 case STATUS_NOT_PAIRED:
502 updateStatus(ThingStatus.ONLINE, ThingStatusDetail.CONFIGURATION_PENDING, status.message);
505 case STATUS_MISSING_PAIRING_CODE:
506 case STATUS_MISSING_PASS_CODE:
507 case STATUS_WRONG_PAIRING_CODE:
508 case STATUS_WRONG_PASS_CODE:
509 case STATUS_WRONG_USER_CREDENTIAL:
510 updateStatus(ThingStatus.ONLINE, ThingStatusDetail.CONFIGURATION_ERROR, status.message);
512 logger.debug("Not logged in: {}", status.message);
514 case STATUS_CAN_NOT_PLAY_NOW:
515 case STATUS_CLOSE_OTHER_APP:
516 case STATUS_COMMAND_NOT_GOOD:
517 case STATUS_COULD_NOT_LOG_IN:
518 case STATUS_DO_LOGIN:
519 case STATUS_MAX_USERS:
520 case STATUS_REGISTER_DEVICE_OVER:
521 case STATUS_RESTART_APP:
522 case STATUS_SOMEONE_ELSE_USING:
523 case STATUS_UPDATE_APP:
524 case STATUS_UPDATE_PS4:
525 updateStatus(ThingStatus.ONLINE, ThingStatusDetail.NONE, status.message);
527 logger.debug("Not logged in: {}", status.message);
530 logger.debug("Unhandled Login response status:{}, message:{}", status, status.message);
535 if (status != null && status != PS4ErrorStatus.STATUS_OK) {
536 logger.debug("App start response: {}", status.message);
540 if (status != null && status != PS4ErrorStatus.STATUS_OK) {
541 logger.debug("Standby response: {}", status.message);
544 case SERVER_STATUS_RSP:
545 if ((statValue & 4) != 0) {
549 updateState(CHANNEL_OSK_TEXT, StringType.valueOf(""));
553 logger.debug("Server status value:{}", statValue);
555 case HTTPD_STATUS_RSP:
556 String httpdStat = PS4PacketHandler.parseHTTPdPacket(rBuffer);
557 logger.debug("HTTPd Response; {}", httpdStat);
558 String secondScrStr = "";
559 int httpStatus = rBuffer.getInt(8);
560 int port = rBuffer.getInt(12);
561 if (httpStatus != 0 && port != 0) {
562 secondScrStr = "http://" + config.ipAddress + ":" + port;
564 updateState(CHANNEL_2ND_SCREEN, StringType.valueOf(secondScrStr));
566 case OSK_CHANGE_STRING_REQ:
567 String oskText = PS4PacketHandler.parseOSKStringChangePacket(rBuffer);
568 updateState(CHANNEL_OSK_TEXT, StringType.valueOf(oskText));
571 case OSK_CONTROL_REQ:
572 case COMMENT_VIEWER_START_RESULT:
573 case SCREEN_SHOT_RSP:
578 logger.debug("Unknown response, command:{}. Missing case.", command);
582 logger.debug("Unknown resp-cmd, size:{}, command:{}, status:{}, data:{}.", size, cmdValue, statValue,
588 private SocketChannel getConnection() throws IOException {
589 return getConnection(true);
592 private SocketChannel getConnection(boolean requiresLogin) throws IOException {
593 SocketChannel channel = null;
594 SocketChannelHandler handler = socketChannelHandler;
595 if (handler == null || !handler.isChannelOpen()) {
597 handler = new SocketChannelHandler();
598 socketChannelHandler = handler;
599 } catch (IOException e) {
600 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
604 channel = handler.getChannel();
605 if (!loggedIn && requiresLogin) {
611 private void sendPacketToPS4(ByteBuffer packet, SocketChannel channel, boolean encrypted, boolean restartTimeout) {
612 PS4Command cmd = PS4Command.valueOfTag(packet.getInt(4));
613 logger.debug("Sending {} packet.", cmd);
616 ByteBuffer outPacket = ps4Crypto.encryptPacket(packet);
617 channel.write(outPacket);
619 channel.write(packet);
621 if (restartTimeout) {
622 setupConnectionTimeout(config.connectionTimeout);
624 } catch (IOException e) {
625 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
629 private void sendPacketEncrypted(ByteBuffer packet, SocketChannel channel) {
630 sendPacketToPS4(packet, channel, true, true);
633 private void sendPacketEncrypted(ByteBuffer packet) {
634 sendPacketEncrypted(packet, true);
637 private void sendPacketEncrypted(ByteBuffer packet, boolean requiresLogin) {
639 SocketChannel channel = getConnection(requiresLogin);
640 if (requiresLogin && !loggedIn) {
641 ScheduledFuture<?> future = scheduler.schedule(
642 () -> sendPacketToPS4(packet, channel, true, requiresLogin), 250, TimeUnit.MILLISECONDS);
643 scheduledFutures.add(future);
644 scheduledFutures.removeIf(ScheduledFuture::isDone);
646 sendPacketToPS4(packet, channel, true, requiresLogin);
648 } catch (IOException e) {
649 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
654 * This is used as a heart beat to let the PS4 know that we are still listening.
656 private void sendStatus() {
657 ByteBuffer outPacket = PS4PacketHandler.makeStatusPacket(0);
658 sendPacketEncrypted(outPacket, false);
661 private void login(SocketChannel channel) {
662 // Send login request
663 ByteBuffer outPacket = PS4PacketHandler.makeLoginPacket(config.userCredential, config.passCode,
665 sendPacketEncrypted(outPacket, channel);
668 private void login() {
670 SocketChannel channel = getConnection(false);
672 } catch (IOException e) {
673 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
678 * This closes the connection with the PS4.
680 private void sendByeBye() {
681 ByteBuffer outPacket = PS4PacketHandler.makeByebyePacket();
682 sendPacketEncrypted(outPacket, false);
685 private void turnOnPS4() {
687 ScheduledFuture<?> future = scheduler.schedule(this::waitAndConnectToPS4, 17, TimeUnit.SECONDS);
688 scheduledFutures.add(future);
689 scheduledFutures.removeIf(ScheduledFuture::isDone);
692 private void waitAndConnectToPS4() {
695 } catch (IOException e) {
696 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
700 private void sendStandby() {
701 ByteBuffer outPacket = PS4PacketHandler.makeStandbyPacket();
702 sendPacketEncrypted(outPacket);
706 * Ask PS4 if the OSK is open so we can get and set text.
708 private void sendOSKStart() {
709 ByteBuffer outPacket = PS4PacketHandler.makeOSKStartPacket();
710 sendPacketEncrypted(outPacket);
714 * Sets the entire OSK string on the PS4.
716 * @param text The text to set in the OSK.
718 private void setOSKText(String text) {
719 logger.debug("Sending osk text packet,\"{}\"", text);
720 ByteBuffer outPacket = PS4PacketHandler.makeOSKStringChangePacket(text);
721 sendPacketEncrypted(outPacket);
725 * Tries to start an application on the PS4.
727 * @param applicationId The unique id for the application (CUSAxxxxx).
729 private void startApplication(String applicationId) {
730 ByteBuffer outPacket = PS4PacketHandler.makeApplicationPacket(applicationId);
731 sendPacketEncrypted(outPacket);
734 private void sendRemoteKey(int pushedKey) {
736 SocketChannelHandler scHandler = socketChannelHandler;
737 int preWait = (scHandler == null || !loggedIn) ? POST_CONNECT_SENDKEY_DELAY_MS : 0;
738 SocketChannel channel = getConnection();
740 ScheduledFuture<?> future = scheduler.schedule(() -> {
741 ByteBuffer outPacket = PS4PacketHandler.makeRemoteControlPacket(PS4_KEY_OPEN_RC);
742 sendPacketEncrypted(outPacket, channel);
743 }, preWait, TimeUnit.MILLISECONDS);
744 scheduledFutures.add(future);
746 future = scheduler.schedule(() -> {
748 ByteBuffer keyPacket = PS4PacketHandler.makeRemoteControlPacket(pushedKey);
749 sendPacketEncrypted(keyPacket, channel);
750 }, preWait + MIN_SENDKEY_DELAY_MS, TimeUnit.MILLISECONDS);
751 scheduledFutures.add(future);
753 future = scheduler.schedule(() -> {
754 ByteBuffer offPacket = PS4PacketHandler.makeRemoteControlPacket(PS4_KEY_OFF);
755 sendPacketEncrypted(offPacket, channel);
756 }, preWait + MIN_SENDKEY_DELAY_MS + MIN_HOLDKEY_DELAY_MS, TimeUnit.MILLISECONDS);
757 scheduledFutures.add(future);
759 future = scheduler.schedule(() -> {
760 ByteBuffer closePacket = PS4PacketHandler.makeRemoteControlPacket(PS4_KEY_CLOSE_RC);
761 sendPacketEncrypted(closePacket, channel);
762 }, preWait + MIN_SENDKEY_DELAY_MS * 2 + MIN_HOLDKEY_DELAY_MS, TimeUnit.MILLISECONDS);
763 scheduledFutures.add(future);
765 } catch (IOException e) {
766 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
768 scheduledFutures.removeIf(ScheduledFuture::isDone);
771 private void parseSearchResponse(DatagramPacket packet) {
772 byte[] data = packet.getData();
773 String message = new String(data, StandardCharsets.UTF_8);
774 String applicationName = "";
775 String applicationId = "";
777 String[] rowStrings = message.trim().split("\\r?\\n");
778 for (String row : rowStrings) {
779 int index = row.indexOf(':');
781 OnOffType power = null;
782 if (row.contains("200")) {
783 power = OnOffType.ON;
784 } else if (row.contains("620")) {
785 power = OnOffType.OFF;
788 updateState(CHANNEL_POWER, power);
789 if (!currentPower.equals(power)) {
790 currentPower = power;
791 if (power.equals(OnOffType.ON) && config.autoConnect) {
792 SocketChannelHandler scHandler = socketChannelHandler;
793 if (scHandler == null || !loggedIn) {
794 logger.debug("Trying to login after power on.");
795 ScheduledFuture<?> future = scheduler.schedule(() -> login(), 20, TimeUnit.SECONDS);
796 scheduledFutures.add(future);
797 scheduledFutures.removeIf(ScheduledFuture::isDone);
801 updateStatus(ThingStatus.ONLINE);
803 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
804 "Could not determine power status.");
808 String key = row.substring(0, index);
809 String value = row.substring(index + 1);
811 case RESPONSE_RUNNING_APP_NAME:
812 applicationName = value;
814 case RESPONSE_RUNNING_APP_TITLEID:
815 applicationId = value;
817 case RESPONSE_HOST_REQUEST_PORT:
818 int port = Integer.parseInt(value);
819 if (currentComPort != port) {
820 currentComPort = port;
821 logger.debug("Host request port: {}", port);
824 case RESPONSE_SYSTEM_VERSION:
825 updateProperty(Thing.PROPERTY_FIRMWARE_VERSION, PlayStationDiscovery.formatPS4Version(value));
832 if (!currentApplication.equals(applicationName)) {
833 currentApplication = applicationName;
834 updateState(CHANNEL_APPLICATION_NAME, StringType.valueOf(applicationName));
835 logger.debug("Current application: {}", applicationName);
837 if (!currentApplicationId.equals(applicationId)) {
838 updateApplicationTitleid(applicationId);
843 * Sets the cached TitleId and tries to download artwork
844 * for application if CHANNEL_APPLICATION_IMAGE is linked.
846 * @param titleId Id of application.
848 private void updateApplicationTitleid(String titleId) {
849 currentApplicationId = titleId;
850 updateState(CHANNEL_APPLICATION_ID, StringType.valueOf(titleId));
851 logger.debug("Current application title id: {}", titleId);
852 if (!isLinked(CHANNEL_APPLICATION_IMAGE)) {
855 LocaleProvider lProvider = localeProvider;
856 Locale locale = (lProvider != null) ? lProvider.getLocale() : Locale.US;
858 RawType artWork = PS4ArtworkHandler.fetchArtworkForTitleid(titleId, config.artworkSize, locale);
859 if (artWork != null) {
860 currentArtwork = artWork;
861 updateState(CHANNEL_APPLICATION_IMAGE, artWork);
862 } else if (!titleId.isEmpty()) {
863 logger.debug("Couldn't fetch artwork for title id: {}", titleId);