]> git.basschouten.com Git - openhab-addons.git/blob
1aa3e5eac461628629794aebe3610f798d8d51e9
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2023 Contributors to the openHAB project
3  *
4  * See the NOTICE file(s) distributed with this work for additional
5  * information.
6  *
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
10  *
11  * SPDX-License-Identifier: EPL-2.0
12  */
13 package org.openhab.binding.playstation.internal;
14
15 import static org.openhab.binding.playstation.internal.PlayStationBindingConstants.*;
16
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;
31 import java.util.Map;
32 import java.util.concurrent.ScheduledFuture;
33 import java.util.concurrent.TimeUnit;
34
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;
55
56 /**
57  * The {@link PS4Handler} is responsible for handling commands, which are
58  * sent to one of the channels.
59  *
60  * @author Fredrik Ahlström - Initial contribution
61  */
62 @NonNullByDefault
63 public class PS4Handler extends BaseThingHandler {
64
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;
74
75     private PS4Configuration config = new PS4Configuration();
76
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;
84
85     // State of PS4
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;
91
92     boolean loggedIn = false;
93     boolean oskOpen = false;
94
95     public PS4Handler(Thing thing, LocaleProvider locProvider, NetworkAddressService network) {
96         super(thing);
97         localeProvider = locProvider;
98         networkAS = network;
99     }
100
101     @Override
102     public void handleConfigurationUpdate(Map<String, Object> configurationParameters) {
103         super.handleConfigurationUpdate(configurationParameters);
104         figureOutLocalIP();
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(() -> {
109                 login();
110                 Configuration editedConfig = editConfiguration();
111                 editedConfig.put(PAIRING_CODE, "");
112                 updateConfiguration(editedConfig);
113             });
114         }
115         setupConnectionTimeout(config.connectionTimeout);
116     }
117
118     @Override
119     public void handleCommand(ChannelUID channelUID, Command command) {
120         if (command instanceof RefreshType) {
121             refreshFromState(channelUID);
122         } else {
123             if (command instanceof StringType stringCommand) {
124                 switch (channelUID.getId()) {
125                     case CHANNEL_APPLICATION_ID:
126                         if (!currentApplicationId.equals(stringCommand.toString())) {
127                             updateApplicationTitleid(stringCommand.toString());
128                             startApplication(currentApplicationId);
129                         }
130                         break;
131                     case CHANNEL_OSK_TEXT:
132                         setOSKText(stringCommand.toString());
133                         break;
134                     case CHANNEL_SEND_KEY:
135                         int ps4Key = 0;
136                         switch (stringCommand.toString()) {
137                             case SEND_KEY_UP:
138                                 ps4Key = PS4_KEY_UP;
139                                 break;
140                             case SEND_KEY_DOWN:
141                                 ps4Key = PS4_KEY_DOWN;
142                                 break;
143                             case SEND_KEY_RIGHT:
144                                 ps4Key = PS4_KEY_RIGHT;
145                                 break;
146                             case SEND_KEY_LEFT:
147                                 ps4Key = PS4_KEY_LEFT;
148                                 break;
149                             case SEND_KEY_ENTER:
150                                 ps4Key = PS4_KEY_ENTER;
151                                 break;
152                             case SEND_KEY_BACK:
153                                 ps4Key = PS4_KEY_BACK;
154                                 break;
155                             case SEND_KEY_OPTION:
156                                 ps4Key = PS4_KEY_OPTION;
157                                 break;
158                             case SEND_KEY_PS:
159                                 ps4Key = PS4_KEY_PS;
160                                 break;
161                             default:
162                                 break;
163                         }
164                         if (ps4Key != 0) {
165                             sendRemoteKey(ps4Key);
166                         }
167                         break;
168                     default:
169                         break;
170                 }
171             } else if (command instanceof OnOffType onOffCommand) {
172                 switch (channelUID.getId()) {
173                     case CHANNEL_POWER:
174                         if (currentPower != onOffCommand) {
175                             currentPower = onOffCommand;
176                             if (currentPower.equals(OnOffType.ON)) {
177                                 turnOnPS4();
178                             } else if (currentPower.equals(OnOffType.OFF)) {
179                                 sendStandby();
180                             }
181                         }
182                         break;
183                     case CHANNEL_CONNECT:
184                         boolean connected = socketChannelHandler != null && socketChannelHandler.isChannelOpen();
185                         if (connected && onOffCommand.equals(OnOffType.OFF)) {
186                             sendByeBye();
187                         } else if (!connected && onOffCommand.equals(OnOffType.ON)) {
188                             scheduler.execute(() -> login());
189                         }
190                         break;
191                     default:
192                         break;
193                 }
194             }
195         }
196     }
197
198     @Override
199     public void initialize() {
200         config = getConfigAs(PS4Configuration.class);
201
202         figureOutLocalIP();
203         updateStatus(ThingStatus.UNKNOWN);
204         setupRefreshTimer();
205     }
206
207     @Override
208     public void dispose() {
209         stopConnection();
210         ScheduledFuture<?> timer = refreshTimer;
211         if (timer != null) {
212             timer.cancel(false);
213             refreshTimer = null;
214         }
215         timer = timeoutTimer;
216         if (timer != null) {
217             timer.cancel(false);
218             timeoutTimer = null;
219         }
220         scheduledFutures.forEach(f -> f.cancel(false));
221         scheduledFutures.clear();
222     }
223
224     /**
225      * Tries to figure out a local IP that can communicate with the PS4.
226      */
227     private void figureOutLocalIP() {
228         if (!config.outboundIP.trim().isEmpty()) {
229             try {
230                 localAddress = InetAddress.getByName(config.outboundIP);
231                 logger.debug("Outbound local IP.\"{}\"", localAddress);
232                 return;
233             } catch (UnknownHostException e) {
234                 // This is expected
235             }
236         }
237         NetworkAddressService network = networkAS;
238         String adr = (network != null) ? network.getPrimaryIpv4HostAddress() : null;
239         if (adr != null) {
240             try {
241                 localAddress = InetAddress.getByName(adr);
242             } catch (UnknownHostException e) {
243                 // Ignore, just let the socket use whatever.
244             }
245         }
246     }
247
248     /**
249      * Sets up a timer for querying the PS4 (using the scheduler) every 10 seconds.
250      */
251     private void setupRefreshTimer() {
252         final ScheduledFuture<?> timer = refreshTimer;
253         if (timer != null) {
254             timer.cancel(false);
255         }
256         refreshTimer = scheduler.scheduleWithFixedDelay(this::updateAllChannels, 0, 10, TimeUnit.SECONDS);
257     }
258
259     /**
260      * Sets up a timer for stopping the connection to the PS4 (using the scheduler) with the given time.
261      *
262      * @param waitTime The time in seconds before the connection is stopped.
263      */
264     private void setupConnectionTimeout(int waitTime) {
265         final ScheduledFuture<?> timer = timeoutTimer;
266         if (timer != null) {
267             timer.cancel(false);
268         }
269         if (waitTime > 0) {
270             timeoutTimer = scheduler.schedule(this::stopConnection, waitTime, TimeUnit.SECONDS);
271         }
272     }
273
274     private void refreshFromState(ChannelUID channelUID) {
275         switch (channelUID.getId()) {
276             case CHANNEL_POWER:
277                 updateState(channelUID, currentPower);
278                 break;
279             case CHANNEL_APPLICATION_NAME:
280                 updateState(channelUID, StringType.valueOf(currentApplication));
281                 break;
282             case CHANNEL_APPLICATION_ID:
283                 updateState(channelUID, StringType.valueOf(currentApplicationId));
284                 break;
285             case CHANNEL_APPLICATION_IMAGE:
286                 updateApplicationTitleid(currentApplicationId);
287                 updateState(channelUID, currentArtwork);
288                 break;
289             case CHANNEL_OSK_TEXT:
290             case CHANNEL_2ND_SCREEN:
291                 updateState(channelUID, UnDefType.UNDEF);
292                 break;
293             case CHANNEL_CONNECT:
294                 boolean connected = socketChannelHandler != null && socketChannelHandler.isChannelOpen();
295                 updateState(channelUID, OnOffType.from(connected));
296                 break;
297             case CHANNEL_SEND_KEY:
298                 break;
299             default:
300                 logger.warn("Channel refresh for {} not implemented!", channelUID.getId());
301         }
302     }
303
304     private void updateAllChannels() {
305         try (DatagramSocket socket = new DatagramSocket(0, localAddress)) {
306             socket.setBroadcast(false);
307             socket.setSoTimeout(SOCKET_TIMEOUT_SECONDS * 1000);
308             InetAddress inetAddress = InetAddress.getByName(config.ipAddress);
309
310             // send discover
311             byte[] discover = PS4PacketHandler.makeSearchPacket();
312             DatagramPacket packet = new DatagramPacket(discover, discover.length, inetAddress, DEFAULT_BROADCAST_PORT);
313             socket.send(packet);
314
315             // wait for response
316             byte[] rxbuf = new byte[256];
317             packet = new DatagramPacket(rxbuf, rxbuf.length);
318             socket.receive(packet);
319             parseSearchResponse(packet);
320         } catch (IOException e) {
321             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
322         }
323     }
324
325     private void stopConnection() {
326         SocketChannelHandler handler = socketChannelHandler;
327         if (handler != null && handler.isChannelOpen()) {
328             sendByeBye();
329         }
330     }
331
332     private void wakeUpPS4() {
333         logger.debug("Waking up PS4...");
334         try (DatagramSocket socket = new DatagramSocket(0, localAddress)) {
335             socket.setBroadcast(false);
336             InetAddress inetAddress = InetAddress.getByName(config.ipAddress);
337             // send wake-up
338             byte[] wakeup = PS4PacketHandler.makeWakeupPacket(config.userCredential);
339             DatagramPacket packet = new DatagramPacket(wakeup, wakeup.length, inetAddress, DEFAULT_BROADCAST_PORT);
340             socket.send(packet);
341         } catch (IOException e) {
342             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
343         }
344     }
345
346     private boolean openComs() {
347         try (DatagramSocket socket = new DatagramSocket(0, localAddress)) {
348             socket.setBroadcast(false);
349             InetAddress inetAddress = InetAddress.getByName(config.ipAddress);
350             // send launch
351             byte[] launch = PS4PacketHandler.makeLaunchPacket(config.userCredential);
352             DatagramPacket packet = new DatagramPacket(launch, launch.length, inetAddress, DEFAULT_BROADCAST_PORT);
353             socket.send(packet);
354             Thread.sleep(100);
355             return true;
356         } catch (IOException e) {
357             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
358         } catch (InterruptedException e) {
359             return true;
360         }
361         return false;
362     }
363
364     private boolean setupConnection(SocketChannel channel) throws IOException {
365         logger.debug("TCP connecting");
366
367         channel.socket().setSoTimeout(2000);
368         channel.configureBlocking(true);
369         channel.connect(new InetSocketAddress(config.ipAddress, currentComPort));
370
371         ByteBuffer outPacket = PS4PacketHandler.makeHelloPacket();
372         sendPacketToPS4(outPacket, channel, false, false);
373
374         // Read hello response
375         final ByteBuffer readBuffer = ByteBuffer.allocate(512).order(ByteOrder.LITTLE_ENDIAN);
376
377         int responseLength = channel.read(readBuffer);
378         if (responseLength > 0) {
379             ps4Crypto.parseHelloResponsePacket(readBuffer);
380         } else {
381             return false;
382         }
383
384         outPacket = ps4Crypto.makeHandshakePacket();
385         sendPacketToPS4(outPacket, channel, false, false);
386         return true;
387     }
388
389     private class SocketChannelHandler extends Thread {
390         private SocketChannel socketChannel;
391
392         public SocketChannelHandler() throws IOException {
393             socketChannel = setupChannel();
394             loggedIn = false;
395             oskOpen = false;
396             start();
397         }
398
399         public SocketChannel getChannel() {
400             if (!socketChannel.isOpen()) {
401                 try {
402                     socketChannel = setupChannel();
403                 } catch (IOException e) {
404                     logger.debug("Couldn't open SocketChannel.{}", e.getMessage());
405                 }
406             }
407             return socketChannel;
408         }
409
410         public boolean isChannelOpen() {
411             return socketChannel.isOpen();
412         }
413
414         private SocketChannel setupChannel() throws IOException {
415             SocketChannel channel = SocketChannel.open();
416             if (!openComs()) {
417                 throw new IOException("Open coms failed");
418             }
419             if (!setupConnection(channel)) {
420                 throw new IOException("Setup connection failed");
421             }
422             updateState(CHANNEL_CONNECT, OnOffType.ON);
423             return channel;
424         }
425
426         @Override
427         public void run() {
428             SocketChannel channel = socketChannel;
429             final ByteBuffer readBuffer = ByteBuffer.allocate(512).order(ByteOrder.LITTLE_ENDIAN);
430             try {
431                 while (channel.read(readBuffer) > 0) {
432                     ByteBuffer messBuffer = ps4Crypto.decryptPacket(readBuffer);
433                     readBuffer.position(0);
434                     PS4Command lastCommand = parseResponsePacket(messBuffer);
435
436                     if (lastCommand == PS4Command.SERVER_STATUS_RSP) {
437                         if (oskOpen && isLinked(CHANNEL_OSK_TEXT)) {
438                             sendOSKStart();
439                         } else {
440                             sendStatus();
441                         }
442                     }
443                 }
444             } catch (IOException e) {
445                 logger.debug("Connection read exception: {}", e.getMessage());
446             } finally {
447                 try {
448                     channel.close();
449                 } catch (IOException e) {
450                     logger.debug("Connection close exception: {}", e.getMessage());
451                 }
452             }
453             updateState(CHANNEL_CONNECT, OnOffType.OFF);
454             logger.debug("SocketHandler done.");
455             ps4Crypto.clearCiphers();
456             loggedIn = false;
457         }
458     }
459
460     private @Nullable PS4Command parseResponsePacket(ByteBuffer rBuffer) {
461         rBuffer.rewind();
462         final int buffSize = rBuffer.remaining();
463         final int size = rBuffer.getInt();
464         if (size > buffSize || size < 12) {
465             logger.debug("Response size ({}) not good, buffer size ({}).", size, buffSize);
466             return null;
467         }
468         int cmdValue = rBuffer.getInt();
469         int statValue = rBuffer.getInt();
470         PS4ErrorStatus status = PS4ErrorStatus.valueOfTag(statValue);
471         PS4Command command = PS4Command.valueOfTag(cmdValue);
472         byte[] respBuff = new byte[size];
473         rBuffer.rewind();
474         rBuffer.get(respBuff);
475         if (command != null) {
476             if (status == null) {
477                 logger.debug("Resp; size:{}, command:{}, statValue:{}, data:{}.", size, command, statValue, respBuff);
478             } else {
479                 logger.debug("Resp; size:{}, command:{}, status:{}, data:{}.", size, command, status, respBuff);
480             }
481             switch (command) {
482                 case LOGIN_RSP:
483                     if (status == null) {
484                         logger.debug("Unhandled Login status value: {}", statValue);
485                         return command;
486                     }
487                     // Read login response
488                     switch (status) {
489                         case STATUS_OK:
490                             updateStatus(ThingStatus.ONLINE, ThingStatusDetail.NONE, status.message);
491                             loggedIn = true;
492                             if (isLinked(CHANNEL_2ND_SCREEN)) {
493                                 scheduler.execute(() -> {
494                                     ByteBuffer outPacket = PS4PacketHandler
495                                             .makeClientIDPacket("com.playstation.mobile2ndscreen", "18.9.3");
496                                     sendPacketEncrypted(outPacket, false);
497                                 });
498                             }
499                             break;
500                         case STATUS_NOT_PAIRED:
501                             updateStatus(ThingStatus.ONLINE, ThingStatusDetail.CONFIGURATION_PENDING, status.message);
502                             loggedIn = false;
503                             break;
504                         case STATUS_MISSING_PAIRING_CODE:
505                         case STATUS_MISSING_PASS_CODE:
506                         case STATUS_WRONG_PAIRING_CODE:
507                         case STATUS_WRONG_PASS_CODE:
508                         case STATUS_WRONG_USER_CREDENTIAL:
509                             updateStatus(ThingStatus.ONLINE, ThingStatusDetail.CONFIGURATION_ERROR, status.message);
510                             loggedIn = false;
511                             logger.debug("Not logged in: {}", status.message);
512                             break;
513                         case STATUS_CAN_NOT_PLAY_NOW:
514                         case STATUS_CLOSE_OTHER_APP:
515                         case STATUS_COMMAND_NOT_GOOD:
516                         case STATUS_COULD_NOT_LOG_IN:
517                         case STATUS_DO_LOGIN:
518                         case STATUS_MAX_USERS:
519                         case STATUS_REGISTER_DEVICE_OVER:
520                         case STATUS_RESTART_APP:
521                         case STATUS_SOMEONE_ELSE_USING:
522                         case STATUS_UPDATE_APP:
523                         case STATUS_UPDATE_PS4:
524                             updateStatus(ThingStatus.ONLINE, ThingStatusDetail.NONE, status.message);
525                             loggedIn = false;
526                             logger.debug("Not logged in: {}", status.message);
527                             break;
528                         default:
529                             logger.debug("Unhandled Login response status:{}, message:{}", status, status.message);
530                             break;
531                     }
532                     break;
533                 case APP_START_RSP:
534                     if (status != null && status != PS4ErrorStatus.STATUS_OK) {
535                         logger.debug("App start response: {}", status.message);
536                     }
537                     break;
538                 case STANDBY_RSP:
539                     if (status != null && status != PS4ErrorStatus.STATUS_OK) {
540                         logger.debug("Standby response: {}", status.message);
541                     }
542                     break;
543                 case SERVER_STATUS_RSP:
544                     if ((statValue & 4) != 0) {
545                         oskOpen = true;
546                     } else {
547                         if (oskOpen) {
548                             updateState(CHANNEL_OSK_TEXT, StringType.valueOf(""));
549                         }
550                         oskOpen = false;
551                     }
552                     logger.debug("Server status value:{}", statValue);
553                     break;
554                 case HTTPD_STATUS_RSP:
555                     String httpdStat = PS4PacketHandler.parseHTTPdPacket(rBuffer);
556                     logger.debug("HTTPd Response; {}", httpdStat);
557                     String secondScrStr = "";
558                     int httpStatus = rBuffer.getInt(8);
559                     int port = rBuffer.getInt(12);
560                     if (httpStatus != 0 && port != 0) {
561                         secondScrStr = "http://" + config.ipAddress + ":" + port;
562                     }
563                     updateState(CHANNEL_2ND_SCREEN, StringType.valueOf(secondScrStr));
564                     break;
565                 case OSK_CHANGE_STRING_REQ:
566                     String oskText = PS4PacketHandler.parseOSKStringChangePacket(rBuffer);
567                     updateState(CHANNEL_OSK_TEXT, StringType.valueOf(oskText));
568                     break;
569                 case OSK_START_RSP:
570                 case OSK_CONTROL_REQ:
571                 case COMMENT_VIEWER_START_RESULT:
572                 case SCREEN_SHOT_RSP:
573                 case APP_START2_RSP:
574                 case LOGOUT_RSP:
575                     break;
576                 default:
577                     logger.debug("Unknown response, command:{}. Missing case.", command);
578                     break;
579             }
580         } else {
581             logger.debug("Unknown resp-cmd, size:{}, command:{}, status:{}, data:{}.", size, cmdValue, statValue,
582                     respBuff);
583         }
584         return command;
585     }
586
587     private SocketChannel getConnection() throws IOException {
588         return getConnection(true);
589     }
590
591     private SocketChannel getConnection(boolean requiresLogin) throws IOException {
592         SocketChannel channel = null;
593         SocketChannelHandler handler = socketChannelHandler;
594         if (handler == null || !handler.isChannelOpen()) {
595             try {
596                 handler = new SocketChannelHandler();
597                 socketChannelHandler = handler;
598             } catch (IOException e) {
599                 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
600                 throw e;
601             }
602         }
603         channel = handler.getChannel();
604         if (!loggedIn && requiresLogin) {
605             login(channel);
606         }
607         return channel;
608     }
609
610     private void sendPacketToPS4(ByteBuffer packet, SocketChannel channel, boolean encrypted, boolean restartTimeout) {
611         PS4Command cmd = PS4Command.valueOfTag(packet.getInt(4));
612         logger.debug("Sending {} packet.", cmd);
613         try {
614             if (encrypted) {
615                 ByteBuffer outPacket = ps4Crypto.encryptPacket(packet);
616                 channel.write(outPacket);
617             } else {
618                 channel.write(packet);
619             }
620             if (restartTimeout) {
621                 setupConnectionTimeout(config.connectionTimeout);
622             }
623         } catch (IOException e) {
624             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
625         }
626     }
627
628     private void sendPacketEncrypted(ByteBuffer packet, SocketChannel channel) {
629         sendPacketToPS4(packet, channel, true, true);
630     }
631
632     private void sendPacketEncrypted(ByteBuffer packet) {
633         sendPacketEncrypted(packet, true);
634     }
635
636     private void sendPacketEncrypted(ByteBuffer packet, boolean requiresLogin) {
637         try {
638             SocketChannel channel = getConnection(requiresLogin);
639             if (requiresLogin && !loggedIn) {
640                 ScheduledFuture<?> future = scheduler.schedule(
641                         () -> sendPacketToPS4(packet, channel, true, requiresLogin), 250, TimeUnit.MILLISECONDS);
642                 scheduledFutures.add(future);
643                 scheduledFutures.removeIf(ScheduledFuture::isDone);
644             } else {
645                 sendPacketToPS4(packet, channel, true, requiresLogin);
646             }
647         } catch (IOException e) {
648             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
649         }
650     }
651
652     /**
653      * This is used as a heart beat to let the PS4 know that we are still listening.
654      */
655     private void sendStatus() {
656         ByteBuffer outPacket = PS4PacketHandler.makeStatusPacket(0);
657         sendPacketEncrypted(outPacket, false);
658     }
659
660     private void login(SocketChannel channel) {
661         // Send login request
662         ByteBuffer outPacket = PS4PacketHandler.makeLoginPacket(config.userCredential, config.passCode,
663                 config.pairingCode);
664         sendPacketEncrypted(outPacket, channel);
665     }
666
667     private void login() {
668         try {
669             SocketChannel channel = getConnection(false);
670             login(channel);
671         } catch (IOException e) {
672             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
673         }
674     }
675
676     /**
677      * This closes the connection with the PS4.
678      */
679     private void sendByeBye() {
680         ByteBuffer outPacket = PS4PacketHandler.makeByebyePacket();
681         sendPacketEncrypted(outPacket, false);
682     }
683
684     private void turnOnPS4() {
685         wakeUpPS4();
686         ScheduledFuture<?> future = scheduler.schedule(this::waitAndConnectToPS4, 17, TimeUnit.SECONDS);
687         scheduledFutures.add(future);
688         scheduledFutures.removeIf(ScheduledFuture::isDone);
689     }
690
691     private void waitAndConnectToPS4() {
692         try {
693             getConnection();
694         } catch (IOException e) {
695             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
696         }
697     }
698
699     private void sendStandby() {
700         ByteBuffer outPacket = PS4PacketHandler.makeStandbyPacket();
701         sendPacketEncrypted(outPacket);
702     }
703
704     /**
705      * Ask PS4 if the OSK is open so we can get and set text.
706      */
707     private void sendOSKStart() {
708         ByteBuffer outPacket = PS4PacketHandler.makeOSKStartPacket();
709         sendPacketEncrypted(outPacket);
710     }
711
712     /**
713      * Sets the entire OSK string on the PS4.
714      *
715      * @param text The text to set in the OSK.
716      */
717     private void setOSKText(String text) {
718         logger.debug("Sending osk text packet,\"{}\"", text);
719         ByteBuffer outPacket = PS4PacketHandler.makeOSKStringChangePacket(text);
720         sendPacketEncrypted(outPacket);
721     }
722
723     /**
724      * Tries to start an application on the PS4.
725      *
726      * @param applicationId The unique id for the application (CUSAxxxxx).
727      */
728     private void startApplication(String applicationId) {
729         ByteBuffer outPacket = PS4PacketHandler.makeApplicationPacket(applicationId);
730         sendPacketEncrypted(outPacket);
731     }
732
733     private void sendRemoteKey(int pushedKey) {
734         try {
735             SocketChannelHandler scHandler = socketChannelHandler;
736             int preWait = (scHandler == null || !loggedIn) ? POST_CONNECT_SENDKEY_DELAY_MS : 0;
737             SocketChannel channel = getConnection();
738
739             ScheduledFuture<?> future = scheduler.schedule(() -> {
740                 ByteBuffer outPacket = PS4PacketHandler.makeRemoteControlPacket(PS4_KEY_OPEN_RC);
741                 sendPacketEncrypted(outPacket, channel);
742             }, preWait, TimeUnit.MILLISECONDS);
743             scheduledFutures.add(future);
744
745             future = scheduler.schedule(() -> {
746                 // Send remote key
747                 ByteBuffer keyPacket = PS4PacketHandler.makeRemoteControlPacket(pushedKey);
748                 sendPacketEncrypted(keyPacket, channel);
749             }, preWait + MIN_SENDKEY_DELAY_MS, TimeUnit.MILLISECONDS);
750             scheduledFutures.add(future);
751
752             future = scheduler.schedule(() -> {
753                 ByteBuffer offPacket = PS4PacketHandler.makeRemoteControlPacket(PS4_KEY_OFF);
754                 sendPacketEncrypted(offPacket, channel);
755             }, preWait + MIN_SENDKEY_DELAY_MS + MIN_HOLDKEY_DELAY_MS, TimeUnit.MILLISECONDS);
756             scheduledFutures.add(future);
757
758             future = scheduler.schedule(() -> {
759                 ByteBuffer closePacket = PS4PacketHandler.makeRemoteControlPacket(PS4_KEY_CLOSE_RC);
760                 sendPacketEncrypted(closePacket, channel);
761             }, preWait + MIN_SENDKEY_DELAY_MS * 2 + MIN_HOLDKEY_DELAY_MS, TimeUnit.MILLISECONDS);
762             scheduledFutures.add(future);
763
764         } catch (IOException e) {
765             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
766         }
767         scheduledFutures.removeIf(ScheduledFuture::isDone);
768     }
769
770     private void parseSearchResponse(DatagramPacket packet) {
771         byte[] data = packet.getData();
772         String message = new String(data, StandardCharsets.UTF_8);
773         String applicationName = "";
774         String applicationId = "";
775
776         String[] rowStrings = message.trim().split("\\r?\\n");
777         for (String row : rowStrings) {
778             int index = row.indexOf(':');
779             if (index == -1) {
780                 OnOffType power = null;
781                 if (row.contains("200")) {
782                     power = OnOffType.ON;
783                 } else if (row.contains("620")) {
784                     power = OnOffType.OFF;
785                 }
786                 if (power != null) {
787                     updateState(CHANNEL_POWER, power);
788                     if (!currentPower.equals(power)) {
789                         currentPower = power;
790                         if (power.equals(OnOffType.ON) && config.autoConnect) {
791                             SocketChannelHandler scHandler = socketChannelHandler;
792                             if (scHandler == null || !loggedIn) {
793                                 logger.debug("Trying to login after power on.");
794                                 ScheduledFuture<?> future = scheduler.schedule(() -> login(), 20, TimeUnit.SECONDS);
795                                 scheduledFutures.add(future);
796                                 scheduledFutures.removeIf(ScheduledFuture::isDone);
797                             }
798                         }
799                     }
800                     updateStatus(ThingStatus.ONLINE);
801                 } else {
802                     updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
803                             "Could not determine power status.");
804                 }
805                 continue;
806             }
807             String key = row.substring(0, index);
808             String value = row.substring(index + 1);
809             switch (key) {
810                 case RESPONSE_RUNNING_APP_NAME:
811                     applicationName = value;
812                     break;
813                 case RESPONSE_RUNNING_APP_TITLEID:
814                     applicationId = value;
815                     break;
816                 case RESPONSE_HOST_REQUEST_PORT:
817                     int port = Integer.parseInt(value);
818                     if (currentComPort != port) {
819                         currentComPort = port;
820                         logger.debug("Host request port: {}", port);
821                     }
822                     break;
823                 case RESPONSE_SYSTEM_VERSION:
824                     updateProperty(Thing.PROPERTY_FIRMWARE_VERSION, PlayStationDiscovery.formatPS4Version(value));
825                     break;
826
827                 default:
828                     break;
829             }
830         }
831         if (!currentApplication.equals(applicationName)) {
832             currentApplication = applicationName;
833             updateState(CHANNEL_APPLICATION_NAME, StringType.valueOf(applicationName));
834             logger.debug("Current application: {}", applicationName);
835         }
836         if (!currentApplicationId.equals(applicationId)) {
837             updateApplicationTitleid(applicationId);
838         }
839     }
840
841     /**
842      * Sets the cached TitleId and tries to download artwork
843      * for application if CHANNEL_APPLICATION_IMAGE is linked.
844      *
845      * @param titleId Id of application.
846      */
847     private void updateApplicationTitleid(String titleId) {
848         currentApplicationId = titleId;
849         updateState(CHANNEL_APPLICATION_ID, StringType.valueOf(titleId));
850         logger.debug("Current application title id: {}", titleId);
851         if (!isLinked(CHANNEL_APPLICATION_IMAGE)) {
852             return;
853         }
854         LocaleProvider lProvider = localeProvider;
855         Locale locale = (lProvider != null) ? lProvider.getLocale() : Locale.US;
856
857         RawType artWork = PS4ArtworkHandler.fetchArtworkForTitleid(titleId, config.artworkSize, locale);
858         if (artWork != null) {
859             currentArtwork = artWork;
860             updateState(CHANNEL_APPLICATION_IMAGE, artWork);
861         } else if (!titleId.isEmpty()) {
862             logger.debug("Couldn't fetch artwork for title id: {}", titleId);
863         }
864     }
865 }