]> git.basschouten.com Git - openhab-addons.git/blob
16ca4e0a6ccd5f5b8580d626322dbfc3d1b04200
[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.discovery;
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.NetworkInterface;
22 import java.net.SocketTimeoutException;
23 import java.net.UnknownHostException;
24 import java.nio.charset.StandardCharsets;
25 import java.util.HashMap;
26 import java.util.Map;
27
28 import org.eclipse.jdt.annotation.NonNullByDefault;
29 import org.eclipse.jdt.annotation.Nullable;
30 import org.openhab.core.config.discovery.AbstractDiscoveryService;
31 import org.openhab.core.config.discovery.DiscoveryResult;
32 import org.openhab.core.config.discovery.DiscoveryResultBuilder;
33 import org.openhab.core.config.discovery.DiscoveryService;
34 import org.openhab.core.net.NetworkAddressService;
35 import org.openhab.core.thing.Thing;
36 import org.openhab.core.thing.ThingUID;
37 import org.openhab.core.util.HexUtils;
38 import org.osgi.service.component.annotations.Component;
39 import org.osgi.service.component.annotations.Reference;
40 import org.slf4j.Logger;
41 import org.slf4j.LoggerFactory;
42
43 /**
44  * The {@link PlayStationDiscovery} is responsible for discovering
45  * all PS4 devices
46  *
47  * @author Fredrik Ahlström - Initial contribution
48  */
49 @NonNullByDefault
50 @Component(service = { DiscoveryService.class, PlayStationDiscovery.class }, configurationPid = "discovery.playstation")
51 public class PlayStationDiscovery extends AbstractDiscoveryService {
52
53     private final Logger logger = LoggerFactory.getLogger(PlayStationDiscovery.class);
54
55     private static final int DISCOVERY_TIMEOUT_SECONDS = 2;
56
57     private @Nullable NetworkAddressService networkAS;
58
59     public PlayStationDiscovery() {
60         super(SUPPORTED_THING_TYPES_UIDS, DISCOVERY_TIMEOUT_SECONDS * 2, true);
61     }
62
63     @Override
64     protected void startScan() {
65         logger.debug("Updating discovered things (new scan)");
66         discoverPS4();
67         discoverPS3();
68     }
69
70     @Reference
71     public void bindNetworkAddressService(NetworkAddressService network) {
72         networkAS = network;
73     }
74
75     private @Nullable InetAddress getBroadcastAdress() {
76         NetworkAddressService nwService = networkAS;
77         if (nwService != null) {
78             try {
79                 String address = nwService.getConfiguredBroadcastAddress();
80                 if (address != null) {
81                     return InetAddress.getByName(address);
82                 } else {
83                     return InetAddress.getByName("255.255.255.255");
84                 }
85             } catch (UnknownHostException e) {
86                 // We catch errors later.
87             }
88         }
89         return null;
90     }
91
92     private @Nullable InetAddress getIPv4Adress() {
93         NetworkAddressService nwService = networkAS;
94         if (nwService != null) {
95             try {
96                 String address = nwService.getPrimaryIpv4HostAddress();
97                 if (address != null) {
98                     return InetAddress.getByName(address);
99                 }
100             } catch (UnknownHostException e) {
101                 // We catch errors later.
102             }
103         }
104         return null;
105     }
106
107     private synchronized void discoverPS4() {
108         logger.debug("Trying to discover all PS4 devices");
109
110         try (DatagramSocket socket = new DatagramSocket(0, getIPv4Adress())) {
111             socket.setBroadcast(true);
112             socket.setSoTimeout(DISCOVERY_TIMEOUT_SECONDS * 1000);
113
114             InetAddress bcAddress = getBroadcastAdress();
115
116             // send discover
117             byte[] discover = "SRCH * HTTP/1.1\ndevice-discovery-protocol-version:00020020\n".getBytes();
118             DatagramPacket packet = new DatagramPacket(discover, discover.length, bcAddress, DEFAULT_BROADCAST_PORT);
119             socket.send(packet);
120             logger.debug("Discover message sent: '{}'", discover);
121
122             // wait for responses
123             while (true) {
124                 byte[] rxbuf = new byte[256];
125                 packet = new DatagramPacket(rxbuf, rxbuf.length);
126                 try {
127                     socket.receive(packet);
128                     parsePS4Packet(packet);
129                 } catch (SocketTimeoutException e) {
130                     break; // leave the endless loop
131                 }
132             }
133         } catch (IOException e) {
134             logger.debug("No PS4 device found. Diagnostic: {}", e.getMessage());
135         }
136     }
137
138     private synchronized void discoverPS3() {
139         logger.trace("Trying to discover all PS3 devices that have \"Connect PS Vita System Using Network\" on.");
140
141         InetAddress bcAddress = getBroadcastAdress();
142         InetAddress localAddress = getIPv4Adress();
143
144         if (localAddress == null || bcAddress == null) {
145             logger.warn("No IP/Broadcast address found. Make sure OpenHab is configured!");
146             return;
147         }
148         try (DatagramSocket socket = new DatagramSocket(0, getIPv4Adress())) {
149             socket.setBroadcast(true);
150             socket.setSoTimeout(DISCOVERY_TIMEOUT_SECONDS * 1000);
151
152             NetworkInterface nic = NetworkInterface.getByInetAddress(localAddress);
153             byte[] macAdr = nic.getHardwareAddress();
154             String macString = HexUtils.bytesToHex(macAdr);
155             // send discover
156             StringBuilder srchBuilder = new StringBuilder("SRCH3 * HTTP/1.1\n");
157             srchBuilder.append("device-id:");
158             srchBuilder.append(macString);
159             srchBuilder.append("01010101010101010101\n");
160             srchBuilder.append("device-type:PS Vita\n");
161             srchBuilder.append("device-class:0\n");
162             srchBuilder.append("device-mac-address:");
163             srchBuilder.append(macString);
164             srchBuilder.append("\n");
165             srchBuilder.append("device-wireless-protocol-version:01000000\n\n");
166             byte[] discover = srchBuilder.toString().getBytes();
167             DatagramPacket packet = new DatagramPacket(discover, discover.length, bcAddress,
168                     DEFAULT_PS3_MEDIA_MANAGER_PORT);
169             socket.send(packet);
170
171             // wait for responses
172             while (true) {
173                 byte[] rxbuf = new byte[512];
174                 packet = new DatagramPacket(rxbuf, rxbuf.length);
175                 try {
176                     socket.receive(packet);
177                     parsePS3Packet(packet);
178                 } catch (SocketTimeoutException e) {
179                     break; // leave the endless loop
180                 }
181             }
182         } catch (IOException e) {
183             logger.debug("No PS3 device found. Diagnostic: {}", e.getMessage());
184         }
185     }
186
187     /**
188      * The response from the PS4 looks something like this:
189      *
190      * HTTP/1.1 200 Ok
191      * host-id:0123456789AB
192      * host-type:PS4
193      * host-name:MyPS4
194      * host-request-port:997
195      * device-discovery-protocol-version:00020020
196      * system-version:07020001
197      * running-app-name:Youtube
198      * running-app-titleid:CUSA01116
199      *
200      * @param packet
201      * @return
202      */
203     private boolean parsePS4Packet(DatagramPacket packet) {
204         byte[] data = packet.getData();
205         String message = new String(data, StandardCharsets.UTF_8);
206         logger.debug("PS4 data '{}', length:{}", message, packet.getLength());
207
208         String ipAddress = packet.getAddress().toString().split("/")[1];
209         String hostId = "";
210         String hostType = "";
211         String hostName = "";
212         String hostPort = "";
213         String protocolVersion = "";
214         String systemVersion = "";
215
216         String[] rowStrings = message.trim().split("\\r?\\n");
217         for (String row : rowStrings) {
218             int index = row.indexOf(':');
219             index = index != -1 ? index : 0;
220             String key = row.substring(0, index);
221             String value = row.substring(index + 1);
222             switch (key) {
223                 case RESPONSE_HOST_ID:
224                     hostId = value;
225                     break;
226                 case RESPONSE_HOST_TYPE:
227                     hostType = value;
228                     break;
229                 case RESPONSE_HOST_NAME:
230                     hostName = value;
231                     break;
232                 case RESPONSE_HOST_REQUEST_PORT:
233                     hostPort = value;
234                     break;
235                 case RESPONSE_DEVICE_DISCOVERY_PROTOCOL_VERSION:
236                     protocolVersion = value;
237                     if (!"00020020".equals(protocolVersion)) {
238                         logger.debug("Different protocol version: '{}'", protocolVersion);
239                     }
240                     break;
241                 case RESPONSE_SYSTEM_VERSION:
242                     systemVersion = value;
243                     break;
244                 default:
245                     break;
246             }
247         }
248         String hwVersion = hwVersionFromHostId(hostId);
249         String modelID = modelNameFromHostTypeAndHWVersion(hostType, hwVersion);
250         Map<String, Object> properties = new HashMap<>();
251         properties.put(IP_ADDRESS, ipAddress);
252         properties.put(IP_PORT, Integer.valueOf(hostPort));
253         properties.put(Thing.PROPERTY_MODEL_ID, modelID);
254         properties.put(Thing.PROPERTY_HARDWARE_VERSION, hwVersion);
255         properties.put(Thing.PROPERTY_FIRMWARE_VERSION, formatPS4Version(systemVersion));
256         properties.put(Thing.PROPERTY_MAC_ADDRESS, hostIdToMacAddress(hostId));
257         ThingUID uid = hostType.equalsIgnoreCase("PS5") ? new ThingUID(THING_TYPE_PS5, hostId)
258                 : new ThingUID(THING_TYPE_PS4, hostId);
259
260         DiscoveryResult result = DiscoveryResultBuilder.create(uid).withProperties(properties).withLabel(hostName)
261                 .withRepresentationProperty(Thing.PROPERTY_MAC_ADDRESS).build();
262         thingDiscovered(result);
263         return true;
264     }
265
266     /**
267      * The response from the PS3 looks like this:
268      *
269      * HTTP/1.1 200 OK
270      * host-id:00000000-0000-0000-0000-123456789abc
271      * host-type:ps3
272      * host-name:MyPS3
273      * host-mtp-protocol-version:1800010
274      * host-request-port:9309
275      * host-wireless-protocol-version:1000000
276      * host-mac-address:123456789abc
277      * host-supported-device:PS Vita, PS Vita TV
278      *
279      * @param packet
280      * @return
281      */
282     private boolean parsePS3Packet(DatagramPacket packet) {
283         byte[] data = packet.getData();
284         String message = new String(data, StandardCharsets.UTF_8);
285         logger.debug("PS3 data '{}', length:{}", message, packet.getLength());
286
287         String ipAddress = packet.getAddress().toString().split("/")[1];
288         String hostId = "";
289         String hostType = "";
290         String hostName = "";
291         String hostPort = "";
292         String protocolVersion = "";
293
294         String[] rowStrings = message.trim().split("\\r?\\n");
295         for (String row : rowStrings) {
296             int index = row.indexOf(':');
297             index = index != -1 ? index : 0;
298             String key = row.substring(0, index);
299             String value = row.substring(index + 1);
300             switch (key) {
301                 case RESPONSE_HOST_ID:
302                     hostId = value;
303                     break;
304                 case RESPONSE_HOST_TYPE:
305                     hostType = value;
306                     break;
307                 case RESPONSE_HOST_NAME:
308                     hostName = value;
309                     break;
310                 case RESPONSE_HOST_REQUEST_PORT:
311                     hostPort = value;
312                     if (!Integer.toString(DEFAULT_PS3_MEDIA_MANAGER_PORT).equals(hostPort)) {
313                         logger.debug("Different host request port: '{}'", hostPort);
314                     }
315                     break;
316                 case RESPONSE_HOST_WIRELESS_PROTOCOL_VERSION:
317                     protocolVersion = value;
318                     if (!"1000000".equals(protocolVersion)) {
319                         logger.debug("Different protocol version: '{}'", protocolVersion);
320                     }
321                     break;
322                 case RESPONSE_HOST_MAC_ADDRESS:
323                     hostId = value;
324                     break;
325                 default:
326                     break;
327             }
328         }
329         String hwVersion = hwVersionFromHostId(hostId);
330         String modelID = modelNameFromHostTypeAndHWVersion(hostType, hwVersion);
331         Map<String, Object> properties = new HashMap<>();
332         properties.put(IP_ADDRESS, ipAddress);
333         properties.put(Thing.PROPERTY_MODEL_ID, modelID);
334         properties.put(Thing.PROPERTY_HARDWARE_VERSION, hwVersion);
335         properties.put(Thing.PROPERTY_MAC_ADDRESS, hostIdToMacAddress(hostId));
336         ThingUID uid = new ThingUID(THING_TYPE_PS3, hostId);
337
338         DiscoveryResult result = DiscoveryResultBuilder.create(uid).withProperties(properties).withLabel(hostName)
339                 .withRepresentationProperty(Thing.PROPERTY_MAC_ADDRESS).build();
340         thingDiscovered(result);
341         return true;
342     }
343
344     private static String hostIdToMacAddress(String hostId) {
345         StringBuilder sb = new StringBuilder();
346         if (hostId.length() >= 12) {
347             for (int i = 0; i < 6; i++) {
348                 sb.append(hostId.substring(i * 2, i * 2 + 2).toLowerCase());
349                 if (i < 5) {
350                     sb.append(':');
351                 }
352             }
353         }
354         return sb.toString();
355     }
356
357     public static String formatPS4Version(String fwVersion) {
358         String resultV = fwVersion;
359         int len = fwVersion.length();
360         for (Character c : fwVersion.toCharArray()) {
361             if (!Character.isDigit(c)) {
362                 return resultV;
363             }
364         }
365         if (len > 4) {
366             resultV = resultV.substring(0, 4) + "." + resultV.substring(4, len);
367             len++;
368         }
369         if (len > 2) {
370             resultV = resultV.substring(0, 2) + "." + resultV.substring(2, len);
371         }
372
373         if (resultV.charAt(0) == '0') {
374             resultV = resultV.substring(1);
375         }
376         return resultV;
377     }
378
379     private static String hwVersionFromHostId(String hostId) {
380         String hwVersion = PS4HW_CUHXXXX;
381         if (hostId.length() >= 12) {
382             final String manufacturer = hostId.substring(0, 6).toLowerCase();
383             final String ethId = hostId.substring(6, 8).toLowerCase();
384             switch (manufacturer) {
385                 case "d44b5e":
386                     hwVersion = PSVHW_PCHXXXX;
387                     break;
388                 case "001315":
389                 case "001fa7":
390                 case "a8e3ee":
391                 case "fc0fe6":
392                 case "00248d":
393                 case "280dfc":
394                 case "0015c1":
395                 case "0019c5":
396                 case "0022a6":
397                 case "0cfe45":
398                 case "f8d0ac":
399                 case "00041f":
400                 case "001d0d":
401                     hwVersion = PS3HW_CECHXXXX;
402                     break;
403                 case "00d9d1":
404                     hwVersion = PS3HW_CECH4000;
405                     break;
406                 case "709e29": // Ethernet
407                 case "b00594": // WiFi
408                     hwVersion = PS4HW_CUH1000;
409                     break;
410                 case "bc60a7": // Ethernet
411                     if (ethId.equals("7b")) {
412                         hwVersion = PS4HW_CUH2000;
413                     }
414                     if (ethId.equals("8f")) {
415                         hwVersion = PS4HW_CUH7000;
416                     }
417                     break;
418                 case "c863f1": // Ethernet
419                 case "f8461c": // Ethernet
420                 case "5cea1d": // WiFi
421                 case "f8da0c": // WiFi
422                     hwVersion = PS4HW_CUH2000;
423                     break;
424                 case "40490f": // WiFi
425                 case "5c9656": // WiFi
426                     if (ethId.equals("07")) {
427                         hwVersion = PS4HW_CUH2000;
428                     }
429                     if (ethId.equals("da")) {
430                         hwVersion = PS4HW_CUH7000;
431                     }
432                     break;
433                 case "2ccc44": // Ethernet
434                 case "dca266": // WiFi
435                     hwVersion = PS4HW_CUH7100;
436                     break;
437                 case "78c881": // Ethernet
438                 case "1c98c1": // WiFi
439                     hwVersion = PS5HW_CFI1000B;
440                     break;
441                 default:
442                     break;
443             }
444         }
445
446         return hwVersion;
447     }
448
449     private static String modelNameFromHostTypeAndHWVersion(String hostType, String hwVersion) {
450         String modelName = "PlayStation 4";
451         switch (hostType.toUpperCase()) {
452             case "PS3":
453                 modelName = "PlayStation 3";
454                 if (hwVersion.startsWith("CECH-2") || hwVersion.startsWith("CECH-3")) {
455                     modelName += " Slim";
456                 } else if (hwVersion.startsWith("CECH-4")) {
457                     modelName += " Super Slim";
458                 }
459                 break;
460             case "PS4":
461                 modelName = "PlayStation 4";
462                 if (hwVersion.startsWith("CUH-2")) {
463                     modelName += " Slim";
464                 } else if (hwVersion.startsWith("CUH-7")) {
465                     modelName += " Pro";
466                 }
467                 break;
468             case "PS5":
469                 modelName = "PlayStation 5";
470                 if (hwVersion.endsWith("B")) {
471                     modelName += " Digital Edition";
472                 }
473                 break;
474             default:
475                 break;
476         }
477         return modelName;
478     }
479 }