2 * Copyright (c) 2010-2022 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.vesync.internal.handlers;
15 import static org.openhab.binding.vesync.internal.VeSyncConstants.*;
16 import static org.openhab.binding.vesync.internal.dto.requests.VeSyncProtocolConstants.V2_BYPASS_ENDPOINT;
18 import java.time.Duration;
19 import java.util.ArrayList;
20 import java.util.HashMap;
21 import java.util.List;
23 import java.util.concurrent.ScheduledFuture;
24 import java.util.concurrent.TimeUnit;
26 import javax.validation.constraints.NotNull;
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;
52 * The {@link VeSyncBaseDeviceHandler} is responsible for handling commands, which are
53 * sent to one of the channels.
55 * @author David Goodyear - Initial contribution
58 public abstract class VeSyncBaseDeviceHandler extends BaseThingHandler {
60 private final Logger logger = LoggerFactory.getLogger(VeSyncBaseDeviceHandler.class);
62 private static final String MARKER_INVALID_DEVICE_KEY = "---INVALID---";
65 protected String deviceLookupKey = MARKER_INVALID_DEVICE_KEY;
67 private static final int CACHE_TIMEOUT_SECOND = 5;
69 private int activePollRate = -2; // -1 is used to deactivate the poll, so default to a different value
71 private @Nullable ScheduledFuture<?> backgroundPollingScheduler;
72 private final Object pollConfigLock = new Object();
74 protected @Nullable VeSyncClient veSyncClient;
76 private volatile long latestReadBackMillis = 0;
79 ScheduledFuture<?> initialPollingTask = null;
82 ScheduledFuture<?> readbackPollTask = null;
84 public VeSyncBaseDeviceHandler(Thing thing) {
88 protected @Nullable Channel findChannelById(final String channelGroupId) {
89 return getThing().getChannel(channelGroupId);
92 protected ExpiringCache<String> lastPollResultCache = new ExpiringCache<>(Duration.ofSeconds(CACHE_TIMEOUT_SECOND),
93 VeSyncBaseDeviceHandler::expireCacheContents);
95 private static @Nullable String expireCacheContents() {
100 public void channelLinked(ChannelUID channelUID) {
101 super.channelLinked(channelUID);
103 scheduler.execute(this::pollForUpdate);
106 protected void setBackgroundPollInterval(final int seconds) {
107 if (activePollRate == seconds) {
110 logger.debug("Reconfiguring devices background polling to {} seconds", seconds);
112 synchronized (pollConfigLock) {
113 final ScheduledFuture<?> job = backgroundPollingScheduler;
115 // Cancel the current scan's and re-schedule as required
116 if (job != null && !job.isCancelled()) {
118 backgroundPollingScheduler = null;
121 logger.trace("Device data is polling every {} seconds", seconds);
122 backgroundPollingScheduler = scheduler.scheduleWithFixedDelay(this::pollForUpdate, seconds, seconds,
125 activePollRate = seconds;
129 public boolean requiresMetaDataFrequentUpdates() {
130 return (MARKER_INVALID_DEVICE_KEY.equals(deviceLookupKey));
133 private @Nullable BridgeHandler getBridgeHandler() {
134 Bridge bridgeRef = getBridge();
135 if (bridgeRef == null) {
138 return bridgeRef.getHandler();
142 protected boolean isDeviceOnline() {
143 BridgeHandler bridgeHandler = getBridgeHandler();
144 if (bridgeHandler != null && bridgeHandler instanceof VeSyncBridgeHandler) {
145 VeSyncBridgeHandler vesyncBridgeHandler = (VeSyncBridgeHandler) bridgeHandler;
147 VeSyncManagedDeviceBase metadata = vesyncBridgeHandler.api.getMacLookupMap().get(deviceLookupKey);
149 if (metadata == null) {
153 return ("online".equals(metadata.connectionStatus));
158 public void updateDeviceMetaData() {
159 Map<String, String> newProps = null;
161 BridgeHandler bridgeHandler = getBridgeHandler();
162 if (bridgeHandler != null && bridgeHandler instanceof VeSyncBridgeHandler) {
163 VeSyncBridgeHandler vesyncBridgeHandler = (VeSyncBridgeHandler) bridgeHandler;
165 VeSyncManagedDeviceBase metadata = vesyncBridgeHandler.api.getMacLookupMap().get(deviceLookupKey);
167 if (metadata == null) {
171 newProps = getMetadataProperities(metadata);
173 // Refresh the device -> protocol mapping
174 deviceLookupKey = getValidatedIdString();
176 if ("online".equals(metadata.connectionStatus)) {
177 updateStatus(ThingStatus.ONLINE);
178 } else if ("offline".equals(metadata.connectionStatus)) {
179 updateStatus(ThingStatus.OFFLINE);
183 if (newProps != null && !newProps.isEmpty()) {
184 this.updateProperties(newProps);
186 if (!isDeviceSupported()) {
187 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
188 "Device Model or Type not supported by this thing");
194 * Override this in classes that extend this, to
196 protected void customiseChannels() {
199 protected String[] getChannelsToRemove() {
200 return new String[] {};
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);
209 channelsToBeRemoved.add(ch);
213 final ThingBuilder builder = editThing().withoutChannels(channelsToBeRemoved);
214 updateThing(builder.build());
218 * Extract the common properties for all devices, from the given meta-data of a device.
220 * @param metadata - the meta-data of a device
221 * @return - Map of common props
223 public Map<String, String> getMetadataProperities(final @Nullable VeSyncManagedDeviceBase metadata) {
224 if (metadata == null) {
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());
235 protected synchronized @Nullable VeSyncClient getVeSyncClient() {
236 if (veSyncClient == null) {
237 Bridge bridge = getBridge();
238 if (bridge == null) {
241 ThingHandler handler = bridge.getHandler();
242 if (handler instanceof VeSyncClient) {
243 veSyncClient = (VeSyncClient) handler;
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();
262 public String getValidatedIdString() {
263 final VeSyncDeviceConfiguration config = getConfigAs(VeSyncDeviceConfiguration.class);
265 BridgeHandler bridgeHandler = getBridgeHandler();
266 if (bridgeHandler != null && bridgeHandler instanceof VeSyncBridgeHandler) {
267 VeSyncBridgeHandler vesyncBridgeHandler = (VeSyncBridgeHandler) bridgeHandler;
269 final String configMac = config.macId;
271 // Try to use the mac directly
272 if (configMac != null) {
273 logger.debug("Searching for device mac id : {}", configMac);
275 VeSyncManagedDeviceBase metadata = vesyncBridgeHandler.api.getMacLookupMap()
276 .get(configMac.toLowerCase());
278 if (metadata != null && metadata.macId != null) {
279 return metadata.macId;
283 final String deviceName = config.deviceName;
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);
290 for (String val : matchedMacIds) {
291 logger.debug("Found MAC match on name with : {}", val);
294 if (matchedMacIds.length != 1) {
295 return MARKER_INVALID_DEVICE_KEY;
298 if (vesyncBridgeHandler.api.getMacLookupMap().get(matchedMacIds[0]) != null) {
299 return matchedMacIds[0];
304 return MARKER_INVALID_DEVICE_KEY;
308 public void initialize() {
309 intializeDeviceForUse();
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");
319 updateStatus(ThingStatus.UNKNOWN);
322 deviceLookupKey = getValidatedIdString();
324 // Populate device props - this is required for polling, to cross-check the device model.
325 updateDeviceMetaData();
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)) {
331 // This will force the bridge to push the configuration parameters for polling to the handler
332 bridge.updateThing(this);
334 // Give the bridge time to build the datamaps of the devices
335 scheduleInitialPoll();
338 private void scheduleInitialPoll() {
339 cancelInitialPoll(false);
340 initialPollingTask = scheduler.schedule(this::pollForUpdate, 10, TimeUnit.SECONDS);
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;
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;
360 public void dispose() {
361 cancelReadbackPoll(true);
362 cancelInitialPoll(true);
365 public void pollForUpdate() {
366 pollForDeviceData(lastPollResultCache);
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).
373 * @param cachedResponse - An Expiring cache that can be utilised to store the responses, to prevent poll bursts by
374 * coalescing the requests.
376 protected abstract void pollForDeviceData(final ExpiringCache<String> cachedResponse);
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.
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.
386 protected final String sendV2BypassControlCommand(final String method,
387 final VeSyncRequestManagedDeviceBypassV2.EmptyPayload payload) {
388 return sendV2BypassControlCommand(method, payload, true);
392 * Send a BypassV2 command to the device. The body of the response is returned.
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
398 * @return - The body of the response, or EMPTY_STRING if the command could not be issued.
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();
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");
416 if (MARKER_INVALID_DEVICE_KEY.equals(deviceLookupKey)) {
417 deviceLookupKey = getValidatedIdString();
419 VeSyncClient client = getVeSyncClient();
420 if (client != null) {
421 return client.reqV2Authorized(url, deviceLookupKey, request);
423 throw new DeviceUnknownException("Missing client");
425 } catch (AuthenticationException e) {
426 logger.debug("Auth exception {}", e.getMessage());
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();
438 * Send a BypassV2 command to the device. The body of the response is returned.
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.
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");
451 VeSyncRequestManagedDeviceBypassV2 readReq = new VeSyncRequestManagedDeviceBypassV2();
452 readReq.payload.method = method;
453 readReq.payload.data = payload;
456 if (MARKER_INVALID_DEVICE_KEY.equals(deviceLookupKey)) {
457 deviceLookupKey = getValidatedIdString();
459 VeSyncClient client = getVeSyncClient();
460 if (client != null) {
461 return client.reqV2Authorized(V2_BYPASS_ENDPOINT, deviceLookupKey, readReq);
463 throw new DeviceUnknownException("Missing client");
465 } catch (AuthenticationException e) {
466 logger.debug("Auth exception {}", e.getMessage());
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();
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");
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();
494 }, 1L, TimeUnit.SECONDS);
497 public void updateBridgeBasedPolls(VeSyncBridgeConfiguration config) {
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
504 * @return - true if the device is supported, false if the device isn't. E.g. Unknown model id in meta-data would
507 protected abstract boolean isDeviceSupported();