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