2 * Copyright (c) 2010-2020 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.neeo.internal.handler;
15 import java.io.IOException;
16 import java.net.InetSocketAddress;
17 import java.net.MalformedURLException;
18 import java.net.Socket;
20 import java.util.HashMap;
21 import java.util.Hashtable;
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;
31 import javax.servlet.ServletException;
32 import javax.ws.rs.client.ClientBuilder;
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;
57 import com.google.gson.Gson;
60 * A subclass of {@link BaseBridgeHandler} is responsible for handling commands and discovery for a
63 * @author Tim Roberts - Initial contribution
66 public class NeeoBrainHandler extends BaseBridgeHandler {
69 private final Logger logger = LoggerFactory.getLogger(NeeoBrainHandler.class);
71 /** The {@link HttpService} to register callbacks */
72 private final HttpService httpService;
74 /** The {@link NetworkAddressService} to use */
75 private final NetworkAddressService networkAddressService;
77 /** The {@link ClientBuilder} to use */
78 private final ClientBuilder clientBuilder;
80 /** GSON implementation - only used to deserialize {@link NeeoAction} */
81 private final Gson gson = new Gson();
83 /** The port the HTTP service is listening on */
84 private final int servicePort;
87 * The initialization task (null until set by {@link #initializeTask()} and set back to null in {@link #dispose()}
89 private final AtomicReference<@Nullable Future<?>> initializationTask = new AtomicReference<>();
91 /** The check status task (not-null when connecting, null otherwise) */
92 private final AtomicReference<@Nullable Future<?>> checkStatus = new AtomicReference<>();
94 /** The lock that protected multi-threaded access to the state variables */
95 private final ReadWriteLock stateLock = new ReentrantReadWriteLock();
97 /** The {@link NeeoBrainApi} (null until set by {@link #initializationTask}) */
99 private NeeoBrainApi neeoBrainApi;
101 /** The path to the forward action servlet - will be null if not enabled */
103 private String servletPath;
105 /** The servlet for forward actions - will be null if not enabled */
107 private NeeoForwardActionsServlet forwardActionServlet;
110 * Instantiates a new neeo brain handler from the {@link Bridge}, service port, {@link HttpService} and
111 * {@link NetworkAddressService}.
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}
118 NeeoBrainHandler(Bridge bridge, int servicePort, HttpService httpService,
119 NetworkAddressService networkAddressService, ClientBuilder clientBuilder) {
122 Objects.requireNonNull(bridge, "bridge cannot be null");
123 Objects.requireNonNull(httpService, "httpService cannot be null");
124 Objects.requireNonNull(networkAddressService, "networkAddressService cannot be null");
126 this.servicePort = servicePort;
127 this.httpService = httpService;
128 this.networkAddressService = networkAddressService;
129 this.clientBuilder = clientBuilder;
133 * Handles any {@Commands} sent - this bridge has no commands and does nothing
136 * org.openhab.core.thing.binding.ThingHandler#handleCommand(org.openhab.core.thing.ChannelUID,
137 * org.openhab.core.types.Command)
140 public void handleCommand(ChannelUID channelUID, Command command) {
144 * Simply cancels any existing initialization tasks and schedules a new task
146 * @see org.openhab.core.thing.binding.BaseThingHandler#initialize()
149 public void initialize() {
150 NeeoUtil.cancel(initializationTask.getAndSet(scheduler.submit(() -> {
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.
159 private void initializeTask() {
160 final Lock writerLock = stateLock.writeLock();
163 NeeoUtil.checkInterrupt();
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");
172 final NeeoBrainApi api = new NeeoBrainApi(ipAddress, clientBuilder);
173 final NeeoBrain brain = api.getBrain();
174 final String brainId = getNeeoBrainId();
176 NeeoUtil.checkInterrupt();
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);
189 if (config.isEnableForwardActions()) {
190 NeeoUtil.checkInterrupt();
192 forwardActionServlet = new NeeoForwardActionsServlet(scheduler, json -> {
193 triggerChannel(NeeoConstants.CHANNEL_BRAIN_FOWARDACTIONS, json);
195 final NeeoAction action = Objects.requireNonNull(gson.fromJson(json, NeeoAction.class));
197 for (final Thing child : getThing().getThings()) {
198 final ThingHandler th = child.getHandler();
199 if (th instanceof NeeoRoomHandler) {
200 ((NeeoRoomHandler) th).processAction(action);
203 }, config.getForwardChain(), clientBuilder);
205 NeeoUtil.checkInterrupt();
207 servletPath = NeeoConstants.WEBAPP_FORWARDACTIONS.replace("{brainid}", brainId);
209 httpService.registerServlet(servletPath, forwardActionServlet, new Hashtable<>(),
210 httpService.createDefaultHttpContext());
212 final URL callbackURL = createCallbackUrl(brainId, config);
213 if (callbackURL == null) {
215 "Unable to create a callback URL because there is no primary address specified (please set the primary address in the configuration)");
217 final URL url = new URL(callbackURL, servletPath);
218 api.registerForwardActions(url);
220 } catch (NamespaceException | ServletException e) {
221 logger.debug("Error registering forward actions to {}: {}", servletPath, e.getMessage(), e);
225 NeeoUtil.checkInterrupt();
226 updateStatus(ThingStatus.ONLINE);
227 NeeoUtil.checkInterrupt();
228 if (config.getCheckStatusInterval() > 0) {
229 NeeoUtil.cancel(checkStatus.getAndSet(scheduler.scheduleWithFixedDelay(() -> {
231 NeeoUtil.checkInterrupt();
232 checkStatus(ipAddress);
233 } catch (InterruptedException e) {
234 // do nothing - we were interrupted and should stop
236 }, config.getCheckStatusInterval(), config.getCheckStatusInterval(), TimeUnit.SECONDS)));
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");
252 * Helper method to add a property to the properties map if the value is not null
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
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);
267 * Gets the {@link NeeoBrainApi} used by this bridge
269 * @return a possibly null {@link NeeoBrainApi}
272 public NeeoBrainApi getNeeoBrainApi() {
273 final Lock readerLock = stateLock.readLock();
283 * Gets the brain id used by this bridge
285 * @return a non-null, non-empty brain id
287 public String getNeeoBrainId() {
288 return getThing().getUID().getId();
292 * Helper method to get the {@link NeeoBrainConfig}
294 * @return the {@link NeeoBrainConfig}
296 private NeeoBrainConfig getBrainConfig() {
297 return getConfigAs(NeeoBrainConfig.class);
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}.
305 * @param ipAddress a non-null, non-empty IP address
307 private void checkStatus(String ipAddress) {
308 NeeoUtil.requireNotEmpty(ipAddress, "ipAddress cannot be empty");
311 try (Socket soc = new Socket()) {
312 soc.connect(new InetSocketAddress(ipAddress, NeeoConstants.DEFAULT_BRAIN_PORT), 5000);
314 logger.debug("Checking connectivity to {}:{} - successful", ipAddress, NeeoConstants.DEFAULT_BRAIN_PORT);
316 if (getThing().getStatus() != ThingStatus.ONLINE) {
317 updateStatus(ThingStatus.ONLINE);
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());
326 logger.debug("Checking connectivity to {}:{} - unsuccessful - still offline", ipAddress,
327 NeeoConstants.DEFAULT_BRAIN_PORT);
333 * Disposes of the bridge by closing/removing the {@link #neeoBrainApi} and canceling/removing any pending
334 * {@link #initializeTask()}
337 public void dispose() {
338 final Lock writerLock = stateLock.writeLock();
341 final NeeoBrainApi api = neeoBrainApi;
344 NeeoUtil.cancel(initializationTask.getAndSet(null));
345 NeeoUtil.cancel(checkStatus.getAndSet(null));
347 if (forwardActionServlet != null) {
348 forwardActionServlet = null;
352 api.deregisterForwardActions();
353 } catch (IOException e) {
354 logger.debug("IOException occurred deregistering the forward actions: {}", e.getMessage(), e);
358 if (servletPath != null) {
359 httpService.unregister(servletPath);
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
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
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");
384 final String ipAddress = networkAddressService.getPrimaryIpv4HostAddress();
385 if (ipAddress == null) {
386 logger.debug("No network interface could be found.");
390 return new URL("http://" + ipAddress + ":" + servicePort);