2 * Copyright (c) 2010-2020 Contributors to the openHAB project
4 * See the NOTICE file(s) distributed with this work for additional
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
11 * SPDX-License-Identifier: EPL-2.0
13 package org.openhab.binding.somfymylink.internal.handler;
15 import static org.openhab.binding.somfymylink.internal.SomfyMyLinkBindingConstants.*;
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;
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;
72 import com.google.gson.Gson;
73 import com.google.gson.JsonObject;
74 import com.google.gson.JsonParser;
77 * The {@link SomfyMyLinkBridgeHandler} is responsible for handling commands, which are
78 * sent to one of the channels.
80 * @author Chris Johnson - Initial contribution
83 public class SomfyMyLinkBridgeHandler extends BaseBridgeHandler {
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];
93 private SomfyMyLinkConfiguration config = new SomfyMyLinkConfiguration();
94 private @Nullable ScheduledFuture<?> heartbeat;
95 private @Nullable SomfyMyLinkStateDescriptionOptionsProvider stateDescriptionProvider;
96 private @Nullable ExecutorService commandExecutor;
99 private final Gson gson = new Gson();
101 public SomfyMyLinkBridgeHandler(Bridge bridge,
102 @Nullable SomfyMyLinkStateDescriptionOptionsProvider stateDescriptionProvider) {
104 this.stateDescriptionProvider = stateDescriptionProvider;
108 public void handleCommand(ChannelUID channelUID, Command command) {
109 logger.debug("Command received on mylink {}", command);
112 if (CHANNEL_SCENES.equals(channelUID.getId())) {
113 if (command instanceof RefreshType) {
117 if (command instanceof StringType) {
118 Integer sceneId = Integer.decode(command.toString());
119 commandScene(sceneId);
122 } catch (SomfyMyLinkException e) {
123 logger.info("Error handling command: {}", e.getMessage());
124 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
129 public void initialize() {
130 logger.info("Initializing mylink");
131 config = getThing().getConfiguration().as(SomfyMyLinkConfiguration.class);
133 commandExecutor = Executors.newSingleThreadExecutor(new NamedThreadFactory(thing.getUID().getAsString(), true));
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,
146 public Collection<Class<? extends ThingHandlerService>> getServices() {
147 return Collections.singleton(SomfyMyLinkDeviceDiscoveryService.class);
150 private boolean validConfiguration(@Nullable SomfyMyLinkConfiguration config) {
151 if (config == null) {
152 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "mylink configuration missing");
156 if (config.ipAddress.isEmpty() || config.systemId.isEmpty()) {
157 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
158 "mylink address or system id not specified");
165 private void cancelHeartbeat() {
166 logger.debug("Stopping heartbeat");
167 ScheduledFuture<?> heartbeat = this.heartbeat;
169 if (heartbeat != null) {
170 logger.debug("Cancelling heartbeat job");
171 heartbeat.cancel(true);
172 this.heartbeat = null;
174 logger.debug("Heartbeat was not active");
178 private void sendHeartbeat() {
180 logger.debug("Sending heartbeat");
182 SomfyMyLinkCommandShadePing command = new SomfyMyLinkCommandShadePing(config.systemId);
183 sendCommandWithResponse(command, SomfyMyLinkPingResponse.class).get();
184 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());
192 public SomfyMyLinkShade[] getShadeList() throws SomfyMyLinkException {
193 SomfyMyLinkCommandShadeList command = new SomfyMyLinkCommandShadeList(config.systemId);
196 SomfyMyLinkShadesResponse response = sendCommandWithResponse(command, SomfyMyLinkShadesResponse.class)
199 if (response != null) {
200 return response.getResult();
202 return EMPTY_SHADE_LIST;
204 } catch (InterruptedException | ExecutionException e) {
205 throw new SomfyMyLinkException("Problem while getting shade list.", e);
209 public SomfyMyLinkScene[] getSceneList() throws SomfyMyLinkException {
210 SomfyMyLinkCommandSceneList command = new SomfyMyLinkCommandSceneList(config.systemId);
211 SomfyMyLinkStateDescriptionOptionsProvider stateDescriptionProvider = this.stateDescriptionProvider;
214 SomfyMyLinkScenesResponse response = sendCommandWithResponse(command, SomfyMyLinkScenesResponse.class)
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()));
223 logger.debug("Setting {} options on bridge", options.size());
225 stateDescriptionProvider.setStateOptions(
226 new ChannelUID(getThing().getUID(), SomfyMyLinkBindingConstants.CHANNEL_SCENES), options);
228 return response.getResult();
230 return EMPTY_SCENE_LIST;
232 } catch (InterruptedException | ExecutionException e) {
233 throw new SomfyMyLinkException("Problem getting scene list.", e);
237 public void commandShadeUp(String targetId) throws SomfyMyLinkException {
238 SomfyMyLinkCommandShadeUp cmd = new SomfyMyLinkCommandShadeUp(targetId, config.systemId);
242 public void commandShadeDown(String targetId) throws SomfyMyLinkException {
243 SomfyMyLinkCommandShadeDown cmd = new SomfyMyLinkCommandShadeDown(targetId, config.systemId);
247 public void commandShadeStop(String targetId) throws SomfyMyLinkException {
248 SomfyMyLinkCommandShadeStop cmd = new SomfyMyLinkCommandShadeStop(targetId, config.systemId);
252 public void commandScene(Integer sceneId) throws SomfyMyLinkException {
253 SomfyMyLinkCommandSceneSet cmd = new SomfyMyLinkCommandSceneSet(sceneId, config.systemId);
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);
268 logger.debug("Sent: {}", json);
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,
279 } catch (Exception e) {
280 logger.warn("Unexpected exception while sending command to mylink: {} Message: {}", json,
283 future.complete(null);
286 future.complete(null);
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);
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))) {
306 logger.debug("Sending: {}", json);
312 T response = parseResponse(in, responseType);
313 future.complete(response);
314 Thread.sleep(CONNECTION_DELAY);
316 } catch (SomfyMyLinkException e) {
317 future.completeExceptionally(e);
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,
329 future.complete(null);
330 } catch (Exception e) {
331 logger.warn("Unexpected exception while sending command to mylink: {} Message: {}", json,
333 future.completeExceptionally(e);
337 future.complete(null);
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();
346 logger.debug("Got full message: {}", jsonObj.toString());
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.");
354 return gson.fromJson(jsonObj, responseType);
357 private Socket getConnection() throws IOException, SomfyMyLinkException {
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);
364 } catch (IOException e) {
365 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
371 public void thingUpdated(Thing thing) {
372 SomfyMyLinkConfiguration newConfig = thing.getConfiguration().as(SomfyMyLinkConfiguration.class);
377 public void dispose() {
379 dispose(commandExecutor);
382 private static void dispose(@Nullable ExecutorService executor) {
383 if (executor != null) {
384 executor.shutdownNow();