]> git.basschouten.com Git - openhab-addons.git/blob
3c797a4ffb64bfadfb0d85f970f8394b2df035c4
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2020 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.neeo.internal.handler;
14
15 import java.io.IOException;
16 import java.net.InetSocketAddress;
17 import java.net.MalformedURLException;
18 import java.net.Socket;
19 import java.net.URL;
20 import java.util.HashMap;
21 import java.util.Hashtable;
22 import java.util.Map;
23 import java.util.Objects;
24 import java.util.concurrent.Future;
25 import java.util.concurrent.TimeUnit;
26 import java.util.concurrent.atomic.AtomicReference;
27 import java.util.concurrent.locks.Lock;
28 import java.util.concurrent.locks.ReadWriteLock;
29 import java.util.concurrent.locks.ReentrantReadWriteLock;
30
31 import javax.servlet.ServletException;
32 import javax.ws.rs.client.ClientBuilder;
33
34 import org.apache.commons.lang.StringUtils;
35 import org.eclipse.jdt.annotation.NonNullByDefault;
36 import org.eclipse.jdt.annotation.Nullable;
37 import org.openhab.binding.neeo.internal.NeeoBrainApi;
38 import org.openhab.binding.neeo.internal.NeeoBrainConfig;
39 import org.openhab.binding.neeo.internal.NeeoConstants;
40 import org.openhab.binding.neeo.internal.NeeoUtil;
41 import org.openhab.binding.neeo.internal.models.NeeoAction;
42 import org.openhab.binding.neeo.internal.models.NeeoBrain;
43 import org.openhab.core.net.NetworkAddressService;
44 import org.openhab.core.thing.Bridge;
45 import org.openhab.core.thing.ChannelUID;
46 import org.openhab.core.thing.Thing;
47 import org.openhab.core.thing.ThingStatus;
48 import org.openhab.core.thing.ThingStatusDetail;
49 import org.openhab.core.thing.binding.BaseBridgeHandler;
50 import org.openhab.core.thing.binding.ThingHandler;
51 import org.openhab.core.types.Command;
52 import org.osgi.service.http.HttpService;
53 import org.osgi.service.http.NamespaceException;
54 import org.slf4j.Logger;
55 import org.slf4j.LoggerFactory;
56
57 import com.google.gson.Gson;
58
59 /**
60  * A subclass of {@link BaseBridgeHandler} is responsible for handling commands and discovery for a
61  * {@link NeeoBrain}
62  *
63  * @author Tim Roberts - Initial contribution
64  */
65 @NonNullByDefault
66 public class NeeoBrainHandler extends BaseBridgeHandler {
67
68     /** The logger */
69     private final Logger logger = LoggerFactory.getLogger(NeeoBrainHandler.class);
70
71     /** The {@link HttpService} to register callbacks */
72     private final HttpService httpService;
73
74     /** The {@link NetworkAddressService} to use */
75     private final NetworkAddressService networkAddressService;
76
77     /** The {@link ClientBuilder} to use */
78     private final ClientBuilder clientBuilder;
79
80     /** GSON implementation - only used to deserialize {@link NeeoAction} */
81     private final Gson gson = new Gson();
82
83     /** The port the HTTP service is listening on */
84     private final int servicePort;
85
86     /**
87      * The initialization task (null until set by {@link #initializeTask()} and set back to null in {@link #dispose()}
88      */
89     private final AtomicReference<@Nullable Future<?>> initializationTask = new AtomicReference<>();
90
91     /** The check status task (not-null when connecting, null otherwise) */
92     private final AtomicReference<@Nullable Future<?>> checkStatus = new AtomicReference<>();
93
94     /** The lock that protected multi-threaded access to the state variables */
95     private final ReadWriteLock stateLock = new ReentrantReadWriteLock();
96
97     /** The {@link NeeoBrainApi} (null until set by {@link #initializationTask}) */
98     @Nullable
99     private NeeoBrainApi neeoBrainApi;
100
101     /** The path to the forward action servlet - will be null if not enabled */
102     @Nullable
103     private String servletPath;
104
105     /** The servlet for forward actions - will be null if not enabled */
106     @Nullable
107     private NeeoForwardActionsServlet forwardActionServlet;
108
109     /**
110      * Instantiates a new neeo brain handler from the {@link Bridge}, service port, {@link HttpService} and
111      * {@link NetworkAddressService}.
112      *
113      * @param bridge the non-null {@link Bridge}
114      * @param servicePort the service port the http service is listening on
115      * @param httpService the non-null {@link HttpService}
116      * @param networkAddressService the non-null {@link NetworkAddressService}
117      */
118     NeeoBrainHandler(Bridge bridge, int servicePort, HttpService httpService,
119             NetworkAddressService networkAddressService, ClientBuilder clientBuilder) {
120         super(bridge);
121
122         Objects.requireNonNull(bridge, "bridge cannot be null");
123         Objects.requireNonNull(httpService, "httpService cannot be null");
124         Objects.requireNonNull(networkAddressService, "networkAddressService cannot be null");
125
126         this.servicePort = servicePort;
127         this.httpService = httpService;
128         this.networkAddressService = networkAddressService;
129         this.clientBuilder = clientBuilder;
130     }
131
132     /**
133      * Handles any {@Commands} sent - this bridge has no commands and does nothing
134      *
135      * @see
136      *      org.openhab.core.thing.binding.ThingHandler#handleCommand(org.openhab.core.thing.ChannelUID,
137      *      org.openhab.core.types.Command)
138      */
139     @Override
140     public void handleCommand(ChannelUID channelUID, Command command) {
141     }
142
143     /**
144      * Simply cancels any existing initialization tasks and schedules a new task
145      *
146      * @see org.openhab.core.thing.binding.BaseThingHandler#initialize()
147      */
148     @Override
149     public void initialize() {
150         NeeoUtil.cancel(initializationTask.getAndSet(scheduler.submit(() -> {
151             initializeTask();
152         })));
153     }
154
155     /**
156      * Initializes the bridge by connecting to the configuration ip address and parsing the results. Properties will be
157      * set and the thing will go online.
158      */
159     private void initializeTask() {
160         final Lock writerLock = stateLock.writeLock();
161         writerLock.lock();
162         try {
163             NeeoUtil.checkInterrupt();
164
165             final NeeoBrainConfig config = getBrainConfig();
166             final String ipAddress = config.getIpAddress();
167             if (ipAddress == null || StringUtils.isEmpty(ipAddress)) {
168                 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
169                         "Brain IP Address must be specified");
170                 return;
171             }
172             final NeeoBrainApi api = new NeeoBrainApi(ipAddress, clientBuilder);
173             final NeeoBrain brain = api.getBrain();
174             final String brainId = getNeeoBrainId();
175
176             NeeoUtil.checkInterrupt();
177             neeoBrainApi = api;
178
179             final Map<String, String> properties = new HashMap<>();
180             addProperty(properties, "Name", brain.getName());
181             addProperty(properties, "Version", brain.getVersion());
182             addProperty(properties, "Label", brain.getLabel());
183             addProperty(properties, "Is Configured", String.valueOf(brain.isConfigured()));
184             addProperty(properties, "Key", brain.getKey());
185             addProperty(properties, "AirKey", brain.getAirkey());
186             addProperty(properties, "Last Change", String.valueOf(brain.getLastChange()));
187             updateProperties(properties);
188
189             if (config.isEnableForwardActions()) {
190                 NeeoUtil.checkInterrupt();
191
192                 forwardActionServlet = new NeeoForwardActionsServlet(scheduler, json -> {
193                     triggerChannel(NeeoConstants.CHANNEL_BRAIN_FOWARDACTIONS, json);
194
195                     final NeeoAction action = Objects.requireNonNull(gson.fromJson(json, NeeoAction.class));
196
197                     for (final Thing child : getThing().getThings()) {
198                         final ThingHandler th = child.getHandler();
199                         if (th instanceof NeeoRoomHandler) {
200                             ((NeeoRoomHandler) th).processAction(action);
201                         }
202                     }
203                 }, config.getForwardChain(), clientBuilder);
204
205                 NeeoUtil.checkInterrupt();
206                 try {
207                     servletPath = NeeoConstants.WEBAPP_FORWARDACTIONS.replace("{brainid}", brainId);
208
209                     httpService.registerServlet(servletPath, forwardActionServlet, new Hashtable<>(),
210                             httpService.createDefaultHttpContext());
211
212                     final URL callbackURL = createCallbackUrl(brainId, config);
213                     if (callbackURL == null) {
214                         logger.debug(
215                                 "Unable to create a callback URL because there is no primary address specified (please set the primary address in the configuration)");
216                     } else {
217                         final URL url = new URL(callbackURL, servletPath);
218                         api.registerForwardActions(url);
219                     }
220                 } catch (NamespaceException | ServletException e) {
221                     logger.debug("Error registering forward actions to {}: {}", servletPath, e.getMessage(), e);
222                 }
223             }
224
225             NeeoUtil.checkInterrupt();
226             updateStatus(ThingStatus.ONLINE);
227             NeeoUtil.checkInterrupt();
228             if (config.getCheckStatusInterval() > 0) {
229                 NeeoUtil.cancel(checkStatus.getAndSet(scheduler.scheduleWithFixedDelay(() -> {
230                     try {
231                         NeeoUtil.checkInterrupt();
232                         checkStatus(ipAddress);
233                     } catch (InterruptedException e) {
234                         // do nothing - we were interrupted and should stop
235                     }
236                 }, config.getCheckStatusInterval(), config.getCheckStatusInterval(), TimeUnit.SECONDS)));
237             }
238         } catch (IOException e) {
239             logger.debug("Exception occurred connecting to brain: {}", e.getMessage(), e);
240             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
241                     "Exception occurred connecting to brain: " + e.getMessage());
242         } catch (InterruptedException e) {
243             logger.debug("Initializtion was interrupted", e);
244             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.HANDLER_INITIALIZING_ERROR,
245                     "Initialization was interrupted");
246         } finally {
247             writerLock.unlock();
248         }
249     }
250
251     /**
252      * Helper method to add a property to the properties map if the value is not null
253      *
254      * @param properties a non-null properties map
255      * @param key a non-null, non-empty key
256      * @param value a possibly null, possibly empty key
257      */
258     private void addProperty(Map<String, String> properties, String key, @Nullable String value) {
259         Objects.requireNonNull(properties, "properties cannot be null");
260         NeeoUtil.requireNotEmpty(key, "key cannot be empty");
261         if (value != null && StringUtils.isNotEmpty(value)) {
262             properties.put(key, value);
263         }
264     }
265
266     /**
267      * Gets the {@link NeeoBrainApi} used by this bridge
268      *
269      * @return a possibly null {@link NeeoBrainApi}
270      */
271     @Nullable
272     public NeeoBrainApi getNeeoBrainApi() {
273         final Lock readerLock = stateLock.readLock();
274         readerLock.lock();
275         try {
276             return neeoBrainApi;
277         } finally {
278             readerLock.unlock();
279         }
280     }
281
282     /**
283      * Gets the brain id used by this bridge
284      *
285      * @return a non-null, non-empty brain id
286      */
287     public String getNeeoBrainId() {
288         return getThing().getUID().getId();
289     }
290
291     /**
292      * Helper method to get the {@link NeeoBrainConfig}
293      *
294      * @return the {@link NeeoBrainConfig}
295      */
296     private NeeoBrainConfig getBrainConfig() {
297         return getConfigAs(NeeoBrainConfig.class);
298     }
299
300     /**
301      * Checks the status of the brain via a quick socket connection. If the status is unavailable and we are
302      * {@link ThingStatus#ONLINE}, then we go {@link ThingStatus#OFFLINE}. If the status is available and we are
303      * {@link ThingStatus#OFFLINE}, we go {@link ThingStatus#ONLINE}.
304      *
305      * @param ipAddress a non-null, non-empty IP address
306      */
307     private void checkStatus(String ipAddress) {
308         NeeoUtil.requireNotEmpty(ipAddress, "ipAddress cannot be empty");
309
310         try {
311             try (Socket soc = new Socket()) {
312                 soc.connect(new InetSocketAddress(ipAddress, NeeoConstants.DEFAULT_BRAIN_PORT), 5000);
313             }
314             logger.debug("Checking connectivity to {}:{} - successful", ipAddress, NeeoConstants.DEFAULT_BRAIN_PORT);
315
316             if (getThing().getStatus() != ThingStatus.ONLINE) {
317                 updateStatus(ThingStatus.ONLINE);
318             }
319         } catch (IOException e) {
320             if (getThing().getStatus() == ThingStatus.ONLINE) {
321                 logger.debug("Checking connectivity to {}:{} - unsuccessful - going offline: {}", ipAddress,
322                         NeeoConstants.DEFAULT_BRAIN_PORT, e.getMessage(), e);
323                 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
324                         "Exception occurred connecting to brain: " + e.getMessage());
325             } else {
326                 logger.debug("Checking connectivity to {}:{} - unsuccessful - still offline", ipAddress,
327                         NeeoConstants.DEFAULT_BRAIN_PORT);
328             }
329         }
330     }
331
332     /**
333      * Disposes of the bridge by closing/removing the {@link #neeoBrainApi} and canceling/removing any pending
334      * {@link #initializeTask()}
335      */
336     @Override
337     public void dispose() {
338         final Lock writerLock = stateLock.writeLock();
339         writerLock.lock();
340         try {
341             final NeeoBrainApi api = neeoBrainApi;
342             neeoBrainApi = null;
343
344             NeeoUtil.cancel(initializationTask.getAndSet(null));
345             NeeoUtil.cancel(checkStatus.getAndSet(null));
346
347             if (forwardActionServlet != null) {
348                 forwardActionServlet = null;
349
350                 if (api != null) {
351                     try {
352                         api.deregisterForwardActions();
353                     } catch (IOException e) {
354                         logger.debug("IOException occurred deregistering the forward actions: {}", e.getMessage(), e);
355                     }
356                 }
357
358                 if (servletPath != null) {
359                     httpService.unregister(servletPath);
360                     servletPath = null;
361                 }
362             }
363
364             NeeoUtil.close(api);
365         } finally {
366             writerLock.unlock();
367         }
368     }
369
370     /**
371      * Creates the URL the brain should callback. Note: if there is multiple interfaces, we try to prefer the one on the
372      * same subnet as the brain
373      *
374      * @param brainId the non-null, non-empty brain identifier
375      * @param config the non-null brain configuration
376      * @return the callback URL
377      * @throws MalformedURLException if the URL is malformed
378      */
379     @Nullable
380     private URL createCallbackUrl(String brainId, NeeoBrainConfig config) throws MalformedURLException {
381         NeeoUtil.requireNotEmpty(brainId, "brainId cannot be empty");
382         Objects.requireNonNull(config, "config cannot be null");
383
384         final String ipAddress = networkAddressService.getPrimaryIpv4HostAddress();
385         if (ipAddress == null) {
386             logger.debug("No network interface could be found.");
387             return null;
388         }
389
390         return new URL("http://" + ipAddress + ":" + servicePort);
391     }
392 }