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.sensibo.internal.handler;
15 import java.io.IOException;
16 import java.lang.reflect.Type;
17 import java.nio.charset.StandardCharsets;
18 import java.time.ZonedDateTime;
19 import java.util.ArrayList;
20 import java.util.List;
21 import java.util.Optional;
22 import java.util.concurrent.ExecutionException;
23 import java.util.concurrent.ScheduledFuture;
24 import java.util.concurrent.TimeUnit;
25 import java.util.concurrent.TimeoutException;
27 import org.eclipse.jdt.annotation.NonNullByDefault;
28 import org.eclipse.jdt.annotation.Nullable;
29 import org.eclipse.jetty.client.HttpClient;
30 import org.eclipse.jetty.client.api.ContentResponse;
31 import org.eclipse.jetty.client.api.Request;
32 import org.eclipse.jetty.client.util.BytesContentProvider;
33 import org.eclipse.jetty.http.HttpMethod;
34 import org.eclipse.jetty.http.HttpStatus;
35 import org.openhab.binding.sensibo.internal.SensiboCommunicationException;
36 import org.openhab.binding.sensibo.internal.SensiboConfigurationException;
37 import org.openhab.binding.sensibo.internal.SensiboException;
38 import org.openhab.binding.sensibo.internal.client.RequestLogger;
39 import org.openhab.binding.sensibo.internal.config.SensiboAccountConfiguration;
40 import org.openhab.binding.sensibo.internal.dto.AbstractRequest;
41 import org.openhab.binding.sensibo.internal.dto.deletetimer.DeleteTimerReponse;
42 import org.openhab.binding.sensibo.internal.dto.deletetimer.DeleteTimerRequest;
43 import org.openhab.binding.sensibo.internal.dto.poddetails.AcStateDTO;
44 import org.openhab.binding.sensibo.internal.dto.poddetails.GetPodsDetailsRequest;
45 import org.openhab.binding.sensibo.internal.dto.poddetails.PodDetailsDTO;
46 import org.openhab.binding.sensibo.internal.dto.pods.GetPodsRequest;
47 import org.openhab.binding.sensibo.internal.dto.pods.PodDTO;
48 import org.openhab.binding.sensibo.internal.dto.setacstateproperty.SetAcStatePropertyReponse;
49 import org.openhab.binding.sensibo.internal.dto.setacstateproperty.SetAcStatePropertyRequest;
50 import org.openhab.binding.sensibo.internal.dto.settimer.SetTimerReponse;
51 import org.openhab.binding.sensibo.internal.dto.settimer.SetTimerRequest;
52 import org.openhab.binding.sensibo.internal.model.AcState;
53 import org.openhab.binding.sensibo.internal.model.SensiboModel;
54 import org.openhab.binding.sensibo.internal.model.SensiboSky;
55 import org.openhab.core.thing.Bridge;
56 import org.openhab.core.thing.ChannelUID;
57 import org.openhab.core.thing.Thing;
58 import org.openhab.core.thing.ThingStatus;
59 import org.openhab.core.thing.ThingStatusDetail;
60 import org.openhab.core.thing.binding.BaseBridgeHandler;
61 import org.openhab.core.thing.binding.ThingHandler;
62 import org.openhab.core.types.Command;
63 import org.slf4j.Logger;
64 import org.slf4j.LoggerFactory;
66 import com.google.gson.Gson;
67 import com.google.gson.GsonBuilder;
68 import com.google.gson.JsonObject;
69 import com.google.gson.JsonParser;
70 import com.google.gson.TypeAdapter;
71 import com.google.gson.reflect.TypeToken;
72 import com.google.gson.stream.JsonReader;
73 import com.google.gson.stream.JsonWriter;
76 * The {@link SensiboAccountHandler} is responsible for handling commands, which are
77 * sent to one of the channels.
79 * @author Arne Seime - Initial contribution
82 public class SensiboAccountHandler extends BaseBridgeHandler {
83 private static final int MIN_TIME_BETWEEEN_MODEL_UPDATES_MS = 30_000;
84 private static final int SECONDS_IN_MINUTE = 60;
85 private static final int REQUEST_TIMEOUT_MS = 10_000;
86 public static String API_ENDPOINT = "https://home.sensibo.com/api";
87 private final Logger logger = LoggerFactory.getLogger(SensiboAccountHandler.class);
88 private final HttpClient httpClient;
89 private final RequestLogger requestLogger;
90 private final Gson gson;
91 private SensiboModel model = new SensiboModel(0);
92 private Optional<ScheduledFuture<?>> statusFuture = Optional.empty();
93 private @NonNullByDefault({}) SensiboAccountConfiguration config;
95 public SensiboAccountHandler(final Bridge bridge, final HttpClient httpClient) {
97 this.httpClient = httpClient;
99 gson = new GsonBuilder().registerTypeAdapter(ZonedDateTime.class, new TypeAdapter<ZonedDateTime>() {
101 public void write(JsonWriter out, @Nullable ZonedDateTime value) throws IOException {
103 out.value(value.toString());
108 public @Nullable ZonedDateTime read(final JsonReader in) throws IOException {
109 return ZonedDateTime.parse(in.nextString());
111 }).setLenient().setPrettyPrinting().create();
113 requestLogger = new RequestLogger(bridge.getUID().getId(), gson);
116 private boolean allowModelUpdate() {
117 final long diffMsSinceLastUpdate = System.currentTimeMillis() - model.getLastUpdated();
118 return diffMsSinceLastUpdate > MIN_TIME_BETWEEEN_MODEL_UPDATES_MS;
121 public SensiboModel getModel() {
126 public void handleCommand(final ChannelUID channelUID, final Command command) {
127 // Ignore commands as none are supported
130 public SensiboAccountConfiguration loadConfigSafely() throws SensiboConfigurationException {
131 SensiboAccountConfiguration loadedConfig = getConfigAs(SensiboAccountConfiguration.class);
132 if (loadedConfig == null) {
133 throw new SensiboConfigurationException("Could not load Sensibo account configuration");
140 public void initialize() {
141 updateStatus(ThingStatus.UNKNOWN);
142 scheduler.execute(this::initializeInternal);
145 private void initializeInternal() {
147 config = loadConfigSafely();
148 logger.debug("Initializing Sensibo Account bridge using config {}", config);
149 model = refreshModel();
150 updateStatus(ThingStatus.ONLINE);
152 logger.debug("Initialization of Sensibo account completed successfully for {}", config);
153 } catch (final SensiboConfigurationException e) {
154 logger.info("Error initializing Sensibo data: {}", e.getMessage());
155 model = new SensiboModel(0); // Empty model
156 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
157 "Error fetching initial data: " + e.getMessage());
158 } catch (final SensiboException e) {
159 logger.info("Error initializing Sensibo data: {}", e.getMessage());
160 model = new SensiboModel(0); // Empty model
161 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
162 "Error fetching initial data: " + e.getMessage());
164 scheduler.schedule(this::initializeInternal, 30, TimeUnit.SECONDS);
169 public void dispose() {
175 * starts this things polling future
177 private void initPolling() {
179 statusFuture = Optional.of(scheduler.scheduleWithFixedDelay(this::updateModelFromServerAndUpdateThingStatus,
180 config.refreshInterval, config.refreshInterval, TimeUnit.SECONDS));
183 protected SensiboModel refreshModel() throws SensiboException {
184 final SensiboModel updatedModel = new SensiboModel(System.currentTimeMillis());
186 final GetPodsRequest getPodsRequest = new GetPodsRequest();
187 final List<PodDTO> pods = sendRequest(buildRequest(getPodsRequest), getPodsRequest,
188 new TypeToken<ArrayList<PodDTO>>() {
191 for (final PodDTO pod : pods) {
192 final GetPodsDetailsRequest getPodsDetailsRequest = new GetPodsDetailsRequest(pod.id);
194 final PodDetailsDTO podDetails = sendRequest(buildGetPodDetailsRequest(getPodsDetailsRequest),
195 getPodsDetailsRequest, new TypeToken<PodDetailsDTO>() {
198 updatedModel.addPod(new SensiboSky(podDetails));
204 private <T> T sendRequest(final Request request, final AbstractRequest req, final Type responseType)
205 throws SensiboException {
207 final ContentResponse contentResponse = request.send();
208 final String responseJson = contentResponse.getContentAsString();
209 if (contentResponse.getStatus() == HttpStatus.OK_200) {
210 final JsonObject o = JsonParser.parseString(responseJson).getAsJsonObject();
211 final String overallStatus = o.get("status").getAsString();
212 if ("success".equals(overallStatus)) {
213 return gson.fromJson(o.get("result"), responseType);
215 throw new SensiboCommunicationException(req, overallStatus);
217 } else if (contentResponse.getStatus() == HttpStatus.FORBIDDEN_403) {
218 throw new SensiboConfigurationException("Invalid API key");
220 throw new SensiboCommunicationException(
221 "Error sending request to Sensibo server. Server responded with " + contentResponse.getStatus()
222 + " and payload " + responseJson);
224 } catch (InterruptedException | TimeoutException | ExecutionException e) {
225 throw new SensiboCommunicationException(
226 String.format("Error sending request to Sensibo server: %s", e.getMessage()), e);
231 * Stops this thing's polling future
233 private void stopPolling() {
234 statusFuture.ifPresent(future -> {
235 if (!future.isCancelled()) {
238 statusFuture = Optional.empty();
242 public void updateModelFromServerAndUpdateThingStatus() {
243 if (allowModelUpdate()) {
245 model = refreshModel();
246 updateThingStatuses();
247 updateStatus(ThingStatus.ONLINE);
248 } catch (SensiboConfigurationException e) {
249 logger.debug("Error updating Sensibo model do to {}", e.getMessage());
250 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR);
251 } catch (SensiboException e) {
252 logger.debug("Error updating Sensibo model do to {}", e.getMessage());
253 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR);
258 private void updateThingStatuses() {
259 final List<Thing> subThings = getThing().getThings();
260 for (final Thing thing : subThings) {
261 final ThingHandler handler = thing.getHandler();
262 if (handler != null) {
263 final SensiboBaseThingHandler mHandler = (SensiboBaseThingHandler) handler;
264 mHandler.updateState(model);
269 private Request buildGetPodDetailsRequest(final GetPodsDetailsRequest getPodsDetailsRequest) {
270 final Request req = buildRequest(getPodsDetailsRequest);
271 req.param("fields", "*");
276 private Request buildRequest(final AbstractRequest req) {
277 Request request = httpClient.newRequest(API_ENDPOINT + req.getRequestUrl()).param("apiKey", config.apiKey)
278 .method(req.getMethod());
279 request.timeout(REQUEST_TIMEOUT_MS, TimeUnit.MILLISECONDS);
281 if (!req.getMethod().contentEquals(HttpMethod.GET.asString())) { // POST, PATCH
282 final String reqJson = gson.toJson(req);
283 request = request.content(new BytesContentProvider(reqJson.getBytes(StandardCharsets.UTF_8)),
287 requestLogger.listenTo(request, new String[] { config.apiKey });
292 public void updateSensiboSkyAcState(final String macAddress, String property, Object value,
293 SensiboBaseThingHandler handler) {
294 model.findSensiboSkyByMacAddress(macAddress).ifPresent(pod -> {
296 SetAcStatePropertyRequest setAcStatePropertyRequest = new SetAcStatePropertyRequest(pod.getId(),
298 Request request = buildRequest(setAcStatePropertyRequest);
299 SetAcStatePropertyReponse response = sendRequest(request, setAcStatePropertyRequest,
300 new TypeToken<SetAcStatePropertyReponse>() {
303 model.updateAcState(macAddress, new AcState(response.acState));
304 handler.updateState(model);
305 } catch (SensiboException e) {
306 logger.debug("Error setting ac state for {}", macAddress, e);
311 public void updateSensiboSkyTimer(final String macAddress, @Nullable Integer secondsFromNow) {
312 model.findSensiboSkyByMacAddress(macAddress).ifPresent(pod -> {
314 if (secondsFromNow != null && secondsFromNow >= SECONDS_IN_MINUTE) {
315 AcStateDTO offState = new AcStateDTO(pod.getAcState().get());
318 SetTimerRequest setTimerRequest = new SetTimerRequest(pod.getId(),
319 secondsFromNow / SECONDS_IN_MINUTE, offState);
320 Request request = buildRequest(setTimerRequest);
321 // No data in response
322 sendRequest(request, setTimerRequest, new TypeToken<SetTimerReponse>() {
325 DeleteTimerRequest setTimerRequest = new DeleteTimerRequest(pod.getId());
326 Request request = buildRequest(setTimerRequest);
327 // No data in response
328 sendRequest(request, setTimerRequest, new TypeToken<DeleteTimerReponse>() {
331 } catch (SensiboException e) {
332 logger.debug("Error setting timer for {}", macAddress, e);