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;
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;
72 import com.google.gson.Gson;
73 import com.google.gson.JsonSyntaxException;
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.
79 * @author Mark Herwege - Initial contribution
82 public class ElroConnectsBridgeHandler extends BaseBridgeHandler {
84 private final Logger logger = LoggerFactory.getLogger(ElroConnectsBridgeHandler.class);
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
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;
94 // Command filter when syncing devices and scenes, other values would filter what gets received
95 private static final String SYNC_COMMAND = "0002";
97 // Regex for valid connectorId
98 private static final Pattern CONNECTOR_ID_PATTERN = Pattern.compile("^ST_([0-9a-f]){12}$");
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);
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
108 private NetworkAddressService networkAddressService;
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;
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]*)");
121 private int refreshInterval = 60;
122 private volatile @Nullable InetAddress addr;
123 private volatile String ctrlKey = "";
125 private volatile @Nullable DatagramSocket socket;
126 private volatile @Nullable DatagramPacket ackPacket;
128 private volatile @Nullable ScheduledFuture<?> syncFuture;
129 private volatile @Nullable CompletableFuture<Boolean> awaitResponse;
131 private ElroConnectsDynamicStateDescriptionProvider stateDescriptionProvider;
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<>();
137 private int currentScene;
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();
143 public ElroConnectsBridgeHandler(Bridge bridge, NetworkAddressService networkAddressService,
144 ElroConnectsDynamicStateDescriptionProvider stateDescriptionProvider) {
146 this.networkAddressService = networkAddressService;
147 this.stateDescriptionProvider = stateDescriptionProvider;
153 public void initialize() {
154 ElroConnectsBridgeConfiguration config = getConfigAs(ElroConnectsBridgeConfiguration.class);
155 connectorId = config.connectorId;
156 refreshInterval = config.refreshInterval;
158 if (connectorId.isEmpty()) {
159 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "Device ID not set");
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");
167 queryString = QUERY_BASE_STRING + connectorId;
169 scheduler.submit(this::startCommunication);
173 public void dispose() {
177 private synchronized void startCommunication() {
178 ElroConnectsBridgeConfiguration config = getConfigAs(ElroConnectsBridgeConfiguration.class);
179 InetAddress addr = this.addr;
181 String ipAddress = config.ipAddress;
182 if (!ipAddress.isEmpty()) {
184 addr = InetAddress.getByName(ipAddress);
186 } catch (UnknownHostException e) {
188 logger.warn("Unknown host for {}, trying to discover address", ipAddress);
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 + ".");
201 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
202 "Error trying to find IP address for connector ID " + connectorId + ".");
207 String ctrlKey = this.ctrlKey;
208 if (ctrlKey.isEmpty()) {
209 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
210 "Communication data error while starting communication.");
215 DatagramSocket socket;
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());
226 ackPacket = new DatagramPacket(ACK, ACK.length, addr, PORT);
228 logger.debug("Connected to connector {} at {}:{}", connectorId, addr, PORT);
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))
237 // First get status, then name. The status response contains the device types needed to instantiate correct
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());
252 scheduleSyncStatus();
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.
259 * @param broadcast, if true find address by broadcast, otherwise simply send to configured address to retrieve key
261 * @return IP address of connector
262 * @throws IOException
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);
276 * Send keep alive message.
278 * @throws IOException
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
286 send(socket, queryString, false);
288 restartCommunication("Error in communication, no socket to send keep alive");
293 * Cleanup socket when the communication with ELRO Connects connector is closed.
296 private synchronized void stopCommunication() {
297 ScheduledFuture<?> sync = syncFuture;
305 DatagramSocket socket = this.socket;
306 if (socket != null && !socket.isClosed()) {
311 logger.debug("Communication stopped");
315 * Close and restart communication with ELRO Connects system, to be called after error in communication.
317 * @param offlineMessage message for thing status
319 private synchronized void restartCommunication(String offlineMessage) {
320 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, offlineMessage);
325 logger.debug("Restart communication");
328 scheduler.schedule(this::startFromRestart, RESTART_DELAY_MS, TimeUnit.MILLISECONDS);
332 private synchronized void startFromRestart() {
334 if (ThingStatus.OFFLINE.equals(thing.getStatus())) {
335 startCommunication();
339 private DatagramSocket createSocket(boolean timeout) throws SocketException {
340 DatagramSocket socket = new DatagramSocket();
341 socket.setBroadcast(true);
343 socket.setSoTimeout(1000);
349 * Read messages received through UDP socket.
353 private void runElroEvents() {
354 DatagramSocket socket = this.socket;
356 if (socket != null) {
357 logger.debug("Listening for messages");
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);
366 } catch (IOException e) {
367 restartCommunication("Communication error in listener: " + e.getMessage());
370 restartCommunication("Error in communication, no socket to start listener");
375 * Schedule regular queries to sync devices and scenes.
377 private void scheduleSyncStatus() {
378 syncFuture = scheduler.scheduleWithFixedDelay(() -> {
384 } catch (IOException e) {
385 restartCommunication("Error in communication refreshing device status: " + e.getMessage());
387 }, refreshInterval, refreshInterval, TimeUnit.SECONDS);
391 * Process response received from K1 Hub and send acknowledgement through open socket.
395 * @throws IOException
397 private void processMessage(DatagramSocket socket, String response) throws IOException {
398 if (!response.startsWith("{")) {
399 // Not a Json to interpret, just ignore
403 ElroConnectsMessage message;
406 json = response.split("\\R")[0];
407 message = gsonIn.fromJson(json, ElroConnectsMessage.class);
409 } catch (JsonSyntaxException ignore) {
410 logger.debug("Cannot decode, not a valid json: {}", json);
414 if (message == null) {
418 switch (message.getCmdId()) {
419 case ELRO_IGNORE_YES_NO:
421 case ELRO_REC_DEVICE_NAME:
422 processDeviceNameMessage(message);
424 case ELRO_REC_DEVICE_STATUS:
425 processDeviceStatusMessage(message);
428 processAlarmTriggerMessage(message);
430 case ELRO_REC_SCENE_NAME:
431 processSceneNameMessage(message);
433 case ELRO_REC_SCENE_TYPE:
434 processSceneTypeMessage(message);
437 processSceneMessage(message);
440 logger.debug("CmdId not implemented: {}", message.getCmdId());
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
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
459 device.setDeviceStatus(deviceStatus);
461 device.updateState();
464 private void processDeviceNameMessage(ElroConnectsMessage message) {
465 String answerContent = message.getAnswerContent();
466 if ("NAME_OVER".equals(answerContent)) {
467 // last message in series received
471 if (answerContent.length() <= 4) {
472 logger.debug("Could not decode answer {}", answerContent);
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);
485 private void processSceneNameMessage(ElroConnectsMessage message) {
486 int sceneId = message.getSceneGroup();
487 String answerContent = message.getAnswerContent();
489 if (sceneId > MAX_DEFAULT_SCENE) {
490 if (answerContent.length() < 44) {
491 logger.debug("Could not decode answer {}", answerContent);
494 sceneName = (new String(HexUtils.hexToBytes(answerContent.substring(6, 38)))).replaceAll("[@$]*", "");
495 scenes.put(sceneId, sceneName);
496 logger.debug("Scene ID {} name: {}", sceneId, sceneName);
500 private void processSceneTypeMessage(ElroConnectsMessage message) {
501 String sceneContent = message.getSceneContent();
502 if ("OVER".equals(sceneContent)) {
503 // last message in series received
506 updateSceneOptions();
510 private void processSceneMessage(ElroConnectsMessage message) {
511 int sceneId = message.getSceneGroup();
513 currentScene = sceneId;
515 updateState(SCENE, new StringType(String.valueOf(currentScene)));
518 private void processAlarmTriggerMessage(ElroConnectsMessage message) {
519 String answerContent = message.getAnswerContent();
520 if (answerContent.length() < 10) {
521 logger.debug("Could not decode answer {}", answerContent);
525 int deviceId = Integer.parseInt(answerContent.substring(6, 10), 16);
527 ElroConnectsDeviceHandler handler = deviceHandlers.get(deviceId);
528 if (handler != null) {
529 handler.triggerAlarm();
531 logger.debug("Device ID {} alarm", deviceId);
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);
539 ElroConnectsDevice device;
545 device = new ElroConnectsDeviceGenericAlarm(deviceId, this);
548 device = new ElroConnectsDeviceCxsmAlarm(deviceId, this);
551 device = new ElroConnectsDevicePowerSocket(deviceId, this);
554 device = new ElroConnectsDeviceEntrySensor(deviceId, this);
557 device = new ElroConnectsDeviceMotionSensor(deviceId, this);
560 device = new ElroConnectsDeviceTemperatureSensor(deviceId, this);
563 logger.debug("Device type {} not supported", deviceType);
566 device.setDeviceType(deviceType);
567 devices.put(deviceId, device);
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.
575 * @param waitResponse true if we want to wait for response for next message to be sent before allowing subsequent
578 private void awaitResponse(boolean waitResponse) {
579 CompletableFuture<Boolean> waiting = awaitResponse;
580 if (waiting != null) {
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");
588 awaitResponse = waitResponse ? new CompletableFuture<>() : null;
592 * This method is called when all responses on a request have been received.
594 private void stopAwaitResponse() {
595 CompletableFuture<Boolean> future = awaitResponse;
596 if (future != null) {
597 future.complete(true);
599 awaitResponse = null;
602 private void sendAck(DatagramSocket socket) throws IOException {
603 logger.debug("Send Ack: {}", ACK_STRING);
604 socket.send(ackPacket);
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);
615 private void send(DatagramSocket socket, String query, boolean broadcast) throws IOException {
616 final InetAddress address = broadcast
617 ? InetAddress.getByName(networkAddressService.getConfiguredBroadcastAddress())
619 if (address == null) {
621 restartCommunication("No broadcast address, check network configuration");
623 restartCommunication("Failed sending, hub address was not set");
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);
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();
642 * Basic method to send an {@link ElroConnectsMessage} to the K1 hub.
645 * @param waitResponse true if no new messages should be allowed to be sent before receiving the full response
646 * @throws IOException
648 private synchronized void sendElroMessage(ElroConnectsMessage elroMessage, boolean waitResponse)
650 DatagramSocket socket = this.socket;
651 if (socket != null) {
652 String message = gsonOut.toJson(elroMessage);
653 awaitResponse(waitResponse);
654 send(socket, message, false);
656 throw new IOException("No socket");
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.
665 * @param deviceCommand ELRO Connects device command string
666 * @throws IOException
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);
678 * Send request to receive all device names.
680 * @throws IOException
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);
692 * Send request to receive all device statuses.
694 * @throws IOException
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);
706 * Send request to sync all devices statuses.
708 * @throws IOException
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);
720 * Send request to get the currently selected scene.
722 * @throws IOException
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,
730 sendElroMessage(elroMessage, true);
734 * Send message to set the current scene.
736 * @throws IOException
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);
748 * Send request to sync all scenes.
750 * @throws IOException
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);
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) {
770 selectScene(((DecimalType) command).intValue());
771 } catch (IOException e) {
772 restartCommunication("Error in communication while setting scene: " + e.getMessage());
780 * We do not get scene delete messages, therefore call this method before requesting list of scenes to clear list of
783 private void resetScenes() {
785 scenes.putAll(DEFAULT_SCENES);
787 updateSceneOptions();
791 * Update state option list for scene selection channel.
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);
800 logger.trace("Scenes: {}", stateOptionList);
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);
812 * Messages need to be sent with consecutive id's. Increment the msgId field and rotate at max unsigned short.
814 * @return new message id
816 private int msgIdIncrement() {
817 return Short.toUnsignedInt(msgId++);
821 * Set the {@link ElroConnectsDeviceHandler} for the device with deviceId, should be called from the thing handler
822 * when initializing the thing.
827 public void setDeviceHandler(int deviceId, ElroConnectsDeviceHandler handler) {
828 deviceHandlers.put(deviceId, handler);
832 * Unset the {@link ElroConnectsDeviceHandler} for the device with deviceId, should be called from the thing handler
833 * when disposing the thing.
838 public void unsetDeviceHandler(int deviceId, ElroConnectsDeviceHandler handler) {
839 deviceHandlers.remove(deviceId, handler);
842 public @Nullable ElroConnectsDeviceHandler getDeviceHandler(int deviceId) {
843 return deviceHandlers.get(deviceId);
846 public @Nullable ElroConnectsDevice getDevice(int deviceId) {
847 return devices.get(deviceId);
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.
856 public Map<Integer, ElroConnectsDevice> getDevices() {
861 public Collection<Class<? extends ThingHandlerService>> getServices() {
862 return Collections.singleton(ElroConnectsDiscoveryService.class);