]> git.basschouten.com Git - openhab-addons.git/blob
71a188e723c9f317f6746b02646bae6a3c152861
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2020 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.allplay.internal.handler;
14
15 import static org.openhab.binding.allplay.internal.AllPlayBindingConstants.*;
16
17 import java.net.URL;
18 import java.net.URLConnection;
19 import java.util.ArrayList;
20 import java.util.List;
21 import java.util.Map;
22 import java.util.concurrent.ScheduledExecutorService;
23 import java.util.concurrent.ScheduledFuture;
24 import java.util.concurrent.TimeUnit;
25
26 import org.apache.commons.io.IOUtils;
27 import org.openhab.binding.allplay.internal.AllPlayBindingConstants;
28 import org.openhab.binding.allplay.internal.AllPlayBindingProperties;
29 import org.openhab.core.common.ThreadPoolManager;
30 import org.openhab.core.io.net.http.HttpUtil;
31 import org.openhab.core.library.types.DecimalType;
32 import org.openhab.core.library.types.IncreaseDecreaseType;
33 import org.openhab.core.library.types.NextPreviousType;
34 import org.openhab.core.library.types.OnOffType;
35 import org.openhab.core.library.types.PercentType;
36 import org.openhab.core.library.types.PlayPauseType;
37 import org.openhab.core.library.types.RawType;
38 import org.openhab.core.library.types.RewindFastforwardType;
39 import org.openhab.core.library.types.StringType;
40 import org.openhab.core.thing.ChannelUID;
41 import org.openhab.core.thing.Thing;
42 import org.openhab.core.thing.ThingRegistry;
43 import org.openhab.core.thing.ThingStatus;
44 import org.openhab.core.thing.ThingStatusDetail;
45 import org.openhab.core.thing.binding.BaseThingHandler;
46 import org.openhab.core.types.Command;
47 import org.openhab.core.types.RefreshType;
48 import org.openhab.core.types.UnDefType;
49 import org.slf4j.Logger;
50 import org.slf4j.LoggerFactory;
51
52 import de.kaizencode.tchaikovsky.AllPlay;
53 import de.kaizencode.tchaikovsky.exception.AllPlayException;
54 import de.kaizencode.tchaikovsky.exception.ConnectionException;
55 import de.kaizencode.tchaikovsky.exception.DiscoveryException;
56 import de.kaizencode.tchaikovsky.exception.SpeakerException;
57 import de.kaizencode.tchaikovsky.listener.SpeakerAnnouncedListener;
58 import de.kaizencode.tchaikovsky.listener.SpeakerChangedListener;
59 import de.kaizencode.tchaikovsky.listener.SpeakerConnectionListener;
60 import de.kaizencode.tchaikovsky.speaker.PlayState;
61 import de.kaizencode.tchaikovsky.speaker.PlayState.State;
62 import de.kaizencode.tchaikovsky.speaker.PlaylistItem;
63 import de.kaizencode.tchaikovsky.speaker.Speaker;
64 import de.kaizencode.tchaikovsky.speaker.Speaker.LoopMode;
65 import de.kaizencode.tchaikovsky.speaker.Speaker.ShuffleMode;
66 import de.kaizencode.tchaikovsky.speaker.VolumeRange;
67 import de.kaizencode.tchaikovsky.speaker.ZoneItem;
68
69 /**
70  * The {@link AllPlayHandler} is responsible for handling commands, which are
71  * sent to one of the channels.
72  *
73  * @author Dominic Lerbs - Initial contribution
74  */
75 public class AllPlayHandler extends BaseThingHandler
76         implements SpeakerChangedListener, SpeakerAnnouncedListener, SpeakerConnectionListener {
77
78     private final Logger logger = LoggerFactory.getLogger(AllPlayHandler.class);
79
80     private final ThingRegistry localThingRegistry;
81     private final AllPlay allPlay;
82     private final AllPlayBindingProperties bindingProperties;
83     private Speaker speaker;
84     private VolumeRange volumeRange;
85
86     private static final String ALLPLAY_THREADPOOL_NAME = "allplayHandler";
87     private ScheduledFuture<?> reconnectionJob;
88     private final ScheduledExecutorService scheduler = ThreadPoolManager.getScheduledPool(ALLPLAY_THREADPOOL_NAME);
89
90     public AllPlayHandler(ThingRegistry thingRegistry, Thing thing, AllPlay allPlay,
91             AllPlayBindingProperties properties) {
92         super(thing);
93         this.localThingRegistry = thingRegistry;
94         this.allPlay = allPlay;
95         this.bindingProperties = properties;
96     }
97
98     @Override
99     public void initialize() {
100         logger.debug("Initializing AllPlay handler for speaker {}", getDeviceId());
101         updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.NONE, "Waiting for speaker to be discovered");
102         try {
103             allPlay.addSpeakerAnnouncedListener(this);
104             discoverSpeaker();
105         } catch (DiscoveryException e) {
106             logger.error("Unable to discover speaker {}", getDeviceId(), e);
107         }
108     }
109
110     /**
111      * Tries to discover the speaker which is associated with this thing.
112      */
113     public void discoverSpeaker() {
114         try {
115             logger.debug("Starting discovery for speaker {}", getDeviceId());
116             allPlay.discoverSpeaker(getDeviceId());
117         } catch (DiscoveryException e) {
118             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
119                     "Unable to discover speaker: " + e.getMessage());
120             logger.error("Unable to discover speaker {}", getDeviceId(), e);
121         }
122     }
123
124     @Override
125     public void onSpeakerAnnounced(Speaker speaker) {
126         logger.debug("Speaker announcement received for speaker {}. Own id is {}", speaker, getDeviceId());
127         if (isHandledSpeaker(speaker)) {
128             logger.debug("Speaker announcement received for handled speaker {}", speaker);
129             if (this.speaker != null) {
130                 // Make sure to disconnect first in case the speaker is re-announced
131                 disconnectFromSpeaker(this.speaker);
132             }
133             this.speaker = speaker;
134             cancelReconnectionJob();
135             try {
136                 connectToSpeaker();
137             } catch (AllPlayException e) {
138                 logger.debug("Connection error", e);
139                 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
140                         "Error while communicating with speaker: " + e.getMessage());
141                 scheduleReconnectionJob(speaker);
142             }
143         }
144     }
145
146     private void connectToSpeaker() throws ConnectionException {
147         if (speaker != null) {
148             logger.debug("Connecting to speaker {}", speaker);
149             speaker.addSpeakerChangedListener(this);
150             speaker.addSpeakerConnectionListener(this);
151             speaker.connect();
152             logger.debug("Connected to speaker {}", speaker);
153             updateStatus(ThingStatus.ONLINE);
154             try {
155                 initSpeakerState();
156             } catch (SpeakerException e) {
157                 logger.error("Unable to init speaker state", e);
158             }
159         } else {
160             logger.error("Speaker {} not discovered yet, cannot connect", getDeviceId());
161         }
162     }
163
164     private void initSpeakerState() throws SpeakerException {
165         cacheVolumeRange();
166         onMuteChanged(speaker.volume().isMute());
167         onLoopModeChanged(speaker.getLoopMode());
168         onShuffleModeChanged(speaker.getShuffleMode());
169         onPlayStateChanged(speaker.getPlayState());
170         onVolumeChanged(speaker.volume().getVolume());
171         onVolumeControlChanged(speaker.volume().isControlEnabled());
172     }
173
174     /**
175      * Cache the volume range as it will not change for the speaker.
176      */
177     private void cacheVolumeRange() throws SpeakerException {
178         volumeRange = speaker.volume().getVolumeRange();
179     }
180
181     @Override
182     public void onConnectionLost(String wellKnownName, int alljoynReasonCode) {
183         if (isHandledSpeaker(wellKnownName)) {
184             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, "Lost connection to speaker");
185             speaker.removeSpeakerConnectionListener(this);
186             speaker.removeSpeakerChangedListener(this);
187             scheduleReconnectionJob(speaker);
188         }
189     }
190
191     @Override
192     public void dispose() {
193         allPlay.removeSpeakerAnnouncedListener(this);
194         if (speaker != null) {
195             disconnectFromSpeaker(speaker);
196         }
197         super.dispose();
198     }
199
200     private void disconnectFromSpeaker(Speaker speaker) {
201         logger.debug("Disconnecting from speaker {}", speaker);
202         speaker.removeSpeakerChangedListener(this);
203         speaker.removeSpeakerConnectionListener(this);
204         cancelReconnectionJob();
205         if (speaker.isConnected()) {
206             speaker.disconnect();
207         }
208     }
209
210     @Override
211     public void handleCommand(ChannelUID channelUID, Command command) {
212         logger.debug("Channel {} triggered with command {}", channelUID.getId(), command);
213         if (isSpeakerReady()) {
214             try {
215                 if (command instanceof RefreshType) {
216                     handleRefreshCommand(channelUID.getId());
217                 } else {
218                     handleSpeakerCommand(channelUID.getId(), command);
219                 }
220             } catch (SpeakerException e) {
221                 logger.error("Unable to execute command {} on channel {}", command, channelUID.getId(), e);
222             }
223         }
224     }
225
226     private void handleSpeakerCommand(String channelId, Command command) throws SpeakerException {
227         switch (channelId) {
228             case CLEAR_ZONE:
229                 if (OnOffType.ON.equals(command)) {
230                     speaker.zoneManager().releaseZone();
231                 }
232                 break;
233             case CONTROL:
234                 handleControlCommand(command);
235                 break;
236             case INPUT:
237                 speaker.input().setInput(command.toString());
238                 break;
239             case LOOP_MODE:
240                 speaker.setLoopMode(LoopMode.parse(command.toString()));
241                 break;
242             case MUTE:
243                 speaker.volume().mute(OnOffType.ON.equals(command));
244                 break;
245             case STOP:
246                 speaker.stop();
247                 break;
248             case SHUFFLE_MODE:
249                 handleShuffleModeCommand(command);
250                 break;
251             case STREAM:
252                 logger.debug("Starting to stream URL: {}", command.toString());
253                 speaker.playItem(command.toString());
254                 break;
255             case VOLUME:
256                 handleVolumeCommand(command);
257                 break;
258             case ZONE_MEMBERS:
259                 handleZoneMembersCommand(command);
260                 break;
261             default:
262                 logger.warn("Unable to handle command {} on unknown channel {}", command, channelId);
263         }
264     }
265
266     private void handleRefreshCommand(String channelId) throws SpeakerException {
267         switch (channelId) {
268             case CURRENT_ARTIST:
269             case CURRENT_ALBUM:
270             case CURRENT_DURATION:
271             case CURRENT_GENRE:
272             case CURRENT_TITLE:
273             case CURRENT_URL:
274                 updatePlaylistItemsState(speaker.getPlayState().getPlaylistItems());
275                 break;
276             case CONTROL:
277                 updatePlayState(speaker.getPlayState());
278                 break;
279             case INPUT:
280                 onInputChanged(speaker.input().getActiveInput());
281                 break;
282             case LOOP_MODE:
283                 onLoopModeChanged(speaker.getLoopMode());
284                 break;
285             case MUTE:
286                 onMuteChanged(speaker.volume().isMute());
287                 break;
288             case SHUFFLE_MODE:
289                 onShuffleModeChanged(speaker.getShuffleMode());
290                 break;
291             case VOLUME:
292                 onVolumeChanged(speaker.volume().getVolume());
293                 onVolumeControlChanged(speaker.volume().isControlEnabled());
294                 break;
295             case ZONE_ID:
296                 updateState(ZONE_ID, new StringType(speaker.getPlayerInfo().getZoneInfo().getZoneId()));
297                 break;
298             default:
299                 logger.debug("REFRESH command not implemented on channel {}", channelId);
300         }
301     }
302
303     private void handleControlCommand(Command command) throws SpeakerException {
304         if (command instanceof PlayPauseType) {
305             if (command == PlayPauseType.PLAY) {
306                 speaker.resume();
307             } else if (command == PlayPauseType.PAUSE) {
308                 speaker.pause();
309             }
310         } else if (command instanceof NextPreviousType) {
311             if (command == NextPreviousType.NEXT) {
312                 speaker.next();
313             } else if (command == NextPreviousType.PREVIOUS) {
314                 speaker.previous();
315             }
316         } else if (command instanceof RewindFastforwardType) {
317             if (command == RewindFastforwardType.FASTFORWARD) {
318                 changeTrackPosition(bindingProperties.getFastForwardSkipTimeInSec() * 1000);
319             } else if (command == RewindFastforwardType.REWIND) {
320                 changeTrackPosition(-bindingProperties.getRewindSkipTimeInSec() * 1000);
321             }
322         } else {
323             logger.warn("Unknown control command: {}", command);
324         }
325     }
326
327     /**
328      * Changes the position in the current track.
329      *
330      * @param positionOffsetInMs The offset to adjust the current position. Can be negative or positive.
331      * @throws SpeakerException Exception if the position could not be changed
332      */
333     private void changeTrackPosition(long positionOffsetInMs) throws SpeakerException {
334         long currentPosition = speaker.getPlayState().getPositionInMs();
335         logger.debug("Jumping from old track position {} ms to new position {} ms", currentPosition,
336                 currentPosition + positionOffsetInMs);
337         speaker.setPosition(currentPosition + positionOffsetInMs);
338     }
339
340     /**
341      * Uses the given {@link Command} to change the volume of the speaker.
342      *
343      * @param command The {@link Command} with the new volume
344      * @throws SpeakerException Exception if the volume change failed
345      */
346     public void handleVolumeCommand(Command command) throws SpeakerException {
347         if (command instanceof PercentType) {
348             speaker.volume().setVolume(convertPercentToAbsoluteVolume((PercentType) command));
349         } else if (command instanceof IncreaseDecreaseType) {
350             int stepSize = (command == IncreaseDecreaseType.DECREASE ? -getVolumeStepSize() : getVolumeStepSize());
351             speaker.volume().adjustVolume(stepSize);
352         }
353     }
354
355     private void handleShuffleModeCommand(Command command) throws SpeakerException {
356         if (OnOffType.ON.equals(command)) {
357             speaker.setShuffleMode(ShuffleMode.SHUFFLE);
358         } else if (OnOffType.OFF.equals(command)) {
359             speaker.setShuffleMode(ShuffleMode.LINEAR);
360         }
361     }
362
363     private void handleZoneMembersCommand(Command command) throws SpeakerException {
364         String[] memberNames = command.toString().split(bindingProperties.getZoneMemberSeparator());
365         logger.debug("{}: Creating new zone with members {}", speaker, String.join(", ", memberNames));
366         List<String> memberIds = new ArrayList<>();
367         for (String memberName : memberNames) {
368             memberIds.add(getHandlerIdByLabel(memberName.trim()));
369         }
370         createZoneInNewThread(memberIds);
371     }
372
373     private void createZoneInNewThread(List<String> memberIds) {
374         scheduler.execute(() -> {
375             try {
376                 // This call blocks up to 10 seconds if one of the members is unreachable,
377                 // therefore it is executed in a new thread
378                 ZoneItem zone = speaker.zoneManager().createZone(memberIds);
379                 logger.debug("{}: Zone {} with member ids {} has been created", speaker, zone.getZoneId(),
380                         String.join(", ", zone.getSlaves().keySet()));
381             } catch (SpeakerException e) {
382                 logger.warn("{}: Cannot create zone", speaker, e);
383             }
384         });
385     }
386
387     @Override
388     public void onPlayStateChanged(PlayState playState) {
389         updatePlayState(playState);
390         updatePlaylistItemsState(playState.getPlaylistItems());
391     }
392
393     @Override
394     public void onPlaylistChanged() {
395         logger.debug("{}: Playlist changed: No action", speaker.getName());
396     }
397
398     @Override
399     public void onLoopModeChanged(LoopMode loopMode) {
400         logger.debug("{}: LoopMode changed to {}", speaker.getName(), loopMode);
401         updateState(LOOP_MODE, new StringType(loopMode.toString()));
402     }
403
404     @Override
405     public void onShuffleModeChanged(ShuffleMode shuffleMode) {
406         logger.debug("{}: ShuffleMode changed to {}", speaker.getName(), shuffleMode);
407         OnOffType shuffleOnOff = (shuffleMode == ShuffleMode.SHUFFLE) ? OnOffType.ON : OnOffType.OFF;
408         updateState(SHUFFLE_MODE, shuffleOnOff);
409     }
410
411     @Override
412     public void onMuteChanged(boolean mute) {
413         logger.debug("{}: Mute changed to {}", speaker.getName(), mute);
414         updateState(MUTE, mute ? OnOffType.ON : OnOffType.OFF);
415     }
416
417     @Override
418     public void onVolumeChanged(int volume) {
419         logger.debug("{}: Volume changed to {}", speaker.getName(), volume);
420         try {
421             updateState(VOLUME, convertAbsoluteVolumeToPercent(volume));
422         } catch (SpeakerException e) {
423             logger.warn("Cannot convert new volume to percent", e);
424         }
425     }
426
427     @Override
428     public void onVolumeControlChanged(boolean enabled) {
429         updateState(VOLUME_CONTROL, enabled ? OnOffType.ON : OnOffType.OFF);
430     }
431
432     @Override
433     public void onZoneChanged(String zoneId, int timestamp, Map<String, Integer> slaves) {
434         logger.debug("{}: Zone changed to {}", speaker.getName(), zoneId);
435         updateState(ZONE_ID, new StringType(zoneId));
436     }
437
438     @Override
439     public void onInputChanged(String input) {
440         logger.debug("{}: Input changed to {}", speaker.getName(), input);
441         updateState(INPUT, new StringType(input));
442     }
443
444     private void updatePlayState(PlayState playState) {
445         logger.debug("{}: PlayState changed to {}", speaker.getName(), playState);
446         updateState(PLAY_STATE, new StringType(playState.getState().toString()));
447
448         if (playState.getState() == State.PLAYING) {
449             updateState(CONTROL, PlayPauseType.PLAY);
450         } else {
451             updateState(CONTROL, PlayPauseType.PAUSE);
452         }
453     }
454
455     private void updatePlaylistItemsState(List<PlaylistItem> items) {
456         if (!items.isEmpty()) {
457             PlaylistItem currentItem = items.iterator().next();
458             updateCurrentItemState(currentItem);
459         } else {
460             updateState(CURRENT_ARTIST, UnDefType.NULL);
461             updateState(CURRENT_ALBUM, UnDefType.NULL);
462             updateState(CURRENT_TITLE, UnDefType.NULL);
463             updateState(CURRENT_GENRE, UnDefType.NULL);
464             updateState(CURRENT_URL, UnDefType.NULL);
465             updateState(COVER_ART_URL, UnDefType.NULL);
466             updateState(COVER_ART, UnDefType.NULL);
467         }
468     }
469
470     private void updateCurrentItemState(PlaylistItem currentItem) {
471         logger.debug("{}: PlaylistItem changed to {}", speaker.getName(), currentItem);
472         updateState(CURRENT_ARTIST, new StringType(currentItem.getArtist()));
473         updateState(CURRENT_ALBUM, new StringType(currentItem.getAlbum()));
474         updateState(CURRENT_TITLE, new StringType(currentItem.getTitle()));
475         updateState(CURRENT_GENRE, new StringType(currentItem.getGenre()));
476         updateDuration(currentItem.getDurationInMs());
477         updateState(CURRENT_URL, new StringType(currentItem.getUrl()));
478         updateCoverArtState(currentItem.getThumbnailUrl());
479
480         try {
481             updateState(CURRENT_USER_DATA, new StringType(String.valueOf(currentItem.getUserData())));
482         } catch (SpeakerException e) {
483             logger.warn("Unable to update current user data: {}", e.getMessage(), e);
484         }
485         logger.debug("MediaType: {}", currentItem.getMediaType());
486     }
487
488     private void updateDuration(long durationInMs) {
489         DecimalType duration = new DecimalType(durationInMs / 1000);
490         duration.format("%d s");
491         updateState(CURRENT_DURATION, duration);
492     }
493
494     private void updateCoverArtState(String coverArtUrl) {
495         try {
496             logger.debug("{}: Cover art URL changed to {}", speaker.getName(), coverArtUrl);
497             updateState(COVER_ART_URL, new StringType(coverArtUrl));
498             if (!coverArtUrl.isEmpty()) {
499                 byte[] bytes = getRawDataFromUrl(coverArtUrl);
500                 String contentType = HttpUtil.guessContentTypeFromData(bytes);
501                 updateState(COVER_ART, new RawType(bytes,
502                         contentType == null || contentType.isEmpty() ? RawType.DEFAULT_MIME_TYPE : contentType));
503             } else {
504                 updateState(COVER_ART, UnDefType.NULL);
505             }
506         } catch (Exception e) {
507             logger.warn("Error getting cover art", e);
508         }
509     }
510
511     /**
512      * Starts streaming the audio at the given URL.
513      *
514      * @param url The URL to stream
515      * @throws SpeakerException Exception if the URL could not be streamed
516      */
517     public void playUrl(String url) throws SpeakerException {
518         if (isSpeakerReady()) {
519             speaker.playItem(url);
520         } else {
521             throw new SpeakerException(
522                     "Cannot play audio stream, speaker " + speaker + " is not discovered/connected!");
523         }
524     }
525
526     /**
527      * @return The current volume of the speaker
528      * @throws SpeakerException Exception if the volume could not be retrieved
529      */
530     public PercentType getVolume() throws SpeakerException {
531         if (isSpeakerReady()) {
532             return convertAbsoluteVolumeToPercent(speaker.volume().getVolume());
533         } else {
534             throw new SpeakerException("Cannot get volume, speaker " + speaker + " is not discovered/connected!");
535         }
536     }
537
538     private byte[] getRawDataFromUrl(String urlString) throws Exception {
539         URL url = new URL(urlString);
540         URLConnection connection = url.openConnection();
541         return IOUtils.toByteArray(connection.getInputStream());
542     }
543
544     private int convertPercentToAbsoluteVolume(PercentType percentVolume) throws SpeakerException {
545         int range = volumeRange.getMax() - volumeRange.getMin();
546         int volume = (percentVolume.shortValue() * range) / 100;
547         logger.debug("Volume {}% has been converted to absolute volume {}", percentVolume.intValue(), volume);
548         return volume;
549     }
550
551     private PercentType convertAbsoluteVolumeToPercent(int volume) throws SpeakerException {
552         int range = volumeRange.getMax() - volumeRange.getMin();
553         int percentVolume = 0;
554         if (range > 0) {
555             percentVolume = (volume * 100) / range;
556         }
557         logger.debug("Absolute volume {} has been converted to volume {}%", volume, percentVolume);
558         return new PercentType(percentVolume);
559     }
560
561     private boolean isSpeakerReady() {
562         if (speaker == null || !speaker.isConnected()) {
563             logger.warn("Cannot execute command, speaker {} is not discovered/connected!", speaker);
564             return false;
565         }
566         return true;
567     }
568
569     /**
570      * @param speaker The {@link Speaker} to check
571      * @return True if the {@link Speaker} is managed by this handler, else false
572      */
573     private boolean isHandledSpeaker(Speaker speaker) {
574         return speaker.getId().equals(getDeviceId());
575     }
576
577     private boolean isHandledSpeaker(String wellKnownName) {
578         return wellKnownName.equals(speaker.details().getWellKnownName());
579     }
580
581     private String getDeviceId() {
582         return (String) getConfig().get(AllPlayBindingConstants.DEVICE_ID);
583     }
584
585     private Integer getVolumeStepSize() {
586         return (Integer) getConfig().get(AllPlayBindingConstants.VOLUME_STEP_SIZE);
587     }
588
589     /**
590      * Schedules a reconnection job.
591      */
592     private void scheduleReconnectionJob(final Speaker speaker) {
593         logger.debug("Scheduling job to rediscover to speaker {}", speaker);
594         // TODO: Check if it makes sense to repeat the discovery every x minutes or if the AllJoyn library is able to
595         // handle re-discovery in _all_ cases.
596         cancelReconnectionJob();
597         reconnectionJob = scheduler.scheduleWithFixedDelay(this::discoverSpeaker, 5, 600, TimeUnit.SECONDS);
598     }
599
600     /**
601      * Cancels a scheduled reconnection job.
602      */
603     private void cancelReconnectionJob() {
604         if (reconnectionJob != null) {
605             reconnectionJob.cancel(true);
606         }
607     }
608
609     private String getHandlerIdByLabel(String thingLabel) throws IllegalStateException {
610         for (Thing thing : localThingRegistry.getAll()) {
611             if (thingLabel.equals(thing.getLabel())) {
612                 return thing.getUID().getId();
613             }
614         }
615         throw new IllegalStateException("Could not find thing with label " + thingLabel);
616     }
617 }