]> git.basschouten.com Git - openhab-addons.git/blob
be17626a9a7199027a6c1d86d1fba870a2127802
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2022 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.neohub.internal;
14
15 import static org.openhab.binding.neohub.internal.NeoHubBindingConstants.*;
16
17 import java.io.IOException;
18 import java.time.Instant;
19 import java.time.temporal.ChronoUnit;
20 import java.util.HashMap;
21 import java.util.List;
22 import java.util.Map;
23 import java.util.concurrent.ScheduledFuture;
24 import java.util.concurrent.TimeUnit;
25 import java.util.concurrent.atomic.AtomicInteger;
26
27 import javax.measure.Unit;
28
29 import org.eclipse.jdt.annotation.NonNullByDefault;
30 import org.eclipse.jdt.annotation.Nullable;
31 import org.openhab.binding.neohub.internal.NeoHubAbstractDeviceData.AbstractRecord;
32 import org.openhab.binding.neohub.internal.NeoHubBindingConstants.NeoHubReturnResult;
33 import org.openhab.core.library.types.QuantityType;
34 import org.openhab.core.library.unit.SIUnits;
35 import org.openhab.core.library.unit.Units;
36 import org.openhab.core.thing.Bridge;
37 import org.openhab.core.thing.ChannelUID;
38 import org.openhab.core.thing.Thing;
39 import org.openhab.core.thing.ThingStatus;
40 import org.openhab.core.thing.ThingStatusDetail;
41 import org.openhab.core.thing.binding.BaseBridgeHandler;
42 import org.openhab.core.thing.binding.ThingHandler;
43 import org.openhab.core.types.Command;
44 import org.openhab.core.types.State;
45 import org.openhab.core.types.UnDefType;
46 import org.slf4j.Logger;
47 import org.slf4j.LoggerFactory;
48
49 import com.google.gson.JsonSyntaxException;
50
51 /**
52  * The {@link NeoHubHandler} is the openHAB Handler for NeoHub devices
53  *
54  * @author Andrew Fiddian-Green - Initial contribution (v2.x binding code)
55  * @author Sebastian Prehn - Initial contribution (v1.x hub communication)
56  *
57  */
58 @NonNullByDefault
59 public class NeoHubHandler extends BaseBridgeHandler {
60
61     private static final String SEE_README = "See documentation chapter \"Connection Refused Errors\"";
62
63     private final Logger logger = LoggerFactory.getLogger(NeoHubHandler.class);
64
65     private final Map<String, Boolean> connectionStates = new HashMap<>();
66
67     private @Nullable NeoHubConfiguration config;
68     private @Nullable NeoHubSocket socket;
69     private @Nullable ScheduledFuture<?> lazyPollingScheduler;
70     private @Nullable ScheduledFuture<?> fastPollingScheduler;
71
72     private final AtomicInteger fastPollingCallsToGo = new AtomicInteger();
73
74     private @Nullable NeoHubReadDcbResponse systemData = null;
75
76     private enum ApiVersion {
77         LEGACY("legacy"),
78         NEW("new");
79
80         public final String label;
81
82         private ApiVersion(String label) {
83             this.label = label;
84         }
85     }
86
87     private ApiVersion apiVersion = ApiVersion.LEGACY;
88     private boolean isApiOnline = false;
89
90     public NeoHubHandler(Bridge bridge) {
91         super(bridge);
92     }
93
94     @Override
95     public void handleCommand(ChannelUID channelUID, Command command) {
96         // future: currently there is nothing to do for a NeoHub
97     }
98
99     @Override
100     public void initialize() {
101         NeoHubConfiguration config = getConfigAs(NeoHubConfiguration.class);
102
103         if (logger.isDebugEnabled()) {
104             logger.debug("hub '{}' hostname={}", getThing().getUID(), config.hostName);
105         }
106
107         if (!MATCHER_IP_ADDRESS.matcher(config.hostName).matches()) {
108             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "parameter hostName must be set!");
109             return;
110         }
111
112         if (logger.isDebugEnabled()) {
113             logger.debug("hub '{}' port={}", getThing().getUID(), config.portNumber);
114         }
115
116         if (config.portNumber <= 0 || config.portNumber > 0xFFFF) {
117             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "portNumber is invalid!");
118             return;
119         }
120
121         if (logger.isDebugEnabled()) {
122             logger.debug("hub '{}' polling interval={}", getThing().getUID(), config.pollingInterval);
123         }
124
125         if (config.pollingInterval < FAST_POLL_INTERVAL || config.pollingInterval > LAZY_POLL_INTERVAL) {
126             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, String
127                     .format("pollingInterval must be in range [%d..%d]!", FAST_POLL_INTERVAL, LAZY_POLL_INTERVAL));
128             return;
129         }
130
131         if (logger.isDebugEnabled()) {
132             logger.debug("hub '{}' socketTimeout={}", getThing().getUID(), config.socketTimeout);
133         }
134
135         if (config.socketTimeout < 5 || config.socketTimeout > 20) {
136             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
137                     String.format("socketTimeout must be in range [%d..%d]!", 5, 20));
138             return;
139         }
140
141         if (logger.isDebugEnabled()) {
142             logger.debug("hub '{}' preferLegacyApi={}", getThing().getUID(), config.preferLegacyApi);
143         }
144
145         NeoHubSocket socket = this.socket = new NeoHubSocket(config.hostName, config.portNumber, config.socketTimeout);
146         this.config = config;
147
148         /*
149          * Try to 'ping' the hub, and if there is a 'connection refused', it is probably due to the mobile App |
150          * Settings | Legacy API Enable switch not being On, so go offline and log a warning message.
151          */
152         try {
153             socket.sendMessage(CMD_CODE_FIRMWARE);
154         } catch (IOException e) {
155             String error = e.getMessage();
156             if (error != null && error.toLowerCase().startsWith("connection refused")) {
157                 logger.warn("CONNECTION REFUSED!! (hub '{}') => {}", getThing().getUID(), SEE_README);
158                 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, SEE_README);
159                 return;
160             }
161         } catch (NeoHubException e) {
162             // NeoHubException won't actually occur here
163         }
164
165         if (logger.isDebugEnabled()) {
166             logger.debug("hub '{}' start background polling..", getThing().getUID());
167         }
168
169         // create a "lazy" polling scheduler
170         ScheduledFuture<?> lazy = this.lazyPollingScheduler;
171         if (lazy == null || lazy.isCancelled()) {
172             this.lazyPollingScheduler = scheduler.scheduleWithFixedDelay(this::lazyPollingSchedulerExecute,
173                     config.pollingInterval, config.pollingInterval, TimeUnit.SECONDS);
174         }
175
176         // create a "fast" polling scheduler
177         fastPollingCallsToGo.set(FAST_POLL_CYCLES);
178         ScheduledFuture<?> fast = this.fastPollingScheduler;
179         if (fast == null || fast.isCancelled()) {
180             this.fastPollingScheduler = scheduler.scheduleWithFixedDelay(this::fastPollingSchedulerExecute,
181                     FAST_POLL_INTERVAL, FAST_POLL_INTERVAL, TimeUnit.SECONDS);
182         }
183
184         updateStatus(ThingStatus.UNKNOWN);
185
186         // start a fast polling burst to ensure the NeHub is initialized quickly
187         startFastPollingBurst();
188     }
189
190     @Override
191     public void dispose() {
192         if (logger.isDebugEnabled()) {
193             logger.debug("hub '{}' stop background polling..", getThing().getUID());
194         }
195
196         // clean up the lazy polling scheduler
197         ScheduledFuture<?> lazy = this.lazyPollingScheduler;
198         if (lazy != null && !lazy.isCancelled()) {
199             lazy.cancel(true);
200             this.lazyPollingScheduler = null;
201         }
202
203         // clean up the fast polling scheduler
204         ScheduledFuture<?> fast = this.fastPollingScheduler;
205         if (fast != null && !fast.isCancelled()) {
206             fast.cancel(true);
207             this.fastPollingScheduler = null;
208         }
209     }
210
211     /*
212      * device handlers call this to initiate a burst of fast polling requests (
213      * improves response time to users when openHAB changes a channel value )
214      */
215     public void startFastPollingBurst() {
216         fastPollingCallsToGo.set(FAST_POLL_CYCLES);
217     }
218
219     /*
220      * device handlers call this method to issue commands to the NeoHub
221      */
222     public synchronized NeoHubReturnResult toNeoHubSendChannelValue(String commandStr) {
223         NeoHubSocket socket = this.socket;
224
225         if (socket == null || config == null) {
226             return NeoHubReturnResult.ERR_INITIALIZATION;
227         }
228
229         try {
230             socket.sendMessage(commandStr);
231
232             // start a fast polling burst (to confirm the status change)
233             startFastPollingBurst();
234
235             return NeoHubReturnResult.SUCCEEDED;
236         } catch (Exception e) {
237             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR);
238             logger.warn(MSG_FMT_SET_VALUE_ERR, getThing().getUID(), commandStr, e.getMessage());
239             return NeoHubReturnResult.ERR_COMMUNICATION;
240         }
241     }
242
243     /**
244      * sends a JSON request to the NeoHub to read the device data
245      *
246      * @return a class that contains the full status of all devices
247      */
248     protected @Nullable NeoHubAbstractDeviceData fromNeoHubGetDeviceData() {
249         NeoHubSocket socket = this.socket;
250
251         if (socket == null || config == null) {
252             logger.warn(MSG_HUB_CONFIG, getThing().getUID());
253             return null;
254         }
255
256         try {
257             String responseJson;
258             NeoHubAbstractDeviceData deviceData;
259
260             if (apiVersion == ApiVersion.LEGACY) {
261                 responseJson = socket.sendMessage(CMD_CODE_INFO);
262                 deviceData = NeoHubInfoResponse.createDeviceData(responseJson);
263             } else {
264                 responseJson = socket.sendMessage(CMD_CODE_GET_LIVE_DATA);
265                 deviceData = NeoHubLiveDeviceData.createDeviceData(responseJson);
266             }
267
268             if (deviceData == null) {
269                 logger.warn(MSG_FMT_DEVICE_POLL_ERR, getThing().getUID(), "failed to create device data response");
270                 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR);
271                 return null;
272             }
273
274             @Nullable
275             List<? extends AbstractRecord> devices = deviceData.getDevices();
276             if (devices == null || devices.isEmpty()) {
277                 logger.warn(MSG_FMT_DEVICE_POLL_ERR, getThing().getUID(), "no devices found");
278                 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR);
279                 return null;
280             }
281
282             if (getThing().getStatus() != ThingStatus.ONLINE) {
283                 updateStatus(ThingStatus.ONLINE, ThingStatusDetail.NONE);
284             }
285
286             // check if we also need to discard and update systemData
287             NeoHubReadDcbResponse systemData = this.systemData;
288             if (systemData != null) {
289                 if (deviceData instanceof NeoHubLiveDeviceData) {
290                     /*
291                      * note: time-stamps are measured in seconds from 1970-01-01T00:00:00Z
292                      *
293                      * new API: discard systemData if its time-stamp is older than the system
294                      * time-stamp on the hub
295                      */
296                     if (systemData.timeStamp < ((NeoHubLiveDeviceData) deviceData).getTimestampSystem()) {
297                         this.systemData = null;
298                     }
299                 } else {
300                     /*
301                      * note: time-stamps are measured in seconds from 1970-01-01T00:00:00Z
302                      *
303                      * legacy API: discard systemData if its time-stamp is older than one hour
304                      */
305                     if (systemData.timeStamp < Instant.now().minus(1, ChronoUnit.HOURS).getEpochSecond()) {
306                         this.systemData = null;
307                     }
308                 }
309             }
310
311             return deviceData;
312         } catch (Exception e) {
313             logger.warn(MSG_FMT_DEVICE_POLL_ERR, getThing().getUID(), e.getMessage());
314             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR);
315             return null;
316         }
317     }
318
319     /**
320      * sends a JSON request to the NeoHub to read the system data
321      *
322      * @return a class that contains the status of the system
323      */
324     protected @Nullable NeoHubReadDcbResponse fromNeoHubReadSystemData() {
325         NeoHubSocket socket = this.socket;
326
327         if (socket == null) {
328             return null;
329         }
330
331         try {
332             String responseJson;
333             NeoHubReadDcbResponse systemData;
334
335             if (apiVersion == ApiVersion.LEGACY) {
336                 responseJson = socket.sendMessage(CMD_CODE_READ_DCB);
337                 systemData = NeoHubReadDcbResponse.createSystemData(responseJson);
338             } else {
339                 responseJson = socket.sendMessage(CMD_CODE_GET_SYSTEM);
340                 systemData = NeoHubReadDcbResponse.createSystemData(responseJson);
341             }
342
343             if (systemData == null) {
344                 logger.warn(MSG_FMT_SYSTEM_POLL_ERR, getThing().getUID(), "failed to create system data response");
345                 return null;
346             }
347
348             String physicalFirmware = systemData.getFirmwareVersion();
349             if (physicalFirmware != null) {
350                 String thingFirmware = getThing().getProperties().get(PROPERTY_FIRMWARE_VERSION);
351                 if (!physicalFirmware.equals(thingFirmware)) {
352                     getThing().setProperty(PROPERTY_FIRMWARE_VERSION, physicalFirmware);
353                 }
354             }
355
356             return systemData;
357         } catch (Exception e) {
358             logger.warn(MSG_FMT_SYSTEM_POLL_ERR, getThing().getUID(), e.getMessage());
359             return null;
360         }
361     }
362
363     /*
364      * this is the callback used by the lazy polling scheduler.. fetches the info
365      * for all devices from the NeoHub, and passes the results the respective device
366      * handlers
367      */
368     private synchronized void lazyPollingSchedulerExecute() {
369         // check which API is supported
370         if (!isApiOnline) {
371             selectApi();
372         }
373
374         NeoHubAbstractDeviceData deviceData = fromNeoHubGetDeviceData();
375         if (deviceData != null) {
376             // dispatch deviceData to each of the hub's owned devices ..
377             List<Thing> children = getThing().getThings();
378             for (Thing child : children) {
379                 ThingHandler device = child.getHandler();
380                 if (device instanceof NeoBaseHandler) {
381                     ((NeoBaseHandler) device).toBaseSendPollResponse(deviceData);
382                 }
383             }
384
385             // evaluate and update the state of our RF mesh QoS channel
386             List<? extends AbstractRecord> devices = deviceData.getDevices();
387             State state;
388             String property;
389
390             if (devices == null || devices.isEmpty()) {
391                 state = UnDefType.UNDEF;
392                 property = "[?/?]";
393             } else {
394                 int totalDeviceCount = devices.size();
395                 int onlineDeviceCount = 0;
396
397                 for (AbstractRecord device : devices) {
398                     String deviceName = device.getDeviceName();
399                     Boolean online = !device.offline();
400
401                     @Nullable
402                     Boolean onlineBefore = connectionStates.put(deviceName, online);
403                     /*
404                      * note: we use logger.info() here to log changes; reason is that the average user does really need
405                      * to know if a device (very occasionally) drops out of the normally reliable RF mesh; however we
406                      * only log it if 1) the state has changed, and 2) either 2a) the device has already been discovered
407                      * by the bridge handler, or 2b) logger debug mode is set
408                      */
409                     if (!online.equals(onlineBefore) && ((onlineBefore != null) || logger.isDebugEnabled())) {
410                         logger.info("hub '{}' device \"{}\" has {} the RF mesh network", getThing().getUID(),
411                                 deviceName, online.booleanValue() ? "joined" : "left");
412                     }
413
414                     if (online.booleanValue()) {
415                         onlineDeviceCount++;
416                     }
417                 }
418                 property = String.format("[%d/%d]", onlineDeviceCount, totalDeviceCount);
419                 state = new QuantityType<>((100.0 * onlineDeviceCount) / totalDeviceCount, Units.PERCENT);
420             }
421             getThing().setProperty(PROPERTY_API_DEVICEINFO, property);
422             updateState(CHAN_MESH_NETWORK_QOS, state);
423         }
424         if (fastPollingCallsToGo.get() > 0) {
425             fastPollingCallsToGo.decrementAndGet();
426         }
427     }
428
429     /*
430      * this is the callback used by the fast polling scheduler.. checks if a fast
431      * polling burst is scheduled, and if so calls lazyPollingSchedulerExecute
432      */
433     private void fastPollingSchedulerExecute() {
434         if (fastPollingCallsToGo.get() > 0) {
435             lazyPollingSchedulerExecute();
436         }
437     }
438
439     /*
440      * select whether to use the old "deprecated" API or the new API
441      */
442     private void selectApi() {
443         boolean supportsLegacyApi = false;
444         boolean supportsFutureApi = false;
445
446         NeoHubSocket socket = this.socket;
447         if (socket != null) {
448             String responseJson;
449             NeoHubReadDcbResponse systemData;
450
451             try {
452                 responseJson = socket.sendMessage(CMD_CODE_READ_DCB);
453                 systemData = NeoHubReadDcbResponse.createSystemData(responseJson);
454                 supportsLegacyApi = systemData != null;
455                 if (!supportsLegacyApi) {
456                     throw new NeoHubException("legacy API not supported");
457                 }
458             } catch (JsonSyntaxException | NeoHubException | IOException e) {
459                 // we learned that this API is not currently supported; no big deal
460                 logger.debug("hub '{}' legacy API is not supported!", getThing().getUID());
461             }
462             try {
463                 responseJson = socket.sendMessage(CMD_CODE_GET_SYSTEM);
464                 systemData = NeoHubReadDcbResponse.createSystemData(responseJson);
465                 supportsFutureApi = systemData != null;
466                 if (!supportsFutureApi) {
467                     throw new NeoHubException(String.format("hub '%s' new API not supported", getThing().getUID()));
468                 }
469             } catch (JsonSyntaxException | NeoHubException | IOException e) {
470                 // we learned that this API is not currently supported; no big deal
471                 logger.debug("hub '{}' new API is not supported!", getThing().getUID());
472             }
473         }
474
475         if (!supportsLegacyApi && !supportsFutureApi) {
476             logger.warn("hub '{}' currently neither legacy nor new API are supported!", getThing().getUID());
477             isApiOnline = false;
478             return;
479         }
480
481         NeoHubConfiguration config = this.config;
482         ApiVersion apiVersion = (supportsLegacyApi && config != null && config.preferLegacyApi) ? ApiVersion.LEGACY
483                 : ApiVersion.NEW;
484         if (apiVersion != this.apiVersion) {
485             logger.debug("hub '{}' changing API version: '{}' => '{}'", getThing().getUID(), this.apiVersion.label,
486                     apiVersion.label);
487             this.apiVersion = apiVersion;
488         }
489
490         if (!apiVersion.label.equals(getThing().getProperties().get(PROPERTY_API_VERSION))) {
491             getThing().setProperty(PROPERTY_API_VERSION, apiVersion.label);
492         }
493
494         this.isApiOnline = true;
495     }
496
497     /*
498      * get the Engineers data
499      */
500     public @Nullable NeoHubGetEngineersData fromNeoHubGetEngineersData() {
501         NeoHubSocket socket = this.socket;
502         if (socket != null) {
503             String responseJson;
504             try {
505                 responseJson = socket.sendMessage(CMD_CODE_GET_ENGINEERS);
506                 return NeoHubGetEngineersData.createEngineersData(responseJson);
507             } catch (JsonSyntaxException | IOException | NeoHubException e) {
508                 logger.warn(MSG_FMT_ENGINEERS_POLL_ERR, getThing().getUID(), e.getMessage());
509             }
510         }
511         return null;
512     }
513
514     public boolean isLegacyApiSelected() {
515         return apiVersion == ApiVersion.LEGACY;
516     }
517
518     public Unit<?> getTemperatureUnit() {
519         NeoHubReadDcbResponse systemData = this.systemData;
520         if (systemData == null) {
521             this.systemData = systemData = fromNeoHubReadSystemData();
522         }
523         if (systemData != null) {
524             return systemData.getTemperatureUnit();
525         }
526         return SIUnits.CELSIUS;
527     }
528 }