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