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