]> git.basschouten.com Git - openhab-addons.git/blob
15bb98ce1a745b8a129e4433694be9077e10e890
[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 NeoHubSocketBase 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         // create a web or TCP socket based on the port number in the configuration
146         NeoHubSocketBase socket;
147         try {
148             if (config.useWebSocket) {
149                 socket = new NeoHubWebSocket(config);
150             } else {
151                 socket = new NeoHubSocket(config);
152             }
153         } catch (NeoHubException e) {
154             logger.debug("\"hub '{}' error creating web/tcp socket: '{}'", getThing().getUID(), e.getMessage());
155             return;
156         }
157
158         this.socket = socket;
159         this.config = config;
160
161         /*
162          * Try to 'ping' the hub, and if there is a 'connection refused', it is probably due to the mobile App |
163          * Settings | Legacy API Enable switch not being On, so go offline and log a warning message.
164          */
165         try {
166             socket.sendMessage(CMD_CODE_FIRMWARE);
167         } catch (IOException e) {
168             String error = e.getMessage();
169             if (error != null && error.toLowerCase().startsWith("connection refused")) {
170                 logger.warn("CONNECTION REFUSED!! (hub '{}') => {}", getThing().getUID(), SEE_README);
171                 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, SEE_README);
172                 return;
173             }
174         } catch (NeoHubException e) {
175             // NeoHubException won't actually occur here
176         }
177
178         if (logger.isDebugEnabled()) {
179             logger.debug("hub '{}' start background polling..", getThing().getUID());
180         }
181
182         // create a "lazy" polling scheduler
183         ScheduledFuture<?> lazy = this.lazyPollingScheduler;
184         if (lazy == null || lazy.isCancelled()) {
185             this.lazyPollingScheduler = scheduler.scheduleWithFixedDelay(this::lazyPollingSchedulerExecute,
186                     config.pollingInterval, config.pollingInterval, TimeUnit.SECONDS);
187         }
188
189         // create a "fast" polling scheduler
190         fastPollingCallsToGo.set(FAST_POLL_CYCLES);
191         ScheduledFuture<?> fast = this.fastPollingScheduler;
192         if (fast == null || fast.isCancelled()) {
193             this.fastPollingScheduler = scheduler.scheduleWithFixedDelay(this::fastPollingSchedulerExecute,
194                     FAST_POLL_INTERVAL, FAST_POLL_INTERVAL, TimeUnit.SECONDS);
195         }
196
197         updateStatus(ThingStatus.UNKNOWN);
198
199         // start a fast polling burst to ensure the NeHub is initialized quickly
200         startFastPollingBurst();
201     }
202
203     @Override
204     public void dispose() {
205         if (logger.isDebugEnabled()) {
206             logger.debug("hub '{}' stop background polling..", getThing().getUID());
207         }
208
209         // clean up the lazy polling scheduler
210         ScheduledFuture<?> lazy = this.lazyPollingScheduler;
211         if (lazy != null && !lazy.isCancelled()) {
212             lazy.cancel(true);
213             this.lazyPollingScheduler = null;
214         }
215
216         // clean up the fast polling scheduler
217         ScheduledFuture<?> fast = this.fastPollingScheduler;
218         if (fast != null && !fast.isCancelled()) {
219             fast.cancel(true);
220             this.fastPollingScheduler = null;
221         }
222
223         NeoHubSocketBase socket = this.socket;
224         if (socket != null) {
225             try {
226                 socket.close();
227             } catch (IOException e) {
228             }
229             this.socket = null;
230         }
231     }
232
233     /*
234      * device handlers call this to initiate a burst of fast polling requests (
235      * improves response time to users when openHAB changes a channel value )
236      */
237     public void startFastPollingBurst() {
238         fastPollingCallsToGo.set(FAST_POLL_CYCLES);
239     }
240
241     /*
242      * device handlers call this method to issue commands to the NeoHub
243      */
244     public synchronized NeoHubReturnResult toNeoHubSendChannelValue(String commandStr) {
245         NeoHubSocketBase socket = this.socket;
246
247         if (socket == null || config == null) {
248             return NeoHubReturnResult.ERR_INITIALIZATION;
249         }
250
251         try {
252             socket.sendMessage(commandStr);
253
254             // start a fast polling burst (to confirm the status change)
255             startFastPollingBurst();
256
257             return NeoHubReturnResult.SUCCEEDED;
258         } catch (Exception e) {
259             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR);
260             logger.warn(MSG_FMT_SET_VALUE_ERR, getThing().getUID(), commandStr, e.getMessage());
261             return NeoHubReturnResult.ERR_COMMUNICATION;
262         }
263     }
264
265     /**
266      * sends a JSON request to the NeoHub to read the device data
267      *
268      * @return a class that contains the full status of all devices
269      */
270     protected @Nullable NeoHubAbstractDeviceData fromNeoHubGetDeviceData() {
271         NeoHubSocketBase socket = this.socket;
272
273         if (socket == null || config == null) {
274             logger.warn(MSG_HUB_CONFIG, getThing().getUID());
275             return null;
276         }
277
278         try {
279             String responseJson;
280             NeoHubAbstractDeviceData deviceData;
281
282             if (apiVersion == ApiVersion.LEGACY) {
283                 responseJson = socket.sendMessage(CMD_CODE_INFO);
284                 deviceData = NeoHubInfoResponse.createDeviceData(responseJson);
285             } else {
286                 responseJson = socket.sendMessage(CMD_CODE_GET_LIVE_DATA);
287                 deviceData = NeoHubLiveDeviceData.createDeviceData(responseJson);
288             }
289
290             if (deviceData == null) {
291                 logger.warn(MSG_FMT_DEVICE_POLL_ERR, getThing().getUID(), "failed to create device data response");
292                 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR);
293                 return null;
294             }
295
296             @Nullable
297             List<? extends AbstractRecord> devices = deviceData.getDevices();
298             if (devices == null || devices.isEmpty()) {
299                 logger.warn(MSG_FMT_DEVICE_POLL_ERR, getThing().getUID(), "no devices found");
300                 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR);
301                 return null;
302             }
303
304             if (getThing().getStatus() != ThingStatus.ONLINE) {
305                 updateStatus(ThingStatus.ONLINE, ThingStatusDetail.NONE);
306             }
307
308             // check if we also need to discard and update systemData
309             NeoHubReadDcbResponse systemData = this.systemData;
310             if (systemData != null) {
311                 if (deviceData instanceof NeoHubLiveDeviceData) {
312                     /*
313                      * note: time-stamps are measured in seconds from 1970-01-01T00:00:00Z
314                      *
315                      * new API: discard systemData if its time-stamp is older than the system
316                      * time-stamp on the hub
317                      */
318                     if (systemData.timeStamp < ((NeoHubLiveDeviceData) deviceData).getTimestampSystem()) {
319                         this.systemData = null;
320                     }
321                 } else {
322                     /*
323                      * note: time-stamps are measured in seconds from 1970-01-01T00:00:00Z
324                      *
325                      * legacy API: discard systemData if its time-stamp is older than one hour
326                      */
327                     if (systemData.timeStamp < Instant.now().minus(1, ChronoUnit.HOURS).getEpochSecond()) {
328                         this.systemData = null;
329                     }
330                 }
331             }
332
333             return deviceData;
334         } catch (Exception e) {
335             logger.warn(MSG_FMT_DEVICE_POLL_ERR, getThing().getUID(), e.getMessage());
336             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR);
337             return null;
338         }
339     }
340
341     /**
342      * sends a JSON request to the NeoHub to read the system data
343      *
344      * @return a class that contains the status of the system
345      */
346     protected @Nullable NeoHubReadDcbResponse fromNeoHubReadSystemData() {
347         NeoHubSocketBase socket = this.socket;
348
349         if (socket == null) {
350             return null;
351         }
352
353         try {
354             String responseJson;
355             NeoHubReadDcbResponse systemData;
356
357             if (apiVersion == ApiVersion.LEGACY) {
358                 responseJson = socket.sendMessage(CMD_CODE_READ_DCB);
359                 systemData = NeoHubReadDcbResponse.createSystemData(responseJson);
360             } else {
361                 responseJson = socket.sendMessage(CMD_CODE_GET_SYSTEM);
362                 systemData = NeoHubReadDcbResponse.createSystemData(responseJson);
363             }
364
365             if (systemData == null) {
366                 logger.warn(MSG_FMT_SYSTEM_POLL_ERR, getThing().getUID(), "failed to create system data response");
367                 return null;
368             }
369
370             String physicalFirmware = systemData.getFirmwareVersion();
371             if (physicalFirmware != null) {
372                 String thingFirmware = getThing().getProperties().get(PROPERTY_FIRMWARE_VERSION);
373                 if (!physicalFirmware.equals(thingFirmware)) {
374                     getThing().setProperty(PROPERTY_FIRMWARE_VERSION, physicalFirmware);
375                 }
376             }
377
378             return systemData;
379         } catch (Exception e) {
380             logger.warn(MSG_FMT_SYSTEM_POLL_ERR, getThing().getUID(), e.getMessage());
381             return null;
382         }
383     }
384
385     /*
386      * this is the callback used by the lazy polling scheduler.. fetches the info
387      * for all devices from the NeoHub, and passes the results the respective device
388      * handlers
389      */
390     private synchronized void lazyPollingSchedulerExecute() {
391         // check which API is supported
392         if (!isApiOnline) {
393             selectApi();
394         }
395
396         NeoHubAbstractDeviceData deviceData = fromNeoHubGetDeviceData();
397         if (deviceData != null) {
398             // dispatch deviceData to each of the hub's owned devices ..
399             List<Thing> children = getThing().getThings();
400             for (Thing child : children) {
401                 ThingHandler device = child.getHandler();
402                 if (device instanceof NeoBaseHandler) {
403                     ((NeoBaseHandler) device).toBaseSendPollResponse(deviceData);
404                 }
405             }
406
407             // evaluate and update the state of our RF mesh QoS channel
408             List<? extends AbstractRecord> devices = deviceData.getDevices();
409             State state;
410             String property;
411
412             if (devices == null || devices.isEmpty()) {
413                 state = UnDefType.UNDEF;
414                 property = "[?/?]";
415             } else {
416                 int totalDeviceCount = devices.size();
417                 int onlineDeviceCount = 0;
418
419                 for (AbstractRecord device : devices) {
420                     String deviceName = device.getDeviceName();
421                     Boolean online = !device.offline();
422
423                     @Nullable
424                     Boolean onlineBefore = connectionStates.put(deviceName, online);
425                     /*
426                      * note: we use logger.info() here to log changes; reason is that the average user does really need
427                      * to know if a device (very occasionally) drops out of the normally reliable RF mesh; however we
428                      * only log it if 1) the state has changed, and 2) either 2a) the device has already been discovered
429                      * by the bridge handler, or 2b) logger debug mode is set
430                      */
431                     if (!online.equals(onlineBefore) && ((onlineBefore != null) || logger.isDebugEnabled())) {
432                         logger.info("hub '{}' device \"{}\" has {} the RF mesh network", getThing().getUID(),
433                                 deviceName, online.booleanValue() ? "joined" : "left");
434                     }
435
436                     if (online.booleanValue()) {
437                         onlineDeviceCount++;
438                     }
439                 }
440                 property = String.format("[%d/%d]", onlineDeviceCount, totalDeviceCount);
441                 state = new QuantityType<>((100.0 * onlineDeviceCount) / totalDeviceCount, Units.PERCENT);
442             }
443             getThing().setProperty(PROPERTY_API_DEVICEINFO, property);
444             updateState(CHAN_MESH_NETWORK_QOS, state);
445         }
446         if (fastPollingCallsToGo.get() > 0) {
447             fastPollingCallsToGo.decrementAndGet();
448         }
449     }
450
451     /*
452      * this is the callback used by the fast polling scheduler.. checks if a fast
453      * polling burst is scheduled, and if so calls lazyPollingSchedulerExecute
454      */
455     private void fastPollingSchedulerExecute() {
456         if (fastPollingCallsToGo.get() > 0) {
457             lazyPollingSchedulerExecute();
458         }
459     }
460
461     /*
462      * select whether to use the old "deprecated" API or the new API
463      */
464     private void selectApi() {
465         boolean supportsLegacyApi = false;
466         boolean supportsFutureApi = false;
467
468         NeoHubSocketBase socket = this.socket;
469         if (socket != null) {
470             String responseJson;
471             NeoHubReadDcbResponse systemData;
472
473             try {
474                 responseJson = socket.sendMessage(CMD_CODE_READ_DCB);
475                 systemData = NeoHubReadDcbResponse.createSystemData(responseJson);
476                 supportsLegacyApi = systemData != null;
477                 if (!supportsLegacyApi) {
478                     throw new NeoHubException("legacy API not supported");
479                 }
480             } catch (JsonSyntaxException | NeoHubException | IOException e) {
481                 // we learned that this API is not currently supported; no big deal
482                 logger.debug("hub '{}' legacy API is not supported!", getThing().getUID());
483             }
484             try {
485                 responseJson = socket.sendMessage(CMD_CODE_GET_SYSTEM);
486                 systemData = NeoHubReadDcbResponse.createSystemData(responseJson);
487                 supportsFutureApi = systemData != null;
488                 if (!supportsFutureApi) {
489                     throw new NeoHubException(String.format("hub '%s' new API not supported", getThing().getUID()));
490                 }
491             } catch (JsonSyntaxException | NeoHubException | IOException e) {
492                 // we learned that this API is not currently supported; no big deal
493                 logger.debug("hub '{}' new API is not supported!", getThing().getUID());
494             }
495         }
496
497         if (!supportsLegacyApi && !supportsFutureApi) {
498             logger.warn("hub '{}' currently neither legacy nor new API are supported!", getThing().getUID());
499             isApiOnline = false;
500             return;
501         }
502
503         NeoHubConfiguration config = this.config;
504         ApiVersion apiVersion = (supportsLegacyApi && config != null && config.preferLegacyApi) ? ApiVersion.LEGACY
505                 : ApiVersion.NEW;
506         if (apiVersion != this.apiVersion) {
507             logger.debug("hub '{}' changing API version: '{}' => '{}'", getThing().getUID(), this.apiVersion.label,
508                     apiVersion.label);
509             this.apiVersion = apiVersion;
510         }
511
512         if (!apiVersion.label.equals(getThing().getProperties().get(PROPERTY_API_VERSION))) {
513             getThing().setProperty(PROPERTY_API_VERSION, apiVersion.label);
514         }
515
516         this.isApiOnline = true;
517     }
518
519     /*
520      * get the Engineers data
521      */
522     public @Nullable NeoHubGetEngineersData fromNeoHubGetEngineersData() {
523         NeoHubSocketBase socket = this.socket;
524         if (socket != null) {
525             String responseJson;
526             try {
527                 responseJson = socket.sendMessage(CMD_CODE_GET_ENGINEERS);
528                 return NeoHubGetEngineersData.createEngineersData(responseJson);
529             } catch (JsonSyntaxException | IOException | NeoHubException e) {
530                 logger.warn(MSG_FMT_ENGINEERS_POLL_ERR, getThing().getUID(), e.getMessage());
531             }
532         }
533         return null;
534     }
535
536     public boolean isLegacyApiSelected() {
537         return apiVersion == ApiVersion.LEGACY;
538     }
539
540     public Unit<?> getTemperatureUnit() {
541         NeoHubReadDcbResponse systemData = this.systemData;
542         if (systemData == null) {
543             this.systemData = systemData = fromNeoHubReadSystemData();
544         }
545         if (systemData != null) {
546             return systemData.getTemperatureUnit();
547         }
548         return SIUnits.CELSIUS;
549     }
550 }