]> git.basschouten.com Git - openhab-addons.git/blob
39190e583a97d6edaea6316db96bda905475c22a
[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) {
124                 switch (channelUID.getId()) {
125                     case CHANNEL_APPLICATION_ID:
126                         if (!currentApplicationId.equals(((StringType) command).toString())) {
127                             updateApplicationTitleid(((StringType) command).toString());
128                             startApplication(currentApplicationId);
129                         }
130                         break;
131                     case CHANNEL_OSK_TEXT:
132                         setOSKText(((StringType) command).toString());
133                         break;
134                     case CHANNEL_SEND_KEY:
135                         int ps4Key = 0;
136                         switch (((StringType) command).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) {
172                 OnOffType onOff = (OnOffType) command;
173                 switch (channelUID.getId()) {
174                     case CHANNEL_POWER:
175                         if (currentPower != onOff) {
176                             currentPower = onOff;
177                             if (currentPower.equals(OnOffType.ON)) {
178                                 turnOnPS4();
179                             } else if (currentPower.equals(OnOffType.OFF)) {
180                                 sendStandby();
181                             }
182                         }
183                         break;
184                     case CHANNEL_CONNECT:
185                         boolean connected = socketChannelHandler != null && socketChannelHandler.isChannelOpen();
186                         if (connected && onOff.equals(OnOffType.OFF)) {
187                             sendByeBye();
188                         } else if (!connected && onOff.equals(OnOffType.ON)) {
189                             scheduler.execute(() -> login());
190                         }
191                         break;
192                     default:
193                         break;
194                 }
195             }
196         }
197     }
198
199     @Override
200     public void initialize() {
201         config = getConfigAs(PS4Configuration.class);
202
203         figureOutLocalIP();
204         updateStatus(ThingStatus.UNKNOWN);
205         setupRefreshTimer();
206     }
207
208     @Override
209     public void dispose() {
210         stopConnection();
211         ScheduledFuture<?> timer = refreshTimer;
212         if (timer != null) {
213             timer.cancel(false);
214             refreshTimer = null;
215         }
216         timer = timeoutTimer;
217         if (timer != null) {
218             timer.cancel(false);
219             timeoutTimer = null;
220         }
221         scheduledFutures.forEach(f -> f.cancel(false));
222         scheduledFutures.clear();
223     }
224
225     /**
226      * Tries to figure out a local IP that can communicate with the PS4.
227      */
228     private void figureOutLocalIP() {
229         if (!config.outboundIP.trim().isEmpty()) {
230             try {
231                 localAddress = InetAddress.getByName(config.outboundIP);
232                 logger.debug("Outbound local IP.\"{}\"", localAddress);
233                 return;
234             } catch (UnknownHostException e) {
235                 // This is expected
236             }
237         }
238         NetworkAddressService network = networkAS;
239         String adr = (network != null) ? network.getPrimaryIpv4HostAddress() : null;
240         if (adr != null) {
241             try {
242                 localAddress = InetAddress.getByName(adr);
243             } catch (UnknownHostException e) {
244                 // Ignore, just let the socket use whatever.
245             }
246         }
247     }
248
249     /**
250      * Sets up a timer for querying the PS4 (using the scheduler) every 10 seconds.
251      */
252     private void setupRefreshTimer() {
253         final ScheduledFuture<?> timer = refreshTimer;
254         if (timer != null) {
255             timer.cancel(false);
256         }
257         refreshTimer = scheduler.scheduleWithFixedDelay(this::updateAllChannels, 0, 10, TimeUnit.SECONDS);
258     }
259
260     /**
261      * Sets up a timer for stopping the connection to the PS4 (using the scheduler) with the given time.
262      *
263      * @param waitTime The time in seconds before the connection is stopped.
264      */
265     private void setupConnectionTimeout(int waitTime) {
266         final ScheduledFuture<?> timer = timeoutTimer;
267         if (timer != null) {
268             timer.cancel(false);
269         }
270         if (waitTime > 0) {
271             timeoutTimer = scheduler.schedule(this::stopConnection, waitTime, TimeUnit.SECONDS);
272         }
273     }
274
275     private void refreshFromState(ChannelUID channelUID) {
276         switch (channelUID.getId()) {
277             case CHANNEL_POWER:
278                 updateState(channelUID, currentPower);
279                 break;
280             case CHANNEL_APPLICATION_NAME:
281                 updateState(channelUID, StringType.valueOf(currentApplication));
282                 break;
283             case CHANNEL_APPLICATION_ID:
284                 updateState(channelUID, StringType.valueOf(currentApplicationId));
285                 break;
286             case CHANNEL_APPLICATION_IMAGE:
287                 updateApplicationTitleid(currentApplicationId);
288                 updateState(channelUID, currentArtwork);
289                 break;
290             case CHANNEL_OSK_TEXT:
291             case CHANNEL_2ND_SCREEN:
292                 updateState(channelUID, UnDefType.UNDEF);
293                 break;
294             case CHANNEL_CONNECT:
295                 boolean connected = socketChannelHandler != null && socketChannelHandler.isChannelOpen();
296                 updateState(channelUID, OnOffType.from(connected));
297                 break;
298             case CHANNEL_SEND_KEY:
299                 break;
300             default:
301                 logger.warn("Channel refresh for {} not implemented!", channelUID.getId());
302         }
303     }
304
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);
310
311             // send discover
312             byte[] discover = PS4PacketHandler.makeSearchPacket();
313             DatagramPacket packet = new DatagramPacket(discover, discover.length, inetAddress, DEFAULT_BROADCAST_PORT);
314             socket.send(packet);
315
316             // wait for response
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());
323         }
324     }
325
326     private void stopConnection() {
327         SocketChannelHandler handler = socketChannelHandler;
328         if (handler != null && handler.isChannelOpen()) {
329             sendByeBye();
330         }
331     }
332
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);
338             // send wake-up
339             byte[] wakeup = PS4PacketHandler.makeWakeupPacket(config.userCredential);
340             DatagramPacket packet = new DatagramPacket(wakeup, wakeup.length, inetAddress, DEFAULT_BROADCAST_PORT);
341             socket.send(packet);
342         } catch (IOException e) {
343             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
344         }
345     }
346
347     private boolean openComs() {
348         try (DatagramSocket socket = new DatagramSocket(0, localAddress)) {
349             socket.setBroadcast(false);
350             InetAddress inetAddress = InetAddress.getByName(config.ipAddress);
351             // send launch
352             byte[] launch = PS4PacketHandler.makeLaunchPacket(config.userCredential);
353             DatagramPacket packet = new DatagramPacket(launch, launch.length, inetAddress, DEFAULT_BROADCAST_PORT);
354             socket.send(packet);
355             Thread.sleep(100);
356             return true;
357         } catch (IOException e) {
358             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
359         } catch (InterruptedException e) {
360             return true;
361         }
362         return false;
363     }
364
365     private boolean setupConnection(SocketChannel channel) throws IOException {
366         logger.debug("TCP connecting");
367
368         channel.socket().setSoTimeout(2000);
369         channel.configureBlocking(true);
370         channel.connect(new InetSocketAddress(config.ipAddress, currentComPort));
371
372         ByteBuffer outPacket = PS4PacketHandler.makeHelloPacket();
373         sendPacketToPS4(outPacket, channel, false, false);
374
375         // Read hello response
376         final ByteBuffer readBuffer = ByteBuffer.allocate(512).order(ByteOrder.LITTLE_ENDIAN);
377
378         int responseLength = channel.read(readBuffer);
379         if (responseLength > 0) {
380             ps4Crypto.parseHelloResponsePacket(readBuffer);
381         } else {
382             return false;
383         }
384
385         outPacket = ps4Crypto.makeHandshakePacket();
386         sendPacketToPS4(outPacket, channel, false, false);
387         return true;
388     }
389
390     private class SocketChannelHandler extends Thread {
391         private SocketChannel socketChannel;
392
393         public SocketChannelHandler() throws IOException {
394             socketChannel = setupChannel();
395             loggedIn = false;
396             oskOpen = false;
397             start();
398         }
399
400         public SocketChannel getChannel() {
401             if (!socketChannel.isOpen()) {
402                 try {
403                     socketChannel = setupChannel();
404                 } catch (IOException e) {
405                     logger.debug("Couldn't open SocketChannel.{}", e.getMessage());
406                 }
407             }
408             return socketChannel;
409         }
410
411         public boolean isChannelOpen() {
412             return socketChannel.isOpen();
413         }
414
415         private SocketChannel setupChannel() throws IOException {
416             SocketChannel channel = SocketChannel.open();
417             if (!openComs()) {
418                 throw new IOException("Open coms failed");
419             }
420             if (!setupConnection(channel)) {
421                 throw new IOException("Setup connection failed");
422             }
423             updateState(CHANNEL_CONNECT, OnOffType.ON);
424             return channel;
425         }
426
427         @Override
428         public void run() {
429             SocketChannel channel = socketChannel;
430             final ByteBuffer readBuffer = ByteBuffer.allocate(512).order(ByteOrder.LITTLE_ENDIAN);
431             try {
432                 while (channel.read(readBuffer) > 0) {
433                     ByteBuffer messBuffer = ps4Crypto.decryptPacket(readBuffer);
434                     readBuffer.position(0);
435                     PS4Command lastCommand = parseResponsePacket(messBuffer);
436
437                     if (lastCommand == PS4Command.SERVER_STATUS_RSP) {
438                         if (oskOpen && isLinked(CHANNEL_OSK_TEXT)) {
439                             sendOSKStart();
440                         } else {
441                             sendStatus();
442                         }
443                     }
444                 }
445             } catch (IOException e) {
446                 logger.debug("Connection read exception: {}", e.getMessage());
447             } finally {
448                 try {
449                     channel.close();
450                 } catch (IOException e) {
451                     logger.debug("Connection close exception: {}", e.getMessage());
452                 }
453             }
454             updateState(CHANNEL_CONNECT, OnOffType.OFF);
455             logger.debug("SocketHandler done.");
456             ps4Crypto.clearCiphers();
457             loggedIn = false;
458         }
459     }
460
461     private @Nullable PS4Command parseResponsePacket(ByteBuffer rBuffer) {
462         rBuffer.rewind();
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);
467             return null;
468         }
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];
474         rBuffer.rewind();
475         rBuffer.get(respBuff);
476         if (command != null) {
477             if (status == null) {
478                 logger.debug("Resp; size:{}, command:{}, statValue:{}, data:{}.", size, command, statValue, respBuff);
479             } else {
480                 logger.debug("Resp; size:{}, command:{}, status:{}, data:{}.", size, command, status, respBuff);
481             }
482             switch (command) {
483                 case LOGIN_RSP:
484                     if (status == null) {
485                         logger.debug("Unhandled Login status value: {}", statValue);
486                         return command;
487                     }
488                     // Read login response
489                     switch (status) {
490                         case STATUS_OK:
491                             updateStatus(ThingStatus.ONLINE, ThingStatusDetail.NONE, status.message);
492                             loggedIn = true;
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);
498                                 });
499                             }
500                             break;
501                         case STATUS_NOT_PAIRED:
502                             updateStatus(ThingStatus.ONLINE, ThingStatusDetail.CONFIGURATION_PENDING, status.message);
503                             loggedIn = false;
504                             break;
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);
511                             loggedIn = false;
512                             logger.debug("Not logged in: {}", status.message);
513                             break;
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);
526                             loggedIn = false;
527                             logger.debug("Not logged in: {}", status.message);
528                             break;
529                         default:
530                             logger.debug("Unhandled Login response status:{}, message:{}", status, status.message);
531                             break;
532                     }
533                     break;
534                 case APP_START_RSP:
535                     if (status != null && status != PS4ErrorStatus.STATUS_OK) {
536                         logger.debug("App start response: {}", status.message);
537                     }
538                     break;
539                 case STANDBY_RSP:
540                     if (status != null && status != PS4ErrorStatus.STATUS_OK) {
541                         logger.debug("Standby response: {}", status.message);
542                     }
543                     break;
544                 case SERVER_STATUS_RSP:
545                     if ((statValue & 4) != 0) {
546                         oskOpen = true;
547                     } else {
548                         if (oskOpen) {
549                             updateState(CHANNEL_OSK_TEXT, StringType.valueOf(""));
550                         }
551                         oskOpen = false;
552                     }
553                     logger.debug("Server status value:{}", statValue);
554                     break;
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;
563                     }
564                     updateState(CHANNEL_2ND_SCREEN, StringType.valueOf(secondScrStr));
565                     break;
566                 case OSK_CHANGE_STRING_REQ:
567                     String oskText = PS4PacketHandler.parseOSKStringChangePacket(rBuffer);
568                     updateState(CHANNEL_OSK_TEXT, StringType.valueOf(oskText));
569                     break;
570                 case OSK_START_RSP:
571                 case OSK_CONTROL_REQ:
572                 case COMMENT_VIEWER_START_RESULT:
573                 case SCREEN_SHOT_RSP:
574                 case APP_START2_RSP:
575                 case LOGOUT_RSP:
576                     break;
577                 default:
578                     logger.debug("Unknown response, command:{}. Missing case.", command);
579                     break;
580             }
581         } else {
582             logger.debug("Unknown resp-cmd, size:{}, command:{}, status:{}, data:{}.", size, cmdValue, statValue,
583                     respBuff);
584         }
585         return command;
586     }
587
588     private SocketChannel getConnection() throws IOException {
589         return getConnection(true);
590     }
591
592     private SocketChannel getConnection(boolean requiresLogin) throws IOException {
593         SocketChannel channel = null;
594         SocketChannelHandler handler = socketChannelHandler;
595         if (handler == null || !handler.isChannelOpen()) {
596             try {
597                 handler = new SocketChannelHandler();
598                 socketChannelHandler = handler;
599             } catch (IOException e) {
600                 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
601                 throw e;
602             }
603         }
604         channel = handler.getChannel();
605         if (!loggedIn && requiresLogin) {
606             login(channel);
607         }
608         return channel;
609     }
610
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);
614         try {
615             if (encrypted) {
616                 ByteBuffer outPacket = ps4Crypto.encryptPacket(packet);
617                 channel.write(outPacket);
618             } else {
619                 channel.write(packet);
620             }
621             if (restartTimeout) {
622                 setupConnectionTimeout(config.connectionTimeout);
623             }
624         } catch (IOException e) {
625             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
626         }
627     }
628
629     private void sendPacketEncrypted(ByteBuffer packet, SocketChannel channel) {
630         sendPacketToPS4(packet, channel, true, true);
631     }
632
633     private void sendPacketEncrypted(ByteBuffer packet) {
634         sendPacketEncrypted(packet, true);
635     }
636
637     private void sendPacketEncrypted(ByteBuffer packet, boolean requiresLogin) {
638         try {
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);
645             } else {
646                 sendPacketToPS4(packet, channel, true, requiresLogin);
647             }
648         } catch (IOException e) {
649             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
650         }
651     }
652
653     /**
654      * This is used as a heart beat to let the PS4 know that we are still listening.
655      */
656     private void sendStatus() {
657         ByteBuffer outPacket = PS4PacketHandler.makeStatusPacket(0);
658         sendPacketEncrypted(outPacket, false);
659     }
660
661     private void login(SocketChannel channel) {
662         // Send login request
663         ByteBuffer outPacket = PS4PacketHandler.makeLoginPacket(config.userCredential, config.passCode,
664                 config.pairingCode);
665         sendPacketEncrypted(outPacket, channel);
666     }
667
668     private void login() {
669         try {
670             SocketChannel channel = getConnection(false);
671             login(channel);
672         } catch (IOException e) {
673             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
674         }
675     }
676
677     /**
678      * This closes the connection with the PS4.
679      */
680     private void sendByeBye() {
681         ByteBuffer outPacket = PS4PacketHandler.makeByebyePacket();
682         sendPacketEncrypted(outPacket, false);
683     }
684
685     private void turnOnPS4() {
686         wakeUpPS4();
687         ScheduledFuture<?> future = scheduler.schedule(this::waitAndConnectToPS4, 17, TimeUnit.SECONDS);
688         scheduledFutures.add(future);
689         scheduledFutures.removeIf(ScheduledFuture::isDone);
690     }
691
692     private void waitAndConnectToPS4() {
693         try {
694             getConnection();
695         } catch (IOException e) {
696             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
697         }
698     }
699
700     private void sendStandby() {
701         ByteBuffer outPacket = PS4PacketHandler.makeStandbyPacket();
702         sendPacketEncrypted(outPacket);
703     }
704
705     /**
706      * Ask PS4 if the OSK is open so we can get and set text.
707      */
708     private void sendOSKStart() {
709         ByteBuffer outPacket = PS4PacketHandler.makeOSKStartPacket();
710         sendPacketEncrypted(outPacket);
711     }
712
713     /**
714      * Sets the entire OSK string on the PS4.
715      *
716      * @param text The text to set in the OSK.
717      */
718     private void setOSKText(String text) {
719         logger.debug("Sending osk text packet,\"{}\"", text);
720         ByteBuffer outPacket = PS4PacketHandler.makeOSKStringChangePacket(text);
721         sendPacketEncrypted(outPacket);
722     }
723
724     /**
725      * Tries to start an application on the PS4.
726      *
727      * @param applicationId The unique id for the application (CUSAxxxxx).
728      */
729     private void startApplication(String applicationId) {
730         ByteBuffer outPacket = PS4PacketHandler.makeApplicationPacket(applicationId);
731         sendPacketEncrypted(outPacket);
732     }
733
734     private void sendRemoteKey(int pushedKey) {
735         try {
736             SocketChannelHandler scHandler = socketChannelHandler;
737             int preWait = (scHandler == null || !loggedIn) ? POST_CONNECT_SENDKEY_DELAY_MS : 0;
738             SocketChannel channel = getConnection();
739
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);
745
746             future = scheduler.schedule(() -> {
747                 // Send remote key
748                 ByteBuffer keyPacket = PS4PacketHandler.makeRemoteControlPacket(pushedKey);
749                 sendPacketEncrypted(keyPacket, channel);
750             }, preWait + MIN_SENDKEY_DELAY_MS, TimeUnit.MILLISECONDS);
751             scheduledFutures.add(future);
752
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);
758
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);
764
765         } catch (IOException e) {
766             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
767         }
768         scheduledFutures.removeIf(ScheduledFuture::isDone);
769     }
770
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 = "";
776
777         String[] rowStrings = message.trim().split("\\r?\\n");
778         for (String row : rowStrings) {
779             int index = row.indexOf(':');
780             if (index == -1) {
781                 OnOffType power = null;
782                 if (row.contains("200")) {
783                     power = OnOffType.ON;
784                 } else if (row.contains("620")) {
785                     power = OnOffType.OFF;
786                 }
787                 if (power != null) {
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);
798                             }
799                         }
800                     }
801                     updateStatus(ThingStatus.ONLINE);
802                 } else {
803                     updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
804                             "Could not determine power status.");
805                 }
806                 continue;
807             }
808             String key = row.substring(0, index);
809             String value = row.substring(index + 1);
810             switch (key) {
811                 case RESPONSE_RUNNING_APP_NAME:
812                     applicationName = value;
813                     break;
814                 case RESPONSE_RUNNING_APP_TITLEID:
815                     applicationId = value;
816                     break;
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);
822                     }
823                     break;
824                 case RESPONSE_SYSTEM_VERSION:
825                     updateProperty(Thing.PROPERTY_FIRMWARE_VERSION, PlayStationDiscovery.formatPS4Version(value));
826                     break;
827
828                 default:
829                     break;
830             }
831         }
832         if (!currentApplication.equals(applicationName)) {
833             currentApplication = applicationName;
834             updateState(CHANNEL_APPLICATION_NAME, StringType.valueOf(applicationName));
835             logger.debug("Current application: {}", applicationName);
836         }
837         if (!currentApplicationId.equals(applicationId)) {
838             updateApplicationTitleid(applicationId);
839         }
840     }
841
842     /**
843      * Sets the cached TitleId and tries to download artwork
844      * for application if CHANNEL_APPLICATION_IMAGE is linked.
845      *
846      * @param titleId Id of application.
847      */
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)) {
853             return;
854         }
855         LocaleProvider lProvider = localeProvider;
856         Locale locale = (lProvider != null) ? lProvider.getLocale() : Locale.US;
857
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);
864         }
865     }
866 }