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