]> git.basschouten.com Git - openhab-addons.git/blob
15c633a59ea853c5b914f540db50e3d385fb18ba
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2023 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.sensibo.internal.handler;
14
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;
26
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;
65
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;
74
75 /**
76  * The {@link SensiboAccountHandler} is responsible for handling commands, which are
77  * sent to one of the channels.
78  *
79  * @author Arne Seime - Initial contribution
80  */
81 @NonNullByDefault
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;
94
95     public SensiboAccountHandler(final Bridge bridge, final HttpClient httpClient) {
96         super(bridge);
97         this.httpClient = httpClient;
98
99         gson = new GsonBuilder().registerTypeAdapter(ZonedDateTime.class, new TypeAdapter<ZonedDateTime>() {
100             @Override
101             public void write(JsonWriter out, @Nullable ZonedDateTime value) throws IOException {
102                 if (value != null) {
103                     out.value(value.toString());
104                 }
105             }
106
107             @Override
108             public @Nullable ZonedDateTime read(final JsonReader in) throws IOException {
109                 return ZonedDateTime.parse(in.nextString());
110             }
111         }).setLenient().setPrettyPrinting().create();
112
113         requestLogger = new RequestLogger(bridge.getUID().getId(), gson);
114     }
115
116     private boolean allowModelUpdate() {
117         final long diffMsSinceLastUpdate = System.currentTimeMillis() - model.getLastUpdated();
118         return diffMsSinceLastUpdate > MIN_TIME_BETWEEEN_MODEL_UPDATES_MS;
119     }
120
121     public SensiboModel getModel() {
122         return model;
123     }
124
125     @Override
126     public void handleCommand(final ChannelUID channelUID, final Command command) {
127         // Ignore commands as none are supported
128     }
129
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");
134         }
135
136         return loadedConfig;
137     }
138
139     @Override
140     public void initialize() {
141         updateStatus(ThingStatus.UNKNOWN);
142         scheduler.execute(this::initializeInternal);
143     }
144
145     private void initializeInternal() {
146         try {
147             config = loadConfigSafely();
148             logger.debug("Initializing Sensibo Account bridge using config {}", config);
149             model = refreshModel();
150             updateStatus(ThingStatus.ONLINE);
151             initPolling();
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());
163             // Reschedule init
164             scheduler.schedule(this::initializeInternal, 30, TimeUnit.SECONDS);
165         }
166     }
167
168     @Override
169     public void dispose() {
170         stopPolling();
171         super.dispose();
172     }
173
174     /**
175      * starts this things polling future
176      */
177     private void initPolling() {
178         stopPolling();
179         statusFuture = Optional.of(scheduler.scheduleWithFixedDelay(this::updateModelFromServerAndUpdateThingStatus,
180                 config.refreshInterval, config.refreshInterval, TimeUnit.SECONDS));
181     }
182
183     protected SensiboModel refreshModel() throws SensiboException {
184         final SensiboModel updatedModel = new SensiboModel(System.currentTimeMillis());
185
186         final GetPodsRequest getPodsRequest = new GetPodsRequest();
187         final List<PodDTO> pods = sendRequest(buildRequest(getPodsRequest), getPodsRequest,
188                 new TypeToken<ArrayList<PodDTO>>() {
189                 }.getType());
190
191         for (final PodDTO pod : pods) {
192             final GetPodsDetailsRequest getPodsDetailsRequest = new GetPodsDetailsRequest(pod.id);
193
194             final PodDetailsDTO podDetails = sendRequest(buildGetPodDetailsRequest(getPodsDetailsRequest),
195                     getPodsDetailsRequest, new TypeToken<PodDetailsDTO>() {
196                     }.getType());
197
198             updatedModel.addPod(new SensiboSky(podDetails));
199         }
200
201         return updatedModel;
202     }
203
204     private <T> T sendRequest(final Request request, final AbstractRequest req, final Type responseType)
205             throws SensiboException {
206         try {
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);
214                 } else {
215                     throw new SensiboCommunicationException(req, overallStatus);
216                 }
217             } else if (contentResponse.getStatus() == HttpStatus.FORBIDDEN_403) {
218                 throw new SensiboConfigurationException("Invalid API key");
219             } else {
220                 throw new SensiboCommunicationException(
221                         "Error sending request to Sensibo server. Server responded with " + contentResponse.getStatus()
222                                 + " and payload " + responseJson);
223             }
224         } catch (InterruptedException | TimeoutException | ExecutionException e) {
225             throw new SensiboCommunicationException(
226                     String.format("Error sending request to Sensibo server: %s", e.getMessage()), e);
227         }
228     }
229
230     /**
231      * Stops this thing's polling future
232      */
233     private void stopPolling() {
234         statusFuture.ifPresent(future -> {
235             if (!future.isCancelled()) {
236                 future.cancel(true);
237             }
238             statusFuture = Optional.empty();
239         });
240     }
241
242     public void updateModelFromServerAndUpdateThingStatus() {
243         if (allowModelUpdate()) {
244             try {
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);
254             }
255         }
256     }
257
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);
265             }
266         }
267     }
268
269     private Request buildGetPodDetailsRequest(final GetPodsDetailsRequest getPodsDetailsRequest) {
270         final Request req = buildRequest(getPodsDetailsRequest);
271         req.param("fields", "*");
272
273         return req;
274     }
275
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);
280
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)),
284                     "application/json");
285         }
286
287         requestLogger.listenTo(request, new String[] { config.apiKey });
288
289         return request;
290     }
291
292     public void updateSensiboSkyAcState(final String macAddress, String property, Object value,
293             SensiboBaseThingHandler handler) {
294         model.findSensiboSkyByMacAddress(macAddress).ifPresent(pod -> {
295             try {
296                 SetAcStatePropertyRequest setAcStatePropertyRequest = new SetAcStatePropertyRequest(pod.getId(),
297                         property, value);
298                 Request request = buildRequest(setAcStatePropertyRequest);
299                 SetAcStatePropertyReponse response = sendRequest(request, setAcStatePropertyRequest,
300                         new TypeToken<SetAcStatePropertyReponse>() {
301                         }.getType());
302
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);
307             }
308         });
309     }
310
311     public void updateSensiboSkyTimer(final String macAddress, @Nullable Integer secondsFromNow) {
312         model.findSensiboSkyByMacAddress(macAddress).ifPresent(pod -> {
313             try {
314                 if (secondsFromNow != null && secondsFromNow >= SECONDS_IN_MINUTE) {
315                     AcStateDTO offState = new AcStateDTO(pod.getAcState().get());
316                     offState.on = false;
317
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>() {
323                     }.getType());
324                 } else {
325                     DeleteTimerRequest setTimerRequest = new DeleteTimerRequest(pod.getId());
326                     Request request = buildRequest(setTimerRequest);
327                     // No data in response
328                     sendRequest(request, setTimerRequest, new TypeToken<DeleteTimerReponse>() {
329                     }.getType());
330                 }
331             } catch (SensiboException e) {
332                 logger.debug("Error setting timer for {}", macAddress, e);
333             }
334         });
335     }
336 }