2 * Copyright (c) 2010-2022 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.elroconnects.internal.handler;
15 import static org.openhab.binding.elroconnects.internal.ElroConnectsBindingConstants.*;
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;
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 import java.util.stream.Collectors;
39 import org.eclipse.jdt.annotation.NonNullByDefault;
40 import org.eclipse.jdt.annotation.Nullable;
41 import org.openhab.binding.elroconnects.internal.ElroConnectsBindingConstants.ElroDeviceType;
42 import org.openhab.binding.elroconnects.internal.ElroConnectsDynamicStateDescriptionProvider;
43 import org.openhab.binding.elroconnects.internal.ElroConnectsMessage;
44 import org.openhab.binding.elroconnects.internal.devices.ElroConnectsDevice;
45 import org.openhab.binding.elroconnects.internal.devices.ElroConnectsDeviceCxsmAlarm;
46 import org.openhab.binding.elroconnects.internal.devices.ElroConnectsDeviceEntrySensor;
47 import org.openhab.binding.elroconnects.internal.devices.ElroConnectsDeviceGenericAlarm;
48 import org.openhab.binding.elroconnects.internal.devices.ElroConnectsDeviceMotionSensor;
49 import org.openhab.binding.elroconnects.internal.devices.ElroConnectsDevicePowerSocket;
50 import org.openhab.binding.elroconnects.internal.devices.ElroConnectsDeviceTemperatureSensor;
51 import org.openhab.binding.elroconnects.internal.discovery.ElroConnectsDiscoveryService;
52 import org.openhab.binding.elroconnects.internal.util.ElroConnectsUtil;
53 import org.openhab.core.common.NamedThreadFactory;
54 import org.openhab.core.config.core.Configuration;
55 import org.openhab.core.library.types.StringType;
56 import org.openhab.core.net.NetworkAddressService;
57 import org.openhab.core.thing.Bridge;
58 import org.openhab.core.thing.Channel;
59 import org.openhab.core.thing.ChannelUID;
60 import org.openhab.core.thing.ThingStatus;
61 import org.openhab.core.thing.ThingStatusDetail;
62 import org.openhab.core.thing.binding.BaseBridgeHandler;
63 import org.openhab.core.thing.binding.ThingHandler;
64 import org.openhab.core.thing.binding.ThingHandlerService;
65 import org.openhab.core.types.Command;
66 import org.openhab.core.types.RefreshType;
67 import org.openhab.core.types.StateDescription;
68 import org.openhab.core.types.StateDescriptionFragmentBuilder;
69 import org.openhab.core.types.StateOption;
70 import org.slf4j.Logger;
71 import org.slf4j.LoggerFactory;
73 import com.google.gson.Gson;
74 import com.google.gson.JsonSyntaxException;
77 * The {@link ElroConnectsBridgeHandler} is the bridge handler responsible to for handling all communication with the
78 * ELRO Connects K1 Hub. All individual device communication passes through the hub.
80 * @author Mark Herwege - Initial contribution
83 public class ElroConnectsBridgeHandler extends BaseBridgeHandler {
85 private final Logger logger = LoggerFactory.getLogger(ElroConnectsBridgeHandler.class);
87 private static final int PORT = 1025; // UDP port for UDP socket communication with K1 hub
88 private static final int RESPONSE_TIMEOUT_MS = 5000; // max time to wait for receiving all responses on a request
90 // Default scene names are not received from K1 hub, so kept here
91 private static final Map<Integer, String> DEFAULT_SCENES = Map.ofEntries(Map.entry(0, "Home"), Map.entry(1, "Away"),
92 Map.entry(2, "Sleep"));
93 private static final int MAX_DEFAULT_SCENE = 2;
95 // Command filter when syncing devices and scenes, other values would filter what gets received
96 private static final String SYNC_COMMAND = "0002";
98 // Regex for valid connectorId
99 private static final Pattern CONNECTOR_ID_PATTERN = Pattern.compile("^ST_([0-9a-f]){12}$");
101 // Message string for acknowledging receipt of data
102 private static final String ACK_STRING = "{\"answer\": \"APP_answer_OK\"}";
103 private static final byte[] ACK = ACK_STRING.getBytes(StandardCharsets.UTF_8);
105 // Connector expects to receive messages with an increasing id for each message
106 // Max msgId is 65536, therefore use short and convert to unsigned Integer when using it
109 private NetworkAddressService networkAddressService;
111 // Used when restarting connection, delay restart for 1s to avoid high network traffic
112 private volatile boolean restart;
113 static final int RESTART_DELAY_MS = 1000;
115 private volatile String connectorId = "";
116 // Used for getting IP address and keep connection alive messages
117 private static final String QUERY_BASE_STRING = "IOT_KEY?";
118 private volatile String queryString = QUERY_BASE_STRING + connectorId;
119 // Regex to retrieve ctrlKey from response on IP address message
120 private static final Pattern CTRL_KEY_PATTERN = Pattern.compile("KEY:([0-9a-f]*)");
122 private int refreshInterval = 60;
123 private volatile @Nullable InetAddress addr;
124 private volatile String ctrlKey = "";
126 private boolean legacyFirmware = false;
128 private volatile @Nullable DatagramSocket socket;
129 private volatile @Nullable DatagramPacket ackPacket;
131 private volatile @Nullable ScheduledFuture<?> syncFuture;
132 private volatile @Nullable CompletableFuture<Boolean> awaitResponse;
134 private ElroConnectsDynamicStateDescriptionProvider stateDescriptionProvider;
136 private final Map<Integer, String> scenes = new ConcurrentHashMap<>();
137 private final Map<Integer, ElroConnectsDevice> devices = new ConcurrentHashMap<>();
138 private final Map<Integer, ElroConnectsDeviceHandler> deviceHandlers = new ConcurrentHashMap<>();
140 private int currentScene;
142 // We only keep 2 gson adapters used to serialize and deserialize all messages sent and received
143 private final Gson gsonOut = new Gson();
144 private Gson gsonIn = new Gson();
146 private @Nullable ElroConnectsDiscoveryService discoveryService = null;
148 public ElroConnectsBridgeHandler(Bridge bridge, NetworkAddressService networkAddressService,
149 ElroConnectsDynamicStateDescriptionProvider stateDescriptionProvider) {
151 this.networkAddressService = networkAddressService;
152 this.stateDescriptionProvider = stateDescriptionProvider;
158 public void initialize() {
159 ElroConnectsBridgeConfiguration config = getConfigAs(ElroConnectsBridgeConfiguration.class);
160 connectorId = config.connectorId;
161 refreshInterval = config.refreshInterval;
162 legacyFirmware = config.legacyFirmware;
164 if (connectorId.isEmpty()) {
165 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, "@text/offline.no-device-id");
167 } else if (!CONNECTOR_ID_PATTERN.matcher(connectorId).matches()) {
168 String msg = String.format("@text/offline.invalid-device-id [ \"%s\" ]", connectorId);
169 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, msg);
173 queryString = QUERY_BASE_STRING + connectorId;
175 scheduler.submit(this::startCommunication);
179 public void dispose() {
183 private synchronized void startCommunication() {
184 ElroConnectsBridgeConfiguration config = getConfigAs(ElroConnectsBridgeConfiguration.class);
185 InetAddress addr = null;
187 // First try with configured IP address if there is one
188 String ipAddress = config.ipAddress;
189 if (!ipAddress.isEmpty()) {
191 this.addr = InetAddress.getByName(ipAddress);
192 addr = getAddr(false);
193 } catch (IOException e) {
194 logger.warn("Unknown host for {}, trying to discover address", ipAddress);
198 // Then try broadcast to detect IP address if configured IP address did not work
201 addr = getAddr(true);
202 } catch (IOException e) {
203 String msg = String.format("@text/offline.find-ip-fail [ \"%s\" ]", connectorId);
204 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, msg);
211 String msg = String.format("@text/offline.find-ip-fail [ \"%s\" ]", connectorId);
212 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, msg);
217 // Found valid IP address, update configuration with detected IP address
218 Configuration configuration = thing.getConfiguration();
219 configuration.put(CONFIG_IP_ADDRESS, addr.getHostAddress());
220 updateConfiguration(configuration);
222 String ctrlKey = this.ctrlKey;
223 if (ctrlKey.isEmpty()) {
224 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
225 "@text/offline.communication-data-error");
230 DatagramSocket socket;
232 socket = createSocket(false);
233 this.socket = socket;
234 } catch (IOException e) {
235 String msg = String.format("@text/offline.communication-error [ \"%s\" ]", e.getMessage());
236 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, msg);
241 ackPacket = new DatagramPacket(ACK, ACK.length, addr, PORT);
243 logger.debug("Connected to connector {} at {}:{}", connectorId, addr, PORT);
246 // Start ELRO Connects listener. This listener will act on all messages coming from ELRO K1 Connector.
247 (new NamedThreadFactory(THREAD_NAME_PREFIX + thing.getUID().getAsString()).newThread(this::runElroEvents))
252 // First get status, then name. The status response contains the device types needed to instantiate correct
260 updateStatus(ThingStatus.ONLINE);
262 // Enable discovery of devices
263 ElroConnectsDiscoveryService service = discoveryService;
264 if (service != null) {
265 service.startBackgroundDiscovery();
267 } catch (IOException e) {
268 String msg = String.format("@text/offline.communication-error [ \"%s\" ]", e.getMessage());
269 restartCommunication(msg);
273 scheduleSyncStatus();
277 * Get the IP address and ctrl key of the connector by broadcasting message with connectorId. This should be used
278 * when initializing the connection. the ctrlKey and addr fields are set.
280 * @param broadcast, if true find address by broadcast, otherwise simply send to configured address to retrieve key
282 * @return IP address of connector
283 * @throws IOException
285 private @Nullable InetAddress getAddr(boolean broadcast) throws IOException {
286 try (DatagramSocket socket = createSocket(true)) {
287 String response = sendAndReceive(socket, queryString, broadcast);
288 Matcher keyMatcher = CTRL_KEY_PATTERN.matcher(response);
289 ctrlKey = keyMatcher.find() ? keyMatcher.group(1) : "";
290 logger.debug("Key: {}", ctrlKey);
297 * Send keep alive message.
299 * @throws IOException
301 private void keepAlive() throws IOException {
302 DatagramSocket socket = this.socket;
303 if (socket != null) {
304 logger.trace("Keep alive");
305 // Sending query string, so the connection with the K1 hub stays alive
307 send(socket, queryString, false);
309 restartCommunication("@text/offline.no-socket");
314 * Cleanup socket when the communication with ELRO Connects connector is closed.
317 private synchronized void stopCommunication() {
318 ScheduledFuture<?> sync = syncFuture;
326 DatagramSocket socket = this.socket;
327 if (socket != null && !socket.isClosed()) {
332 logger.debug("Communication stopped");
336 * Close and restart communication with ELRO Connects system, to be called after error in communication.
338 * @param offlineMessage message for thing status
340 private synchronized void restartCommunication(String offlineMessage) {
341 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, offlineMessage);
346 logger.debug("Restart communication");
349 scheduler.schedule(this::startFromRestart, RESTART_DELAY_MS, TimeUnit.MILLISECONDS);
353 private synchronized void startFromRestart() {
355 if (ThingStatus.OFFLINE.equals(thing.getStatus())) {
356 startCommunication();
360 private DatagramSocket createSocket(boolean timeout) throws SocketException {
361 DatagramSocket socket = new DatagramSocket();
362 socket.setBroadcast(true);
364 socket.setSoTimeout(1000);
370 * Read messages received through UDP socket.
374 private void runElroEvents() {
375 DatagramSocket socket = this.socket;
377 if (socket != null) {
378 logger.debug("Listening for messages");
381 byte[] buffer = new byte[4096];
382 DatagramPacket packet = new DatagramPacket(buffer, buffer.length);
383 while (!Thread.interrupted()) {
384 String response = receive(socket, packet);
385 processMessage(socket, response);
387 } catch (IOException e) {
388 String msg = String.format("@text/offline.communication-error [ \"%s\" ]", e.getMessage());
389 restartCommunication(msg);
392 restartCommunication("@text/offline.no-socket");
397 * Schedule regular queries to sync devices and scenes.
399 private void scheduleSyncStatus() {
400 syncFuture = scheduler.scheduleWithFixedDelay(() -> {
406 } catch (IOException e) {
407 String msg = String.format("@text/offline.communication-error [ \"%s\" ]", e.getMessage());
408 restartCommunication(msg);
410 }, refreshInterval, refreshInterval, TimeUnit.SECONDS);
414 * Process response received from K1 Hub and send acknowledgement through open socket.
418 * @throws IOException
420 private void processMessage(DatagramSocket socket, String response) throws IOException {
421 if (!response.startsWith("{")) {
422 // Not a Json to interpret, just ignore
426 ElroConnectsMessage message;
429 json = response.split("\\R")[0];
430 message = gsonIn.fromJson(json, ElroConnectsMessage.class);
432 } catch (JsonSyntaxException ignore) {
433 logger.debug("Cannot decode, not a valid json: {}", json);
437 if (message == null) {
441 switch (message.getCmdId()) {
442 case ELRO_IGNORE_YES_NO:
444 case ELRO_REC_DEVICE_NAME:
445 processDeviceNameMessage(message);
447 case ELRO_REC_DEVICE_STATUS:
448 processDeviceStatusMessage(message);
451 processAlarmTriggerMessage(message);
453 case ELRO_REC_SCENE_NAME:
454 processSceneNameMessage(message);
456 case ELRO_REC_SCENE_TYPE:
457 processSceneTypeMessage(message);
460 processSceneMessage(message);
463 logger.debug("CmdId not implemented: {}", message.getCmdId());
467 private void processDeviceStatusMessage(ElroConnectsMessage message) {
468 int deviceId = message.getDeviceId();
469 String deviceStatus = message.getDeviceStatus();
470 if ("OVER".equals(deviceStatus)) {
471 // last message in series received
476 ElroConnectsDevice device = devices.get(deviceId);
477 device = (device == null) ? addDevice(message) : device;
478 if (device == null) {
479 // device type not recognized, could not be added
482 device.setDeviceStatus(deviceStatus);
484 device.updateState();
487 private void processDeviceNameMessage(ElroConnectsMessage message) {
488 String answerContent = message.getAnswerContent();
489 if ("NAME_OVER".equals(answerContent)) {
490 // last message in series received
494 if (answerContent.length() <= 4) {
495 logger.debug("Could not decode answer {}", answerContent);
499 int deviceId = Integer.parseInt(answerContent.substring(0, 4), 16);
500 String deviceName = ElroConnectsUtil.decode(answerContent.substring(4));
501 ElroConnectsDevice device = devices.get(deviceId);
502 if (device != null) {
503 device.setDeviceName(deviceName);
504 logger.debug("Device ID {} name: {}", deviceId, deviceName);
508 private void processSceneNameMessage(ElroConnectsMessage message) {
509 int sceneId = message.getSceneGroup();
510 String answerContent = message.getAnswerContent();
512 if (sceneId > MAX_DEFAULT_SCENE) {
513 if (answerContent.length() < 44) {
514 logger.debug("Could not decode answer {}", answerContent);
517 sceneName = ElroConnectsUtil.decode(answerContent.substring(6, 38));
518 scenes.put(sceneId, sceneName);
519 logger.debug("Scene ID {} name: {}", sceneId, sceneName);
523 private void processSceneTypeMessage(ElroConnectsMessage message) {
524 String sceneContent = message.getSceneContent();
525 if ("OVER".equals(sceneContent)) {
526 // last message in series received
529 updateSceneOptions();
533 private void processSceneMessage(ElroConnectsMessage message) {
534 int sceneId = message.getSceneGroup();
536 currentScene = sceneId;
538 updateState(SCENE, new StringType(String.valueOf(currentScene)));
541 private void processAlarmTriggerMessage(ElroConnectsMessage message) {
542 String answerContent = message.getAnswerContent();
543 if (answerContent.length() < 10) {
544 logger.debug("Could not decode answer {}", answerContent);
548 int deviceId = Integer.parseInt(answerContent.substring(6, 10), 16);
550 ElroConnectsDeviceHandler handler = deviceHandlers.get(deviceId);
551 if (handler != null) {
552 handler.triggerAlarm();
554 // Also trigger an alarm on the bridge, so the alarm also comes through when no thing for the device is
556 triggerChannel(ALARM, Integer.toString(deviceId));
557 logger.debug("Device ID {} alarm", deviceId);
559 if (answerContent.length() < 22) {
560 logger.debug("Could not get device status from alarm message for device {}", deviceId);
563 String deviceStatus = answerContent.substring(14, 22);
564 ElroConnectsDevice device = devices.get(deviceId);
565 if (device != null) {
566 device.setDeviceStatus(deviceStatus);
567 device.updateState();
571 private @Nullable ElroConnectsDevice addDevice(ElroConnectsMessage message) {
572 int deviceId = message.getDeviceId();
573 String deviceType = message.getDeviceName();
574 ElroDeviceType type = TYPE_MAP.getOrDefault(deviceType, ElroDeviceType.DEFAULT);
576 ElroConnectsDevice device;
582 device = new ElroConnectsDeviceGenericAlarm(deviceId, this);
585 device = new ElroConnectsDeviceCxsmAlarm(deviceId, this);
588 device = new ElroConnectsDevicePowerSocket(deviceId, this);
591 device = new ElroConnectsDeviceEntrySensor(deviceId, this);
594 device = new ElroConnectsDeviceMotionSensor(deviceId, this);
597 device = new ElroConnectsDeviceTemperatureSensor(deviceId, this);
600 logger.debug("Device type {} not supported", deviceType);
603 device.setDeviceType(deviceType);
604 devices.put(deviceId, device);
609 * Just before sending message, this method should be called to make sure we wait for all responses that are still
610 * expected to be received. The last response will be indicated by a token in the last response message.
612 * @param waitResponse true if we want to wait for response for next message to be sent before allowing subsequent
615 private void awaitResponse(boolean waitResponse) {
616 CompletableFuture<Boolean> waiting = awaitResponse;
617 if (waiting != null) {
619 logger.trace("Waiting for previous response before sending");
620 waiting.get(RESPONSE_TIMEOUT_MS, TimeUnit.MILLISECONDS);
621 } catch (InterruptedException | ExecutionException | TimeoutException ignore) {
622 logger.trace("Wait for previous response timed out");
625 awaitResponse = waitResponse ? new CompletableFuture<>() : null;
629 * This method is called when all responses on a request have been received.
631 private void stopAwaitResponse() {
632 CompletableFuture<Boolean> future = awaitResponse;
633 if (future != null) {
634 future.complete(true);
636 awaitResponse = null;
639 private void sendAck(DatagramSocket socket) throws IOException {
640 logger.debug("Send Ack: {}", ACK_STRING);
641 socket.send(ackPacket);
644 private String sendAndReceive(DatagramSocket socket, String query, boolean broadcast)
645 throws UnknownHostException, IOException {
646 send(socket, query, broadcast);
647 byte[] buffer = new byte[4096];
648 DatagramPacket packet = new DatagramPacket(buffer, buffer.length);
649 return receive(socket, packet);
652 private void send(DatagramSocket socket, String query, boolean broadcast) throws IOException {
653 final InetAddress address = broadcast
654 ? InetAddress.getByName(networkAddressService.getConfiguredBroadcastAddress())
656 if (address == null) {
658 restartCommunication("@text/offline.no-broadcast-address");
660 restartCommunication("@text/offline.no-hub-address");
664 logger.debug("Send: {}", query);
665 final byte[] queryBuffer = query.getBytes(StandardCharsets.UTF_8);
666 DatagramPacket queryPacket = new DatagramPacket(queryBuffer, queryBuffer.length, address, PORT);
667 socket.send(queryPacket);
670 private String receive(DatagramSocket socket, DatagramPacket packet) throws IOException {
671 socket.receive(packet);
672 String response = new String(packet.getData(), packet.getOffset(), packet.getLength(), StandardCharsets.UTF_8);
673 logger.debug("Received: {}", response);
674 addr = packet.getAddress();
679 * Basic method to send an {@link ElroConnectsMessage} to the K1 hub.
682 * @param waitResponse true if no new messages should be allowed to be sent before receiving the full response
683 * @throws IOException
685 private synchronized void sendElroMessage(ElroConnectsMessage elroMessage, boolean waitResponse)
687 DatagramSocket socket = this.socket;
688 if (socket != null) {
689 String message = gsonOut.toJson(elroMessage);
690 awaitResponse(waitResponse);
691 send(socket, message, false);
693 throw new IOException("No socket");
698 * Send device control command. The device command string various by device type. The calling method is responsible
699 * for creating the appropriate command string.
702 * @param deviceCommand ELRO Connects device command string
703 * @throws IOException
705 public void deviceControl(int deviceId, String deviceCommand) throws IOException {
706 String connectorId = this.connectorId;
707 String ctrlKey = this.ctrlKey;
708 logger.debug("Device control {}, status {}", deviceId, deviceCommand);
709 ElroConnectsMessage elroMessage = new ElroConnectsMessage(msgIdIncrement(), connectorId, ctrlKey,
710 ELRO_DEVICE_CONTROL, legacyFirmware).withDeviceId(deviceId).withDeviceStatus(deviceCommand);
711 sendElroMessage(elroMessage, false);
714 public void renameDevice(int deviceId, String deviceName) throws IOException {
715 String connectorId = this.connectorId;
716 String ctrlKey = this.ctrlKey;
717 String encodedName = ElroConnectsUtil.encode(deviceName, 15);
718 encodedName = encodedName + ElroConnectsUtil.crc16(encodedName);
719 logger.debug("Rename device {} to {}", deviceId, deviceName);
720 ElroConnectsMessage elroMessage = new ElroConnectsMessage(msgIdIncrement(), connectorId, ctrlKey,
721 ELRO_DEVICE_RENAME, legacyFirmware).withDeviceId(deviceId).withDeviceName(encodedName);
722 sendElroMessage(elroMessage, false);
725 private void joinDevice() throws IOException {
726 String connectorId = this.connectorId;
727 String ctrlKey = this.ctrlKey;
728 logger.debug("Put hub in join device mode");
729 ElroConnectsMessage elroMessage = new ElroConnectsMessage(msgIdIncrement(), connectorId, ctrlKey,
731 sendElroMessage(elroMessage, false);
734 private void cancelJoinDevice() throws IOException {
735 String connectorId = this.connectorId;
736 String ctrlKey = this.ctrlKey;
737 logger.debug("Cancel hub in join device mode");
738 ElroConnectsMessage elroMessage = new ElroConnectsMessage(msgIdIncrement(), connectorId, ctrlKey,
739 ELRO_DEVICE_CANCEL_JOIN);
740 sendElroMessage(elroMessage, false);
743 private void removeDevice(int deviceId) throws IOException {
744 if (devices.remove(deviceId) == null) {
745 logger.debug("Device {} not known, cannot remove", deviceId);
748 ThingHandler handler = getDeviceHandler(deviceId);
749 if (handler != null) {
752 String connectorId = this.connectorId;
753 String ctrlKey = this.ctrlKey;
754 logger.debug("Remove device {} from hub", deviceId);
755 ElroConnectsMessage elroMessage = new ElroConnectsMessage(msgIdIncrement(), connectorId, ctrlKey,
756 ELRO_DEVICE_REMOVE, legacyFirmware).withDeviceId(deviceId);
757 sendElroMessage(elroMessage, false);
760 private void replaceDevice(int deviceId) throws IOException {
761 if (getDevice(deviceId) == null) {
762 logger.debug("Device {} not known, cannot replace", deviceId);
765 String connectorId = this.connectorId;
766 String ctrlKey = this.ctrlKey;
767 logger.debug("Replace device {} in hub", deviceId);
768 ElroConnectsMessage elroMessage = new ElroConnectsMessage(msgIdIncrement(), connectorId, ctrlKey,
769 ELRO_DEVICE_REPLACE, legacyFirmware).withDeviceId(deviceId);
770 sendElroMessage(elroMessage, false);
774 * Send request to receive all device names.
776 * @throws IOException
778 private void getDeviceNames() throws IOException {
779 String connectorId = this.connectorId;
780 String ctrlKey = this.ctrlKey;
781 logger.debug("Get device names");
782 ElroConnectsMessage elroMessage = new ElroConnectsMessage(msgIdIncrement(), connectorId, ctrlKey,
783 ELRO_GET_DEVICE_NAME).withDeviceId(0);
784 sendElroMessage(elroMessage, true);
788 * Send request to receive all device statuses.
790 * @throws IOException
792 private void getDeviceStatuses() throws IOException {
793 String connectorId = this.connectorId;
794 String ctrlKey = this.ctrlKey;
795 logger.debug("Get all equipment status");
796 ElroConnectsMessage elroMessage = new ElroConnectsMessage(msgIdIncrement(), connectorId, ctrlKey,
797 ELRO_GET_DEVICE_STATUSES);
798 sendElroMessage(elroMessage, true);
802 * Send request to sync all devices statuses.
804 * @throws IOException
806 private void syncDevices() throws IOException {
807 String connectorId = this.connectorId;
808 String ctrlKey = this.ctrlKey;
809 logger.debug("Sync device status");
810 ElroConnectsMessage elroMessage = new ElroConnectsMessage(msgIdIncrement(), connectorId, ctrlKey,
811 ELRO_SYNC_DEVICES).withDeviceStatus(SYNC_COMMAND);
812 sendElroMessage(elroMessage, true);
816 * Send request to get the currently selected scene.
818 * @throws IOException
820 private void getCurrentScene() throws IOException {
821 String connectorId = this.connectorId;
822 String ctrlKey = this.ctrlKey;
823 logger.debug("Get current scene");
824 ElroConnectsMessage elroMessage = new ElroConnectsMessage(msgIdIncrement(), connectorId, ctrlKey,
826 sendElroMessage(elroMessage, true);
830 * Send message to set the current scene.
832 * @throws IOException
834 private void selectScene(int scene) throws IOException {
835 String connectorId = this.connectorId;
836 String ctrlKey = this.ctrlKey;
837 logger.debug("Select scene {}", scene);
838 ElroConnectsMessage elroMessage = new ElroConnectsMessage(msgIdIncrement(), connectorId, ctrlKey,
839 ELRO_SELECT_SCENE, legacyFirmware).withSceneType(scene);
840 sendElroMessage(elroMessage, false);
844 * Send request to sync all scenes.
846 * @throws IOException
848 private void syncScenes() throws IOException {
849 String connectorId = this.connectorId;
850 String ctrlKey = this.ctrlKey;
851 logger.debug("Sync scenes");
852 ElroConnectsMessage elroMessage = new ElroConnectsMessage(msgIdIncrement(), connectorId, ctrlKey,
853 ELRO_SYNC_SCENES, legacyFirmware).withSceneGroup(0).withSceneContent(SYNC_COMMAND)
854 .withAnswerContent(SYNC_COMMAND);
855 sendElroMessage(elroMessage, true);
859 public void handleCommand(ChannelUID channelUID, Command command) {
860 logger.debug("Channel {}, command {}, type {}", channelUID, command, command.getClass());
862 if (SCENE.equals(channelUID.getId())) {
863 if (command instanceof RefreshType) {
864 updateState(SCENE, new StringType(String.valueOf(currentScene)));
865 } else if (command instanceof StringType) {
867 selectScene(Integer.valueOf(((StringType) command).toString()));
868 } catch (NumberFormatException nfe) {
869 logger.debug("Cannot interpret scene command {}", command);
873 } catch (IOException e) {
874 String msg = String.format("@text/offline.communication-error [ \"%s\" ]", e.getMessage());
875 restartCommunication(msg);
880 * We do not get scene delete messages, therefore call this method before requesting list of scenes to clear list of
883 private void resetScenes() {
885 scenes.putAll(DEFAULT_SCENES);
887 updateSceneOptions();
891 * Update state option list for scene selection channel.
893 private void updateSceneOptions() {
894 // update the command scene command options
895 List<StateOption> stateOptionList = new ArrayList<>();
896 scenes.forEach((id, scene) -> {
897 StateOption option = new StateOption(Integer.toString(id), scene);
898 stateOptionList.add(option);
900 logger.trace("Scenes: {}", stateOptionList);
902 Channel channel = thing.getChannel(SCENE);
903 if (channel != null) {
904 ChannelUID channelUID = channel.getUID();
905 StateDescription stateDescription = StateDescriptionFragmentBuilder.create().withReadOnly(false)
906 .withOptions(stateOptionList).build().toStateDescription();
907 stateDescriptionProvider.setDescription(channelUID, stateDescription);
912 * Messages need to be sent with consecutive id's. Increment the msgId field and rotate at max unsigned short.
914 * @return new message id
916 private int msgIdIncrement() {
917 return Short.toUnsignedInt(msgId++);
921 * Set the {@link ElroConnectsDeviceHandler} for the device with deviceId, should be called from the thing handler
922 * when initializing the thing.
927 public void setDeviceHandler(int deviceId, ElroConnectsDeviceHandler handler) {
928 deviceHandlers.put(deviceId, handler);
932 * Unset the {@link ElroConnectsDeviceHandler} for the device with deviceId, should be called from the thing handler
933 * when disposing the thing.
938 public void unsetDeviceHandler(int deviceId, ElroConnectsDeviceHandler handler) {
939 deviceHandlers.remove(deviceId, handler);
942 public @Nullable ElroConnectsDeviceHandler getDeviceHandler(int deviceId) {
943 return deviceHandlers.get(deviceId);
946 public String getConnectorId() {
950 public @Nullable ElroConnectsDevice getDevice(int deviceId) {
951 return devices.get(deviceId);
955 * Get full list of devices connected to the K1 hub. This can be used by the {@link ElroConnectsDiscoveryService} to
956 * scan for devices connected to the K1 hub.
960 public Map<Integer, ElroConnectsDevice> getDevices() {
965 public Collection<Class<? extends ThingHandlerService>> getServices() {
966 return Collections.singleton(ElroConnectsDiscoveryService.class);
969 public Map<Integer, String> listDevicesFromConsole() {
970 return devices.entrySet().stream()
971 .collect(Collectors.toMap(e -> e.getKey(), e -> e.getValue().getDeviceName()));
974 public void refreshFromConsole() {
979 } catch (IOException e) {
980 String msg = String.format("@text/offline.communication-error [ \"%s\" ]", e.getMessage());
981 restartCommunication(msg);
985 public void joinDeviceFromConsole() {
988 } catch (IOException e) {
989 String msg = String.format("@text/offline.communication-error [ \"%s\" ]", e.getMessage());
990 restartCommunication(msg);
994 public void cancelJoinDeviceFromConsole() {
997 } catch (IOException e) {
998 String msg = String.format("@text/offline.communication-error [ \"%s\" ]", e.getMessage());
999 restartCommunication(msg);
1003 public boolean renameDeviceFromConsole(int deviceId, String deviceName) {
1004 if (getDevice(deviceId) == null) {
1008 renameDevice(deviceId, deviceName);
1009 } catch (IOException e) {
1010 String msg = String.format("@text/offline.communication-error [ \"%s\" ]", e.getMessage());
1011 restartCommunication(msg);
1016 public boolean removeDeviceFromConsole(int deviceId) {
1017 if (getDevice(deviceId) == null) {
1021 removeDevice(deviceId);
1022 } catch (IOException e) {
1023 String msg = String.format("@text/offline.communication-error [ \"%s\" ]", e.getMessage());
1024 restartCommunication(msg);
1029 public boolean replaceDeviceFromConsole(int deviceId) {
1030 if (getDevice(deviceId) == null) {
1034 replaceDevice(deviceId);
1035 } catch (IOException e) {
1036 String msg = String.format("@text/offline.communication-error [ \"%s\" ]", e.getMessage());
1037 restartCommunication(msg);
1042 public void setDiscoveryService(ElroConnectsDiscoveryService discoveryService) {
1043 this.discoveryService = discoveryService;