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