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