2 * Copyright (c) 2010-2023 Contributors to the openHAB project
4 * See the NOTICE file(s) distributed with this work for additional
7 * This program and the accompanying materials are made available under the
8 * terms of the Eclipse Public License 2.0 which is available at
9 * http://www.eclipse.org/legal/epl-2.0
11 * SPDX-License-Identifier: EPL-2.0
13 package org.openhab.binding.playstation.internal.discovery;
15 import static org.openhab.binding.playstation.internal.PlayStationBindingConstants.*;
17 import java.io.IOException;
18 import java.net.DatagramPacket;
19 import java.net.DatagramSocket;
20 import java.net.InetAddress;
21 import java.net.NetworkInterface;
22 import java.net.SocketTimeoutException;
23 import java.net.UnknownHostException;
24 import java.nio.charset.StandardCharsets;
25 import java.util.HashMap;
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;
44 * The {@link PlayStationDiscovery} is responsible for discovering
47 * @author Fredrik Ahlström - Initial contribution
50 @Component(service = { DiscoveryService.class, PlayStationDiscovery.class }, configurationPid = "discovery.playstation")
51 public class PlayStationDiscovery extends AbstractDiscoveryService {
53 private final Logger logger = LoggerFactory.getLogger(PlayStationDiscovery.class);
55 private static final int DISCOVERY_TIMEOUT_SECONDS = 2;
57 private @Nullable NetworkAddressService networkAS;
59 public PlayStationDiscovery() {
60 super(SUPPORTED_THING_TYPES_UIDS, DISCOVERY_TIMEOUT_SECONDS * 2, true);
64 protected void startScan() {
65 logger.debug("Updating discovered things (new scan)");
71 public void bindNetworkAddressService(NetworkAddressService network) {
75 private @Nullable InetAddress getBroadcastAdress() {
76 NetworkAddressService nwService = networkAS;
77 if (nwService != null) {
79 String address = nwService.getConfiguredBroadcastAddress();
80 if (address != null) {
81 return InetAddress.getByName(address);
83 return InetAddress.getByName("255.255.255.255");
85 } catch (UnknownHostException e) {
86 // We catch errors later.
92 private @Nullable InetAddress getIPv4Adress() {
93 NetworkAddressService nwService = networkAS;
94 if (nwService != null) {
96 String address = nwService.getPrimaryIpv4HostAddress();
97 if (address != null) {
98 return InetAddress.getByName(address);
100 } catch (UnknownHostException e) {
101 // We catch errors later.
107 private synchronized void discoverPS4() {
108 logger.debug("Trying to discover all PS4 devices");
110 try (DatagramSocket socket = new DatagramSocket(0, getIPv4Adress())) {
111 socket.setBroadcast(true);
112 socket.setSoTimeout(DISCOVERY_TIMEOUT_SECONDS * 1000);
114 InetAddress bcAddress = getBroadcastAdress();
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);
120 logger.debug("Discover message sent: '{}'", discover);
122 // wait for responses
124 byte[] rxbuf = new byte[256];
125 packet = new DatagramPacket(rxbuf, rxbuf.length);
127 socket.receive(packet);
128 parsePS4Packet(packet);
129 } catch (SocketTimeoutException e) {
130 break; // leave the endless loop
133 } catch (IOException e) {
134 logger.debug("No PS4 device found. Diagnostic: {}", e.getMessage());
138 private synchronized void discoverPS3() {
139 logger.trace("Trying to discover all PS3 devices that have \"Connect PS Vita System Using Network\" on.");
141 InetAddress bcAddress = getBroadcastAdress();
142 InetAddress localAddress = getIPv4Adress();
144 if (localAddress == null || bcAddress == null) {
145 logger.warn("No IP/Broadcast address found. Make sure OpenHab is configured!");
148 try (DatagramSocket socket = new DatagramSocket(0, getIPv4Adress())) {
149 socket.setBroadcast(true);
150 socket.setSoTimeout(DISCOVERY_TIMEOUT_SECONDS * 1000);
152 NetworkInterface nic = NetworkInterface.getByInetAddress(localAddress);
153 byte[] macAdr = nic.getHardwareAddress();
154 String macString = HexUtils.bytesToHex(macAdr);
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);
171 // wait for responses
173 byte[] rxbuf = new byte[512];
174 packet = new DatagramPacket(rxbuf, rxbuf.length);
176 socket.receive(packet);
177 parsePS3Packet(packet);
178 } catch (SocketTimeoutException e) {
179 break; // leave the endless loop
182 } catch (IOException e) {
183 logger.debug("No PS3 device found. Diagnostic: {}", e.getMessage());
188 * The response from the PS4 looks something like this:
191 * host-id:0123456789AB
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
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());
208 String ipAddress = packet.getAddress().toString().split("/")[1];
210 String hostType = "";
211 String hostName = "";
212 String hostPort = "";
213 String protocolVersion = "";
214 String systemVersion = "";
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);
223 case RESPONSE_HOST_ID:
226 case RESPONSE_HOST_TYPE:
229 case RESPONSE_HOST_NAME:
232 case RESPONSE_HOST_REQUEST_PORT:
235 case RESPONSE_DEVICE_DISCOVERY_PROTOCOL_VERSION:
236 protocolVersion = value;
237 if (!"00020020".equals(protocolVersion)) {
238 logger.debug("Different protocol version: '{}'", protocolVersion);
241 case RESPONSE_SYSTEM_VERSION:
242 systemVersion = value;
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 = "PS5".equalsIgnoreCase(hostType) ? new ThingUID(THING_TYPE_PS5, hostId)
258 : new ThingUID(THING_TYPE_PS4, hostId);
260 DiscoveryResult result = DiscoveryResultBuilder.create(uid).withProperties(properties).withLabel(hostName)
261 .withRepresentationProperty(Thing.PROPERTY_MAC_ADDRESS).build();
262 thingDiscovered(result);
267 * The response from the PS3 looks like this:
270 * host-id:00000000-0000-0000-0000-123456789abc
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
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());
287 String ipAddress = packet.getAddress().toString().split("/")[1];
289 String hostType = "";
290 String hostName = "";
291 String hostPort = "";
292 String protocolVersion = "";
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);
301 case RESPONSE_HOST_ID:
304 case RESPONSE_HOST_TYPE:
307 case RESPONSE_HOST_NAME:
310 case RESPONSE_HOST_REQUEST_PORT:
312 if (!Integer.toString(DEFAULT_PS3_MEDIA_MANAGER_PORT).equals(hostPort)) {
313 logger.debug("Different host request port: '{}'", hostPort);
316 case RESPONSE_HOST_WIRELESS_PROTOCOL_VERSION:
317 protocolVersion = value;
318 if (!"1000000".equals(protocolVersion)) {
319 logger.debug("Different protocol version: '{}'", protocolVersion);
322 case RESPONSE_HOST_MAC_ADDRESS:
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);
338 DiscoveryResult result = DiscoveryResultBuilder.create(uid).withProperties(properties).withLabel(hostName)
339 .withRepresentationProperty(Thing.PROPERTY_MAC_ADDRESS).build();
340 thingDiscovered(result);
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());
354 return sb.toString();
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)) {
366 resultV = resultV.substring(0, 4) + "." + resultV.substring(4, len);
370 resultV = resultV.substring(0, 2) + "." + resultV.substring(2, len);
373 if (resultV.charAt(0) == '0') {
374 resultV = resultV.substring(1);
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) {
386 hwVersion = PSVHW_PCHXXXX;
401 hwVersion = PS3HW_CECHXXXX;
404 hwVersion = PS3HW_CECH4000;
406 case "709e29": // Ethernet
407 case "b00594": // WiFi
408 hwVersion = PS4HW_CUH1000;
410 case "bc60a7": // Ethernet
411 if ("7b".equals(ethId)) {
412 hwVersion = PS4HW_CUH2000;
414 if ("8f".equals(ethId)) {
415 hwVersion = PS4HW_CUH7000;
418 case "c863f1": // Ethernet
419 case "f8461c": // Ethernet
420 case "5cea1d": // WiFi
421 case "f8da0c": // WiFi
422 hwVersion = PS4HW_CUH2000;
424 case "40490f": // WiFi
425 case "5c9656": // WiFi
426 if ("07".equals(ethId)) {
427 hwVersion = PS4HW_CUH2000;
429 if ("da".equals(ethId)) {
430 hwVersion = PS4HW_CUH7000;
433 case "2ccc44": // Ethernet
434 case "dca266": // WiFi
435 hwVersion = PS4HW_CUH7100;
437 case "78c881": // Ethernet
438 case "1c98c1": // WiFi
439 hwVersion = PS5HW_CFI1000B;
449 private static String modelNameFromHostTypeAndHWVersion(String hostType, String hwVersion) {
450 String modelName = "PlayStation 4";
451 switch (hostType.toUpperCase()) {
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";
461 modelName = "PlayStation 4";
462 if (hwVersion.startsWith("CUH-2")) {
463 modelName += " Slim";
464 } else if (hwVersion.startsWith("CUH-7")) {
469 modelName = "PlayStation 5";
470 if (hwVersion.endsWith("B")) {
471 modelName += " Digital Edition";