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 public static String API_ENDPOINT = "https://home.sensibo.com/api";
86 private final Logger logger = LoggerFactory.getLogger(SensiboAccountHandler.class);
87 private final HttpClient httpClient;
88 private final RequestLogger requestLogger;
89 private final Gson gson;
90 private SensiboModel model = new SensiboModel(0);
91 private Optional<ScheduledFuture<?>> statusFuture = Optional.empty();
92 private @NonNullByDefault({}) SensiboAccountConfiguration config;
94 public SensiboAccountHandler(final Bridge bridge, final HttpClient httpClient) {
96 this.httpClient = httpClient;
98 gson = new GsonBuilder().registerTypeAdapter(ZonedDateTime.class, new TypeAdapter<ZonedDateTime>() {
100 public void write(JsonWriter out, @Nullable ZonedDateTime value) throws IOException {
102 out.value(value.toString());
107 public @Nullable ZonedDateTime read(final JsonReader in) throws IOException {
108 return ZonedDateTime.parse(in.nextString());
110 }).setLenient().setPrettyPrinting().create();
112 requestLogger = new RequestLogger(bridge.getUID().getId(), gson);
115 private boolean allowModelUpdate() {
116 final long diffMsSinceLastUpdate = System.currentTimeMillis() - model.getLastUpdated();
117 return diffMsSinceLastUpdate > MIN_TIME_BETWEEEN_MODEL_UPDATES_MS;
120 public SensiboModel getModel() {
125 public void handleCommand(final ChannelUID channelUID, final Command command) {
126 // Ignore commands as none are supported
129 public SensiboAccountConfiguration loadConfigSafely() throws SensiboConfigurationException {
130 SensiboAccountConfiguration loadedConfig = getConfigAs(SensiboAccountConfiguration.class);
131 if (loadedConfig == null) {
132 throw new SensiboConfigurationException("Could not load Sensibo account configuration");
139 public void initialize() {
140 updateStatus(ThingStatus.UNKNOWN);
141 scheduler.execute(this::initializeInternal);
144 private void initializeInternal() {
146 config = loadConfigSafely();
147 logger.debug("Initializing Sensibo Account bridge using config {}", config);
148 model = refreshModel();
149 updateStatus(ThingStatus.ONLINE);
151 logger.debug("Initialization of Sensibo account completed successfully for {}", config);
152 } catch (final SensiboConfigurationException e) {
153 logger.info("Error initializing Sensibo data: {}", e.getMessage());
154 model = new SensiboModel(0); // Empty model
155 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
156 "Error fetching initial data: " + e.getMessage());
157 } catch (final SensiboException e) {
158 logger.info("Error initializing Sensibo data: {}", e.getMessage());
159 model = new SensiboModel(0); // Empty model
160 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
161 "Error fetching initial data: " + e.getMessage());
163 scheduler.schedule(this::initializeInternal, 30, TimeUnit.SECONDS);
168 public void dispose() {
174 * starts this things polling future
176 private void initPolling() {
178 statusFuture = Optional.of(scheduler.scheduleWithFixedDelay(this::updateModelFromServerAndUpdateThingStatus,
179 config.refreshInterval, config.refreshInterval, TimeUnit.SECONDS));
182 protected SensiboModel refreshModel() throws SensiboException {
183 final SensiboModel updatedModel = new SensiboModel(System.currentTimeMillis());
185 final GetPodsRequest getPodsRequest = new GetPodsRequest();
186 final List<PodDTO> pods = sendRequest(buildRequest(getPodsRequest), getPodsRequest,
187 new TypeToken<ArrayList<PodDTO>>() {
190 for (final PodDTO pod : pods) {
191 final GetPodsDetailsRequest getPodsDetailsRequest = new GetPodsDetailsRequest(pod.id);
193 final PodDetailsDTO podDetails = sendRequest(buildGetPodDetailsRequest(getPodsDetailsRequest),
194 getPodsDetailsRequest, new TypeToken<PodDetailsDTO>() {
197 updatedModel.addPod(new SensiboSky(podDetails));
203 private <T> T sendRequest(final Request request, final AbstractRequest req, final Type responseType)
204 throws SensiboException {
206 final ContentResponse contentResponse = request.send();
207 final String responseJson = contentResponse.getContentAsString();
208 if (contentResponse.getStatus() == HttpStatus.OK_200) {
209 final JsonObject o = JsonParser.parseString(responseJson).getAsJsonObject();
210 final String overallStatus = o.get("status").getAsString();
211 if ("success".equals(overallStatus)) {
212 return gson.fromJson(o.get("result"), responseType);
214 throw new SensiboCommunicationException(req, overallStatus);
216 } else if (contentResponse.getStatus() == HttpStatus.FORBIDDEN_403) {
217 throw new SensiboConfigurationException("Invalid API key");
219 throw new SensiboCommunicationException(
220 "Error sending request to Sensibo server. Server responded with " + contentResponse.getStatus()
221 + " and payload " + responseJson);
223 } catch (InterruptedException | TimeoutException | ExecutionException e) {
224 throw new SensiboCommunicationException(
225 String.format("Error sending request to Sensibo server: %s", e.getMessage()), e);
230 * Stops this thing's polling future
232 private void stopPolling() {
233 statusFuture.ifPresent(future -> {
234 if (!future.isCancelled()) {
237 statusFuture = Optional.empty();
241 public void updateModelFromServerAndUpdateThingStatus() {
242 if (allowModelUpdate()) {
244 model = refreshModel();
245 updateThingStatuses();
246 updateStatus(ThingStatus.ONLINE);
247 } catch (SensiboConfigurationException e) {
248 logger.debug("Error updating Sensibo model do to {}", e.getMessage());
249 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR);
250 } catch (SensiboException e) {
251 logger.debug("Error updating Sensibo model do to {}", e.getMessage());
252 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR);
257 private void updateThingStatuses() {
258 final List<Thing> subThings = getThing().getThings();
259 for (final Thing thing : subThings) {
260 final ThingHandler handler = thing.getHandler();
261 if (handler != null) {
262 final SensiboBaseThingHandler mHandler = (SensiboBaseThingHandler) handler;
263 mHandler.updateState(model);
268 private Request buildGetPodDetailsRequest(final GetPodsDetailsRequest getPodsDetailsRequest) {
269 final Request req = buildRequest(getPodsDetailsRequest);
270 req.param("fields", "*");
275 private Request buildRequest(final AbstractRequest req) {
276 Request request = httpClient.newRequest(API_ENDPOINT + req.getRequestUrl()).param("apiKey", config.apiKey)
277 .method(req.getMethod());
279 if (!req.getMethod().contentEquals(HttpMethod.GET.asString())) { // POST, PATCH
280 final String reqJson = gson.toJson(req);
281 request = request.content(new BytesContentProvider(reqJson.getBytes(StandardCharsets.UTF_8)),
285 requestLogger.listenTo(request, new String[] { config.apiKey });
290 public void updateSensiboSkyAcState(final String macAddress, String property, Object value,
291 SensiboBaseThingHandler handler) {
292 model.findSensiboSkyByMacAddress(macAddress).ifPresent(pod -> {
294 SetAcStatePropertyRequest setAcStatePropertyRequest = new SetAcStatePropertyRequest(pod.getId(),
296 Request request = buildRequest(setAcStatePropertyRequest);
297 SetAcStatePropertyReponse response = sendRequest(request, setAcStatePropertyRequest,
298 new TypeToken<SetAcStatePropertyReponse>() {
301 model.updateAcState(macAddress, new AcState(response.acState));
302 handler.updateState(model);
303 } catch (SensiboException e) {
304 logger.debug("Error setting ac state for {}", macAddress, e);
309 public void updateSensiboSkyTimer(final String macAddress, @Nullable Integer secondsFromNow) {
310 model.findSensiboSkyByMacAddress(macAddress).ifPresent(pod -> {
312 if (secondsFromNow != null && secondsFromNow >= SECONDS_IN_MINUTE) {
313 AcStateDTO offState = new AcStateDTO(pod.getAcState().get());
316 SetTimerRequest setTimerRequest = new SetTimerRequest(pod.getId(),
317 secondsFromNow / SECONDS_IN_MINUTE, offState);
318 Request request = buildRequest(setTimerRequest);
319 // No data in response
320 sendRequest(request, setTimerRequest, new TypeToken<SetTimerReponse>() {
323 DeleteTimerRequest setTimerRequest = new DeleteTimerRequest(pod.getId());
324 Request request = buildRequest(setTimerRequest);
325 // No data in response
326 sendRequest(request, setTimerRequest, new TypeToken<DeleteTimerReponse>() {
329 } catch (SensiboException e) {
330 logger.debug("Error setting timer for {}", macAddress, e);