]> git.basschouten.com Git - openhab-addons.git/blob
f5d0dddf8ec15bef62c92b03ca5cab00d5542163
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2022 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.elroconnects.internal.handler;
14
15 import static org.openhab.binding.elroconnects.internal.ElroConnectsBindingConstants.*;
16
17 import java.io.IOException;
18 import java.net.DatagramPacket;
19 import java.net.DatagramSocket;
20 import java.net.InetAddress;
21 import java.net.SocketException;
22 import java.net.UnknownHostException;
23 import java.nio.charset.StandardCharsets;
24 import java.util.ArrayList;
25 import java.util.Collection;
26 import java.util.Collections;
27 import java.util.List;
28 import java.util.Map;
29 import java.util.concurrent.CompletableFuture;
30 import java.util.concurrent.ConcurrentHashMap;
31 import java.util.concurrent.ExecutionException;
32 import java.util.concurrent.ScheduledFuture;
33 import java.util.concurrent.TimeUnit;
34 import java.util.concurrent.TimeoutException;
35 import java.util.regex.Matcher;
36 import java.util.regex.Pattern;
37
38 import org.eclipse.jdt.annotation.NonNullByDefault;
39 import org.eclipse.jdt.annotation.Nullable;
40 import org.openhab.binding.elroconnects.internal.ElroConnectsBindingConstants.ElroDeviceType;
41 import org.openhab.binding.elroconnects.internal.ElroConnectsDynamicStateDescriptionProvider;
42 import org.openhab.binding.elroconnects.internal.ElroConnectsMessage;
43 import org.openhab.binding.elroconnects.internal.devices.ElroConnectsDevice;
44 import org.openhab.binding.elroconnects.internal.devices.ElroConnectsDeviceCxsmAlarm;
45 import org.openhab.binding.elroconnects.internal.devices.ElroConnectsDeviceEntrySensor;
46 import org.openhab.binding.elroconnects.internal.devices.ElroConnectsDeviceGenericAlarm;
47 import org.openhab.binding.elroconnects.internal.devices.ElroConnectsDeviceMotionSensor;
48 import org.openhab.binding.elroconnects.internal.devices.ElroConnectsDevicePowerSocket;
49 import org.openhab.binding.elroconnects.internal.devices.ElroConnectsDeviceTemperatureSensor;
50 import org.openhab.binding.elroconnects.internal.discovery.ElroConnectsDiscoveryService;
51 import org.openhab.binding.elroconnects.internal.util.ElroConnectsUtil;
52 import org.openhab.core.common.NamedThreadFactory;
53 import org.openhab.core.library.types.DecimalType;
54 import org.openhab.core.library.types.StringType;
55 import org.openhab.core.net.NetworkAddressService;
56 import org.openhab.core.thing.Bridge;
57 import org.openhab.core.thing.Channel;
58 import org.openhab.core.thing.ChannelUID;
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.StateDescription;
66 import org.openhab.core.types.StateDescriptionFragmentBuilder;
67 import org.openhab.core.types.StateOption;
68 import org.openhab.core.util.HexUtils;
69 import org.slf4j.Logger;
70 import org.slf4j.LoggerFactory;
71
72 import com.google.gson.Gson;
73 import com.google.gson.JsonSyntaxException;
74
75 /**
76  * The {@link ElroConnectsBridgeHandler} is the bridge handler responsible to for handling all communication with the
77  * ELRO Connects K1 Hub. All individual device communication passes through the hub.
78  *
79  * @author Mark Herwege - Initial contribution
80  */
81 @NonNullByDefault
82 public class ElroConnectsBridgeHandler extends BaseBridgeHandler {
83
84     private final Logger logger = LoggerFactory.getLogger(ElroConnectsBridgeHandler.class);
85
86     private static final int PORT = 1025; // UDP port for UDP socket communication with K1 hub
87     private static final int RESPONSE_TIMEOUT_MS = 5000; // max time to wait for receiving all responses on a request
88
89     // Default scene names are not received from K1 hub, so kept here
90     private static final Map<Integer, String> DEFAULT_SCENES = Map.ofEntries(Map.entry(0, "Home"), Map.entry(1, "Away"),
91             Map.entry(2, "Sleep"));
92     private static final int MAX_DEFAULT_SCENE = 2;
93
94     // Command filter when syncing devices and scenes, other values would filter what gets received
95     private static final String SYNC_COMMAND = "0002";
96
97     // Regex for valid connectorId
98     private static final Pattern CONNECTOR_ID_PATTERN = Pattern.compile("^ST_([0-9a-f]){12}$");
99
100     // Message string for acknowledging receipt of data
101     private static final String ACK_STRING = "{\"answer\": \"APP_answer_OK\"}";
102     private static final byte[] ACK = ACK_STRING.getBytes(StandardCharsets.UTF_8);
103
104     // Connector expects to receive messages with an increasing id for each message
105     // Max msgId is 65536, therefore use short and convert to unsigned Integer when using it
106     private short msgId;
107
108     private NetworkAddressService networkAddressService;
109
110     // Used when restarting connection, delay restart for 1s to avoid high network traffic
111     private volatile boolean restart;
112     static final int RESTART_DELAY_MS = 1000;
113
114     private volatile String connectorId = "";
115     // Used for getting IP address and keep connection alive messages
116     private static final String QUERY_BASE_STRING = "IOT_KEY?";
117     private volatile String queryString = QUERY_BASE_STRING + connectorId;
118     // Regex to retrieve ctrlKey from response on IP address message
119     private static final Pattern CTRL_KEY_PATTERN = Pattern.compile("KEY:([0-9a-f]*)");
120
121     private int refreshInterval = 60;
122     private volatile @Nullable InetAddress addr;
123     private volatile String ctrlKey = "";
124
125     private volatile @Nullable DatagramSocket socket;
126     private volatile @Nullable DatagramPacket ackPacket;
127
128     private volatile @Nullable ScheduledFuture<?> syncFuture;
129     private volatile @Nullable CompletableFuture<Boolean> awaitResponse;
130
131     private ElroConnectsDynamicStateDescriptionProvider stateDescriptionProvider;
132
133     private final Map<Integer, String> scenes = new ConcurrentHashMap<>();
134     private final Map<Integer, ElroConnectsDevice> devices = new ConcurrentHashMap<>();
135     private final Map<Integer, ElroConnectsDeviceHandler> deviceHandlers = new ConcurrentHashMap<>();
136
137     private int currentScene;
138
139     // We only keep 2 gson adapters used to serialize and deserialize all messages sent and received
140     private final Gson gsonOut = new Gson();
141     private Gson gsonIn = new Gson();
142
143     public ElroConnectsBridgeHandler(Bridge bridge, NetworkAddressService networkAddressService,
144             ElroConnectsDynamicStateDescriptionProvider stateDescriptionProvider) {
145         super(bridge);
146         this.networkAddressService = networkAddressService;
147         this.stateDescriptionProvider = stateDescriptionProvider;
148
149         resetScenes();
150     }
151
152     @Override
153     public void initialize() {
154         ElroConnectsBridgeConfiguration config = getConfigAs(ElroConnectsBridgeConfiguration.class);
155         connectorId = config.connectorId;
156         refreshInterval = config.refreshInterval;
157
158         if (connectorId.isEmpty()) {
159             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "Device ID not set");
160             return;
161         } else if (!CONNECTOR_ID_PATTERN.matcher(connectorId).matches()) {
162             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
163                     "Device ID not of format ST_xxxxxxxxxxxx with xxxxxxxxxxxx the lowercase MAC address of the connector");
164             return;
165         }
166
167         queryString = QUERY_BASE_STRING + connectorId;
168
169         scheduler.submit(this::startCommunication);
170     }
171
172     @Override
173     public void dispose() {
174         stopCommunication();
175     }
176
177     private synchronized void startCommunication() {
178         ElroConnectsBridgeConfiguration config = getConfigAs(ElroConnectsBridgeConfiguration.class);
179         InetAddress addr = this.addr;
180
181         String ipAddress = config.ipAddress;
182         if (!ipAddress.isEmpty()) {
183             try {
184                 addr = InetAddress.getByName(ipAddress);
185                 this.addr = addr;
186             } catch (UnknownHostException e) {
187                 addr = null;
188                 logger.warn("Unknown host for {}, trying to discover address", ipAddress);
189             }
190         }
191
192         try {
193             addr = getAddr(addr == null);
194         } catch (IOException e) {
195             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
196                     "Error trying to find IP address for connector ID " + connectorId + ".");
197             stopCommunication();
198             return;
199         }
200         if (addr == null) {
201             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
202                     "Error trying to find IP address for connector ID " + connectorId + ".");
203             stopCommunication();
204             return;
205         }
206
207         String ctrlKey = this.ctrlKey;
208         if (ctrlKey.isEmpty()) {
209             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
210                     "Communication data error while starting communication.");
211             stopCommunication();
212             return;
213         }
214
215         DatagramSocket socket;
216         try {
217             socket = createSocket(false);
218             this.socket = socket;
219         } catch (IOException e) {
220             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
221                     "Socket error while starting communication: " + e.getMessage());
222             stopCommunication();
223             return;
224         }
225
226         ackPacket = new DatagramPacket(ACK, ACK.length, addr, PORT);
227
228         logger.debug("Connected to connector {} at {}:{}", connectorId, addr, PORT);
229
230         try {
231             // Start ELRO Connects listener. This listener will act on all messages coming from ELRO K1 Connector.
232             (new NamedThreadFactory(THREAD_NAME_PREFIX + thing.getUID().getAsString()).newThread(this::runElroEvents))
233                     .start();
234
235             keepAlive();
236
237             // First get status, then name. The status response contains the device types needed to instantiate correct
238             // classes.
239             getDeviceStatuses();
240             getDeviceNames();
241
242             syncScenes();
243             getCurrentScene();
244
245             updateStatus(ThingStatus.ONLINE);
246             updateState(SCENE, new StringType(String.valueOf(currentScene)));
247         } catch (IOException e) {
248             restartCommunication("Error in communication getting initial data: " + e.getMessage());
249             return;
250         }
251
252         scheduleSyncStatus();
253     }
254
255     /**
256      * Get the IP address and ctrl key of the connector by broadcasting message with connectorId. This should be used
257      * when initializing the connection. the ctrlKey field is set.
258      *
259      * @param broadcast, if true find address by broadcast, otherwise simply send to configured address to retrieve key
260      *            only
261      * @return IP address of connector
262      * @throws IOException
263      */
264     private @Nullable InetAddress getAddr(boolean broadcast) throws IOException {
265         try (DatagramSocket socket = createSocket(true)) {
266             String response = sendAndReceive(socket, queryString, broadcast);
267             Matcher keyMatcher = CTRL_KEY_PATTERN.matcher(response);
268             ctrlKey = keyMatcher.find() ? keyMatcher.group(1) : "";
269             logger.debug("Key: {}", ctrlKey);
270
271             return addr;
272         }
273     }
274
275     /**
276      * Send keep alive message.
277      *
278      * @throws IOException
279      */
280     private void keepAlive() throws IOException {
281         DatagramSocket socket = this.socket;
282         if (socket != null) {
283             logger.trace("Keep alive");
284             // Sending query string, so the connection with the K1 hub stays alive
285             awaitResponse(true);
286             send(socket, queryString, false);
287         } else {
288             restartCommunication("Error in communication, no socket to send keep alive");
289         }
290     }
291
292     /**
293      * Cleanup socket when the communication with ELRO Connects connector is closed.
294      *
295      */
296     private synchronized void stopCommunication() {
297         ScheduledFuture<?> sync = syncFuture;
298         if (sync != null) {
299             sync.cancel(true);
300         }
301         syncFuture = null;
302
303         stopAwaitResponse();
304
305         DatagramSocket socket = this.socket;
306         if (socket != null && !socket.isClosed()) {
307             socket.close();
308         }
309         this.socket = null;
310
311         logger.debug("Communication stopped");
312     }
313
314     /**
315      * Close and restart communication with ELRO Connects system, to be called after error in communication.
316      *
317      * @param offlineMessage message for thing status
318      */
319     private synchronized void restartCommunication(String offlineMessage) {
320         updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, offlineMessage);
321
322         stopCommunication();
323
324         if (!restart) {
325             logger.debug("Restart communication");
326
327             restart = true;
328             scheduler.schedule(this::startFromRestart, RESTART_DELAY_MS, TimeUnit.MILLISECONDS);
329         }
330     }
331
332     private synchronized void startFromRestart() {
333         restart = false;
334         if (ThingStatus.OFFLINE.equals(thing.getStatus())) {
335             startCommunication();
336         }
337     }
338
339     private DatagramSocket createSocket(boolean timeout) throws SocketException {
340         DatagramSocket socket = new DatagramSocket();
341         socket.setBroadcast(true);
342         if (timeout) {
343             socket.setSoTimeout(1000);
344         }
345         return socket;
346     }
347
348     /**
349      * Read messages received through UDP socket.
350      *
351      * @param socket
352      */
353     private void runElroEvents() {
354         DatagramSocket socket = this.socket;
355
356         if (socket != null) {
357             logger.debug("Listening for messages");
358
359             try {
360                 byte[] buffer = new byte[4096];
361                 DatagramPacket packet = new DatagramPacket(buffer, buffer.length);
362                 while (!Thread.interrupted()) {
363                     String response = receive(socket, buffer, packet);
364                     processMessage(socket, response);
365                 }
366             } catch (IOException e) {
367                 restartCommunication("Communication error in listener: " + e.getMessage());
368             }
369         } else {
370             restartCommunication("Error in communication, no socket to start listener");
371         }
372     }
373
374     /**
375      * Schedule regular queries to sync devices and scenes.
376      */
377     private void scheduleSyncStatus() {
378         syncFuture = scheduler.scheduleWithFixedDelay(() -> {
379             try {
380                 keepAlive();
381                 syncDevices();
382                 syncScenes();
383                 getCurrentScene();
384             } catch (IOException e) {
385                 restartCommunication("Error in communication refreshing device status: " + e.getMessage());
386             }
387         }, refreshInterval, refreshInterval, TimeUnit.SECONDS);
388     }
389
390     /**
391      * Process response received from K1 Hub and send acknowledgement through open socket.
392      *
393      * @param socket
394      * @param response
395      * @throws IOException
396      */
397     private void processMessage(DatagramSocket socket, String response) throws IOException {
398         if (!response.startsWith("{")) {
399             // Not a Json to interpret, just ignore
400             stopAwaitResponse();
401             return;
402         }
403         ElroConnectsMessage message;
404         String json = "";
405         try {
406             json = response.split("\\R")[0];
407             message = gsonIn.fromJson(json, ElroConnectsMessage.class);
408             sendAck(socket);
409         } catch (JsonSyntaxException ignore) {
410             logger.debug("Cannot decode, not a valid json: {}", json);
411             return;
412         }
413
414         if (message == null) {
415             return;
416         }
417
418         switch (message.getCmdId()) {
419             case ELRO_IGNORE_YES_NO:
420                 break;
421             case ELRO_REC_DEVICE_NAME:
422                 processDeviceNameMessage(message);
423                 break;
424             case ELRO_REC_DEVICE_STATUS:
425                 processDeviceStatusMessage(message);
426                 break;
427             case ELRO_REC_ALARM:
428                 processAlarmTriggerMessage(message);
429                 break;
430             case ELRO_REC_SCENE_NAME:
431                 processSceneNameMessage(message);
432                 break;
433             case ELRO_REC_SCENE_TYPE:
434                 processSceneTypeMessage(message);
435                 break;
436             case ELRO_REC_SCENE:
437                 processSceneMessage(message);
438                 break;
439             default:
440                 logger.debug("CmdId not implemented: {}", message.getCmdId());
441         }
442     }
443
444     private void processDeviceStatusMessage(ElroConnectsMessage message) {
445         int deviceId = message.getDeviceId();
446         String deviceStatus = message.getDeviceStatus();
447         if ("OVER".equals(deviceStatus)) {
448             // last message in series received
449             stopAwaitResponse();
450             return;
451         }
452
453         ElroConnectsDevice device = devices.get(deviceId);
454         device = (device == null) ? addDevice(message) : device;
455         if (device == null) {
456             // device type not recognized, could not be added
457             return;
458         }
459         device.setDeviceStatus(deviceStatus);
460
461         device.updateState();
462     }
463
464     private void processDeviceNameMessage(ElroConnectsMessage message) {
465         String answerContent = message.getAnswerContent();
466         if ("NAME_OVER".equals(answerContent)) {
467             // last message in series received
468             stopAwaitResponse();
469             return;
470         }
471         if (answerContent.length() <= 4) {
472             logger.debug("Could not decode answer {}", answerContent);
473             return;
474         }
475
476         int deviceId = Integer.parseInt(answerContent.substring(0, 4), 16);
477         String deviceName = (new String(HexUtils.hexToBytes(answerContent.substring(4)))).replaceAll("[@$]*", "");
478         ElroConnectsDevice device = devices.get(deviceId);
479         if (device != null) {
480             device.setDeviceName(deviceName);
481             logger.debug("Device ID {} name: {}", deviceId, deviceName);
482         }
483     }
484
485     private void processSceneNameMessage(ElroConnectsMessage message) {
486         int sceneId = message.getSceneGroup();
487         String answerContent = message.getAnswerContent();
488         String sceneName;
489         if (sceneId > MAX_DEFAULT_SCENE) {
490             if (answerContent.length() < 44) {
491                 logger.debug("Could not decode answer {}", answerContent);
492                 return;
493             }
494             sceneName = (new String(HexUtils.hexToBytes(answerContent.substring(6, 38)))).replaceAll("[@$]*", "");
495             scenes.put(sceneId, sceneName);
496             logger.debug("Scene ID {} name: {}", sceneId, sceneName);
497         }
498     }
499
500     private void processSceneTypeMessage(ElroConnectsMessage message) {
501         String sceneContent = message.getSceneContent();
502         if ("OVER".equals(sceneContent)) {
503             // last message in series received
504             stopAwaitResponse();
505
506             updateSceneOptions();
507         }
508     }
509
510     private void processSceneMessage(ElroConnectsMessage message) {
511         int sceneId = message.getSceneGroup();
512
513         currentScene = sceneId;
514
515         updateState(SCENE, new StringType(String.valueOf(currentScene)));
516     }
517
518     private void processAlarmTriggerMessage(ElroConnectsMessage message) {
519         String answerContent = message.getAnswerContent();
520         if (answerContent.length() < 10) {
521             logger.debug("Could not decode answer {}", answerContent);
522             return;
523         }
524
525         int deviceId = Integer.parseInt(answerContent.substring(6, 10), 16);
526
527         ElroConnectsDeviceHandler handler = deviceHandlers.get(deviceId);
528         if (handler != null) {
529             handler.triggerAlarm();
530         }
531         logger.debug("Device ID {} alarm", deviceId);
532     }
533
534     private @Nullable ElroConnectsDevice addDevice(ElroConnectsMessage message) {
535         int deviceId = message.getDeviceId();
536         String deviceType = message.getDeviceName();
537         ElroDeviceType type = TYPE_MAP.getOrDefault(deviceType, ElroDeviceType.DEFAULT);
538
539         ElroConnectsDevice device;
540         switch (type) {
541             case CO_ALARM:
542             case SM_ALARM:
543             case WT_ALARM:
544             case THERMAL_ALARM:
545                 device = new ElroConnectsDeviceGenericAlarm(deviceId, this);
546                 break;
547             case CXSM_ALARM:
548                 device = new ElroConnectsDeviceCxsmAlarm(deviceId, this);
549                 break;
550             case POWERSOCKET:
551                 device = new ElroConnectsDevicePowerSocket(deviceId, this);
552                 break;
553             case ENTRY_SENSOR:
554                 device = new ElroConnectsDeviceEntrySensor(deviceId, this);
555                 break;
556             case MOTION_SENSOR:
557                 device = new ElroConnectsDeviceMotionSensor(deviceId, this);
558                 break;
559             case TH_SENSOR:
560                 device = new ElroConnectsDeviceTemperatureSensor(deviceId, this);
561                 break;
562             default:
563                 logger.debug("Device type {} not supported", deviceType);
564                 return null;
565         }
566         device.setDeviceType(deviceType);
567         devices.put(deviceId, device);
568         return device;
569     }
570
571     /**
572      * Just before sending message, this method should be called to make sure we wait for all responses that are still
573      * expected to be received. The last response will be indicated by a token in the last response message.
574      *
575      * @param waitResponse true if we want to wait for response for next message to be sent before allowing subsequent
576      *            message
577      */
578     private void awaitResponse(boolean waitResponse) {
579         CompletableFuture<Boolean> waiting = awaitResponse;
580         if (waiting != null) {
581             try {
582                 logger.trace("Waiting for previous response before sending");
583                 waiting.get(RESPONSE_TIMEOUT_MS, TimeUnit.MILLISECONDS);
584             } catch (InterruptedException | ExecutionException | TimeoutException ignore) {
585                 logger.trace("Wait for previous response timed out");
586             }
587         }
588         awaitResponse = waitResponse ? new CompletableFuture<>() : null;
589     }
590
591     /**
592      * This method is called when all responses on a request have been received.
593      */
594     private void stopAwaitResponse() {
595         CompletableFuture<Boolean> future = awaitResponse;
596         if (future != null) {
597             future.complete(true);
598         }
599         awaitResponse = null;
600     }
601
602     private void sendAck(DatagramSocket socket) throws IOException {
603         logger.debug("Send Ack: {}", ACK_STRING);
604         socket.send(ackPacket);
605     }
606
607     private String sendAndReceive(DatagramSocket socket, String query, boolean broadcast)
608             throws UnknownHostException, IOException {
609         send(socket, query, broadcast);
610         byte[] buffer = new byte[4096];
611         DatagramPacket packet = new DatagramPacket(buffer, buffer.length);
612         return receive(socket, buffer, packet);
613     }
614
615     private void send(DatagramSocket socket, String query, boolean broadcast) throws IOException {
616         final InetAddress address = broadcast
617                 ? InetAddress.getByName(networkAddressService.getConfiguredBroadcastAddress())
618                 : addr;
619         if (address == null) {
620             if (broadcast) {
621                 restartCommunication("No broadcast address, check network configuration");
622             } else {
623                 restartCommunication("Failed sending, hub address was not set");
624             }
625             return;
626         }
627         logger.debug("Send: {}", query);
628         final byte[] queryBuffer = query.getBytes(StandardCharsets.UTF_8);
629         DatagramPacket queryPacket = new DatagramPacket(queryBuffer, queryBuffer.length, address, PORT);
630         socket.send(queryPacket);
631     }
632
633     private String receive(DatagramSocket socket, byte[] buffer, DatagramPacket packet) throws IOException {
634         socket.receive(packet);
635         String response = new String(packet.getData(), packet.getOffset(), packet.getLength());
636         logger.debug("Received: {}", response);
637         addr = packet.getAddress();
638         return response;
639     }
640
641     /**
642      * Basic method to send an {@link ElroConnectsMessage} to the K1 hub.
643      *
644      * @param elroMessage
645      * @param waitResponse true if no new messages should be allowed to be sent before receiving the full response
646      * @throws IOException
647      */
648     private synchronized void sendElroMessage(ElroConnectsMessage elroMessage, boolean waitResponse)
649             throws IOException {
650         DatagramSocket socket = this.socket;
651         if (socket != null) {
652             String message = gsonOut.toJson(elroMessage);
653             awaitResponse(waitResponse);
654             send(socket, message, false);
655         } else {
656             throw new IOException("No socket");
657         }
658     }
659
660     /**
661      * Send device control command. The device command string various by device type. The calling method is responsible
662      * for creating the appropriate command string.
663      *
664      * @param deviceId
665      * @param deviceCommand ELRO Connects device command string
666      * @throws IOException
667      */
668     public void deviceControl(int deviceId, String deviceCommand) throws IOException {
669         String connectorId = this.connectorId;
670         String ctrlKey = this.ctrlKey;
671         logger.debug("Device control {}, status {}", deviceId, deviceCommand);
672         ElroConnectsMessage elroMessage = new ElroConnectsMessage(msgIdIncrement(), connectorId, ctrlKey,
673                 ELRO_DEVICE_CONTROL).withDeviceId(ElroConnectsUtil.encode(deviceId)).withDeviceStatus(deviceCommand);
674         sendElroMessage(elroMessage, false);
675     }
676
677     /**
678      * Send request to receive all device names.
679      *
680      * @throws IOException
681      */
682     private void getDeviceNames() throws IOException {
683         String connectorId = this.connectorId;
684         String ctrlKey = this.ctrlKey;
685         logger.debug("Get device names");
686         ElroConnectsMessage elroMessage = new ElroConnectsMessage(msgIdIncrement(), connectorId, ctrlKey,
687                 ELRO_GET_DEVICE_NAME).withDeviceId(0);
688         sendElroMessage(elroMessage, true);
689     }
690
691     /**
692      * Send request to receive all device statuses.
693      *
694      * @throws IOException
695      */
696     private void getDeviceStatuses() throws IOException {
697         String connectorId = this.connectorId;
698         String ctrlKey = this.ctrlKey;
699         logger.debug("Get all equipment status");
700         ElroConnectsMessage elroMessage = new ElroConnectsMessage(msgIdIncrement(), connectorId, ctrlKey,
701                 ELRO_GET_DEVICE_STATUSES);
702         sendElroMessage(elroMessage, true);
703     }
704
705     /**
706      * Send request to sync all devices statuses.
707      *
708      * @throws IOException
709      */
710     private void syncDevices() throws IOException {
711         String connectorId = this.connectorId;
712         String ctrlKey = this.ctrlKey;
713         logger.debug("Sync device status");
714         ElroConnectsMessage elroMessage = new ElroConnectsMessage(msgIdIncrement(), connectorId, ctrlKey,
715                 ELRO_SYNC_DEVICES).withDeviceStatus(SYNC_COMMAND);
716         sendElroMessage(elroMessage, true);
717     }
718
719     /**
720      * Send request to get the currently selected scene.
721      *
722      * @throws IOException
723      */
724     private void getCurrentScene() throws IOException {
725         String connectorId = this.connectorId;
726         String ctrlKey = this.ctrlKey;
727         logger.debug("Get current scene");
728         ElroConnectsMessage elroMessage = new ElroConnectsMessage(msgIdIncrement(), connectorId, ctrlKey,
729                 ELRO_GET_SCENE);
730         sendElroMessage(elroMessage, true);
731     }
732
733     /**
734      * Send message to set the current scene.
735      *
736      * @throws IOException
737      */
738     private void selectScene(int scene) throws IOException {
739         String connectorId = this.connectorId;
740         String ctrlKey = this.ctrlKey;
741         logger.debug("Select scene {}", scene);
742         ElroConnectsMessage elroMessage = new ElroConnectsMessage(msgIdIncrement(), connectorId, ctrlKey,
743                 ELRO_SELECT_SCENE).withSceneType(ElroConnectsUtil.encode(scene));
744         sendElroMessage(elroMessage, false);
745     }
746
747     /**
748      * Send request to sync all scenes.
749      *
750      * @throws IOException
751      */
752     private void syncScenes() throws IOException {
753         String connectorId = this.connectorId;
754         String ctrlKey = this.ctrlKey;
755         logger.debug("Sync scenes");
756         ElroConnectsMessage elroMessage = new ElroConnectsMessage(msgIdIncrement(), connectorId, ctrlKey,
757                 ELRO_SYNC_SCENES).withSceneGroup(ElroConnectsUtil.encode(0)).withSceneContent(SYNC_COMMAND)
758                         .withAnswerContent(SYNC_COMMAND);
759         sendElroMessage(elroMessage, true);
760     }
761
762     @Override
763     public void handleCommand(ChannelUID channelUID, Command command) {
764         logger.debug("Channel {}, command {}, type {}", channelUID, command, command.getClass());
765         if (SCENE.equals(channelUID.getId())) {
766             if (command instanceof RefreshType) {
767                 updateState(SCENE, new StringType(String.valueOf(currentScene)));
768             } else if (command instanceof DecimalType) {
769                 try {
770                     selectScene(((DecimalType) command).intValue());
771                 } catch (IOException e) {
772                     restartCommunication("Error in communication while setting scene: " + e.getMessage());
773                     return;
774                 }
775             }
776         }
777     }
778
779     /**
780      * We do not get scene delete messages, therefore call this method before requesting list of scenes to clear list of
781      * scenes.
782      */
783     private void resetScenes() {
784         scenes.clear();
785         scenes.putAll(DEFAULT_SCENES);
786
787         updateSceneOptions();
788     }
789
790     /**
791      * Update state option list for scene selection channel.
792      */
793     private void updateSceneOptions() {
794         // update the command scene command options
795         List<StateOption> stateOptionList = new ArrayList<>();
796         scenes.forEach((id, scene) -> {
797             StateOption option = new StateOption(Integer.toString(id), scene);
798             stateOptionList.add(option);
799         });
800         logger.trace("Scenes: {}", stateOptionList);
801
802         Channel channel = thing.getChannel(SCENE);
803         if (channel != null) {
804             ChannelUID channelUID = channel.getUID();
805             StateDescription stateDescription = StateDescriptionFragmentBuilder.create().withReadOnly(false)
806                     .withOptions(stateOptionList).build().toStateDescription();
807             stateDescriptionProvider.setDescription(channelUID, stateDescription);
808         }
809     }
810
811     /**
812      * Messages need to be sent with consecutive id's. Increment the msgId field and rotate at max unsigned short.
813      *
814      * @return new message id
815      */
816     private int msgIdIncrement() {
817         return Short.toUnsignedInt(msgId++);
818     }
819
820     /**
821      * Set the {@link ElroConnectsDeviceHandler} for the device with deviceId, should be called from the thing handler
822      * when initializing the thing.
823      *
824      * @param deviceId
825      * @param handler
826      */
827     public void setDeviceHandler(int deviceId, ElroConnectsDeviceHandler handler) {
828         deviceHandlers.put(deviceId, handler);
829     }
830
831     /**
832      * Unset the {@link ElroConnectsDeviceHandler} for the device with deviceId, should be called from the thing handler
833      * when disposing the thing.
834      *
835      * @param deviceId
836      * @param handler
837      */
838     public void unsetDeviceHandler(int deviceId, ElroConnectsDeviceHandler handler) {
839         deviceHandlers.remove(deviceId, handler);
840     }
841
842     public @Nullable ElroConnectsDeviceHandler getDeviceHandler(int deviceId) {
843         return deviceHandlers.get(deviceId);
844     }
845
846     public @Nullable ElroConnectsDevice getDevice(int deviceId) {
847         return devices.get(deviceId);
848     }
849
850     /**
851      * Get full list of devices connected to the K1 hub. This can be used by the {@link ElroConnectsDiscoveryService} to
852      * scan for devices connected to the K1 hub.
853      *
854      * @return devices
855      */
856     public Map<Integer, ElroConnectsDevice> getDevices() {
857         return devices;
858     }
859
860     @Override
861     public Collection<Class<? extends ThingHandlerService>> getServices() {
862         return Collections.singleton(ElroConnectsDiscoveryService.class);
863     }
864 }