2 * Copyright (c) 2010-2024 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.kaleidescape.internal.discovery;
15 import static org.openhab.binding.kaleidescape.internal.KaleidescapeBindingConstants.*;
17 import java.io.BufferedReader;
18 import java.io.IOException;
19 import java.io.InputStream;
20 import java.io.InputStreamReader;
21 import java.io.OutputStream;
22 import java.io.PrintWriter;
23 import java.net.DatagramPacket;
24 import java.net.DatagramSocket;
25 import java.net.InetSocketAddress;
26 import java.net.Socket;
27 import java.net.SocketTimeoutException;
28 import java.util.Arrays;
29 import java.util.Collections;
30 import java.util.HashMap;
31 import java.util.HashSet;
33 import java.util.concurrent.ExecutorService;
34 import java.util.concurrent.Executors;
35 import java.util.concurrent.TimeUnit;
36 import java.util.stream.Collectors;
37 import java.util.stream.Stream;
39 import org.eclipse.jdt.annotation.NonNullByDefault;
40 import org.eclipse.jdt.annotation.Nullable;
41 import org.openhab.core.common.NamedThreadFactory;
42 import org.openhab.core.config.discovery.AbstractDiscoveryService;
43 import org.openhab.core.config.discovery.DiscoveryResultBuilder;
44 import org.openhab.core.config.discovery.DiscoveryService;
45 import org.openhab.core.thing.ThingTypeUID;
46 import org.openhab.core.thing.ThingUID;
47 import org.osgi.service.component.annotations.Activate;
48 import org.osgi.service.component.annotations.Component;
49 import org.slf4j.Logger;
50 import org.slf4j.LoggerFactory;
53 * The {@link KaleidescapeDiscoveryService} class allow manual discovery of Kaleidescape components.
55 * @author Chris Graham - Initial contribution
56 * @author Michael Lobstein - Adapted for the Kaleidescape binding
60 @Component(service = DiscoveryService.class, configurationPid = "discovery.kaleidescape")
61 public class KaleidescapeDiscoveryService extends AbstractDiscoveryService {
62 private final Logger logger = LoggerFactory.getLogger(KaleidescapeDiscoveryService.class);
63 private static final Set<ThingTypeUID> SUPPORTED_THING_TYPES_UIDS = Collections
64 .unmodifiableSet(Stream.of(THING_TYPE_PLAYER, THING_TYPE_CINEMA_ONE, THING_TYPE_ALTO, THING_TYPE_STRATO)
65 .collect(Collectors.toSet()));
67 private static final int K_HEARTBEAT_PORT = 1443;
70 private static final String PLAYER = "Player";
71 private static final String CINEMA_ONE = "Cinema One";
72 private static final String ALTO = "Alto";
73 private static final String STRATO = "Strato";
74 private static final String STRATO_S = "Strato S";
75 private static final String DISC_VAULT = "Disc Vault";
77 private static final Set<String> ALLOWED_DEVICES = new HashSet<>(
78 Arrays.asList(PLAYER, CINEMA_ONE, ALTO, STRATO, STRATO_S, DISC_VAULT));
81 private ExecutorService executorService = null;
84 * Whether we are currently scanning or not
86 private boolean scanning;
88 private Set<String> foundIPs = new HashSet<>();
91 public KaleidescapeDiscoveryService() {
92 super(SUPPORTED_THING_TYPES_UIDS, DISCOVERY_DEFAULT_TIMEOUT_RATE_MS, DISCOVERY_DEFAULT_AUTO_DISCOVER);
96 public Set<ThingTypeUID> getSupportedThingTypes() {
97 return SUPPORTED_THING_TYPES_UIDS;
103 * Starts the scan. This discovery will:
105 * <li>Create a listening thread that opens up a broadcast {@link DatagramSocket} on port {@link #K_HEARTBEAT_PORT}
106 * and will receive any {@link DatagramPacket} that comes in</li>
107 * <li>The source IP address of the {@link DatagramPacket} is interrogated to verify it is a Kaleidescape component
108 * and will create a new thing from it</li>
110 * The process will continue until {@link #stopScan()} is called.
113 protected void startScan() {
114 logger.debug("Starting discovery of Kaleidescape components.");
116 if (executorService != null) {
120 final ExecutorService service = Executors.newFixedThreadPool(DISCOVERY_THREAD_POOL_SIZE,
121 new NamedThreadFactory("OH-binding-discovery.kaleidescape", true));
122 executorService = service;
127 service.execute(() -> {
129 DatagramSocket dSocket = new DatagramSocket(K_HEARTBEAT_PORT);
130 dSocket.setSoTimeout(DISCOVERY_DEFAULT_TIMEOUT_RATE_MS);
131 dSocket.setBroadcast(true);
134 DatagramPacket receivePacket = new DatagramPacket(new byte[1], 1);
136 dSocket.receive(receivePacket);
138 if (!foundIPs.contains(receivePacket.getAddress().getHostAddress())) {
139 String foundIp = receivePacket.getAddress().getHostAddress();
140 logger.debug("RECEIVED Kaleidescape packet from: {}", foundIp);
141 foundIPs.add(foundIp);
142 isKaleidescapeDevice(foundIp);
144 } catch (SocketTimeoutException e) {
151 } catch (IOException e) {
152 logger.debug("KaleidescapeDiscoveryService IOException: {}", e.getMessage(), e);
160 * Stops the discovery scan. We set {@link #scanning} to false (allowing the listening thread to end naturally
161 * within {@link #DISCOVERY_DEFAULT_TIMEOUT_RATE_MS} * 5 time then shutdown the {@link #ExecutorService}
164 protected synchronized void stopScan() {
166 ExecutorService service = executorService;
167 if (service == null) {
174 service.awaitTermination(DISCOVERY_DEFAULT_TIMEOUT_RATE_MS * 5, TimeUnit.MILLISECONDS);
175 } catch (InterruptedException e) {
178 executorService = null;
182 * Tries to establish a connection to the specified ip address and then interrogate the component,
183 * creates a discovery result if a valid component is found.
185 * @param ipAddress IP address to connect to
187 private void isKaleidescapeDevice(String ipAddress) {
188 try (Socket socket = new Socket()) {
189 socket.connect(new InetSocketAddress(ipAddress, DEFAULT_API_PORT), DISCOVERY_DEFAULT_IP_TIMEOUT_RATE_MS);
191 OutputStream output = socket.getOutputStream();
192 PrintWriter writer = new PrintWriter(output, true);
194 // query the component to see if it has video zones, the device type, friendly name, and serial number
195 writer.println("01/1/GET_NUM_ZONES:");
196 writer.println("01/1/GET_DEVICE_TYPE_NAME:");
197 writer.println("01/1/GET_FRIENDLY_NAME:");
198 writer.println("01/1/GET_DEVICE_INFO:");
200 InputStream input = socket.getInputStream();
202 BufferedReader reader = new BufferedReader(new InputStreamReader(input));
204 ThingTypeUID thingTypeUid = THING_TYPE_PLAYER;
205 String friendlyName = EMPTY;
206 String serialNumber = EMPTY;
207 String componentType = EMPTY;
209 String videoZone = null;
210 String audioZone = null;
213 while ((line = reader.readLine()) != null) {
214 String[] strArr = line.split(":");
216 if (strArr.length >= 4) {
219 videoZone = strArr[2];
220 audioZone = strArr[3];
222 case "DEVICE_TYPE_NAME":
223 componentType = strArr[2];
225 case "FRIENDLY_NAME":
226 friendlyName = strArr[2];
229 serialNumber = strArr[3].trim(); // take off leading zeros
233 logger.debug("isKaleidescapeDevice() - Unable to process line: {}", line);
238 // stop after reading four lines
244 // see if we have a video zone
245 if ("01".equals(videoZone)) {
246 // now check if we are one of the allowed types
247 if (ALLOWED_DEVICES.contains(componentType)) {
248 if (STRATO_S.equals(componentType) || STRATO.equals(componentType)) {
249 thingTypeUid = THING_TYPE_STRATO;
252 // A 'Player' without an audio zone is really a Strato C
253 // does not work yet, Strato C erroneously reports "01" for audio zones
254 // so we are unable to differentiate a Strato C from a Premiere player
255 if ("00".equals(audioZone) && PLAYER.equals(componentType)) {
256 thingTypeUid = THING_TYPE_STRATO;
260 if (ALTO.equals(componentType)) {
261 thingTypeUid = THING_TYPE_ALTO;
265 if (CINEMA_ONE.equals(componentType)) {
266 thingTypeUid = THING_TYPE_CINEMA_ONE;
269 // A Disc Vault with a video zone (the M700 vault), just call it a THING_TYPE_PLAYER
270 if (DISC_VAULT.equals(componentType)) {
271 thingTypeUid = THING_TYPE_PLAYER;
274 // default THING_TYPE_PLAYER
275 submitDiscoveryResults(thingTypeUid, ipAddress, friendlyName, serialNumber);
278 logger.debug("No Suitable Kaleidescape component found at IP address ({})", ipAddress);
284 } catch (IOException e) {
285 logger.debug("isKaleidescapeDevice() IOException: {}", e.getMessage());
290 * Create a new Thing with an IP address and Component type given. Uses default port.
292 * @param thingTypeUid ThingTypeUID of detected Kaleidescape component.
293 * @param ip IP address of the Kaleidescape component as a string.
294 * @param friendlyName Name of Kaleidescape component as a string.
295 * @param serialNumber Serial Number of Kaleidescape component as a string.
297 private void submitDiscoveryResults(ThingTypeUID thingTypeUid, String ip, String friendlyName,
298 String serialNumber) {
299 ThingUID uid = new ThingUID(thingTypeUid, serialNumber);
301 HashMap<String, Object> properties = new HashMap<>();
303 properties.put("host", ip);
304 properties.put("port", DEFAULT_API_PORT);
306 thingDiscovered(DiscoveryResultBuilder.create(uid).withProperties(properties).withRepresentationProperty("host")
307 .withLabel(friendlyName).build());