2 * Copyright (c) 2010-2024 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.salus.internal.handler;
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;
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;
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;
51 import com.github.benmanes.caffeine.cache.Caffeine;
52 import com.github.benmanes.caffeine.cache.LoadingCache;
55 * @author Martin GrzeĊlowski - Initial contribution
58 public final class CloudBridgeHandler extends BaseBridgeHandler implements CloudApi {
59 private Logger logger = LoggerFactory.getLogger(CloudBridgeHandler.class.getName());
60 private final HttpClientFactory httpClientFactory;
62 private LoadingCache<String, SortedSet<DeviceProperty<?>>> devicePropertiesCache;
64 private SalusApi salusApi;
66 private ScheduledFuture<?> scheduledFuture;
68 public CloudBridgeHandler(Bridge bridge, HttpClientFactory httpClientFactory) {
70 this.httpClientFactory = httpClientFactory;
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");
80 RestClient httpClient = new HttpClient(httpClientFactory.getCommonHttpClient());
81 if (config.getMaxHttpRetries() > 0) {
82 httpClient = new RetryHttpClient(httpClient, config.getMaxHttpRetries());
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(".", "_") + "]");
90 ScheduledExecutorService scheduledPool = ThreadPoolManager.getScheduledPool(SalusBindingConstants.BINDING_ID);
91 scheduledPool.schedule(() -> tryConnectToCloud(localSalusApi), 1, MICROSECONDS);
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);
100 // Do NOT set state to online to prevent it to flip from online to offline
101 // check *tryConnectToCloud(SalusApi)*
104 private void tryConnectToCloud(SalusApi localSalusApi) {
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() + "\"]");
115 private void refreshCloudDevices() {
116 logger.debug("Refreshing devices from CloudBridgeHandler");
117 if (!(thing instanceof Bridge bridge)) {
118 logger.debug("No bridge, refresh cancelled");
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());
129 ThingHandler handler = thing.getHandler();
130 if (handler == null) {
131 logger.debug("No handler for thing {} refresh cancelled", thing.getUID());
134 thing.getChannels().forEach(channel -> handler.handleCommand(channel.getUID(), REFRESH));
137 var local = salusApi;
139 tryConnectToCloud(local);
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,
151 public void dispose() {
152 ScheduledFuture<?> localScheduledFuture = scheduledFuture;
153 if (localScheduledFuture != null) {
154 localScheduledFuture.cancel(true);
155 scheduledFuture = null;
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);
167 public boolean setValueForProperty(String dsn, String propertyName, Object value) throws SalusApiException {
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))) {
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);
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();
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);
190 DeviceProperty<?> prop = property.get();
191 if (setValue instanceof Boolean b && prop instanceof DeviceProperty.BooleanDeviceProperty boolProp) {
192 boolProp.setValue(b);
195 if (setValue instanceof String s && prop instanceof DeviceProperty.StringDeviceProperty stringProp) {
196 stringProp.setValue(s);
199 if (setValue instanceof Number l && prop instanceof DeviceProperty.LongDeviceProperty longProp) {
200 longProp.setValue(l.longValue());
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);
208 } catch (SalusApiException ex) {
209 devicePropertiesCache.invalidateAll();
215 public SortedSet<Device> findDevices() throws SalusApiException {
216 return requireNonNull(this.salusApi).findDevices();
220 public Optional<Device> findDevice(String dsn) throws SalusApiException {
221 return findDevices().stream().filter(device -> device.dsn().equals(dsn)).findFirst();