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