]> git.basschouten.com Git - openhab-addons.git/blob
7c9994dda421c6b705638f92712493b3b9d5e53c
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2024 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.kaleidescape.internal.discovery;
14
15 import static org.openhab.binding.kaleidescape.internal.KaleidescapeBindingConstants.*;
16
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;
32 import java.util.Set;
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;
38
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;
51
52 /**
53  * The {@link KaleidescapeDiscoveryService} class allow manual discovery of Kaleidescape components.
54  *
55  * @author Chris Graham - Initial contribution
56  * @author Michael Lobstein - Adapted for the Kaleidescape binding
57  *
58  */
59 @NonNullByDefault
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()));
66
67     private static final int K_HEARTBEAT_PORT = 1443;
68
69     // Component Types
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";
76
77     private static final Set<String> ALLOWED_DEVICES = new HashSet<>(
78             Arrays.asList(PLAYER, CINEMA_ONE, ALTO, STRATO, STRATO_S, DISC_VAULT));
79
80     @Nullable
81     private ExecutorService executorService = null;
82
83     /**
84      * Whether we are currently scanning or not
85      */
86     private boolean scanning;
87
88     private Set<String> foundIPs = new HashSet<>();
89
90     @Activate
91     public KaleidescapeDiscoveryService() {
92         super(SUPPORTED_THING_TYPES_UIDS, DISCOVERY_DEFAULT_TIMEOUT_RATE_MS, DISCOVERY_DEFAULT_AUTO_DISCOVER);
93     }
94
95     @Override
96     public Set<ThingTypeUID> getSupportedThingTypes() {
97         return SUPPORTED_THING_TYPES_UIDS;
98     }
99
100     /**
101      * {@inheritDoc}
102      *
103      * Starts the scan. This discovery will:
104      * <ul>
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>
109      * </ul>
110      * The process will continue until {@link #stopScan()} is called.
111      */
112     @Override
113     protected void startScan() {
114         logger.debug("Starting discovery of Kaleidescape components.");
115
116         if (executorService != null) {
117             stopScan();
118         }
119
120         final ExecutorService service = Executors.newFixedThreadPool(DISCOVERY_THREAD_POOL_SIZE,
121                 new NamedThreadFactory("OH-binding-discovery.kaleidescape", true));
122         executorService = service;
123
124         scanning = true;
125         foundIPs.clear();
126
127         service.execute(() -> {
128             try {
129                 DatagramSocket dSocket = new DatagramSocket(K_HEARTBEAT_PORT);
130                 dSocket.setSoTimeout(DISCOVERY_DEFAULT_TIMEOUT_RATE_MS);
131                 dSocket.setBroadcast(true);
132
133                 while (scanning) {
134                     DatagramPacket receivePacket = new DatagramPacket(new byte[1], 1);
135                     try {
136                         dSocket.receive(receivePacket);
137
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);
143                         }
144                     } catch (SocketTimeoutException e) {
145                         // ignore
146                         continue;
147                     }
148                 }
149
150                 dSocket.close();
151             } catch (IOException e) {
152                 logger.debug("KaleidescapeDiscoveryService IOException: {}", e.getMessage(), e);
153             }
154         });
155     }
156
157     /**
158      * {@inheritDoc}
159      *
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}
162      */
163     @Override
164     protected synchronized void stopScan() {
165         super.stopScan();
166         ExecutorService service = executorService;
167         if (service == null) {
168             return;
169         }
170
171         scanning = false;
172
173         try {
174             service.awaitTermination(DISCOVERY_DEFAULT_TIMEOUT_RATE_MS * 5, TimeUnit.MILLISECONDS);
175         } catch (InterruptedException e) {
176         }
177         service.shutdown();
178         executorService = null;
179     }
180
181     /**
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.
184      *
185      * @param ipAddress IP address to connect to
186      */
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);
190
191             OutputStream output = socket.getOutputStream();
192             PrintWriter writer = new PrintWriter(output, true);
193
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:");
199
200             InputStream input = socket.getInputStream();
201
202             BufferedReader reader = new BufferedReader(new InputStreamReader(input));
203
204             ThingTypeUID thingTypeUid = THING_TYPE_PLAYER;
205             String friendlyName = EMPTY;
206             String serialNumber = EMPTY;
207             String componentType = EMPTY;
208             String line;
209             String videoZone = null;
210             String audioZone = null;
211             int lineCount = 0;
212
213             while ((line = reader.readLine()) != null) {
214                 String[] strArr = line.split(":");
215
216                 if (strArr.length >= 4) {
217                     switch (strArr[1]) {
218                         case "NUM_ZONES":
219                             videoZone = strArr[2];
220                             audioZone = strArr[3];
221                             break;
222                         case "DEVICE_TYPE_NAME":
223                             componentType = strArr[2];
224                             break;
225                         case "FRIENDLY_NAME":
226                             friendlyName = strArr[2];
227                             break;
228                         case "DEVICE_INFO":
229                             serialNumber = strArr[3].trim(); // take off leading zeros
230                             break;
231                     }
232                 } else {
233                     logger.debug("isKaleidescapeDevice() - Unable to process line: {}", line);
234                 }
235
236                 lineCount++;
237
238                 // stop after reading four lines
239                 if (lineCount > 3) {
240                     break;
241                 }
242             }
243
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;
250                     }
251
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;
257                     }
258
259                     // Alto
260                     if (ALTO.equals(componentType)) {
261                         thingTypeUid = THING_TYPE_ALTO;
262                     }
263
264                     // Cinema One
265                     if (CINEMA_ONE.equals(componentType)) {
266                         thingTypeUid = THING_TYPE_CINEMA_ONE;
267                     }
268
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;
272                     }
273
274                     // default THING_TYPE_PLAYER
275                     submitDiscoveryResults(thingTypeUid, ipAddress, friendlyName, serialNumber);
276                 }
277             } else {
278                 logger.debug("No Suitable Kaleidescape component found at IP address ({})", ipAddress);
279             }
280             reader.close();
281             input.close();
282             writer.close();
283             output.close();
284         } catch (IOException e) {
285             logger.debug("isKaleidescapeDevice() IOException: {}", e.getMessage());
286         }
287     }
288
289     /**
290      * Create a new Thing with an IP address and Component type given. Uses default port.
291      *
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.
296      */
297     private void submitDiscoveryResults(ThingTypeUID thingTypeUid, String ip, String friendlyName,
298             String serialNumber) {
299         ThingUID uid = new ThingUID(thingTypeUid, serialNumber);
300
301         HashMap<String, Object> properties = new HashMap<>();
302
303         properties.put("host", ip);
304         properties.put("port", DEFAULT_API_PORT);
305
306         thingDiscovered(DiscoveryResultBuilder.create(uid).withProperties(properties).withRepresentationProperty("host")
307                 .withLabel(friendlyName).build());
308     }
309 }