2 * Copyright (c) 2010-2023 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.*;
17 import java.util.Collection;
18 import java.util.HashMap;
21 import java.util.concurrent.CopyOnWriteArrayList;
22 import java.util.concurrent.ScheduledFuture;
23 import java.util.concurrent.TimeUnit;
25 import javax.validation.constraints.NotNull;
27 import org.eclipse.jdt.annotation.NonNullByDefault;
28 import org.eclipse.jdt.annotation.Nullable;
29 import org.openhab.binding.vesync.internal.VeSyncBridgeConfiguration;
30 import org.openhab.binding.vesync.internal.api.IHttpClientProvider;
31 import org.openhab.binding.vesync.internal.api.VeSyncV2ApiHelper;
32 import org.openhab.binding.vesync.internal.discovery.DeviceMetaDataUpdatedHandler;
33 import org.openhab.binding.vesync.internal.discovery.VeSyncDiscoveryService;
34 import org.openhab.binding.vesync.internal.dto.requests.VeSyncAuthenticatedRequest;
35 import org.openhab.binding.vesync.internal.dto.responses.VeSyncManagedDeviceBase;
36 import org.openhab.binding.vesync.internal.dto.responses.VeSyncUserSession;
37 import org.openhab.binding.vesync.internal.exceptions.AuthenticationException;
38 import org.openhab.binding.vesync.internal.exceptions.DeviceUnknownException;
39 import org.openhab.core.thing.Bridge;
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.ThingUID;
45 import org.openhab.core.thing.binding.BaseBridgeHandler;
46 import org.openhab.core.thing.binding.ThingHandler;
47 import org.openhab.core.thing.binding.ThingHandlerService;
48 import org.openhab.core.types.Command;
49 import org.slf4j.Logger;
50 import org.slf4j.LoggerFactory;
53 * The {@link VeSyncBridgeHandler} is responsible for handling the bridge things created to use the VeSync
54 * API. This way, the user credentials may be entered only once.
56 * @author David Goodyear - Initial Contribution
59 public class VeSyncBridgeHandler extends BaseBridgeHandler implements VeSyncClient {
61 private static final int DEFAULT_DEVICE_SCAN_INTERVAL = 600;
62 private static final int DEFAULT_DEVICE_SCAN_RECOVERY_INTERVAL = 60;
63 private static final int DEFAULT_DEVICE_SCAN_DISABLED = -1;
65 private final Logger logger = LoggerFactory.getLogger(VeSyncBridgeHandler.class);
67 private @Nullable ScheduledFuture<?> backgroundDiscoveryPollingJob;
69 protected final VeSyncV2ApiHelper api = new VeSyncV2ApiHelper();
70 private IHttpClientProvider httpClientProvider;
72 private volatile int backgroundScanTime = -1;
73 private final Object scanConfigLock = new Object();
75 public VeSyncBridgeHandler(Bridge bridge, @NotNull IHttpClientProvider httpClientProvider) {
77 this.httpClientProvider = httpClientProvider;
80 public ThingUID getUID() {
81 return thing.getUID();
84 protected void checkIfIncreaseScanRateRequired() {
85 logger.trace("Checking if increased background scanning for new devices / base information is required");
86 boolean frequentScanReq = false;
87 for (Thing th : getThing().getThings()) {
88 ThingHandler handler = th.getHandler();
89 if (handler instanceof VeSyncBaseDeviceHandler veSyncBaseDeviceHandler) {
90 if (veSyncBaseDeviceHandler.requiresMetaDataFrequentUpdates()) {
91 frequentScanReq = true;
98 && api.getMacLookupMap().values().stream().anyMatch(x -> "offline".equals(x.connectionStatus))) {
99 frequentScanReq = true;
102 if (frequentScanReq) {
103 setBackgroundScanInterval(DEFAULT_DEVICE_SCAN_RECOVERY_INTERVAL);
105 setBackgroundScanInterval(DEFAULT_DEVICE_SCAN_INTERVAL);
109 protected void setBackgroundScanInterval(final int seconds) {
110 synchronized (scanConfigLock) {
111 ScheduledFuture<?> job = backgroundDiscoveryPollingJob;
112 if (backgroundScanTime != seconds) {
114 logger.trace("Scheduling background scanning for new devices / base information every {} seconds",
117 logger.trace("Disabling background scanning for new devices / base information");
119 // Cancel the current scan's and re-schedule as required
120 if (job != null && !job.isCancelled()) {
122 backgroundDiscoveryPollingJob = null;
125 backgroundDiscoveryPollingJob = scheduler.scheduleWithFixedDelay(
126 this::runDeviceScanSequenceNoAuthErrors, seconds, seconds, TimeUnit.SECONDS);
128 backgroundScanTime = seconds;
133 public void registerMetaDataUpdatedHandler(DeviceMetaDataUpdatedHandler dmduh) {
137 public void unregisterMetaDataUpdatedHandler(DeviceMetaDataUpdatedHandler dmduh) {
138 handlers.remove(dmduh);
141 private final CopyOnWriteArrayList<DeviceMetaDataUpdatedHandler> handlers = new CopyOnWriteArrayList<>();
143 public void runDeviceScanSequenceNoAuthErrors() {
145 runDeviceScanSequence();
146 updateStatus(ThingStatus.ONLINE);
147 } catch (AuthenticationException ae) {
148 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "Check login credentials");
152 public void runDeviceScanSequence() throws AuthenticationException {
153 logger.trace("Scanning for new devices / base information now");
154 api.discoverDevices();
155 handlers.forEach(x -> x.handleMetadataRetrieved(this));
156 checkIfIncreaseScanRateRequired();
161 public java.util.stream.Stream<@NotNull VeSyncManagedDeviceBase> getAirPurifiersMetadata() {
162 return api.getMacLookupMap().values().stream().filter(x -> !VeSyncBaseDeviceHandler
163 .getDeviceFamilyMetadata(x.getDeviceType(), VeSyncDeviceAirPurifierHandler.DEV_TYPE_FAMILY_AIR_PURIFIER,
164 VeSyncDeviceAirPurifierHandler.SUPPORTED_MODEL_FAMILIES)
165 .equals(VeSyncBaseDeviceHandler.UNKNOWN));
168 public java.util.stream.Stream<@NotNull VeSyncManagedDeviceBase> getAirHumidifiersMetadata() {
169 return api.getMacLookupMap().values().stream()
170 .filter(x -> !VeSyncBaseDeviceHandler
171 .getDeviceFamilyMetadata(x.getDeviceType(),
172 VeSyncDeviceAirHumidifierHandler.DEV_TYPE_FAMILY_AIR_HUMIDIFIER,
173 VeSyncDeviceAirHumidifierHandler.SUPPORTED_MODEL_FAMILIES)
174 .equals(VeSyncBaseDeviceHandler.UNKNOWN));
177 protected void updateThings() {
178 final VeSyncBridgeConfiguration config = getConfigAs(VeSyncBridgeConfiguration.class);
179 getThing().getThings().forEach((th) -> updateThing(config, th.getHandler()));
182 public void updateThing(ThingHandler handler) {
183 final VeSyncBridgeConfiguration config = getConfigAs(VeSyncBridgeConfiguration.class);
184 updateThing(config, handler);
187 private void updateThing(VeSyncBridgeConfiguration config, @Nullable ThingHandler handler) {
188 if (handler instanceof VeSyncBaseDeviceHandler veSyncBaseDeviceHandler) {
189 veSyncBaseDeviceHandler.updateDeviceMetaData();
190 veSyncBaseDeviceHandler.updateBridgeBasedPolls(config);
195 public Collection<Class<? extends ThingHandlerService>> getServices() {
196 return Set.of(VeSyncDiscoveryService.class);
200 public void initialize() {
201 api.setHttpClient(httpClientProvider.getHttpClient());
203 VeSyncBridgeConfiguration config = getConfigAs(VeSyncBridgeConfiguration.class);
205 scheduler.submit(() -> {
206 final String passwordMd5 = VeSyncV2ApiHelper.calculateMd5(config.password);
209 api.login(config.username, passwordMd5, "Europe/London");
210 api.updateBridgeData(this);
211 runDeviceScanSequence();
212 updateStatus(ThingStatus.ONLINE);
213 } catch (final AuthenticationException ae) {
214 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "Check login credentials");
215 // The background scan will keep trying to authenticate in case the users credentials are updated on the
217 // to match the binding's configuration.
223 public void dispose() {
224 setBackgroundScanInterval(DEFAULT_DEVICE_SCAN_DISABLED);
225 api.setHttpClient(null);
229 public void handleCommand(ChannelUID channelUID, Command command) {
230 logger.warn("Handling command for VeSync bridge handler.");
233 public void handleNewUserSession(final @Nullable VeSyncUserSession userSessionData) {
234 final Map<String, String> newProps = new HashMap<>();
235 if (userSessionData != null) {
236 newProps.put(DEVICE_PROP_BRIDGE_REG_TS, userSessionData.registerTime);
237 newProps.put(DEVICE_PROP_BRIDGE_COUNTRY_CODE, userSessionData.countryCode);
238 newProps.put(DEVICE_PROP_BRIDGE_ACCEPT_LANG, userSessionData.acceptLanguage);
240 this.updateProperties(newProps);
244 public String reqV2Authorized(final String url, final String macId, final VeSyncAuthenticatedRequest requestData)
245 throws AuthenticationException, DeviceUnknownException {
246 return api.reqV2Authorized(url, macId, requestData);