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