]> git.basschouten.com Git - openhab-addons.git/blob
94e045870b7bcc7e0000f43af9f92daccf3018f9
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2020 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.somfymylink.internal.handler;
14
15 import static org.openhab.binding.somfymylink.internal.SomfyMyLinkBindingConstants.*;
16
17 import java.io.BufferedReader;
18 import java.io.IOException;
19 import java.io.InputStreamReader;
20 import java.io.OutputStreamWriter;
21 import java.io.Reader;
22 import java.io.Writer;
23 import java.net.Socket;
24 import java.net.SocketTimeoutException;
25 import java.nio.charset.StandardCharsets;
26 import java.util.ArrayList;
27 import java.util.Collection;
28 import java.util.Collections;
29 import java.util.List;
30 import java.util.concurrent.CompletableFuture;
31 import java.util.concurrent.ExecutionException;
32 import java.util.concurrent.ExecutorService;
33 import java.util.concurrent.Executors;
34 import java.util.concurrent.ScheduledFuture;
35 import java.util.concurrent.TimeUnit;
36
37 import org.eclipse.jdt.annotation.NonNullByDefault;
38 import org.eclipse.jdt.annotation.Nullable;
39 import org.openhab.binding.somfymylink.internal.SomfyMyLinkBindingConstants;
40 import org.openhab.binding.somfymylink.internal.config.SomfyMyLinkConfiguration;
41 import org.openhab.binding.somfymylink.internal.discovery.SomfyMyLinkDeviceDiscoveryService;
42 import org.openhab.binding.somfymylink.internal.model.SomfyMyLinkCommandBase;
43 import org.openhab.binding.somfymylink.internal.model.SomfyMyLinkCommandSceneList;
44 import org.openhab.binding.somfymylink.internal.model.SomfyMyLinkCommandSceneSet;
45 import org.openhab.binding.somfymylink.internal.model.SomfyMyLinkCommandShadeDown;
46 import org.openhab.binding.somfymylink.internal.model.SomfyMyLinkCommandShadeList;
47 import org.openhab.binding.somfymylink.internal.model.SomfyMyLinkCommandShadePing;
48 import org.openhab.binding.somfymylink.internal.model.SomfyMyLinkCommandShadeStop;
49 import org.openhab.binding.somfymylink.internal.model.SomfyMyLinkCommandShadeUp;
50 import org.openhab.binding.somfymylink.internal.model.SomfyMyLinkErrorResponse;
51 import org.openhab.binding.somfymylink.internal.model.SomfyMyLinkPingResponse;
52 import org.openhab.binding.somfymylink.internal.model.SomfyMyLinkResponseBase;
53 import org.openhab.binding.somfymylink.internal.model.SomfyMyLinkScene;
54 import org.openhab.binding.somfymylink.internal.model.SomfyMyLinkScenesResponse;
55 import org.openhab.binding.somfymylink.internal.model.SomfyMyLinkShade;
56 import org.openhab.binding.somfymylink.internal.model.SomfyMyLinkShadesResponse;
57 import org.openhab.core.common.NamedThreadFactory;
58 import org.openhab.core.library.types.StringType;
59 import org.openhab.core.thing.Bridge;
60 import org.openhab.core.thing.ChannelUID;
61 import org.openhab.core.thing.Thing;
62 import org.openhab.core.thing.ThingStatus;
63 import org.openhab.core.thing.ThingStatusDetail;
64 import org.openhab.core.thing.binding.BaseBridgeHandler;
65 import org.openhab.core.thing.binding.ThingHandlerService;
66 import org.openhab.core.types.Command;
67 import org.openhab.core.types.RefreshType;
68 import org.openhab.core.types.StateOption;
69 import org.slf4j.Logger;
70 import org.slf4j.LoggerFactory;
71
72 import com.google.gson.Gson;
73 import com.google.gson.JsonObject;
74 import com.google.gson.JsonParser;
75
76 /**
77  * The {@link SomfyMyLinkBridgeHandler} is responsible for handling commands, which are
78  * sent to one of the channels.
79  *
80  * @author Chris Johnson - Initial contribution
81  */
82 @NonNullByDefault
83 public class SomfyMyLinkBridgeHandler extends BaseBridgeHandler {
84
85     private final Logger logger = LoggerFactory.getLogger(SomfyMyLinkBridgeHandler.class);
86     private static final int HEARTBEAT_MINUTES = 2;
87     private static final int MYLINK_PORT = 44100;
88     private static final int MYLINK_DEFAULT_TIMEOUT = 5000;
89     private static final int CONNECTION_DELAY = 1000;
90     private static final SomfyMyLinkShade[] EMPTY_SHADE_LIST = new SomfyMyLinkShade[0];
91     private static final SomfyMyLinkScene[] EMPTY_SCENE_LIST = new SomfyMyLinkScene[0];
92
93     private SomfyMyLinkConfiguration config = new SomfyMyLinkConfiguration();
94     private @Nullable ScheduledFuture<?> heartbeat;
95     private @Nullable SomfyMyLinkStateDescriptionOptionsProvider stateDescriptionProvider;
96     private @Nullable ExecutorService commandExecutor;
97
98     // Gson & parser
99     private final Gson gson = new Gson();
100
101     public SomfyMyLinkBridgeHandler(Bridge bridge,
102             @Nullable SomfyMyLinkStateDescriptionOptionsProvider stateDescriptionProvider) {
103         super(bridge);
104         this.stateDescriptionProvider = stateDescriptionProvider;
105     }
106
107     @Override
108     public void handleCommand(ChannelUID channelUID, Command command) {
109         logger.debug("Command received on mylink {}", command);
110
111         try {
112             if (CHANNEL_SCENES.equals(channelUID.getId())) {
113                 if (command instanceof RefreshType) {
114                     return;
115                 }
116
117                 if (command instanceof StringType) {
118                     Integer sceneId = Integer.decode(command.toString());
119                     commandScene(sceneId);
120                 }
121             }
122         } catch (SomfyMyLinkException e) {
123             logger.info("Error handling command: {}", e.getMessage());
124             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
125         }
126     }
127
128     @Override
129     public void initialize() {
130         logger.info("Initializing mylink");
131         config = getThing().getConfiguration().as(SomfyMyLinkConfiguration.class);
132
133         commandExecutor = Executors.newSingleThreadExecutor(new NamedThreadFactory(thing.getUID().getAsString(), true));
134
135         if (validConfiguration(config)) {
136             // start the keepalive process
137             if (heartbeat == null) {
138                 logger.info("Starting heartbeat job every {} min", HEARTBEAT_MINUTES);
139                 heartbeat = this.scheduler.scheduleWithFixedDelay(this::sendHeartbeat, 0, HEARTBEAT_MINUTES,
140                         TimeUnit.MINUTES);
141             }
142         }
143     }
144
145     @Override
146     public Collection<Class<? extends ThingHandlerService>> getServices() {
147         return Collections.singleton(SomfyMyLinkDeviceDiscoveryService.class);
148     }
149
150     private boolean validConfiguration(@Nullable SomfyMyLinkConfiguration config) {
151         if (config == null) {
152             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "mylink configuration missing");
153             return false;
154         }
155
156         if (config.ipAddress.isEmpty() || config.systemId.isEmpty()) {
157             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
158                     "mylink address or system id not specified");
159             return false;
160         }
161
162         return true;
163     }
164
165     private void cancelHeartbeat() {
166         logger.debug("Stopping heartbeat");
167         ScheduledFuture<?> heartbeat = this.heartbeat;
168
169         if (heartbeat != null) {
170             logger.debug("Cancelling heartbeat job");
171             heartbeat.cancel(true);
172             this.heartbeat = null;
173         } else {
174             logger.debug("Heartbeat was not active");
175         }
176     }
177
178     private void sendHeartbeat() {
179         try {
180             logger.debug("Sending heartbeat");
181
182             SomfyMyLinkCommandShadePing command = new SomfyMyLinkCommandShadePing(config.systemId);
183             sendCommandWithResponse(command, SomfyMyLinkPingResponse.class).get();
184             updateStatus(ThingStatus.ONLINE);
185
186         } catch (SomfyMyLinkException | InterruptedException | ExecutionException e) {
187             logger.warn("Problem with mylink during heartbeat: {}", e.getMessage());
188             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
189         }
190     }
191
192     public SomfyMyLinkShade[] getShadeList() throws SomfyMyLinkException {
193         SomfyMyLinkCommandShadeList command = new SomfyMyLinkCommandShadeList(config.systemId);
194
195         try {
196             SomfyMyLinkShadesResponse response = sendCommandWithResponse(command, SomfyMyLinkShadesResponse.class)
197                     .get();
198
199             if (response != null) {
200                 return response.getResult();
201             } else {
202                 return EMPTY_SHADE_LIST;
203             }
204         } catch (InterruptedException | ExecutionException e) {
205             throw new SomfyMyLinkException("Problem while getting shade list.", e);
206         }
207     }
208
209     public SomfyMyLinkScene[] getSceneList() throws SomfyMyLinkException {
210         SomfyMyLinkCommandSceneList command = new SomfyMyLinkCommandSceneList(config.systemId);
211         SomfyMyLinkStateDescriptionOptionsProvider stateDescriptionProvider = this.stateDescriptionProvider;
212
213         try {
214             SomfyMyLinkScenesResponse response = sendCommandWithResponse(command, SomfyMyLinkScenesResponse.class)
215                     .get();
216
217             if (response != null && stateDescriptionProvider != null) {
218                 List<StateOption> options = new ArrayList<>();
219                 for (SomfyMyLinkScene scene : response.result) {
220                     options.add(new StateOption(scene.getTargetID(), scene.getName()));
221                 }
222
223                 logger.debug("Setting {} options on bridge", options.size());
224
225                 stateDescriptionProvider.setStateOptions(
226                         new ChannelUID(getThing().getUID(), SomfyMyLinkBindingConstants.CHANNEL_SCENES), options);
227
228                 return response.getResult();
229             } else {
230                 return EMPTY_SCENE_LIST;
231             }
232         } catch (InterruptedException | ExecutionException e) {
233             throw new SomfyMyLinkException("Problem getting scene list.", e);
234         }
235     }
236
237     public void commandShadeUp(String targetId) throws SomfyMyLinkException {
238         SomfyMyLinkCommandShadeUp cmd = new SomfyMyLinkCommandShadeUp(targetId, config.systemId);
239         sendCommand(cmd);
240     }
241
242     public void commandShadeDown(String targetId) throws SomfyMyLinkException {
243         SomfyMyLinkCommandShadeDown cmd = new SomfyMyLinkCommandShadeDown(targetId, config.systemId);
244         sendCommand(cmd);
245     }
246
247     public void commandShadeStop(String targetId) throws SomfyMyLinkException {
248         SomfyMyLinkCommandShadeStop cmd = new SomfyMyLinkCommandShadeStop(targetId, config.systemId);
249         sendCommand(cmd);
250     }
251
252     public void commandScene(Integer sceneId) throws SomfyMyLinkException {
253         SomfyMyLinkCommandSceneSet cmd = new SomfyMyLinkCommandSceneSet(sceneId, config.systemId);
254         sendCommand(cmd);
255     }
256
257     private CompletableFuture<@Nullable Void> sendCommand(SomfyMyLinkCommandBase command) {
258         CompletableFuture<@Nullable Void> future = new CompletableFuture<>();
259         ExecutorService commandExecutor = this.commandExecutor;
260         if (commandExecutor != null) {
261             commandExecutor.execute(() -> {
262                 String json = gson.toJson(command);
263                 try (Socket socket = getConnection();
264                         Writer out = new OutputStreamWriter(socket.getOutputStream(), StandardCharsets.US_ASCII)) {
265                     logger.debug("Sending: {}", json);
266                     out.write(json);
267                     out.flush();
268                     logger.debug("Sent: {}", json);
269
270                     // give time for mylink to process
271                     Thread.sleep(CONNECTION_DELAY);
272                 } catch (SocketTimeoutException e) {
273                     logger.warn("Timeout sending command to mylink: {} Message: {}", json, e.getMessage());
274                 } catch (IOException e) {
275                     logger.warn("Problem sending command to mylink: {} Message: {}", json, e.getMessage());
276                 } catch (InterruptedException e) {
277                     logger.warn("Interrupted while waiting after sending command to mylink: {} Message: {}", json,
278                             e.getMessage());
279                 } catch (Exception e) {
280                     logger.warn("Unexpected exception while sending command to mylink: {} Message: {}", json,
281                             e.getMessage());
282                 }
283                 future.complete(null);
284             });
285         } else {
286             future.complete(null);
287         }
288
289         return future;
290     }
291
292     private <T extends SomfyMyLinkResponseBase> CompletableFuture<@Nullable T> sendCommandWithResponse(
293             SomfyMyLinkCommandBase command, Class<T> responseType) {
294         CompletableFuture<@Nullable T> future = new CompletableFuture<>();
295         ExecutorService commandExecutor = this.commandExecutor;
296         if (commandExecutor != null) {
297             commandExecutor.submit(() -> {
298                 String json = gson.toJson(command);
299
300                 try (Socket socket = getConnection();
301                         Writer out = new OutputStreamWriter(socket.getOutputStream(), StandardCharsets.US_ASCII);
302                         BufferedReader in = new BufferedReader(
303                                 new InputStreamReader(socket.getInputStream(), StandardCharsets.US_ASCII))) {
304
305                     // send the command
306                     logger.debug("Sending: {}", json);
307                     out.write(json);
308                     out.flush();
309
310                     // read the response
311                     try {
312                         T response = parseResponse(in, responseType);
313                         future.complete(response);
314                         Thread.sleep(CONNECTION_DELAY);
315                         return;
316                     } catch (SomfyMyLinkException e) {
317                         future.completeExceptionally(e);
318                         return;
319                     }
320                 } catch (SocketTimeoutException e) {
321                     logger.warn("Timeout sending command to mylink: {} Message: {}", json, e.getMessage());
322                     future.completeExceptionally(new SomfyMyLinkException("Timeout sending command to mylink", e));
323                 } catch (IOException e) {
324                     logger.warn("Problem sending command to mylink: {} Message: {}", json, e.getMessage());
325                     future.completeExceptionally(new SomfyMyLinkException("Problem sending command to mylink", e));
326                 } catch (InterruptedException e) {
327                     logger.warn("Interrupted while waiting after sending command to mylink: {} Message: {}", json,
328                             e.getMessage());
329                     future.complete(null);
330                 } catch (Exception e) {
331                     logger.warn("Unexpected exception while sending command to mylink: {} Message: {}", json,
332                             e.getMessage());
333                     future.completeExceptionally(e);
334                 }
335             });
336         } else {
337             future.complete(null);
338         }
339         return future;
340     }
341
342     private <T extends SomfyMyLinkResponseBase> T parseResponse(Reader reader, Class<T> responseType) {
343         JsonParser parser = new JsonParser();
344         JsonObject jsonObj = parser.parse(gson.newJsonReader(reader)).getAsJsonObject();
345
346         logger.debug("Got full message: {}", jsonObj.toString());
347
348         if (jsonObj.has("error")) {
349             SomfyMyLinkErrorResponse errorResponse = gson.fromJson(jsonObj, SomfyMyLinkErrorResponse.class);
350             logger.info("Error parsing mylink response: {}", errorResponse.error.message);
351             throw new SomfyMyLinkException("Incomplete message.");
352         }
353
354         return gson.fromJson(jsonObj, responseType);
355     }
356
357     private Socket getConnection() throws IOException, SomfyMyLinkException {
358         try {
359             logger.debug("Getting connection to mylink on: {}  Post: {}", config.ipAddress, MYLINK_PORT);
360             String myLinkAddress = config.ipAddress;
361             Socket socket = new Socket(myLinkAddress, MYLINK_PORT);
362             socket.setSoTimeout(MYLINK_DEFAULT_TIMEOUT);
363             return socket;
364         } catch (IOException e) {
365             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
366             throw e;
367         }
368     }
369
370     @Override
371     public void thingUpdated(Thing thing) {
372         SomfyMyLinkConfiguration newConfig = thing.getConfiguration().as(SomfyMyLinkConfiguration.class);
373         config = newConfig;
374     }
375
376     @Override
377     public void dispose() {
378         cancelHeartbeat();
379         dispose(commandExecutor);
380     }
381
382     private static void dispose(@Nullable ExecutorService executor) {
383         if (executor != null) {
384             executor.shutdownNow();
385         }
386     }
387 }