2 * Copyright (c) 2010-2021 Contributors to the openHAB project
4 * See the NOTICE file(s) distributed with this work for additional
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
11 * SPDX-License-Identifier: EPL-2.0
13 package org.openhab.binding.neohub.internal;
15 import static org.openhab.binding.neohub.internal.NeoHubBindingConstants.*;
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;
23 import java.util.concurrent.ScheduledFuture;
24 import java.util.concurrent.TimeUnit;
25 import java.util.concurrent.atomic.AtomicInteger;
27 import javax.measure.Unit;
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;
49 import com.google.gson.JsonSyntaxException;
52 * The {@link NeoHubHandler} is the openHAB Handler for NeoHub devices
54 * @author Andrew Fiddian-Green - Initial contribution (v2.x binding code)
55 * @author Sebastian Prehn - Initial contribution (v1.x hub communication)
59 public class NeoHubHandler extends BaseBridgeHandler {
61 private final Logger logger = LoggerFactory.getLogger(NeoHubHandler.class);
63 private final Map<String, Boolean> connectionStates = new HashMap<>();
65 private @Nullable NeoHubConfiguration config;
66 private @Nullable NeoHubSocket socket;
67 private @Nullable ScheduledFuture<?> lazyPollingScheduler;
68 private @Nullable ScheduledFuture<?> fastPollingScheduler;
70 private final AtomicInteger fastPollingCallsToGo = new AtomicInteger();
72 private @Nullable NeoHubReadDcbResponse systemData = null;
74 private boolean isLegacyApiSelected = true;
75 private boolean isApiOnline = false;
77 public NeoHubHandler(Bridge bridge) {
82 public void handleCommand(ChannelUID channelUID, Command command) {
83 // future: currently there is nothing to do for a NeoHub
87 public void initialize() {
88 NeoHubConfiguration config = getConfigAs(NeoHubConfiguration.class);
90 if (logger.isDebugEnabled()) {
91 logger.debug("hostname={}", config.hostName);
94 if (!MATCHER_IP_ADDRESS.matcher(config.hostName).matches()) {
95 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "parameter hostName must be set!");
99 if (logger.isDebugEnabled()) {
100 logger.debug("port={}", config.portNumber);
103 if (config.portNumber <= 0 || config.portNumber > 0xFFFF) {
104 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "portNumber is invalid!");
108 if (logger.isDebugEnabled()) {
109 logger.debug("polling interval={}", config.pollingInterval);
112 if (config.pollingInterval < FAST_POLL_INTERVAL || config.pollingInterval > LAZY_POLL_INTERVAL) {
113 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, String
114 .format("pollingInterval must be in range [%d..%d]!", FAST_POLL_INTERVAL, LAZY_POLL_INTERVAL));
118 if (logger.isDebugEnabled()) {
119 logger.debug("socketTimeout={}", config.socketTimeout);
122 if (config.socketTimeout < 5 || config.socketTimeout > 20) {
123 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
124 String.format("socketTimeout must be in range [%d..%d]!", 5, 20));
128 if (logger.isDebugEnabled()) {
129 logger.debug("preferLegacyApi={}", config.preferLegacyApi);
132 socket = new NeoHubSocket(config.hostName, config.portNumber, config.socketTimeout);
133 this.config = config;
135 if (logger.isDebugEnabled()) {
136 logger.debug("start background polling..");
139 // create a "lazy" polling scheduler
140 ScheduledFuture<?> lazy = this.lazyPollingScheduler;
141 if (lazy == null || lazy.isCancelled()) {
142 this.lazyPollingScheduler = scheduler.scheduleWithFixedDelay(this::lazyPollingSchedulerExecute,
143 config.pollingInterval, config.pollingInterval, TimeUnit.SECONDS);
146 // create a "fast" polling scheduler
147 fastPollingCallsToGo.set(FAST_POLL_CYCLES);
148 ScheduledFuture<?> fast = this.fastPollingScheduler;
149 if (fast == null || fast.isCancelled()) {
150 this.fastPollingScheduler = scheduler.scheduleWithFixedDelay(this::fastPollingSchedulerExecute,
151 FAST_POLL_INTERVAL, FAST_POLL_INTERVAL, TimeUnit.SECONDS);
154 updateStatus(ThingStatus.UNKNOWN);
156 // start a fast polling burst to ensure the NeHub is initialized quickly
157 startFastPollingBurst();
161 public void dispose() {
162 if (logger.isDebugEnabled()) {
163 logger.debug("stop background polling..");
166 // clean up the lazy polling scheduler
167 ScheduledFuture<?> lazy = this.lazyPollingScheduler;
168 if (lazy != null && !lazy.isCancelled()) {
170 this.lazyPollingScheduler = null;
173 // clean up the fast polling scheduler
174 ScheduledFuture<?> fast = this.fastPollingScheduler;
175 if (fast != null && !fast.isCancelled()) {
177 this.fastPollingScheduler = null;
182 * device handlers call this to initiate a burst of fast polling requests (
183 * improves response time to users when openHAB changes a channel value )
185 public void startFastPollingBurst() {
186 fastPollingCallsToGo.set(FAST_POLL_CYCLES);
190 * device handlers call this method to issue commands to the NeoHub
192 public synchronized NeoHubReturnResult toNeoHubSendChannelValue(String commandStr) {
193 NeoHubSocket socket = this.socket;
195 if (socket == null || config == null) {
196 return NeoHubReturnResult.ERR_INITIALIZATION;
200 socket.sendMessage(commandStr);
202 // start a fast polling burst (to confirm the status change)
203 startFastPollingBurst();
205 return NeoHubReturnResult.SUCCEEDED;
206 } catch (Exception e) {
207 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR);
208 logger.warn(MSG_FMT_SET_VALUE_ERR, commandStr, e.getMessage());
209 return NeoHubReturnResult.ERR_COMMUNICATION;
214 * sends a JSON request to the NeoHub to read the device data
216 * @return a class that contains the full status of all devices
218 protected @Nullable NeoHubAbstractDeviceData fromNeoHubGetDeviceData() {
219 NeoHubSocket socket = this.socket;
221 if (socket == null || config == null) {
222 logger.warn(MSG_HUB_CONFIG);
228 NeoHubAbstractDeviceData deviceData;
230 if (isLegacyApiSelected) {
231 responseJson = socket.sendMessage(CMD_CODE_INFO);
232 deviceData = NeoHubInfoResponse.createDeviceData(responseJson);
234 responseJson = socket.sendMessage(CMD_CODE_GET_LIVE_DATA);
235 deviceData = NeoHubLiveDeviceData.createDeviceData(responseJson);
238 if (deviceData == null) {
239 logger.warn(MSG_FMT_DEVICE_POLL_ERR, "failed to create device data response");
240 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR);
245 List<? extends AbstractRecord> devices = deviceData.getDevices();
246 if (devices == null || devices.size() == 0) {
247 logger.warn(MSG_FMT_DEVICE_POLL_ERR, "no devices found");
248 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR);
252 if (getThing().getStatus() != ThingStatus.ONLINE) {
253 updateStatus(ThingStatus.ONLINE, ThingStatusDetail.NONE);
256 // check if we also need to discard and update systemData
257 NeoHubReadDcbResponse systemData = this.systemData;
258 if (systemData != null) {
259 if (deviceData instanceof NeoHubLiveDeviceData) {
261 * note: time-stamps are measured in seconds from 1970-01-01T00:00:00Z
263 * new API: discard systemData if its time-stamp is older than the system
264 * time-stamp on the hub
266 if (systemData.timeStamp < ((NeoHubLiveDeviceData) deviceData).getTimestampSystem()) {
267 this.systemData = null;
271 * note: time-stamps are measured in seconds from 1970-01-01T00:00:00Z
273 * legacy API: discard systemData if its time-stamp is older than one hour
275 if (systemData.timeStamp < Instant.now().minus(1, ChronoUnit.HOURS).getEpochSecond()) {
276 this.systemData = null;
282 } catch (Exception e) {
283 logger.warn(MSG_FMT_DEVICE_POLL_ERR, e.getMessage());
284 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR);
290 * sends a JSON request to the NeoHub to read the system data
292 * @return a class that contains the status of the system
294 protected @Nullable NeoHubReadDcbResponse fromNeoHubReadSystemData() {
295 NeoHubSocket socket = this.socket;
297 if (socket == null) {
303 NeoHubReadDcbResponse systemData;
305 if (isLegacyApiSelected) {
306 responseJson = socket.sendMessage(CMD_CODE_READ_DCB);
307 systemData = NeoHubReadDcbResponse.createSystemData(responseJson);
309 responseJson = socket.sendMessage(CMD_CODE_GET_SYSTEM);
310 systemData = NeoHubReadDcbResponse.createSystemData(responseJson);
313 if (systemData == null) {
314 logger.warn(MSG_FMT_SYSTEM_POLL_ERR, "failed to create system data response");
319 } catch (Exception e) {
320 logger.warn(MSG_FMT_SYSTEM_POLL_ERR, e.getMessage());
326 * this is the callback used by the lazy polling scheduler.. fetches the info
327 * for all devices from the NeoHub, and passes the results the respective device
330 private synchronized void lazyPollingSchedulerExecute() {
331 // check which API is supported
336 NeoHubAbstractDeviceData deviceData = fromNeoHubGetDeviceData();
337 if (deviceData != null) {
338 // dispatch deviceData to each of the hub's owned devices ..
339 List<Thing> children = getThing().getThings();
340 for (Thing child : children) {
341 ThingHandler device = child.getHandler();
342 if (device instanceof NeoBaseHandler) {
343 ((NeoBaseHandler) device).toBaseSendPollResponse(deviceData);
347 // evaluate and update the state of our RF mesh QoS channel
348 List<? extends AbstractRecord> devices = deviceData.getDevices();
351 if (devices == null || devices.isEmpty()) {
352 state = UnDefType.UNDEF;
354 int totalDeviceCount = devices.size();
355 int onlineDeviceCount = 0;
357 for (AbstractRecord device : devices) {
358 String deviceName = device.getDeviceName();
359 Boolean online = !device.offline();
362 Boolean onlineBefore = connectionStates.put(deviceName, online);
363 if (!online.equals(onlineBefore)) {
364 logger.info("device \"{}\" has {} the RF mesh network", deviceName,
365 online.booleanValue() ? "joined" : "left");
368 if (online.booleanValue()) {
372 state = new QuantityType<>((100.0 * onlineDeviceCount) / totalDeviceCount, Units.PERCENT);
374 updateState(CHAN_MESH_NETWORK_QOS, state);
376 if (fastPollingCallsToGo.get() > 0) {
377 fastPollingCallsToGo.decrementAndGet();
382 * this is the callback used by the fast polling scheduler.. checks if a fast
383 * polling burst is scheduled, and if so calls lazyPollingSchedulerExecute
385 private void fastPollingSchedulerExecute() {
386 if (fastPollingCallsToGo.get() > 0) {
387 lazyPollingSchedulerExecute();
392 * select whether to use the old "deprecated" API or the new API
394 private void selectApi() {
395 boolean supportsLegacyApi = false;
396 boolean supportsFutureApi = false;
398 NeoHubSocket socket = this.socket;
399 if (socket != null) {
401 NeoHubReadDcbResponse systemData;
404 responseJson = socket.sendMessage(CMD_CODE_READ_DCB);
405 systemData = NeoHubReadDcbResponse.createSystemData(responseJson);
406 supportsLegacyApi = systemData != null;
407 if (!supportsLegacyApi) {
408 throw new NeoHubException("legacy API not supported");
410 } catch (JsonSyntaxException | NeoHubException | IOException e) {
411 // we learned that this API is not currently supported; no big deal
412 logger.debug("Legacy API is not supported!");
415 responseJson = socket.sendMessage(CMD_CODE_GET_SYSTEM);
416 systemData = NeoHubReadDcbResponse.createSystemData(responseJson);
417 supportsFutureApi = systemData != null;
418 if (!supportsFutureApi) {
419 throw new NeoHubException("new API not supported");
421 } catch (JsonSyntaxException | NeoHubException | IOException e) {
422 // we learned that this API is not currently supported; no big deal
423 logger.debug("New API is not supported!");
427 if (!supportsLegacyApi && !supportsFutureApi) {
428 logger.warn("Currently neither legacy nor new API are supported!");
433 NeoHubConfiguration config = this.config;
434 boolean isLegacyApiSelected = (supportsLegacyApi && config != null && config.preferLegacyApi);
435 if (isLegacyApiSelected != this.isLegacyApiSelected) {
436 logger.info("Changing API version: {}",
437 isLegacyApiSelected ? "\"new\" => \"legacy\"" : "\"legacy\" => \"new\"");
439 this.isLegacyApiSelected = isLegacyApiSelected;
440 this.isApiOnline = true;
444 * get the Engineers data
446 public @Nullable NeoHubGetEngineersData fromNeoHubGetEngineersData() {
447 NeoHubSocket socket = this.socket;
448 if (socket != null) {
451 responseJson = socket.sendMessage(CMD_CODE_GET_ENGINEERS);
452 return NeoHubGetEngineersData.createEngineersData(responseJson);
453 } catch (JsonSyntaxException | IOException | NeoHubException e) {
454 logger.warn(MSG_FMT_ENGINEERS_POLL_ERR, e.getMessage());
460 public boolean isLegacyApiSelected() {
461 return isLegacyApiSelected;
464 public Unit<?> getTemperatureUnit() {
465 NeoHubReadDcbResponse systemData = this.systemData;
466 if (systemData == null) {
467 this.systemData = systemData = fromNeoHubReadSystemData();
469 if (systemData != null) {
470 return systemData.getTemperatureUnit();
472 return SIUnits.CELSIUS;