]> git.basschouten.com Git - openhab-addons.git/blob
2030f4a718d980877ce64d72c65efca5111fb6ec
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2024 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.linktap.internal;
14
15 import static org.openhab.binding.linktap.internal.LinkTapBindingConstants.*;
16 import static org.openhab.binding.linktap.protocol.frames.TLGatewayFrame.*;
17 import static org.openhab.binding.linktap.protocol.frames.ValidationError.Cause.BUG;
18 import static org.openhab.binding.linktap.protocol.frames.ValidationError.Cause.USER;
19
20 import java.io.IOException;
21 import java.net.InetAddress;
22 import java.net.InetSocketAddress;
23 import java.net.Socket;
24 import java.net.UnknownHostException;
25 import java.time.Duration;
26 import java.util.Arrays;
27 import java.util.Collection;
28 import java.util.Map;
29 import java.util.Objects;
30 import java.util.Optional;
31 import java.util.Set;
32 import java.util.UUID;
33 import java.util.concurrent.ConcurrentHashMap;
34 import java.util.concurrent.ConcurrentMap;
35 import java.util.concurrent.CopyOnWriteArrayList;
36 import java.util.concurrent.ScheduledFuture;
37 import java.util.concurrent.TimeUnit;
38 import java.util.stream.Collectors;
39 import java.util.stream.Stream;
40
41 import javax.validation.constraints.NotNull;
42
43 import org.eclipse.jdt.annotation.NonNullByDefault;
44 import org.eclipse.jdt.annotation.Nullable;
45 import org.openhab.binding.linktap.configuration.LinkTapBridgeConfiguration;
46 import org.openhab.binding.linktap.protocol.frames.GatewayConfigResp;
47 import org.openhab.binding.linktap.protocol.frames.GatewayDeviceResponse;
48 import org.openhab.binding.linktap.protocol.frames.TLGatewayFrame;
49 import org.openhab.binding.linktap.protocol.frames.ValidationError;
50 import org.openhab.binding.linktap.protocol.http.CommandNotSupportedException;
51 import org.openhab.binding.linktap.protocol.http.DeviceIdException;
52 import org.openhab.binding.linktap.protocol.http.GatewayIdException;
53 import org.openhab.binding.linktap.protocol.http.InvalidParameterException;
54 import org.openhab.binding.linktap.protocol.http.LinkTapException;
55 import org.openhab.binding.linktap.protocol.http.NotTapLinkGatewayException;
56 import org.openhab.binding.linktap.protocol.http.TransientCommunicationIssueException;
57 import org.openhab.binding.linktap.protocol.http.WebServerApi;
58 import org.openhab.binding.linktap.protocol.servers.BindingServlet;
59 import org.openhab.binding.linktap.protocol.servers.IHttpClientProvider;
60 import org.openhab.core.cache.ExpiringCache;
61 import org.openhab.core.config.discovery.DiscoveryServiceRegistry;
62 import org.openhab.core.i18n.LocaleProvider;
63 import org.openhab.core.i18n.TranslationProvider;
64 import org.openhab.core.thing.Bridge;
65 import org.openhab.core.thing.ChannelUID;
66 import org.openhab.core.thing.Thing;
67 import org.openhab.core.thing.ThingStatus;
68 import org.openhab.core.thing.ThingStatusDetail;
69 import org.openhab.core.thing.ThingUID;
70 import org.openhab.core.thing.binding.BaseBridgeHandler;
71 import org.openhab.core.thing.binding.ThingHandler;
72 import org.openhab.core.thing.binding.ThingHandlerService;
73 import org.openhab.core.types.Command;
74 import org.osgi.framework.Bundle;
75 import org.osgi.framework.FrameworkUtil;
76 import org.osgi.service.component.annotations.Activate;
77 import org.osgi.service.component.annotations.Reference;
78 import org.slf4j.Logger;
79 import org.slf4j.LoggerFactory;
80
81 /**
82  * The {@link LinkTapBridgeHandler} class defines the handler for a LinkTapHandler
83  *
84  * @author David Goodyear - Initial contribution
85  */
86 @NonNullByDefault
87 public class LinkTapBridgeHandler extends BaseBridgeHandler {
88
89     public static final LookupWrapper<@Nullable LinkTapBridgeHandler> ADDR_LOOKUP = new LookupWrapper<>();
90     public static final LookupWrapper<@Nullable LinkTapBridgeHandler> GW_ID_LOOKUP = new LookupWrapper<>();
91     public static final LookupWrapper<@Nullable LinkTapHandler> DEV_ID_LOOKUP = new LookupWrapper<>();
92     public static final LookupWrapper<@Nullable String> MDNS_LOOKUP = new LookupWrapper<>();
93     private static final long MIN_TIME_BETWEEN_MDNS_SCANS_MS = 600000;
94
95     private final DiscoveryServiceRegistry discoverySrvReg;
96     private final TranslationProvider translationProvider;
97     private final LocaleProvider localeProvider;
98     private final Bundle bundle;
99     private final Logger logger = LoggerFactory.getLogger(LinkTapBridgeHandler.class);
100     private final Object schedulerLock = new Object();
101     private final Object reconnectFutureLock = new Object();
102     private final Object getConfigLock = new Object();
103
104     private volatile String currentGwId = "";
105     private volatile LinkTapBridgeConfiguration config = new LinkTapBridgeConfiguration();
106     private volatile long lastGwCommandRecvTs = 0L;
107     private volatile long lastMdnsScanMillis = -1L;
108
109     private String bridgeKey = "";
110     private IHttpClientProvider httpClientProvider;
111     private @Nullable ScheduledFuture<?> backgroundGwPollingScheduler;
112     private @Nullable ScheduledFuture<?> connectRepair = null;
113
114     protected ExpiringCache<String> lastGetConfigCache = new ExpiringCache<>(Duration.ofSeconds(10),
115             LinkTapBridgeHandler::expireCacheContents);
116
117     private static @Nullable String expireCacheContents() {
118         return null;
119     }
120
121     @Activate
122     public LinkTapBridgeHandler(final Bridge bridge, IHttpClientProvider httpClientProvider,
123             @Reference DiscoveryServiceRegistry discoveryService, @Reference TranslationProvider translationProvider,
124             @Reference LocaleProvider localeProvider) {
125         super(bridge);
126         this.httpClientProvider = httpClientProvider;
127         this.discoverySrvReg = discoveryService;
128         this.translationProvider = translationProvider;
129         this.localeProvider = localeProvider;
130         this.bundle = FrameworkUtil.getBundle(getClass());
131         TransactionProcessor.getInstance().setTranslationProviderInfo(translationProvider, localeProvider, bundle);
132         WebServerApi.getInstance().setTranslationProviderInfo(translationProvider, localeProvider, bundle);
133         BindingServlet.getInstance().setTranslationProviderInfo(translationProvider, localeProvider, bundle);
134     }
135
136     public String getLocalizedText(String key, @Nullable Object @Nullable... arguments) {
137         String result = translationProvider.getText(bundle, key, key, localeProvider.getLocale(), arguments);
138         return Objects.nonNull(result) ? result : key;
139     }
140
141     private void startGwPolling() {
142         synchronized (schedulerLock) {
143             cancelGwPolling();
144             backgroundGwPollingScheduler = scheduler.scheduleWithFixedDelay(() -> {
145                 if (lastGwCommandRecvTs + 120000 < System.currentTimeMillis()) {
146                     getGatewayConfiguration();
147                 }
148             }, 5000, 120000, TimeUnit.MILLISECONDS);
149         }
150     }
151
152     private void cancelGwPolling() {
153         synchronized (schedulerLock) {
154             final ScheduledFuture<?> ref = backgroundGwPollingScheduler;
155             if (ref != null) {
156                 ref.cancel(true);
157                 backgroundGwPollingScheduler = null;
158             }
159         }
160     }
161
162     private void requestMdnsScan() {
163         final long sysMillis = System.currentTimeMillis();
164         if (lastMdnsScanMillis + MIN_TIME_BETWEEN_MDNS_SCANS_MS < sysMillis) {
165             logger.debug("Requesting MDNS Scan");
166             discoverySrvReg.startScan(THING_TYPE_GATEWAY, null);
167             lastMdnsScanMillis = sysMillis;
168         } else {
169             logger.trace("Not requesting MDNS Scan last ran under 10 min's ago");
170         }
171     }
172
173     @Override
174     public void initialize() {
175         updateStatus(ThingStatus.UNKNOWN);
176         config = getConfigAs(LinkTapBridgeConfiguration.class);
177         scheduleReconnect(0);
178     }
179
180     @Override
181     public void dispose() {
182         cancelReconnect();
183         cancelGwPolling();
184         deregisterBridge(this);
185         GW_ID_LOOKUP.deregisterItem(currentGwId, this, () -> {
186         });
187     }
188
189     @Override
190     public Collection<Class<? extends ThingHandlerService>> getServices() {
191         return Set.of(LinkTapDeviceDiscoveryService.class);
192     }
193
194     public @Nullable String getGatewayId() {
195         return currentGwId;
196     }
197
198     private void deregisterBridge(final LinkTapBridgeHandler ref) {
199         if (!bridgeKey.isEmpty()) {
200             ADDR_LOOKUP.deregisterItem(bridgeKey, ref, () -> {
201                 BindingServlet.getInstance().unregisterServlet();
202             });
203             bridgeKey = "";
204         }
205     }
206
207     private boolean registerBridge(final LinkTapBridgeHandler ref) {
208         final WebServerApi api = WebServerApi.getInstance();
209         api.setHttpClient(httpClientProvider.getHttpClient());
210         try {
211             final String host = getHostname();
212
213             if (!bridgeKey.equals(host)) {
214                 deregisterBridge(this);
215                 bridgeKey = host;
216             }
217
218             if (!ADDR_LOOKUP.registerItem(bridgeKey, this, () -> {
219                 BindingServlet.getInstance().registerServlet();
220             })) {
221                 return false;
222             }
223         } catch (UnknownHostException e) {
224             deregisterBridge(this);
225             return false;
226         }
227         return true;
228     }
229
230     public void getGatewayConfiguration() {
231         String resp = "";
232         synchronized (getConfigLock) {
233             resp = lastGetConfigCache.getValue();
234             if (lastGetConfigCache.isExpired() || resp == null || resp.isBlank()) {
235                 TLGatewayFrame req = new TLGatewayFrame(CMD_GET_CONFIGURATION);
236                 resp = sendApiRequest(req);
237                 GatewayDeviceResponse respFrame = LinkTapBindingConstants.GSON.fromJson(resp,
238                         GatewayDeviceResponse.class);
239                 // The system may not have picked up the ID before in which case - extract it from the error response
240                 // and re-run the request to ensure a full configuration data-set is retrieved.
241                 // This is normally populated as part of the sendApiRequest sequencing where the gateway id is
242                 // auto-added,
243                 // if available.
244                 if (req.gatewayId.isEmpty() && respFrame != null
245                         && respFrame.getRes() == GatewayDeviceResponse.ResultStatus.RET_GATEWAY_ID_NOT_MATCHED) {
246                     // Use the response GW_ID from the error response - to re-request with the correct ID
247                     // This only happens in occasional startup race conditions, but this removes a low change
248                     // bug being hit.
249                     req.gatewayId = respFrame.gatewayId;
250                     resp = sendApiRequest(req);
251                 }
252                 lastGetConfigCache.putValue(resp);
253             }
254
255         }
256
257         final GatewayConfigResp gwConfig = LinkTapBindingConstants.GSON.fromJson(resp, GatewayConfigResp.class);
258         if (gwConfig == null) {
259             return;
260         }
261         currentGwId = gwConfig.gatewayId;
262
263         final String version = gwConfig.version;
264         final String volUnit = gwConfig.volumeUnit;
265         final String[] devIds = gwConfig.endDevices;
266         final String[] devNames = gwConfig.deviceNames;
267         final Integer utcOffset = gwConfig.utfOfs;
268         if (!version.equals(editProperties().get(BRIDGE_PROP_GW_VER))) {
269             final Map<String, String> props = editProperties();
270             props.put(BRIDGE_PROP_GW_VER, version);
271             updateProperties(props);
272             return;
273         }
274         if (!volUnit.equals(editProperties().get(BRIDGE_PROP_VOL_UNIT))) {
275             final Map<String, String> props = editProperties();
276             props.put(BRIDGE_PROP_VOL_UNIT, volUnit);
277             updateProperties(props);
278         }
279         if (utcOffset != DEFAULT_INT) { // This is only in later firmwares
280             final String strVal = String.valueOf(utcOffset);
281             if (!strVal.equals(editProperties().get(BRIDGE_PROP_UTC_OFFSET))) {
282                 final Map<String, String> props = editProperties();
283                 props.put(BRIDGE_PROP_UTC_OFFSET, strVal);
284                 updateProperties(props);
285             }
286         }
287
288         boolean updatedDeviceInfo = devIds.length != discoveredDevices.size();
289
290         for (int i = 0; i < devIds.length; ++i) {
291             LinkTapDeviceMetadata deviceInfo = new LinkTapDeviceMetadata(devIds[i], devNames[i]);
292             LinkTapDeviceMetadata replaced = discoveredDevices.put(deviceInfo.deviceId, deviceInfo);
293             if (replaced != null
294                     && (!replaced.deviceId.equals(devIds[i]) || !replaced.deviceName.equals(devNames[i]))) {
295                 updatedDeviceInfo = true;
296             }
297         }
298
299         handlers.forEach(x -> x.handleMetadataRetrieved(this));
300
301         if (updatedDeviceInfo) {
302             this.scheduler.execute(() -> {
303                 for (Thing el : getThing().getThings()) {
304                     final ThingHandler th = el.getHandler();
305                     if (th instanceof IBridgeData bridgeData) {
306                         bridgeData.handleBridgeDataUpdated();
307                     }
308                 }
309             });
310         }
311     }
312
313     public String sendApiRequest(final TLGatewayFrame req) {
314         final UUID uid = UUID.randomUUID();
315
316         final WebServerApi api = WebServerApi.getInstance();
317         String host = "Unresolved";
318         try {
319             host = getHostname();
320             final boolean confirmGateway = req.command != TLGatewayFrame.CMD_GET_CONFIGURATION;
321             if (confirmGateway && (host.isEmpty() || currentGwId.isEmpty())) {
322                 logger.warn("{}", getLocalizedText("warning.host-gw-unknown-for-cmd", host, currentGwId, req.command));
323                 return "";
324             }
325             if (req.gatewayId.isEmpty()) {
326                 req.gatewayId = currentGwId;
327             }
328             final String reqData = LinkTapBindingConstants.GSON.toJson(req);
329             logger.debug("{} = APP BRIDGE -> GW -> Request {}", uid, reqData);
330             final String respData = api.sendRequest(host, reqData);
331             logger.debug("{} = APP BRIDGE -> GW -> Response {}", uid, respData);
332             final TLGatewayFrame gwResponseFrame = LinkTapBindingConstants.GSON.fromJson(respData,
333                     TLGatewayFrame.class);
334             if (confirmGateway && gwResponseFrame != null && !gwResponseFrame.gatewayId.equals(req.gatewayId)) {
335                 logger.warn("{}", getLocalizedText("warning.response-from-wrong-gw-id", uid, req.gatewayId,
336                         gwResponseFrame.gatewayId));
337                 return "";
338             }
339             if (gwResponseFrame != null && req.command != gwResponseFrame.command) {
340                 logger.warn("{}",
341                         getLocalizedText("warning.incorrect-cmd-resp", uid, req.command, gwResponseFrame.command));
342                 return "";
343             }
344             return respData;
345         } catch (NotTapLinkGatewayException e) {
346             logger.warn("{}", getLocalizedText("warning.not-taplink-gw", uid, host));
347         } catch (UnknownHostException e) {
348             logger.warn("{}", getLocalizedText("warning.comms-issue-auto-retry", uid, e.getMessage()));
349             scheduleReconnect();
350         } catch (TransientCommunicationIssueException e) {
351             logger.warn("{}", getLocalizedText("warning.comms-issue-auto-retry", uid, getLocalizedText(e.getI18Key())));
352             scheduleReconnect();
353         }
354         return "";
355     }
356
357     private void connect() {
358         // Check if we can resolve the remote host, if so then it can be mapped back to a bridge handler.
359         // If not further communications would fail - so it's offline.
360         if (!registerBridge(this)) {
361             requestMdnsScan();
362             scheduleReconnect();
363             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
364                     getLocalizedText("bridge.error.host-not-found"));
365             return;
366         }
367
368         final WebServerApi api = WebServerApi.getInstance();
369         api.setHttpClient(httpClientProvider.getHttpClient());
370         try {
371             final Map<String, String> bridgeProps = api.getBridgeProperities(bridgeKey);
372             if (!bridgeProps.isEmpty()) {
373                 final String readGwId = bridgeProps.get(BRIDGE_PROP_GW_ID);
374                 if (readGwId != null) {
375                     currentGwId = readGwId;
376                 }
377                 final Map<String, String> currentProps = editProperties();
378                 currentProps.putAll(bridgeProps);
379                 updateProperties(currentProps);
380             } else {
381                 if (!api.unlockWebInterface(bridgeKey, config.username, config.password)) {
382                     updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
383                             getLocalizedText("bridge.error.check-credentials"));
384                     return;
385                 }
386             }
387
388             getGatewayConfiguration();
389
390             // Update the GW ID -> this bridge lookup
391             GW_ID_LOOKUP.registerItem(currentGwId, this, () -> {
392             });
393
394             if (Thread.currentThread().isInterrupted()) {
395                 return;
396             }
397
398             @NotNull
399             final String hostname = getHostname(config);
400
401             String localServerAddr = "";
402             try (Socket socket = new Socket()) {
403                 try {
404                     socket.connect(new InetSocketAddress(hostname, 80), 1500);
405                 } catch (IOException e) {
406                     logger.warn("{}", getLocalizedText("warning.failed-local-address-detection", e.getMessage()));
407                     throw new TransientCommunicationIssueException("Local address lookup failure",
408                             "exception.local-addr-lookup-failure");
409                 }
410                 localServerAddr = socket.getLocalAddress().getHostAddress();
411                 logger.trace("Local address for connectivity is {}", socket.getLocalAddress().getHostAddress());
412             } catch (IOException e) {
413                 logger.trace("Failed to connect to remote device due to exception", e);
414             }
415
416             final String servletEp = BindingServlet.getServletAddress(localServerAddr,
417                     getLocalizedText("warning.no-http-server-port"));
418             final Optional<String> servletEpOpt = (!servletEp.isEmpty()) ? Optional.of(servletEp) : Optional.empty();
419             api.configureBridge(hostname, Optional.of(config.enableMDNS), Optional.of(config.enableJSONComms),
420                     servletEpOpt);
421             updateStatus(ThingStatus.ONLINE);
422             if (Thread.currentThread().isInterrupted()) {
423                 return;
424             }
425             startGwPolling();
426             connectRepair = null;
427
428             final Firmware firmware = new Firmware(getThing().getProperties().get(BRIDGE_PROP_GW_VER));
429             if (!firmware.supportsLocalConfig()) {
430                 logger.warn("{}", getLocalizedText("warning.fw-update-local-config", getThing().getLabel(),
431                         firmware.getRecommendedMinVer()));
432             }
433         } catch (InterruptedException ignored) {
434         } catch (LinkTapException | NotTapLinkGatewayException e) {
435             deregisterBridge(this);
436             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
437                     getLocalizedText("bridge.error.target-is-not-gateway"));
438         } catch (TransientCommunicationIssueException e) {
439             scheduleReconnect();
440             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
441                     getLocalizedText("bridge.error.cannot-connect"));
442         } catch (UnknownHostException e) {
443             scheduleReconnect();
444             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
445                     getLocalizedText("bridge.error.unknown-host"));
446         }
447     }
448
449     private void scheduleReconnect() {
450         scheduleReconnect(15);
451     }
452
453     public void attemptReconnectIfNeeded() {
454         if (ThingStatus.OFFLINE.equals(getThing().getStatus())) {
455             synchronized (reconnectFutureLock) {
456                 if (connectRepair != null) {
457                     scheduleReconnect(0);
458                 }
459             }
460         }
461     }
462
463     private void scheduleReconnect(int seconds) {
464         if (seconds < 1) {
465             seconds = 1;
466         }
467         logger.trace("Scheduling connection re-attempt in {} seconds", seconds);
468         synchronized (reconnectFutureLock) {
469             cancelReconnect();
470             connectRepair = scheduler.schedule(this::connect, seconds, TimeUnit.SECONDS); // Schedule a retry
471         }
472     }
473
474     private void cancelReconnect() {
475         synchronized (reconnectFutureLock) {
476             final @Nullable ScheduledFuture<?> ref = connectRepair;
477             if (ref != null) {
478                 ref.cancel(true);
479                 connectRepair = null;
480             }
481         }
482     }
483
484     @Override
485     public void handleCommand(final ChannelUID channelUID, final Command command) {
486     }
487
488     protected @NotNull String getHostname() throws UnknownHostException {
489         return getHostname(config);
490     }
491
492     private @NotNull String getHostname(final LinkTapBridgeConfiguration config) throws UnknownHostException {
493         @NotNull
494         String hostname = config.host;
495         final String mdnsLookup = MDNS_LOOKUP.getItem(hostname);
496         if (mdnsLookup != null) {
497             hostname = mdnsLookup;
498         }
499         return InetAddress.getByName(hostname).getHostAddress();
500     }
501
502     private final Object singleCommLock = new Object();
503
504     public String sendRequest(final TLGatewayFrame frame) throws DeviceIdException, InvalidParameterException {
505         // Validate the payload is within the expected limits for the device its being sent to
506         if (config.enforceProtocolLimits) {
507             final Collection<ValidationError> errors = frame.getValidationErrors();
508             if (!errors.isEmpty()) {
509                 final String bugs = errors.stream().filter(x -> x.getCause() == BUG).map(ValidationError::toString)
510                         .collect(Collectors.joining(","));
511                 final String userDataIssues = errors.stream().filter(x -> x.getCause() == USER)
512                         .map(ValidationError::toString).collect(Collectors.joining(","));
513                 if (!bugs.isEmpty()) {
514                     logger.warn("{}",
515                             getLocalizedText("bug-report.unexpected-payload-failure", getThing().getLabel(), bugs));
516                 }
517                 if (!userDataIssues.isEmpty()) {
518                     logger.warn("{}", getLocalizedText("warning.user-data-payload-failure", getThing().getLabel(),
519                             userDataIssues));
520                 }
521                 return "";
522             }
523         }
524         final TransactionProcessor tp = TransactionProcessor.getInstance();
525         final String gatewayId = getGatewayId();
526         if (gatewayId == null) {
527             logger.warn("{}", getLocalizedText("warning.error-with-gw-id"));
528             return "";
529         }
530         frame.gatewayId = gatewayId;
531         // The gateway is a single device that may have to do RF, limit the comm's to ensure
532         // it can maintain a good QoS. Responses for most commands are very fast on a reasonable network.
533         try {
534             synchronized (singleCommLock) {
535                 try {
536                     return tp.sendRequest(this, frame);
537                 } catch (final CommandNotSupportedException cnse) {
538                     logger.warn("{}",
539                             getLocalizedText("warning.device-no-accept", getThing().getLabel(), cnse.getMessage()));
540                 }
541             }
542         } catch (final GatewayIdException gide) {
543             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, gide.getI18Key());
544         }
545         return "";
546     }
547
548     public ThingUID getUID() {
549         return thing.getUID();
550     }
551
552     /**
553      * Discovery handling of Gateway owned Devices
554      */
555
556     public void registerMetaDataUpdatedHandler(DeviceMetaDataUpdatedHandler dmduh) {
557         handlers.add(dmduh);
558     }
559
560     public void unregisterMetaDataUpdatedHandler(DeviceMetaDataUpdatedHandler dmduh) {
561         handlers.remove(dmduh);
562     }
563
564     private final CopyOnWriteArrayList<DeviceMetaDataUpdatedHandler> handlers = new CopyOnWriteArrayList<>();
565
566     private ConcurrentMap<String, LinkTapDeviceMetadata> discoveredDevices = new ConcurrentHashMap<>();
567
568     public final Stream<LinkTapDeviceMetadata> getDiscoveredDevices() {
569         return discoveredDevices.values().stream();
570     }
571
572     public final Map<String, LinkTapDeviceMetadata> getDeviceLookup() {
573         return discoveredDevices;
574     }
575
576     public void processGatewayCommand(final int commandId, final String frame) {
577         logger.debug("{} processing gateway request with command {}", this.getThing().getLabel(), commandId);
578         // Store this so that the only when necessary can polls be done - aka
579         // no direct from Gateway communications.
580         lastGwCommandRecvTs = System.currentTimeMillis();
581         switch (commandId) {
582             case CMD_HANDSHAKE:
583                 lastGetConfigCache.invalidateValue();
584                 processCommand0(frame);
585                 break;
586             case CMD_RAINFALL_DATA:
587             case CMD_NOTIFICATION_WATERING_SKIPPED:
588             case CMD_DATETIME_SYNC:
589                 logger.debug("No implementation for command {} for processing the GW request", commandId);
590         }
591     }
592
593     private void processCommand0(final String request) {
594         final GatewayConfigResp decoded = LinkTapBindingConstants.GSON.fromJson(request, GatewayConfigResp.class);
595
596         // Check the current version property matches and if not update it
597         final String currentVerKnown = editProperties().get(BRIDGE_PROP_GW_VER);
598         if (decoded != null && currentVerKnown != null && !decoded.version.isEmpty()) {
599             if (!currentVerKnown.equals(decoded.version)) {
600                 final Map<String, String> currentProps = editProperties();
601                 currentProps.put(BRIDGE_PROP_GW_VER, decoded.version);
602                 updateProperties(currentProps);
603             }
604         }
605         final String currentVolUnit = editProperties().get(BRIDGE_PROP_VOL_UNIT);
606         if (decoded != null && currentVolUnit != null && !decoded.volumeUnit.isEmpty()) {
607             if (!currentVolUnit.equals(decoded.volumeUnit)) {
608                 final Map<String, String> currentProps = editProperties();
609                 currentProps.put(BRIDGE_PROP_VOL_UNIT, decoded.volumeUnit);
610                 updateProperties(currentProps);
611             }
612         }
613         final String[] devices = decoded != null ? decoded.endDevices : EMPTY_STRING_ARRAY;
614         // Go through all the device ID's returned check we know about them.
615         // If not a background scan should be done
616         boolean fullScanRequired = false;
617         if (discoveredDevices.size() != devices.length) {
618             fullScanRequired = true;
619         }
620         if (!discoveredDevices.keySet().containsAll(Arrays.stream(devices).toList())) {
621             fullScanRequired = true;
622         }
623         if (fullScanRequired) {
624             logger.trace("The configured devices have changed a full scan should be run");
625             scheduler.execute(this::getGatewayConfiguration);
626         }
627     }
628
629     @Override
630     public void childHandlerDisposed(ThingHandler childHandler, Thing childThing) {
631         scheduler.execute(this::getGatewayConfiguration);
632         super.childHandlerDisposed(childHandler, childThing);
633     }
634 }