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