]> git.basschouten.com Git - openhab-addons.git/blob
98ee6dbaf53cdb3ac5aaf5630c4814fd0f1740ed
[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.salus.internal.handler;
14
15 import static java.util.Objects.requireNonNull;
16 import static java.util.concurrent.TimeUnit.*;
17 import static org.openhab.core.thing.ThingStatus.OFFLINE;
18 import static org.openhab.core.thing.ThingStatus.ONLINE;
19 import static org.openhab.core.thing.ThingStatusDetail.*;
20 import static org.openhab.core.types.RefreshType.REFRESH;
21
22 import java.time.Duration;
23 import java.util.List;
24 import java.util.Optional;
25 import java.util.SortedSet;
26 import java.util.concurrent.ScheduledExecutorService;
27 import java.util.concurrent.ScheduledFuture;
28
29 import org.eclipse.jdt.annotation.NonNullByDefault;
30 import org.eclipse.jdt.annotation.Nullable;
31 import org.openhab.binding.salus.internal.SalusBindingConstants;
32 import org.openhab.binding.salus.internal.rest.Device;
33 import org.openhab.binding.salus.internal.rest.DeviceProperty;
34 import org.openhab.binding.salus.internal.rest.GsonMapper;
35 import org.openhab.binding.salus.internal.rest.HttpClient;
36 import org.openhab.binding.salus.internal.rest.RestClient;
37 import org.openhab.binding.salus.internal.rest.RetryHttpClient;
38 import org.openhab.binding.salus.internal.rest.SalusApi;
39 import org.openhab.binding.salus.internal.rest.SalusApiException;
40 import org.openhab.core.common.ThreadPoolManager;
41 import org.openhab.core.io.net.http.HttpClientFactory;
42 import org.openhab.core.thing.Bridge;
43 import org.openhab.core.thing.ChannelUID;
44 import org.openhab.core.thing.Thing;
45 import org.openhab.core.thing.binding.BaseBridgeHandler;
46 import org.openhab.core.thing.binding.ThingHandler;
47 import org.openhab.core.types.Command;
48 import org.slf4j.Logger;
49 import org.slf4j.LoggerFactory;
50
51 import com.github.benmanes.caffeine.cache.Caffeine;
52 import com.github.benmanes.caffeine.cache.LoadingCache;
53
54 /**
55  * @author Martin GrzeĊ›lowski - Initial contribution
56  */
57 @NonNullByDefault
58 public final class CloudBridgeHandler extends BaseBridgeHandler implements CloudApi {
59     private Logger logger = LoggerFactory.getLogger(CloudBridgeHandler.class.getName());
60     private final HttpClientFactory httpClientFactory;
61     @NonNullByDefault({})
62     private LoadingCache<String, SortedSet<DeviceProperty<?>>> devicePropertiesCache;
63     @Nullable
64     private SalusApi salusApi;
65     @Nullable
66     private ScheduledFuture<?> scheduledFuture;
67
68     public CloudBridgeHandler(Bridge bridge, HttpClientFactory httpClientFactory) {
69         super(bridge);
70         this.httpClientFactory = httpClientFactory;
71     }
72
73     @Override
74     public void initialize() {
75         CloudBridgeConfig config = this.getConfigAs(CloudBridgeConfig.class);
76         if (!config.isValid()) {
77             updateStatus(OFFLINE, CONFIGURATION_ERROR, "@text/cloud-bridge-handler.initialize.username-pass-not-valid");
78             return;
79         }
80         RestClient httpClient = new HttpClient(httpClientFactory.getCommonHttpClient());
81         if (config.getMaxHttpRetries() > 0) {
82             httpClient = new RetryHttpClient(httpClient, config.getMaxHttpRetries());
83         }
84         @Nullable
85         SalusApi localSalusApi = salusApi = new SalusApi(config.getUsername(), config.getPassword().toCharArray(),
86                 config.getUrl(), httpClient, GsonMapper.INSTANCE);
87         logger = LoggerFactory
88                 .getLogger(CloudBridgeHandler.class.getName() + "[" + config.getUsername().replace(".", "_") + "]");
89
90         ScheduledExecutorService scheduledPool = ThreadPoolManager.getScheduledPool(SalusBindingConstants.BINDING_ID);
91         scheduledPool.schedule(() -> tryConnectToCloud(localSalusApi), 1, MICROSECONDS);
92
93         this.devicePropertiesCache = Caffeine.newBuilder().maximumSize(10_000)
94                 .expireAfterWrite(Duration.ofSeconds(config.getPropertiesRefreshInterval()))
95                 .refreshAfterWrite(Duration.ofSeconds(config.getPropertiesRefreshInterval()))
96                 .build(this::findPropertiesForDevice);
97         this.scheduledFuture = scheduledPool.scheduleWithFixedDelay(this::refreshCloudDevices,
98                 config.getRefreshInterval() * 2, config.getRefreshInterval(), SECONDS);
99
100         // Do NOT set state to online to prevent it to flip from online to offline
101         // check *tryConnectToCloud(SalusApi)*
102     }
103
104     private void tryConnectToCloud(SalusApi localSalusApi) {
105         try {
106             localSalusApi.findDevices();
107             // there is a connection with the cloud
108             updateStatus(ONLINE);
109         } catch (SalusApiException ex) {
110             updateStatus(OFFLINE, COMMUNICATION_ERROR,
111                     "@text/cloud-bridge-handler.initialize.cannot-connect-to-cloud [\"" + ex.getMessage() + "\"]");
112         }
113     }
114
115     private void refreshCloudDevices() {
116         logger.debug("Refreshing devices from CloudBridgeHandler");
117         if (!(thing instanceof Bridge bridge)) {
118             logger.debug("No bridge, refresh cancelled");
119             return;
120         }
121         List<Thing> things = bridge.getThings();
122         for (Thing thing : things) {
123             if (!thing.isEnabled()) {
124                 logger.debug("Thing {} is disabled, refresh cancelled", thing.getUID());
125                 continue;
126             }
127
128             @Nullable
129             ThingHandler handler = thing.getHandler();
130             if (handler == null) {
131                 logger.debug("No handler for thing {} refresh cancelled", thing.getUID());
132                 continue;
133             }
134             thing.getChannels().forEach(channel -> handler.handleCommand(channel.getUID(), REFRESH));
135         }
136
137         var local = salusApi;
138         if (local != null) {
139             tryConnectToCloud(local);
140         }
141     }
142
143     @Override
144     public void handleCommand(ChannelUID channelUID, Command command) {
145         // no commands in this bridge
146         logger.debug("Bridge does not support any commands to any channels. channelUID={}, command={}", channelUID,
147                 command);
148     }
149
150     @Override
151     public void dispose() {
152         ScheduledFuture<?> localScheduledFuture = scheduledFuture;
153         if (localScheduledFuture != null) {
154             localScheduledFuture.cancel(true);
155             scheduledFuture = null;
156         }
157         super.dispose();
158     }
159
160     @Override
161     public SortedSet<DeviceProperty<?>> findPropertiesForDevice(String dsn) throws SalusApiException {
162         logger.debug("Finding properties for device {} using salusClient", dsn);
163         return requireNonNull(salusApi).findDeviceProperties(dsn);
164     }
165
166     @Override
167     public boolean setValueForProperty(String dsn, String propertyName, Object value) throws SalusApiException {
168         try {
169             @Nullable
170             SalusApi api = requireNonNull(salusApi);
171             logger.debug("Setting property {} on device {} to value {} using salusClient", propertyName, dsn, value);
172             Object setValue = api.setValueForProperty(dsn, propertyName, value);
173             if ((!(setValue instanceof Boolean) && !(setValue instanceof String) && !(setValue instanceof Number))) {
174                 logger.warn(
175                         "Cannot set value {} ({}) for property {} on device {} because it is not a Boolean, String, Long or Integer",
176                         setValue, setValue.getClass().getSimpleName(), propertyName, dsn);
177                 return false;
178             }
179             var properties = devicePropertiesCache.get(dsn);
180             Optional<DeviceProperty<?>> property = requireNonNull(properties).stream()
181                     .filter(prop -> prop.getName().equals(propertyName)).findFirst();
182             if (property.isEmpty()) {
183                 String simpleName = setValue.getClass().getSimpleName();
184                 logger.warn(
185                         "Cannot set value {} ({}) for property {} on device {} because it is not found in the cache. Invalidating cache",
186                         setValue, simpleName, propertyName, dsn);
187                 devicePropertiesCache.invalidate(dsn);
188                 return false;
189             }
190             DeviceProperty<?> prop = property.get();
191             if (setValue instanceof Boolean b && prop instanceof DeviceProperty.BooleanDeviceProperty boolProp) {
192                 boolProp.setValue(b);
193                 return true;
194             }
195             if (setValue instanceof String s && prop instanceof DeviceProperty.StringDeviceProperty stringProp) {
196                 stringProp.setValue(s);
197                 return true;
198             }
199             if (setValue instanceof Number l && prop instanceof DeviceProperty.LongDeviceProperty longProp) {
200                 longProp.setValue(l.longValue());
201                 return true;
202             }
203
204             logger.warn(
205                     "Cannot set value {} ({}) for property {} ({}) on device {} because value class does not match property class",
206                     setValue, setValue.getClass().getSimpleName(), propertyName, prop.getClass().getSimpleName(), dsn);
207             return false;
208         } catch (SalusApiException ex) {
209             devicePropertiesCache.invalidateAll();
210             throw ex;
211         }
212     }
213
214     @Override
215     public SortedSet<Device> findDevices() throws SalusApiException {
216         return requireNonNull(this.salusApi).findDevices();
217     }
218
219     @Override
220     public Optional<Device> findDevice(String dsn) throws SalusApiException {
221         return findDevices().stream().filter(device -> device.dsn().equals(dsn)).findFirst();
222     }
223 }