2 * Copyright (c) 2010-2023 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.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-connector-id");
167 } else if (!CONNECTOR_ID_PATTERN.matcher(connectorId).matches()) {
168 String msg = String.format("@text/offline.invalid-connector-id [ \"%s\" ]", connectorId);
169 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, msg);
173 queryString = QUERY_BASE_STRING + connectorId;
175 updateStatus(ThingStatus.UNKNOWN);
177 scheduler.submit(this::startCommunication);
181 public void dispose() {
185 private synchronized void startCommunication() {
186 ElroConnectsBridgeConfiguration config = getConfigAs(ElroConnectsBridgeConfiguration.class);
187 InetAddress addr = null;
189 // First try with configured IP address if there is one
190 String ipAddress = config.ipAddress;
191 if (!ipAddress.isEmpty()) {
193 this.addr = InetAddress.getByName(ipAddress);
194 addr = getAddr(false);
195 } catch (IOException e) {
196 logger.warn("Unknown host for {}, trying to discover address", ipAddress);
200 // Then try broadcast to detect IP address if configured IP address did not work
203 addr = getAddr(true);
204 } catch (IOException e) {
205 String msg = String.format("@text/offline.find-ip-fail [ \"%s\" ]", connectorId);
206 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, msg);
213 String msg = String.format("@text/offline.find-ip-fail [ \"%s\" ]", connectorId);
214 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, msg);
219 // Found valid IP address, update configuration with detected IP address
220 Configuration configuration = thing.getConfiguration();
221 configuration.put(CONFIG_IP_ADDRESS, addr.getHostAddress());
222 updateConfiguration(configuration);
224 String ctrlKey = this.ctrlKey;
225 if (ctrlKey.isEmpty()) {
226 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
227 "@text/offline.communication-data-error");
232 DatagramSocket socket;
234 socket = createSocket(false);
235 this.socket = socket;
236 } catch (IOException e) {
237 String msg = String.format("@text/offline.communication-error [ \"%s\" ]", e.getMessage());
238 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, msg);
243 ackPacket = new DatagramPacket(ACK, ACK.length, addr, PORT);
245 logger.debug("Connected to connector {} at {}:{}", connectorId, addr, PORT);
248 // Start ELRO Connects listener. This listener will act on all messages coming from ELRO K1 Connector.
249 (new NamedThreadFactory(THREAD_NAME_PREFIX + thing.getUID().getAsString()).newThread(this::runElroEvents))
254 // First get status, then name. The status response contains the device types needed to instantiate correct
262 updateStatus(ThingStatus.ONLINE);
264 // Enable discovery of devices
265 ElroConnectsDiscoveryService service = discoveryService;
266 if (service != null) {
267 service.startBackgroundDiscovery();
269 } catch (IOException e) {
270 String msg = String.format("@text/offline.communication-error [ \"%s\" ]", e.getMessage());
271 restartCommunication(msg);
275 scheduleSyncStatus();
279 * Get the IP address and ctrl key of the connector by broadcasting message with connectorId. This should be used
280 * when initializing the connection. the ctrlKey and addr fields are set.
282 * @param broadcast, if true find address by broadcast, otherwise simply send to configured address to retrieve key
284 * @return IP address of connector
285 * @throws IOException
287 private @Nullable InetAddress getAddr(boolean broadcast) throws IOException {
288 try (DatagramSocket socket = createSocket(true)) {
289 String response = sendAndReceive(socket, queryString, broadcast);
290 Matcher keyMatcher = CTRL_KEY_PATTERN.matcher(response);
291 ctrlKey = keyMatcher.find() ? keyMatcher.group(1) : "";
292 logger.debug("Key: {}", ctrlKey);
299 * Send keep alive message.
301 * @throws IOException
303 private void keepAlive() throws IOException {
304 DatagramSocket socket = this.socket;
305 if (socket != null) {
306 logger.trace("Keep alive");
307 // Sending query string, so the connection with the K1 hub stays alive
309 send(socket, queryString, false);
311 restartCommunication("@text/offline.no-socket");
316 * Cleanup socket when the communication with ELRO Connects connector is closed.
319 private synchronized void stopCommunication() {
320 ScheduledFuture<?> sync = syncFuture;
328 DatagramSocket socket = this.socket;
329 if (socket != null && !socket.isClosed()) {
334 logger.debug("Communication stopped");
338 * Close and restart communication with ELRO Connects system, to be called after error in communication.
340 * @param offlineMessage message for thing status
342 private synchronized void restartCommunication(String offlineMessage) {
343 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, offlineMessage);
348 logger.debug("Restart communication");
351 scheduler.schedule(this::startFromRestart, RESTART_DELAY_MS, TimeUnit.MILLISECONDS);
355 private synchronized void startFromRestart() {
357 if (ThingStatus.OFFLINE.equals(thing.getStatus())) {
358 startCommunication();
362 private DatagramSocket createSocket(boolean timeout) throws SocketException {
363 DatagramSocket socket = new DatagramSocket();
364 socket.setBroadcast(true);
366 socket.setSoTimeout(1000);
372 * Read messages received through UDP socket.
376 private void runElroEvents() {
377 DatagramSocket socket = this.socket;
379 if (socket != null) {
380 logger.debug("Listening for messages");
383 byte[] buffer = new byte[4096];
384 DatagramPacket packet = new DatagramPacket(buffer, buffer.length);
385 while (!Thread.interrupted()) {
386 String response = receive(socket, packet);
387 processMessage(socket, response);
389 } catch (IOException e) {
390 String msg = String.format("@text/offline.communication-error [ \"%s\" ]", e.getMessage());
391 restartCommunication(msg);
394 restartCommunication("@text/offline.no-socket");
399 * Schedule regular queries to sync devices and scenes.
401 private void scheduleSyncStatus() {
402 syncFuture = scheduler.scheduleWithFixedDelay(() -> {
408 } catch (IOException e) {
409 String msg = String.format("@text/offline.communication-error [ \"%s\" ]", e.getMessage());
410 restartCommunication(msg);
412 }, refreshInterval, refreshInterval, TimeUnit.SECONDS);
416 * Process response received from K1 Hub and send acknowledgement through open socket.
420 * @throws IOException
422 private void processMessage(DatagramSocket socket, String response) throws IOException {
423 if (!response.startsWith("{")) {
424 // Not a Json to interpret, just ignore
428 ElroConnectsMessage message;
431 json = response.split("\\R")[0];
432 message = gsonIn.fromJson(json, ElroConnectsMessage.class);
434 } catch (JsonSyntaxException ignore) {
435 logger.debug("Cannot decode, not a valid json: {}", json);
439 if (message == null) {
443 switch (message.getCmdId()) {
444 case ELRO_IGNORE_YES_NO:
446 case ELRO_REC_DEVICE_NAME:
447 processDeviceNameMessage(message);
449 case ELRO_REC_DEVICE_STATUS:
450 processDeviceStatusMessage(message);
453 processAlarmTriggerMessage(message);
455 case ELRO_REC_SCENE_NAME:
456 processSceneNameMessage(message);
458 case ELRO_REC_SCENE_TYPE:
459 processSceneTypeMessage(message);
462 processSceneMessage(message);
465 logger.debug("CmdId not implemented: {}", message.getCmdId());
469 private void processDeviceStatusMessage(ElroConnectsMessage message) {
470 int deviceId = message.getDeviceId();
471 String deviceStatus = message.getDeviceStatus();
472 if ("OVER".equals(deviceStatus)) {
473 // last message in series received
478 ElroConnectsDevice device = devices.get(deviceId);
479 device = (device == null) ? addDevice(message) : device;
480 if (device == null) {
481 // device type not recognized, could not be added
484 device.setDeviceStatus(deviceStatus);
486 device.updateState();
489 private void processDeviceNameMessage(ElroConnectsMessage message) {
490 String answerContent = message.getAnswerContent();
491 if ("NAME_OVER".equals(answerContent)) {
492 // last message in series received
496 if (answerContent.length() <= 4) {
497 logger.debug("Could not decode answer {}", answerContent);
501 int deviceId = Integer.parseInt(answerContent.substring(0, 4), 16);
502 String deviceName = ElroConnectsUtil.decode(answerContent.substring(4));
503 ElroConnectsDevice device = devices.get(deviceId);
504 if (device != null) {
505 device.setDeviceName(deviceName);
506 logger.debug("Device ID {} name: {}", deviceId, deviceName);
510 private void processSceneNameMessage(ElroConnectsMessage message) {
511 int sceneId = message.getSceneGroup();
512 String answerContent = message.getAnswerContent();
514 if (sceneId > MAX_DEFAULT_SCENE) {
515 if (answerContent.length() < 44) {
516 logger.debug("Could not decode answer {}", answerContent);
519 sceneName = ElroConnectsUtil.decode(answerContent.substring(6, 38));
520 scenes.put(sceneId, sceneName);
521 logger.debug("Scene ID {} name: {}", sceneId, sceneName);
525 private void processSceneTypeMessage(ElroConnectsMessage message) {
526 String sceneContent = message.getSceneContent();
527 if ("OVER".equals(sceneContent)) {
528 // last message in series received
531 updateSceneOptions();
535 private void processSceneMessage(ElroConnectsMessage message) {
536 int sceneId = message.getSceneGroup();
538 currentScene = sceneId;
540 updateState(SCENE, new StringType(String.valueOf(currentScene)));
543 private void processAlarmTriggerMessage(ElroConnectsMessage message) {
544 String answerContent = message.getAnswerContent();
545 if (answerContent.length() < 10) {
546 logger.debug("Could not decode answer {}", answerContent);
550 int deviceId = Integer.parseInt(answerContent.substring(6, 10), 16);
552 ElroConnectsDeviceHandler handler = deviceHandlers.get(deviceId);
553 if (handler != null) {
554 handler.triggerAlarm();
556 // Also trigger an alarm on the bridge, so the alarm also comes through when no thing for the device is
558 triggerChannel(ALARM, Integer.toString(deviceId));
559 logger.debug("Device ID {} alarm", deviceId);
561 if (answerContent.length() < 22) {
562 logger.debug("Could not get device status from alarm message for device {}", deviceId);
565 String deviceStatus = answerContent.substring(14, 22);
566 ElroConnectsDevice device = devices.get(deviceId);
567 if (device != null) {
568 device.setDeviceStatus(deviceStatus);
569 device.updateState();
573 private @Nullable ElroConnectsDevice addDevice(ElroConnectsMessage message) {
574 int deviceId = message.getDeviceId();
575 String deviceType = message.getDeviceName();
576 ElroDeviceType type = TYPE_MAP.getOrDefault(deviceType, ElroDeviceType.DEFAULT);
578 ElroConnectsDevice device;
584 device = new ElroConnectsDeviceGenericAlarm(deviceId, this);
587 device = new ElroConnectsDeviceCxsmAlarm(deviceId, this);
590 device = new ElroConnectsDevicePowerSocket(deviceId, this);
593 device = new ElroConnectsDeviceEntrySensor(deviceId, this);
596 device = new ElroConnectsDeviceMotionSensor(deviceId, this);
599 device = new ElroConnectsDeviceTemperatureSensor(deviceId, this);
602 logger.debug("Device type {} not supported", deviceType);
605 device.setDeviceType(deviceType);
606 devices.put(deviceId, device);
611 * Just before sending message, this method should be called to make sure we wait for all responses that are still
612 * expected to be received. The last response will be indicated by a token in the last response message.
614 * @param waitResponse true if we want to wait for response for next message to be sent before allowing subsequent
617 private void awaitResponse(boolean waitResponse) {
618 CompletableFuture<Boolean> waiting = awaitResponse;
619 if (waiting != null) {
621 logger.trace("Waiting for previous response before sending");
622 waiting.get(RESPONSE_TIMEOUT_MS, TimeUnit.MILLISECONDS);
623 } catch (InterruptedException | ExecutionException | TimeoutException ignore) {
624 logger.trace("Wait for previous response timed out");
627 awaitResponse = waitResponse ? new CompletableFuture<>() : null;
631 * This method is called when all responses on a request have been received.
633 private void stopAwaitResponse() {
634 CompletableFuture<Boolean> future = awaitResponse;
635 if (future != null) {
636 future.complete(true);
638 awaitResponse = null;
641 private void sendAck(DatagramSocket socket) throws IOException {
642 logger.debug("Send Ack: {}", ACK_STRING);
643 socket.send(ackPacket);
646 private String sendAndReceive(DatagramSocket socket, String query, boolean broadcast)
647 throws UnknownHostException, IOException {
648 send(socket, query, broadcast);
649 byte[] buffer = new byte[4096];
650 DatagramPacket packet = new DatagramPacket(buffer, buffer.length);
651 return receive(socket, packet);
654 private void send(DatagramSocket socket, String query, boolean broadcast) throws IOException {
655 final InetAddress address = broadcast
656 ? InetAddress.getByName(networkAddressService.getConfiguredBroadcastAddress())
658 if (address == null) {
660 restartCommunication("@text/offline.no-broadcast-address");
662 restartCommunication("@text/offline.no-hub-address");
666 logger.debug("Send: {}", query);
667 final byte[] queryBuffer = query.getBytes(StandardCharsets.UTF_8);
668 DatagramPacket queryPacket = new DatagramPacket(queryBuffer, queryBuffer.length, address, PORT);
669 socket.send(queryPacket);
672 private String receive(DatagramSocket socket, DatagramPacket packet) throws IOException {
673 socket.receive(packet);
674 String response = new String(packet.getData(), packet.getOffset(), packet.getLength(), StandardCharsets.UTF_8);
675 logger.debug("Received: {}", response);
676 addr = packet.getAddress();
681 * Basic method to send an {@link ElroConnectsMessage} to the K1 hub.
684 * @param waitResponse true if no new messages should be allowed to be sent before receiving the full response
685 * @throws IOException
687 private synchronized void sendElroMessage(ElroConnectsMessage elroMessage, boolean waitResponse)
689 DatagramSocket socket = this.socket;
690 if (socket != null) {
691 String message = gsonOut.toJson(elroMessage);
692 awaitResponse(waitResponse);
693 send(socket, message, false);
695 throw new IOException("No socket");
700 * Send device control command. The device command string various by device type. The calling method is responsible
701 * for creating the appropriate command string.
704 * @param deviceCommand ELRO Connects device command string
705 * @throws IOException
707 public void deviceControl(int deviceId, String deviceCommand) throws IOException {
708 String connectorId = this.connectorId;
709 String ctrlKey = this.ctrlKey;
710 logger.debug("Device control {}, status {}", deviceId, deviceCommand);
711 ElroConnectsMessage elroMessage = new ElroConnectsMessage(msgIdIncrement(), connectorId, ctrlKey,
712 ELRO_DEVICE_CONTROL, legacyFirmware).withDeviceId(deviceId).withDeviceStatus(deviceCommand);
713 sendElroMessage(elroMessage, false);
716 public void renameDevice(int deviceId, String deviceName) throws IOException {
717 String connectorId = this.connectorId;
718 String ctrlKey = this.ctrlKey;
719 String encodedName = ElroConnectsUtil.encode(deviceName, 15);
720 encodedName = encodedName + ElroConnectsUtil.crc16(encodedName);
721 logger.debug("Rename device {} to {}", deviceId, deviceName);
722 ElroConnectsMessage elroMessage = new ElroConnectsMessage(msgIdIncrement(), connectorId, ctrlKey,
723 ELRO_DEVICE_RENAME, legacyFirmware).withDeviceId(deviceId).withDeviceName(encodedName);
724 sendElroMessage(elroMessage, false);
727 private void joinDevice() throws IOException {
728 String connectorId = this.connectorId;
729 String ctrlKey = this.ctrlKey;
730 logger.debug("Put hub in join device mode");
731 ElroConnectsMessage elroMessage = new ElroConnectsMessage(msgIdIncrement(), connectorId, ctrlKey,
733 sendElroMessage(elroMessage, false);
736 private void cancelJoinDevice() throws IOException {
737 String connectorId = this.connectorId;
738 String ctrlKey = this.ctrlKey;
739 logger.debug("Cancel hub in join device mode");
740 ElroConnectsMessage elroMessage = new ElroConnectsMessage(msgIdIncrement(), connectorId, ctrlKey,
741 ELRO_DEVICE_CANCEL_JOIN);
742 sendElroMessage(elroMessage, false);
745 private void removeDevice(int deviceId) throws IOException {
746 if (devices.remove(deviceId) == null) {
747 logger.debug("Device {} not known, cannot remove", deviceId);
750 ThingHandler handler = getDeviceHandler(deviceId);
751 if (handler != null) {
754 String connectorId = this.connectorId;
755 String ctrlKey = this.ctrlKey;
756 logger.debug("Remove device {} from hub", deviceId);
757 ElroConnectsMessage elroMessage = new ElroConnectsMessage(msgIdIncrement(), connectorId, ctrlKey,
758 ELRO_DEVICE_REMOVE, legacyFirmware).withDeviceId(deviceId);
759 sendElroMessage(elroMessage, false);
762 private void replaceDevice(int deviceId) throws IOException {
763 if (getDevice(deviceId) == null) {
764 logger.debug("Device {} not known, cannot replace", deviceId);
767 String connectorId = this.connectorId;
768 String ctrlKey = this.ctrlKey;
769 logger.debug("Replace device {} in hub", deviceId);
770 ElroConnectsMessage elroMessage = new ElroConnectsMessage(msgIdIncrement(), connectorId, ctrlKey,
771 ELRO_DEVICE_REPLACE, legacyFirmware).withDeviceId(deviceId);
772 sendElroMessage(elroMessage, false);
776 * Send request to receive all device names.
778 * @throws IOException
780 private void getDeviceNames() throws IOException {
781 String connectorId = this.connectorId;
782 String ctrlKey = this.ctrlKey;
783 logger.debug("Get device names");
784 ElroConnectsMessage elroMessage = new ElroConnectsMessage(msgIdIncrement(), connectorId, ctrlKey,
785 ELRO_GET_DEVICE_NAME).withDeviceId(0);
786 sendElroMessage(elroMessage, true);
790 * Send request to receive all device statuses.
792 * @throws IOException
794 private void getDeviceStatuses() throws IOException {
795 String connectorId = this.connectorId;
796 String ctrlKey = this.ctrlKey;
797 logger.debug("Get all equipment status");
798 ElroConnectsMessage elroMessage = new ElroConnectsMessage(msgIdIncrement(), connectorId, ctrlKey,
799 ELRO_GET_DEVICE_STATUSES);
800 sendElroMessage(elroMessage, true);
804 * Send request to sync all devices statuses.
806 * @throws IOException
808 private void syncDevices() throws IOException {
809 String connectorId = this.connectorId;
810 String ctrlKey = this.ctrlKey;
811 logger.debug("Sync device status");
812 ElroConnectsMessage elroMessage = new ElroConnectsMessage(msgIdIncrement(), connectorId, ctrlKey,
813 ELRO_SYNC_DEVICES).withDeviceStatus(SYNC_COMMAND);
814 sendElroMessage(elroMessage, true);
818 * Send request to get the currently selected scene.
820 * @throws IOException
822 private void getCurrentScene() throws IOException {
823 String connectorId = this.connectorId;
824 String ctrlKey = this.ctrlKey;
825 logger.debug("Get current scene");
826 ElroConnectsMessage elroMessage = new ElroConnectsMessage(msgIdIncrement(), connectorId, ctrlKey,
828 sendElroMessage(elroMessage, true);
832 * Send message to set the current scene.
834 * @throws IOException
836 private void selectScene(int scene) throws IOException {
837 String connectorId = this.connectorId;
838 String ctrlKey = this.ctrlKey;
839 logger.debug("Select scene {}", scene);
840 ElroConnectsMessage elroMessage = new ElroConnectsMessage(msgIdIncrement(), connectorId, ctrlKey,
841 ELRO_SELECT_SCENE, legacyFirmware).withSceneType(scene);
842 sendElroMessage(elroMessage, false);
846 * Send request to sync all scenes.
848 * @throws IOException
850 private void syncScenes() throws IOException {
851 String connectorId = this.connectorId;
852 String ctrlKey = this.ctrlKey;
853 logger.debug("Sync scenes");
854 ElroConnectsMessage elroMessage = new ElroConnectsMessage(msgIdIncrement(), connectorId, ctrlKey,
855 ELRO_SYNC_SCENES, legacyFirmware).withSceneGroup(0).withSceneContent(SYNC_COMMAND)
856 .withAnswerContent(SYNC_COMMAND);
857 sendElroMessage(elroMessage, true);
861 public void handleCommand(ChannelUID channelUID, Command command) {
862 logger.debug("Channel {}, command {}, type {}", channelUID, command, command.getClass());
864 if (SCENE.equals(channelUID.getId())) {
865 if (command instanceof RefreshType) {
866 updateState(SCENE, new StringType(String.valueOf(currentScene)));
867 } else if (command instanceof StringType stringCommand) {
869 selectScene(Integer.valueOf(stringCommand.toString()));
870 } catch (NumberFormatException nfe) {
871 logger.debug("Cannot interpret scene command {}", command);
875 } catch (IOException e) {
876 String msg = String.format("@text/offline.communication-error [ \"%s\" ]", e.getMessage());
877 restartCommunication(msg);
882 * We do not get scene delete messages, therefore call this method before requesting list of scenes to clear list of
885 private void resetScenes() {
887 scenes.putAll(DEFAULT_SCENES);
889 updateSceneOptions();
893 * Update state option list for scene selection channel.
895 private void updateSceneOptions() {
896 // update the command scene command options
897 List<StateOption> stateOptionList = new ArrayList<>();
898 scenes.forEach((id, scene) -> {
899 StateOption option = new StateOption(Integer.toString(id), scene);
900 stateOptionList.add(option);
902 logger.trace("Scenes: {}", stateOptionList);
904 Channel channel = thing.getChannel(SCENE);
905 if (channel != null) {
906 ChannelUID channelUID = channel.getUID();
907 StateDescription stateDescription = StateDescriptionFragmentBuilder.create().withReadOnly(false)
908 .withOptions(stateOptionList).build().toStateDescription();
909 stateDescriptionProvider.setDescription(channelUID, stateDescription);
914 * Messages need to be sent with consecutive id's. Increment the msgId field and rotate at max unsigned short.
916 * @return new message id
918 private int msgIdIncrement() {
919 return Short.toUnsignedInt(msgId++);
923 * Set the {@link ElroConnectsDeviceHandler} for the device with deviceId, should be called from the thing handler
924 * when initializing the thing.
929 public void setDeviceHandler(int deviceId, ElroConnectsDeviceHandler handler) {
930 deviceHandlers.put(deviceId, handler);
934 * Unset the {@link ElroConnectsDeviceHandler} for the device with deviceId, should be called from the thing handler
935 * when disposing the thing.
940 public void unsetDeviceHandler(int deviceId, ElroConnectsDeviceHandler handler) {
941 deviceHandlers.remove(deviceId, handler);
944 public @Nullable ElroConnectsDeviceHandler getDeviceHandler(int deviceId) {
945 return deviceHandlers.get(deviceId);
948 public String getConnectorId() {
952 public @Nullable ElroConnectsDevice getDevice(int deviceId) {
953 return devices.get(deviceId);
957 * Get full list of devices connected to the K1 hub. This can be used by the {@link ElroConnectsDiscoveryService} to
958 * scan for devices connected to the K1 hub.
962 public Map<Integer, ElroConnectsDevice> getDevices() {
967 public Collection<Class<? extends ThingHandlerService>> getServices() {
968 return Set.of(ElroConnectsDiscoveryService.class);
971 public Map<Integer, String> listDevicesFromConsole() {
972 return devices.entrySet().stream()
973 .collect(Collectors.toMap(e -> e.getKey(), e -> e.getValue().getDeviceName()));
976 public void refreshFromConsole() {
981 } catch (IOException e) {
982 String msg = String.format("@text/offline.communication-error [ \"%s\" ]", e.getMessage());
983 restartCommunication(msg);
987 public void joinDeviceFromConsole() {
990 } catch (IOException e) {
991 String msg = String.format("@text/offline.communication-error [ \"%s\" ]", e.getMessage());
992 restartCommunication(msg);
996 public void cancelJoinDeviceFromConsole() {
999 } catch (IOException e) {
1000 String msg = String.format("@text/offline.communication-error [ \"%s\" ]", e.getMessage());
1001 restartCommunication(msg);
1005 public boolean renameDeviceFromConsole(int deviceId, String deviceName) {
1006 if (getDevice(deviceId) == null) {
1010 renameDevice(deviceId, deviceName);
1011 } catch (IOException e) {
1012 String msg = String.format("@text/offline.communication-error [ \"%s\" ]", e.getMessage());
1013 restartCommunication(msg);
1018 public boolean removeDeviceFromConsole(int deviceId) {
1019 if (getDevice(deviceId) == null) {
1023 removeDevice(deviceId);
1024 } catch (IOException e) {
1025 String msg = String.format("@text/offline.communication-error [ \"%s\" ]", e.getMessage());
1026 restartCommunication(msg);
1031 public boolean replaceDeviceFromConsole(int deviceId) {
1032 if (getDevice(deviceId) == null) {
1036 replaceDevice(deviceId);
1037 } catch (IOException e) {
1038 String msg = String.format("@text/offline.communication-error [ \"%s\" ]", e.getMessage());
1039 restartCommunication(msg);
1044 public void setDiscoveryService(ElroConnectsDiscoveryService discoveryService) {
1045 this.discoveryService = discoveryService;