]> git.basschouten.com Git - openhab-addons.git/blob
681c4fe597e2a48ce9cd058eee4e11b6943cf690
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2022 Contributors to the openHAB project
3  *
4  * See the NOTICE file(s) distributed with this work for additional
5  * information.
6  *
7  * This program and the accompanying materials are made available under the
8  * terms of the Eclipse Public License 2.0 which is available at
9  * http://www.eclipse.org/legal/epl-2.0
10  *
11  * SPDX-License-Identifier: EPL-2.0
12  */
13 package org.openhab.binding.vesync.internal.handlers;
14
15 import static org.openhab.binding.vesync.internal.VeSyncConstants.*;
16 import static org.openhab.binding.vesync.internal.dto.requests.VeSyncProtocolConstants.V2_BYPASS_ENDPOINT;
17
18 import java.time.Duration;
19 import java.util.ArrayList;
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
26 import javax.validation.constraints.NotNull;
27
28 import org.eclipse.jdt.annotation.NonNullByDefault;
29 import org.eclipse.jdt.annotation.Nullable;
30 import org.openhab.binding.vesync.internal.VeSyncBridgeConfiguration;
31 import org.openhab.binding.vesync.internal.VeSyncDeviceConfiguration;
32 import org.openhab.binding.vesync.internal.dto.requests.VeSyncAuthenticatedRequest;
33 import org.openhab.binding.vesync.internal.dto.requests.VeSyncRequestManagedDeviceBypassV2;
34 import org.openhab.binding.vesync.internal.dto.responses.VeSyncManagedDeviceBase;
35 import org.openhab.binding.vesync.internal.exceptions.AuthenticationException;
36 import org.openhab.binding.vesync.internal.exceptions.DeviceUnknownException;
37 import org.openhab.core.cache.ExpiringCache;
38 import org.openhab.core.thing.Bridge;
39 import org.openhab.core.thing.Channel;
40 import org.openhab.core.thing.ChannelUID;
41 import org.openhab.core.thing.Thing;
42 import org.openhab.core.thing.ThingStatus;
43 import org.openhab.core.thing.ThingStatusDetail;
44 import org.openhab.core.thing.binding.BaseThingHandler;
45 import org.openhab.core.thing.binding.BridgeHandler;
46 import org.openhab.core.thing.binding.ThingHandler;
47 import org.openhab.core.thing.binding.builder.ThingBuilder;
48 import org.slf4j.Logger;
49 import org.slf4j.LoggerFactory;
50
51 /**
52  * The {@link VeSyncBaseDeviceHandler} is responsible for handling commands, which are
53  * sent to one of the channels.
54  *
55  * @author David Goodyear - Initial contribution
56  */
57 @NonNullByDefault
58 public abstract class VeSyncBaseDeviceHandler extends BaseThingHandler {
59
60     private final Logger logger = LoggerFactory.getLogger(VeSyncBaseDeviceHandler.class);
61
62     private static final String MARKER_INVALID_DEVICE_KEY = "---INVALID---";
63
64     @NotNull
65     protected String deviceLookupKey = MARKER_INVALID_DEVICE_KEY;
66
67     private static final int CACHE_TIMEOUT_SECOND = 5;
68
69     private int activePollRate = -2; // -1 is used to deactivate the poll, so default to a different value
70
71     private @Nullable ScheduledFuture<?> backgroundPollingScheduler;
72     private final Object pollConfigLock = new Object();
73
74     protected @Nullable VeSyncClient veSyncClient;
75
76     private volatile long latestReadBackMillis = 0;
77
78     @Nullable
79     ScheduledFuture<?> initialPollingTask = null;
80
81     @Nullable
82     ScheduledFuture<?> readbackPollTask = null;
83
84     public VeSyncBaseDeviceHandler(Thing thing) {
85         super(thing);
86     }
87
88     protected @Nullable Channel findChannelById(final String channelGroupId) {
89         return getThing().getChannel(channelGroupId);
90     }
91
92     protected ExpiringCache<String> lastPollResultCache = new ExpiringCache<>(Duration.ofSeconds(CACHE_TIMEOUT_SECOND),
93             VeSyncBaseDeviceHandler::expireCacheContents);
94
95     private static @Nullable String expireCacheContents() {
96         return null;
97     }
98
99     @Override
100     public void channelLinked(ChannelUID channelUID) {
101         super.channelLinked(channelUID);
102
103         scheduler.execute(this::pollForUpdate);
104     }
105
106     protected void setBackgroundPollInterval(final int seconds) {
107         if (activePollRate == seconds) {
108             return;
109         }
110         logger.debug("Reconfiguring devices background polling to {} seconds", seconds);
111
112         synchronized (pollConfigLock) {
113             final ScheduledFuture<?> job = backgroundPollingScheduler;
114
115             // Cancel the current scan's and re-schedule as required
116             if (job != null && !job.isCancelled()) {
117                 job.cancel(true);
118                 backgroundPollingScheduler = null;
119             }
120             if (seconds > 0) {
121                 logger.trace("Device data is polling every {} seconds", seconds);
122                 backgroundPollingScheduler = scheduler.scheduleWithFixedDelay(this::pollForUpdate, seconds, seconds,
123                         TimeUnit.SECONDS);
124             }
125             activePollRate = seconds;
126         }
127     }
128
129     public boolean requiresMetaDataFrequentUpdates() {
130         return (MARKER_INVALID_DEVICE_KEY.equals(deviceLookupKey));
131     }
132
133     private @Nullable BridgeHandler getBridgeHandler() {
134         Bridge bridgeRef = getBridge();
135         if (bridgeRef == null) {
136             return null;
137         } else {
138             return bridgeRef.getHandler();
139         }
140     }
141
142     protected boolean isDeviceOnline() {
143         BridgeHandler bridgeHandler = getBridgeHandler();
144         if (bridgeHandler != null && bridgeHandler instanceof VeSyncBridgeHandler) {
145             VeSyncBridgeHandler vesyncBridgeHandler = (VeSyncBridgeHandler) bridgeHandler;
146             @Nullable
147             VeSyncManagedDeviceBase metadata = vesyncBridgeHandler.api.getMacLookupMap().get(deviceLookupKey);
148
149             if (metadata == null) {
150                 return false;
151             }
152
153             return ("online".equals(metadata.connectionStatus));
154         }
155         return false;
156     }
157
158     public void updateDeviceMetaData() {
159         Map<String, String> newProps = null;
160
161         BridgeHandler bridgeHandler = getBridgeHandler();
162         if (bridgeHandler != null && bridgeHandler instanceof VeSyncBridgeHandler) {
163             VeSyncBridgeHandler vesyncBridgeHandler = (VeSyncBridgeHandler) bridgeHandler;
164             @Nullable
165             VeSyncManagedDeviceBase metadata = vesyncBridgeHandler.api.getMacLookupMap().get(deviceLookupKey);
166
167             if (metadata == null) {
168                 return;
169             }
170
171             newProps = getMetadataProperities(metadata);
172
173             // Refresh the device -> protocol mapping
174             deviceLookupKey = getValidatedIdString();
175
176             if ("online".equals(metadata.connectionStatus)) {
177                 updateStatus(ThingStatus.ONLINE);
178             } else if ("offline".equals(metadata.connectionStatus)) {
179                 updateStatus(ThingStatus.OFFLINE);
180             }
181         }
182
183         if (newProps != null && !newProps.isEmpty()) {
184             this.updateProperties(newProps);
185             removeChannels();
186             if (!isDeviceSupported()) {
187                 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
188                         "Device Model or Type not supported by this thing");
189             }
190         }
191     }
192
193     /**
194      * Override this in classes that extend this, to
195      */
196     protected void customiseChannels() {
197     }
198
199     protected String[] getChannelsToRemove() {
200         return new String[] {};
201     }
202
203     private void removeChannels() {
204         final String[] channelsToRemove = getChannelsToRemove();
205         final List<Channel> channelsToBeRemoved = new ArrayList<>();
206         for (String name : channelsToRemove) {
207             Channel ch = getThing().getChannel(name);
208             if (ch != null) {
209                 channelsToBeRemoved.add(ch);
210             }
211         }
212
213         final ThingBuilder builder = editThing().withoutChannels(channelsToBeRemoved);
214         updateThing(builder.build());
215     }
216
217     /**
218      * Extract the common properties for all devices, from the given meta-data of a device.
219      * 
220      * @param metadata - the meta-data of a device
221      * @return - Map of common props
222      */
223     public Map<String, String> getMetadataProperities(final @Nullable VeSyncManagedDeviceBase metadata) {
224         if (metadata == null) {
225             return Map.of();
226         }
227         final Map<String, String> newProps = new HashMap<>(4);
228         newProps.put(DEVICE_PROP_DEVICE_MAC_ID, metadata.getMacId());
229         newProps.put(DEVICE_PROP_DEVICE_NAME, metadata.getDeviceName());
230         newProps.put(DEVICE_PROP_DEVICE_TYPE, metadata.getDeviceType());
231         newProps.put(DEVICE_PROP_DEVICE_UUID, metadata.getUuid());
232         return newProps;
233     }
234
235     protected synchronized @Nullable VeSyncClient getVeSyncClient() {
236         if (veSyncClient == null) {
237             Bridge bridge = getBridge();
238             if (bridge == null) {
239                 return null;
240             }
241             ThingHandler handler = bridge.getHandler();
242             if (handler instanceof VeSyncClient) {
243                 veSyncClient = (VeSyncClient) handler;
244             } else {
245                 return null;
246             }
247         }
248         return veSyncClient;
249     }
250
251     protected void requestBridgeFreqScanMetadataIfReq() {
252         if (requiresMetaDataFrequentUpdates()) {
253             BridgeHandler bridgeHandler = getBridgeHandler();
254             if (bridgeHandler != null && bridgeHandler instanceof VeSyncBridgeHandler) {
255                 VeSyncBridgeHandler vesyncBridgeHandler = (VeSyncBridgeHandler) bridgeHandler;
256                 vesyncBridgeHandler.checkIfIncreaseScanRateRequired();
257             }
258         }
259     }
260
261     @NotNull
262     public String getValidatedIdString() {
263         final VeSyncDeviceConfiguration config = getConfigAs(VeSyncDeviceConfiguration.class);
264
265         BridgeHandler bridgeHandler = getBridgeHandler();
266         if (bridgeHandler != null && bridgeHandler instanceof VeSyncBridgeHandler) {
267             VeSyncBridgeHandler vesyncBridgeHandler = (VeSyncBridgeHandler) bridgeHandler;
268
269             final String configMac = config.macId;
270
271             // Try to use the mac directly
272             if (configMac != null) {
273                 logger.debug("Searching for device mac id : {}", configMac);
274                 @Nullable
275                 VeSyncManagedDeviceBase metadata = vesyncBridgeHandler.api.getMacLookupMap()
276                         .get(configMac.toLowerCase());
277
278                 if (metadata != null && metadata.macId != null) {
279                     return metadata.macId;
280                 }
281             }
282
283             final String deviceName = config.deviceName;
284
285             // Check if the device name can be matched to a single device
286             if (deviceName != null) {
287                 final String[] matchedMacIds = vesyncBridgeHandler.api.getMacLookupMap().values().stream()
288                         .filter(x -> deviceName.equals(x.deviceName)).map(x -> x.macId).toArray(String[]::new);
289
290                 for (String val : matchedMacIds) {
291                     logger.debug("Found MAC match on name with : {}", val);
292                 }
293
294                 if (matchedMacIds.length != 1) {
295                     return MARKER_INVALID_DEVICE_KEY;
296                 }
297
298                 if (vesyncBridgeHandler.api.getMacLookupMap().get(matchedMacIds[0]) != null) {
299                     return matchedMacIds[0];
300                 }
301             }
302         }
303
304         return MARKER_INVALID_DEVICE_KEY;
305     }
306
307     @Override
308     public void initialize() {
309         intializeDeviceForUse();
310     }
311
312     private void intializeDeviceForUse() {
313         // Sanity check basic setup
314         final VeSyncBridgeHandler bridge = (VeSyncBridgeHandler) getBridgeHandler();
315         if (bridge == null) {
316             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_UNINITIALIZED, "Missing bridge for API link");
317             return;
318         } else {
319             updateStatus(ThingStatus.UNKNOWN);
320         }
321
322         deviceLookupKey = getValidatedIdString();
323
324         // Populate device props - this is required for polling, to cross-check the device model.
325         updateDeviceMetaData();
326
327         // If the base device class marks it as offline there is an issue that will prevent normal operation
328         if (getThing().getStatus().equals(ThingStatus.OFFLINE)) {
329             return;
330         }
331         // This will force the bridge to push the configuration parameters for polling to the handler
332         bridge.updateThing(this);
333
334         // Give the bridge time to build the datamaps of the devices
335         scheduleInitialPoll();
336     }
337
338     private void scheduleInitialPoll() {
339         cancelInitialPoll(false);
340         initialPollingTask = scheduler.schedule(this::pollForUpdate, 10, TimeUnit.SECONDS);
341     }
342
343     private void cancelInitialPoll(final boolean interruptAllowed) {
344         final ScheduledFuture<?> pollJob = initialPollingTask;
345         if (pollJob != null && !pollJob.isCancelled()) {
346             pollJob.cancel(interruptAllowed);
347             initialPollingTask = null;
348         }
349     }
350
351     private void cancelReadbackPoll(final boolean interruptAllowed) {
352         final ScheduledFuture<?> pollJob = readbackPollTask;
353         if (pollJob != null && !pollJob.isCancelled()) {
354             pollJob.cancel(interruptAllowed);
355             readbackPollTask = null;
356         }
357     }
358
359     @Override
360     public void dispose() {
361         cancelReadbackPoll(true);
362         cancelInitialPoll(true);
363     }
364
365     public void pollForUpdate() {
366         pollForDeviceData(lastPollResultCache);
367     }
368
369     /**
370      * This should be implemented by subclasses to provide the implementation for polling the specific
371      * data for the type the class is responsible for. (Excluding meta data).
372      *
373      * @param cachedResponse - An Expiring cache that can be utilised to store the responses, to prevent poll bursts by
374      *            coalescing the requests.
375      */
376     protected abstract void pollForDeviceData(final ExpiringCache<String> cachedResponse);
377
378     /**
379      * Send a BypassV2 command to the device. The body of the response is returned, a poll is done if the request
380      * should have been dispatched.
381      * 
382      * @param method - the V2 bypass method
383      * @param payload - The payload to send in within the V2 bypass command
384      * @return - The body of the response, or EMPTY_STRING if the command could not be issued.
385      */
386     protected final String sendV2BypassControlCommand(final String method,
387             final VeSyncRequestManagedDeviceBypassV2.EmptyPayload payload) {
388         return sendV2BypassControlCommand(method, payload, true);
389     }
390
391     /**
392      * Send a BypassV2 command to the device. The body of the response is returned.
393      * 
394      * @param method - the V2 bypass method
395      * @param payload - The payload to send in within the V2 bypass command
396      * @param readbackDevice - if set to true after the command has been issued, whether a poll of the devices data
397      *            should be run.
398      * @return - The body of the response, or EMPTY_STRING if the command could not be issued.
399      */
400     protected final String sendV2BypassControlCommand(final String method,
401             final VeSyncRequestManagedDeviceBypassV2.EmptyPayload payload, final boolean readbackDevice) {
402         final String result = sendV2BypassCommand(method, payload);
403         if (!result.equals(EMPTY_STRING) && readbackDevice) {
404             performReadbackPoll();
405         }
406         return result;
407     }
408
409     public final String sendV1Command(final String method, final String url, final VeSyncAuthenticatedRequest request) {
410         if (ThingStatus.OFFLINE.equals(this.thing.getStatus())) {
411             logger.debug("Command blocked as device is offline");
412             return EMPTY_STRING;
413         }
414
415         try {
416             if (MARKER_INVALID_DEVICE_KEY.equals(deviceLookupKey)) {
417                 deviceLookupKey = getValidatedIdString();
418             }
419             VeSyncClient client = getVeSyncClient();
420             if (client != null) {
421                 return client.reqV2Authorized(url, deviceLookupKey, request);
422             } else {
423                 throw new DeviceUnknownException("Missing client");
424             }
425         } catch (AuthenticationException e) {
426             logger.debug("Auth exception {}", e.getMessage());
427             return EMPTY_STRING;
428         } catch (final DeviceUnknownException e) {
429             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
430                     "Check configuration details - " + e.getMessage());
431             // In case the name is updated server side - request the scan rate is increased
432             requestBridgeFreqScanMetadataIfReq();
433             return EMPTY_STRING;
434         }
435     }
436
437     /**
438      * Send a BypassV2 command to the device. The body of the response is returned.
439      * 
440      * @param method - the V2 bypass method
441      * @param payload - The payload to send in within the V2 bypass command
442      * @return - The body of the response, or EMPTY_STRING if the command could not be issued.
443      */
444     protected final String sendV2BypassCommand(final String method,
445             final VeSyncRequestManagedDeviceBypassV2.EmptyPayload payload) {
446         if (ThingStatus.OFFLINE.equals(this.thing.getStatus())) {
447             logger.debug("Command blocked as device is offline");
448             return EMPTY_STRING;
449         }
450
451         VeSyncRequestManagedDeviceBypassV2 readReq = new VeSyncRequestManagedDeviceBypassV2();
452         readReq.payload.method = method;
453         readReq.payload.data = payload;
454
455         try {
456             if (MARKER_INVALID_DEVICE_KEY.equals(deviceLookupKey)) {
457                 deviceLookupKey = getValidatedIdString();
458             }
459             VeSyncClient client = getVeSyncClient();
460             if (client != null) {
461                 return client.reqV2Authorized(V2_BYPASS_ENDPOINT, deviceLookupKey, readReq);
462             } else {
463                 throw new DeviceUnknownException("Missing client");
464             }
465         } catch (AuthenticationException e) {
466             logger.debug("Auth exception {}", e.getMessage());
467             return EMPTY_STRING;
468         } catch (final DeviceUnknownException e) {
469             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
470                     "Check configuration details - " + e.getMessage());
471             // In case the name is updated server side - request the scan rate is increased
472             requestBridgeFreqScanMetadataIfReq();
473             return EMPTY_STRING;
474         }
475     }
476
477     // Given several changes may be done at the same time, or in close proximity, delay the read-back to catch
478     // multiple read-back's, so a single update can handle them.
479     public void performReadbackPoll() {
480         final long requestSystemMillis = System.currentTimeMillis();
481         latestReadBackMillis = requestSystemMillis;
482         cancelReadbackPoll(false);
483         readbackPollTask = scheduler.schedule(() -> {
484             // This is a historical poll, ignore it
485             if (requestSystemMillis != latestReadBackMillis) {
486                 logger.trace("Poll read-back cancelled, another later one is scheduled to happen");
487                 return;
488             }
489             logger.trace("Read-back poll executing");
490             // Read-backs should never use the cached data - but may provide it for poll's that coincide with
491             // the caches alive duration.
492             lastPollResultCache.invalidateValue();
493             pollForUpdate();
494         }, 1L, TimeUnit.SECONDS);
495     }
496
497     public void updateBridgeBasedPolls(VeSyncBridgeConfiguration config) {
498     }
499
500     /**
501      * Subclasses should implement this method, and return true if the device is a model it can support
502      * interoperability with. If it cannot be determind to be a mode
503      *
504      * @return - true if the device is supported, false if the device isn't. E.g. Unknown model id in meta-data would
505      *         return false.
506      */
507     protected abstract boolean isDeviceSupported();
508 }