]> git.basschouten.com Git - openhab-addons.git/blob
62651cdd9ab41bd1d72450f06e15564684f16abf
[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.somfymylink.internal.handler;
14
15 import static org.openhab.binding.somfymylink.internal.SomfyMyLinkBindingConstants.CHANNEL_SCENES;
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.Objects;
31 import java.util.concurrent.CompletableFuture;
32 import java.util.concurrent.ExecutionException;
33 import java.util.concurrent.ExecutorService;
34 import java.util.concurrent.Executors;
35 import java.util.concurrent.ScheduledFuture;
36 import java.util.concurrent.TimeUnit;
37
38 import org.eclipse.jdt.annotation.NonNullByDefault;
39 import org.eclipse.jdt.annotation.Nullable;
40 import org.openhab.binding.somfymylink.internal.SomfyMyLinkBindingConstants;
41 import org.openhab.binding.somfymylink.internal.config.SomfyMyLinkConfiguration;
42 import org.openhab.binding.somfymylink.internal.discovery.SomfyMyLinkDeviceDiscoveryService;
43 import org.openhab.binding.somfymylink.internal.model.SomfyMyLinkCommandBase;
44 import org.openhab.binding.somfymylink.internal.model.SomfyMyLinkCommandSceneList;
45 import org.openhab.binding.somfymylink.internal.model.SomfyMyLinkCommandSceneSet;
46 import org.openhab.binding.somfymylink.internal.model.SomfyMyLinkCommandShadeDown;
47 import org.openhab.binding.somfymylink.internal.model.SomfyMyLinkCommandShadeList;
48 import org.openhab.binding.somfymylink.internal.model.SomfyMyLinkCommandShadePing;
49 import org.openhab.binding.somfymylink.internal.model.SomfyMyLinkCommandShadeStop;
50 import org.openhab.binding.somfymylink.internal.model.SomfyMyLinkCommandShadeUp;
51 import org.openhab.binding.somfymylink.internal.model.SomfyMyLinkErrorResponse;
52 import org.openhab.binding.somfymylink.internal.model.SomfyMyLinkPingResponse;
53 import org.openhab.binding.somfymylink.internal.model.SomfyMyLinkResponseBase;
54 import org.openhab.binding.somfymylink.internal.model.SomfyMyLinkScene;
55 import org.openhab.binding.somfymylink.internal.model.SomfyMyLinkScenesResponse;
56 import org.openhab.binding.somfymylink.internal.model.SomfyMyLinkShade;
57 import org.openhab.binding.somfymylink.internal.model.SomfyMyLinkShadesResponse;
58 import org.openhab.core.common.NamedThreadFactory;
59 import org.openhab.core.library.types.StringType;
60 import org.openhab.core.thing.Bridge;
61 import org.openhab.core.thing.ChannelUID;
62 import org.openhab.core.thing.Thing;
63 import org.openhab.core.thing.ThingStatus;
64 import org.openhab.core.thing.ThingStatusDetail;
65 import org.openhab.core.thing.binding.BaseBridgeHandler;
66 import org.openhab.core.thing.binding.ThingHandlerService;
67 import org.openhab.core.types.Command;
68 import org.openhab.core.types.RefreshType;
69 import org.openhab.core.types.StateOption;
70 import org.slf4j.Logger;
71 import org.slf4j.LoggerFactory;
72
73 import com.google.gson.Gson;
74 import com.google.gson.JsonObject;
75 import com.google.gson.JsonParser;
76
77 /**
78  * The {@link SomfyMyLinkBridgeHandler} is responsible for handling commands, which are
79  * sent to one of the channels.
80  *
81  * @author Chris Johnson - Initial contribution
82  */
83 @NonNullByDefault
84 public class SomfyMyLinkBridgeHandler extends BaseBridgeHandler {
85
86     private final Logger logger = LoggerFactory.getLogger(SomfyMyLinkBridgeHandler.class);
87     private static final int HEARTBEAT_MINUTES = 2;
88     private static final int MYLINK_PORT = 44100;
89     private static final int MYLINK_DEFAULT_TIMEOUT = 5000;
90     private static final int CONNECTION_DELAY = 1000;
91     private static final SomfyMyLinkShade[] EMPTY_SHADE_LIST = new SomfyMyLinkShade[0];
92     private static final SomfyMyLinkScene[] EMPTY_SCENE_LIST = new SomfyMyLinkScene[0];
93
94     private SomfyMyLinkConfiguration config = new SomfyMyLinkConfiguration();
95     private @Nullable ScheduledFuture<?> heartbeat;
96     private @Nullable SomfyMyLinkStateDescriptionOptionsProvider stateDescriptionProvider;
97     private @Nullable ExecutorService commandExecutor;
98
99     // Gson & parser
100     private final Gson gson = new Gson();
101
102     public SomfyMyLinkBridgeHandler(Bridge bridge,
103             @Nullable SomfyMyLinkStateDescriptionOptionsProvider stateDescriptionProvider) {
104         super(bridge);
105         this.stateDescriptionProvider = stateDescriptionProvider;
106     }
107
108     @Override
109     public void handleCommand(ChannelUID channelUID, Command command) {
110         logger.debug("Command received on mylink {}", command);
111
112         try {
113             if (CHANNEL_SCENES.equals(channelUID.getId())) {
114                 if (command instanceof RefreshType) {
115                     return;
116                 }
117
118                 if (command instanceof StringType) {
119                     Integer sceneId = Integer.decode(command.toString());
120                     commandScene(sceneId);
121                 }
122             }
123         } catch (SomfyMyLinkException e) {
124             logger.info("Error handling command: {}", e.getMessage());
125             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
126         }
127     }
128
129     @Override
130     public void initialize() {
131         logger.info("Initializing mylink");
132         config = getThing().getConfiguration().as(SomfyMyLinkConfiguration.class);
133
134         commandExecutor = Executors.newSingleThreadExecutor(new NamedThreadFactory(thing.getUID().getAsString(), true));
135
136         if (validConfiguration(config)) {
137             // start the keepalive process
138             if (heartbeat == null) {
139                 logger.info("Starting heartbeat job every {} min", HEARTBEAT_MINUTES);
140                 heartbeat = this.scheduler.scheduleWithFixedDelay(this::sendHeartbeat, 0, HEARTBEAT_MINUTES,
141                         TimeUnit.MINUTES);
142             }
143         }
144     }
145
146     @Override
147     public Collection<Class<? extends ThingHandlerService>> getServices() {
148         return Collections.singleton(SomfyMyLinkDeviceDiscoveryService.class);
149     }
150
151     private boolean validConfiguration(@Nullable SomfyMyLinkConfiguration config) {
152         if (config == null) {
153             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "mylink configuration missing");
154             return false;
155         }
156
157         if (config.ipAddress.isEmpty() || config.systemId.isEmpty()) {
158             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
159                     "mylink address or system id not specified");
160             return false;
161         }
162
163         return true;
164     }
165
166     private void cancelHeartbeat() {
167         logger.debug("Stopping heartbeat");
168         ScheduledFuture<?> heartbeat = this.heartbeat;
169
170         if (heartbeat != null) {
171             logger.debug("Cancelling heartbeat job");
172             heartbeat.cancel(true);
173             this.heartbeat = null;
174         } else {
175             logger.debug("Heartbeat was not active");
176         }
177     }
178
179     private void sendHeartbeat() {
180         try {
181             logger.debug("Sending heartbeat");
182
183             SomfyMyLinkCommandShadePing command = new SomfyMyLinkCommandShadePing(config.systemId);
184             sendCommandWithResponse(command, SomfyMyLinkPingResponse.class).get();
185             updateStatus(ThingStatus.ONLINE);
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                     // send the command
305                     logger.debug("Sending: {}", json);
306                     out.write(json);
307                     out.flush();
308
309                     // read the response
310                     try {
311                         T response = parseResponse(in, responseType);
312                         future.complete(response);
313                         Thread.sleep(CONNECTION_DELAY);
314                         return;
315                     } catch (SomfyMyLinkException e) {
316                         future.completeExceptionally(e);
317                         return;
318                     }
319                 } catch (SocketTimeoutException e) {
320                     logger.warn("Timeout sending command to mylink: {} Message: {}", json, e.getMessage());
321                     future.completeExceptionally(new SomfyMyLinkException("Timeout sending command to mylink", e));
322                 } catch (IOException e) {
323                     logger.warn("Problem sending command to mylink: {} Message: {}", json, e.getMessage());
324                     future.completeExceptionally(new SomfyMyLinkException("Problem sending command to mylink", e));
325                 } catch (InterruptedException e) {
326                     logger.warn("Interrupted while waiting after sending command to mylink: {} Message: {}", json,
327                             e.getMessage());
328                     future.complete(null);
329                 } catch (Exception e) {
330                     logger.warn("Unexpected exception while sending command to mylink: {} Message: {}", json,
331                             e.getMessage());
332                     future.completeExceptionally(e);
333                 }
334             });
335         } else {
336             future.complete(null);
337         }
338         return future;
339     }
340
341     private <T extends SomfyMyLinkResponseBase> T parseResponse(Reader reader, Class<T> responseType) {
342         JsonObject jsonObj = JsonParser.parseReader(gson.newJsonReader(reader)).getAsJsonObject();
343
344         logger.debug("Got full message: {}", jsonObj.toString());
345
346         if (jsonObj.has("error")) {
347             SomfyMyLinkErrorResponse errorResponse = gson.fromJson(jsonObj, SomfyMyLinkErrorResponse.class);
348             logger.info("Error parsing mylink response: {}", errorResponse.error.message);
349             throw new SomfyMyLinkException("Incomplete message.");
350         }
351
352         return Objects.requireNonNull(gson.fromJson(jsonObj, responseType));
353     }
354
355     private Socket getConnection() throws IOException, SomfyMyLinkException {
356         try {
357             logger.debug("Getting connection to mylink on: {}  Post: {}", config.ipAddress, MYLINK_PORT);
358             String myLinkAddress = config.ipAddress;
359             Socket socket = new Socket(myLinkAddress, MYLINK_PORT);
360             socket.setSoTimeout(MYLINK_DEFAULT_TIMEOUT);
361             return socket;
362         } catch (IOException e) {
363             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
364             throw e;
365         }
366     }
367
368     @Override
369     public void thingUpdated(Thing thing) {
370         SomfyMyLinkConfiguration newConfig = thing.getConfiguration().as(SomfyMyLinkConfiguration.class);
371         config = newConfig;
372     }
373
374     @Override
375     public void dispose() {
376         cancelHeartbeat();
377         dispose(commandExecutor);
378     }
379
380     private static void dispose(@Nullable ExecutorService executor) {
381         if (executor != null) {
382             executor.shutdownNow();
383         }
384     }
385 }