]> git.basschouten.com Git - openhab-addons.git/blob
c95be4270afbab2ae35fbccb948eb51ffe4d043d
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2021 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 final Logger logger = LoggerFactory.getLogger(NeoHubHandler.class);
62
63     private final Map<String, Boolean> connectionStates = new HashMap<>();
64
65     private @Nullable NeoHubConfiguration config;
66     private @Nullable NeoHubSocket socket;
67     private @Nullable ScheduledFuture<?> lazyPollingScheduler;
68     private @Nullable ScheduledFuture<?> fastPollingScheduler;
69
70     private final AtomicInteger fastPollingCallsToGo = new AtomicInteger();
71
72     private @Nullable NeoHubReadDcbResponse systemData = null;
73
74     private boolean isLegacyApiSelected = true;
75     private boolean isApiOnline = false;
76
77     public NeoHubHandler(Bridge bridge) {
78         super(bridge);
79     }
80
81     @Override
82     public void handleCommand(ChannelUID channelUID, Command command) {
83         // future: currently there is nothing to do for a NeoHub
84     }
85
86     @Override
87     public void initialize() {
88         NeoHubConfiguration config = getConfigAs(NeoHubConfiguration.class);
89
90         if (logger.isDebugEnabled()) {
91             logger.debug("hostname={}", config.hostName);
92         }
93
94         if (!MATCHER_IP_ADDRESS.matcher(config.hostName).matches()) {
95             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "parameter hostName must be set!");
96             return;
97         }
98
99         if (logger.isDebugEnabled()) {
100             logger.debug("port={}", config.portNumber);
101         }
102
103         if (config.portNumber <= 0 || config.portNumber > 0xFFFF) {
104             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "portNumber is invalid!");
105             return;
106         }
107
108         if (logger.isDebugEnabled()) {
109             logger.debug("polling interval={}", config.pollingInterval);
110         }
111
112         if (config.pollingInterval < FAST_POLL_INTERVAL || config.pollingInterval > LAZY_POLL_INTERVAL) {
113             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, String
114                     .format("pollingInterval must be in range [%d..%d]!", FAST_POLL_INTERVAL, LAZY_POLL_INTERVAL));
115             return;
116         }
117
118         if (logger.isDebugEnabled()) {
119             logger.debug("socketTimeout={}", config.socketTimeout);
120         }
121
122         if (config.socketTimeout < 5 || config.socketTimeout > 20) {
123             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
124                     String.format("socketTimeout must be in range [%d..%d]!", 5, 20));
125             return;
126         }
127
128         if (logger.isDebugEnabled()) {
129             logger.debug("preferLegacyApi={}", config.preferLegacyApi);
130         }
131
132         socket = new NeoHubSocket(config.hostName, config.portNumber, config.socketTimeout);
133         this.config = config;
134
135         if (logger.isDebugEnabled()) {
136             logger.debug("start background polling..");
137         }
138
139         // create a "lazy" polling scheduler
140         ScheduledFuture<?> lazy = this.lazyPollingScheduler;
141         if (lazy == null || lazy.isCancelled()) {
142             this.lazyPollingScheduler = scheduler.scheduleWithFixedDelay(this::lazyPollingSchedulerExecute,
143                     config.pollingInterval, config.pollingInterval, TimeUnit.SECONDS);
144         }
145
146         // create a "fast" polling scheduler
147         fastPollingCallsToGo.set(FAST_POLL_CYCLES);
148         ScheduledFuture<?> fast = this.fastPollingScheduler;
149         if (fast == null || fast.isCancelled()) {
150             this.fastPollingScheduler = scheduler.scheduleWithFixedDelay(this::fastPollingSchedulerExecute,
151                     FAST_POLL_INTERVAL, FAST_POLL_INTERVAL, TimeUnit.SECONDS);
152         }
153
154         updateStatus(ThingStatus.UNKNOWN);
155
156         // start a fast polling burst to ensure the NeHub is initialized quickly
157         startFastPollingBurst();
158     }
159
160     @Override
161     public void dispose() {
162         if (logger.isDebugEnabled()) {
163             logger.debug("stop background polling..");
164         }
165
166         // clean up the lazy polling scheduler
167         ScheduledFuture<?> lazy = this.lazyPollingScheduler;
168         if (lazy != null && !lazy.isCancelled()) {
169             lazy.cancel(true);
170             this.lazyPollingScheduler = null;
171         }
172
173         // clean up the fast polling scheduler
174         ScheduledFuture<?> fast = this.fastPollingScheduler;
175         if (fast != null && !fast.isCancelled()) {
176             fast.cancel(true);
177             this.fastPollingScheduler = null;
178         }
179     }
180
181     /*
182      * device handlers call this to initiate a burst of fast polling requests (
183      * improves response time to users when openHAB changes a channel value )
184      */
185     public void startFastPollingBurst() {
186         fastPollingCallsToGo.set(FAST_POLL_CYCLES);
187     }
188
189     /*
190      * device handlers call this method to issue commands to the NeoHub
191      */
192     public synchronized NeoHubReturnResult toNeoHubSendChannelValue(String commandStr) {
193         NeoHubSocket socket = this.socket;
194
195         if (socket == null || config == null) {
196             return NeoHubReturnResult.ERR_INITIALIZATION;
197         }
198
199         try {
200             socket.sendMessage(commandStr);
201
202             // start a fast polling burst (to confirm the status change)
203             startFastPollingBurst();
204
205             return NeoHubReturnResult.SUCCEEDED;
206         } catch (Exception e) {
207             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR);
208             logger.warn(MSG_FMT_SET_VALUE_ERR, commandStr, e.getMessage());
209             return NeoHubReturnResult.ERR_COMMUNICATION;
210         }
211     }
212
213     /**
214      * sends a JSON request to the NeoHub to read the device data
215      *
216      * @return a class that contains the full status of all devices
217      */
218     protected @Nullable NeoHubAbstractDeviceData fromNeoHubGetDeviceData() {
219         NeoHubSocket socket = this.socket;
220
221         if (socket == null || config == null) {
222             logger.warn(MSG_HUB_CONFIG);
223             return null;
224         }
225
226         try {
227             String responseJson;
228             NeoHubAbstractDeviceData deviceData;
229
230             if (isLegacyApiSelected) {
231                 responseJson = socket.sendMessage(CMD_CODE_INFO);
232                 deviceData = NeoHubInfoResponse.createDeviceData(responseJson);
233             } else {
234                 responseJson = socket.sendMessage(CMD_CODE_GET_LIVE_DATA);
235                 deviceData = NeoHubLiveDeviceData.createDeviceData(responseJson);
236             }
237
238             if (deviceData == null) {
239                 logger.warn(MSG_FMT_DEVICE_POLL_ERR, "failed to create device data response");
240                 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR);
241                 return null;
242             }
243
244             @Nullable
245             List<? extends AbstractRecord> devices = deviceData.getDevices();
246             if (devices == null || devices.size() == 0) {
247                 logger.warn(MSG_FMT_DEVICE_POLL_ERR, "no devices found");
248                 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR);
249                 return null;
250             }
251
252             if (getThing().getStatus() != ThingStatus.ONLINE) {
253                 updateStatus(ThingStatus.ONLINE, ThingStatusDetail.NONE);
254             }
255
256             // check if we also need to discard and update systemData
257             NeoHubReadDcbResponse systemData = this.systemData;
258             if (systemData != null) {
259                 if (deviceData instanceof NeoHubLiveDeviceData) {
260                     /*
261                      * note: time-stamps are measured in seconds from 1970-01-01T00:00:00Z
262                      *
263                      * new API: discard systemData if its time-stamp is older than the system
264                      * time-stamp on the hub
265                      */
266                     if (systemData.timeStamp < ((NeoHubLiveDeviceData) deviceData).getTimestampSystem()) {
267                         this.systemData = null;
268                     }
269                 } else {
270                     /*
271                      * note: time-stamps are measured in seconds from 1970-01-01T00:00:00Z
272                      *
273                      * legacy API: discard systemData if its time-stamp is older than one hour
274                      */
275                     if (systemData.timeStamp < Instant.now().minus(1, ChronoUnit.HOURS).getEpochSecond()) {
276                         this.systemData = null;
277                     }
278                 }
279             }
280
281             return deviceData;
282         } catch (Exception e) {
283             logger.warn(MSG_FMT_DEVICE_POLL_ERR, e.getMessage());
284             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR);
285             return null;
286         }
287     }
288
289     /**
290      * sends a JSON request to the NeoHub to read the system data
291      *
292      * @return a class that contains the status of the system
293      */
294     protected @Nullable NeoHubReadDcbResponse fromNeoHubReadSystemData() {
295         NeoHubSocket socket = this.socket;
296
297         if (socket == null) {
298             return null;
299         }
300
301         try {
302             String responseJson;
303             NeoHubReadDcbResponse systemData;
304
305             if (isLegacyApiSelected) {
306                 responseJson = socket.sendMessage(CMD_CODE_READ_DCB);
307                 systemData = NeoHubReadDcbResponse.createSystemData(responseJson);
308             } else {
309                 responseJson = socket.sendMessage(CMD_CODE_GET_SYSTEM);
310                 systemData = NeoHubReadDcbResponse.createSystemData(responseJson);
311             }
312
313             if (systemData == null) {
314                 logger.warn(MSG_FMT_SYSTEM_POLL_ERR, "failed to create system data response");
315                 return null;
316             }
317
318             return systemData;
319         } catch (Exception e) {
320             logger.warn(MSG_FMT_SYSTEM_POLL_ERR, e.getMessage());
321             return null;
322         }
323     }
324
325     /*
326      * this is the callback used by the lazy polling scheduler.. fetches the info
327      * for all devices from the NeoHub, and passes the results the respective device
328      * handlers
329      */
330     private synchronized void lazyPollingSchedulerExecute() {
331         // check which API is supported
332         if (!isApiOnline) {
333             selectApi();
334         }
335
336         NeoHubAbstractDeviceData deviceData = fromNeoHubGetDeviceData();
337         if (deviceData != null) {
338             // dispatch deviceData to each of the hub's owned devices ..
339             List<Thing> children = getThing().getThings();
340             for (Thing child : children) {
341                 ThingHandler device = child.getHandler();
342                 if (device instanceof NeoBaseHandler) {
343                     ((NeoBaseHandler) device).toBaseSendPollResponse(deviceData);
344                 }
345             }
346
347             // evaluate and update the state of our RF mesh QoS channel
348             List<? extends AbstractRecord> devices = deviceData.getDevices();
349             State state;
350
351             if (devices == null || devices.isEmpty()) {
352                 state = UnDefType.UNDEF;
353             } else {
354                 int totalDeviceCount = devices.size();
355                 int onlineDeviceCount = 0;
356
357                 for (AbstractRecord device : devices) {
358                     String deviceName = device.getDeviceName();
359                     Boolean online = !device.offline();
360
361                     @Nullable
362                     Boolean onlineBefore = connectionStates.put(deviceName, online);
363                     if (!online.equals(onlineBefore)) {
364                         logger.info("device \"{}\" has {} the RF mesh network", deviceName,
365                                 online.booleanValue() ? "joined" : "left");
366                     }
367
368                     if (online.booleanValue()) {
369                         onlineDeviceCount++;
370                     }
371                 }
372                 state = new QuantityType<>((100.0 * onlineDeviceCount) / totalDeviceCount, Units.PERCENT);
373             }
374             updateState(CHAN_MESH_NETWORK_QOS, state);
375         }
376         if (fastPollingCallsToGo.get() > 0) {
377             fastPollingCallsToGo.decrementAndGet();
378         }
379     }
380
381     /*
382      * this is the callback used by the fast polling scheduler.. checks if a fast
383      * polling burst is scheduled, and if so calls lazyPollingSchedulerExecute
384      */
385     private void fastPollingSchedulerExecute() {
386         if (fastPollingCallsToGo.get() > 0) {
387             lazyPollingSchedulerExecute();
388         }
389     }
390
391     /*
392      * select whether to use the old "deprecated" API or the new API
393      */
394     private void selectApi() {
395         boolean supportsLegacyApi = false;
396         boolean supportsFutureApi = false;
397
398         NeoHubSocket socket = this.socket;
399         if (socket != null) {
400             String responseJson;
401             NeoHubReadDcbResponse systemData;
402
403             try {
404                 responseJson = socket.sendMessage(CMD_CODE_READ_DCB);
405                 systemData = NeoHubReadDcbResponse.createSystemData(responseJson);
406                 supportsLegacyApi = systemData != null;
407                 if (!supportsLegacyApi) {
408                     throw new NeoHubException("legacy API not supported");
409                 }
410             } catch (JsonSyntaxException | NeoHubException | IOException e) {
411                 // we learned that this API is not currently supported; no big deal
412                 logger.debug("Legacy API is not supported!");
413             }
414             try {
415                 responseJson = socket.sendMessage(CMD_CODE_GET_SYSTEM);
416                 systemData = NeoHubReadDcbResponse.createSystemData(responseJson);
417                 supportsFutureApi = systemData != null;
418                 if (!supportsFutureApi) {
419                     throw new NeoHubException("new API not supported");
420                 }
421             } catch (JsonSyntaxException | NeoHubException | IOException e) {
422                 // we learned that this API is not currently supported; no big deal
423                 logger.debug("New API is not supported!");
424             }
425         }
426
427         if (!supportsLegacyApi && !supportsFutureApi) {
428             logger.warn("Currently neither legacy nor new API are supported!");
429             isApiOnline = false;
430             return;
431         }
432
433         NeoHubConfiguration config = this.config;
434         boolean isLegacyApiSelected = (supportsLegacyApi && config != null && config.preferLegacyApi);
435         if (isLegacyApiSelected != this.isLegacyApiSelected) {
436             logger.info("Changing API version: {}",
437                     isLegacyApiSelected ? "\"new\" => \"legacy\"" : "\"legacy\" => \"new\"");
438         }
439         this.isLegacyApiSelected = isLegacyApiSelected;
440         this.isApiOnline = true;
441     }
442
443     /*
444      * get the Engineers data
445      */
446     public @Nullable NeoHubGetEngineersData fromNeoHubGetEngineersData() {
447         NeoHubSocket socket = this.socket;
448         if (socket != null) {
449             String responseJson;
450             try {
451                 responseJson = socket.sendMessage(CMD_CODE_GET_ENGINEERS);
452                 return NeoHubGetEngineersData.createEngineersData(responseJson);
453             } catch (JsonSyntaxException | IOException | NeoHubException e) {
454                 logger.warn(MSG_FMT_ENGINEERS_POLL_ERR, e.getMessage());
455             }
456         }
457         return null;
458     }
459
460     public boolean isLegacyApiSelected() {
461         return isLegacyApiSelected;
462     }
463
464     public Unit<?> getTemperatureUnit() {
465         NeoHubReadDcbResponse systemData = this.systemData;
466         if (systemData == null) {
467             this.systemData = systemData = fromNeoHubReadSystemData();
468         }
469         if (systemData != null) {
470             return systemData.getTemperatureUnit();
471         }
472         return SIUnits.CELSIUS;
473     }
474 }