2 * Copyright (c) 2010-2021 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.telegram.internal.action;
15 import static org.openhab.binding.telegram.internal.TelegramBindingConstants.PHOTO_EXTENSIONS;
17 import java.io.ByteArrayInputStream;
18 import java.io.IOException;
19 import java.io.InputStream;
20 import java.net.MalformedURLException;
23 import java.nio.charset.StandardCharsets;
24 import java.nio.file.Path;
25 import java.util.Base64;
26 import java.util.concurrent.ExecutionException;
27 import java.util.concurrent.TimeUnit;
29 import org.eclipse.jdt.annotation.NonNullByDefault;
30 import org.eclipse.jdt.annotation.Nullable;
31 import org.eclipse.jetty.client.HttpClient;
32 import org.eclipse.jetty.client.api.Authentication;
33 import org.eclipse.jetty.client.api.AuthenticationStore;
34 import org.eclipse.jetty.client.api.ContentResponse;
35 import org.eclipse.jetty.client.api.Request;
36 import org.eclipse.jetty.client.util.FutureResponseListener;
37 import org.eclipse.jetty.http.HttpHeader;
38 import org.eclipse.jetty.http.HttpMethod;
39 import org.openhab.binding.telegram.internal.TelegramHandler;
40 import org.openhab.core.automation.annotation.ActionInput;
41 import org.openhab.core.automation.annotation.RuleAction;
42 import org.openhab.core.thing.binding.ThingActions;
43 import org.openhab.core.thing.binding.ThingActionsScope;
44 import org.openhab.core.thing.binding.ThingHandler;
45 import org.slf4j.Logger;
46 import org.slf4j.LoggerFactory;
48 import com.pengrad.telegrambot.model.request.InlineKeyboardButton;
49 import com.pengrad.telegrambot.model.request.InlineKeyboardMarkup;
50 import com.pengrad.telegrambot.request.AnswerCallbackQuery;
51 import com.pengrad.telegrambot.request.EditMessageReplyMarkup;
52 import com.pengrad.telegrambot.request.SendAnimation;
53 import com.pengrad.telegrambot.request.SendMessage;
54 import com.pengrad.telegrambot.request.SendPhoto;
55 import com.pengrad.telegrambot.request.SendVideo;
56 import com.pengrad.telegrambot.response.BaseResponse;
57 import com.pengrad.telegrambot.response.SendResponse;
60 * Provides the actions for the Telegram API.
62 * @author Alexander Krasnogolowy - Initial contribution
64 @ThingActionsScope(name = "telegram")
66 public class TelegramActions implements ThingActions {
67 private final Logger logger = LoggerFactory.getLogger(TelegramActions.class);
68 private @Nullable TelegramHandler handler;
70 private boolean evaluateResponse(@Nullable BaseResponse response) {
71 if (response != null && !response.isOk()) {
72 logger.warn("Failed to send telegram message: {}", response.description());
78 private static class BasicResult implements Authentication.Result {
80 private final HttpHeader header;
81 private final URI uri;
82 private final String value;
84 public BasicResult(HttpHeader header, URI uri, String value) {
96 public void apply(@Nullable Request request) {
97 if (request != null) {
98 request.header(this.header, this.value);
103 public String toString() {
104 return String.format("Basic authentication result for %s", this.uri);
108 @RuleAction(label = "send an answer", description = "Send a Telegram answer using the Telegram API.")
109 public boolean sendTelegramAnswer(@ActionInput(name = "chatId") @Nullable Long chatId,
110 @ActionInput(name = "replyId") @Nullable String replyId,
111 @ActionInput(name = "message") @Nullable String message) {
112 if (replyId == null) {
113 logger.warn("ReplyId not defined; action skipped.");
116 if (chatId == null) {
117 logger.warn("chatId not defined; action skipped.");
120 TelegramHandler localHandler = handler;
121 if (localHandler != null) {
122 String callbackId = localHandler.getCallbackId(chatId, replyId);
123 if (callbackId != null) {
124 AnswerCallbackQuery answerCallbackQuery = new AnswerCallbackQuery(
125 localHandler.getCallbackId(chatId, replyId));
126 logger.debug("AnswerCallbackQuery for chatId {} and replyId {} is the callbackId {}", chatId, replyId,
127 localHandler.getCallbackId(chatId, replyId));
128 // we could directly set the text here, but this
129 // doesn't result in a real message only in a
130 // little popup or in an alert, so the only purpose
131 // is to stop the progress bar on client side
132 if (!evaluateResponse(localHandler.execute(answerCallbackQuery))) {
136 Integer messageId = localHandler.removeMessageId(chatId, replyId);
137 if (messageId == null) {
138 logger.warn("messageId could not be found for chatId {} and replyId {}", chatId, replyId);
141 logger.debug("remove messageId {} for chatId {} and replyId {}", messageId, chatId, replyId);
143 EditMessageReplyMarkup editReplyMarkup = new EditMessageReplyMarkup(chatId, messageId.intValue())
144 .replyMarkup(new InlineKeyboardMarkup(new InlineKeyboardButton[0]));// remove reply markup from
146 if (!evaluateResponse(localHandler.execute(editReplyMarkup))) {
149 return message != null ? sendTelegram(chatId, message) : true;
154 @RuleAction(label = "send an answer", description = "Send a Telegram answer using the Telegram API.")
155 public boolean sendTelegramAnswer(@ActionInput(name = "replyId") @Nullable String replyId,
156 @ActionInput(name = "message") @Nullable String message) {
157 TelegramHandler localHandler = handler;
158 if (localHandler != null) {
159 for (Long chatId : localHandler.getReceiverChatIds()) {
160 if (!sendTelegramAnswer(chatId, replyId, message)) {
168 @RuleAction(label = "send a message", description = "Send a Telegram message using the Telegram API.")
169 public boolean sendTelegram(@ActionInput(name = "chatId") @Nullable Long chatId,
170 @ActionInput(name = "message") @Nullable String message) {
171 return sendTelegramGeneral(chatId, message, (String) null);
174 @RuleAction(label = "send a message", description = "Send a Telegram message using the Telegram API.")
175 public boolean sendTelegram(@ActionInput(name = "message") @Nullable String message) {
176 TelegramHandler localHandler = handler;
177 if (localHandler != null) {
178 for (Long chatId : localHandler.getReceiverChatIds()) {
179 if (!sendTelegram(chatId, message)) {
187 @RuleAction(label = "send a message", description = "Send a Telegram using the Telegram API.")
188 public boolean sendTelegramQuery(@ActionInput(name = "chatId") @Nullable Long chatId,
189 @ActionInput(name = "message") @Nullable String message,
190 @ActionInput(name = "replyId") @Nullable String replyId,
191 @ActionInput(name = "buttons") @Nullable String... buttons) {
192 return sendTelegramGeneral(chatId, message, replyId, buttons);
195 @RuleAction(label = "send a message", description = "Send a Telegram using the Telegram API.")
196 public boolean sendTelegramQuery(@ActionInput(name = "message") @Nullable String message,
197 @ActionInput(name = "replyId") @Nullable String replyId,
198 @ActionInput(name = "buttons") @Nullable String... buttons) {
199 TelegramHandler localHandler = handler;
200 if (localHandler != null) {
201 for (Long chatId : localHandler.getReceiverChatIds()) {
202 if (!sendTelegramQuery(chatId, message, replyId, buttons)) {
210 private boolean sendTelegramGeneral(@ActionInput(name = "chatId") @Nullable Long chatId, @Nullable String message,
211 @Nullable String replyId, @Nullable String... buttons) {
212 if (message == null) {
213 logger.warn("Message not defined; action skipped.");
216 if (chatId == null) {
217 logger.warn("chatId not defined; action skipped.");
220 TelegramHandler localHandler = handler;
221 if (localHandler != null) {
222 SendMessage sendMessage = new SendMessage(chatId, message);
223 if (localHandler.getParseMode() != null) {
224 sendMessage.parseMode(localHandler.getParseMode());
226 if (replyId != null) {
227 if (!replyId.contains(" ")) {
228 if (buttons.length > 0) {
229 InlineKeyboardButton[][] keyboard2D = new InlineKeyboardButton[1][];
230 InlineKeyboardButton[] keyboard = new InlineKeyboardButton[buttons.length];
231 keyboard2D[0] = keyboard;
232 for (int i = 0; i < buttons.length; i++) {
233 keyboard[i] = new InlineKeyboardButton(buttons[i]).callbackData(replyId + " " + buttons[i]);
235 InlineKeyboardMarkup keyBoardMarkup = new InlineKeyboardMarkup(keyboard2D);
236 sendMessage.replyMarkup(keyBoardMarkup);
239 "The replyId {} for message {} is given, but no buttons are defined. ReplyMarkup will be ignored.",
243 logger.warn("replyId {} must not contain spaces. ReplyMarkup will be ignored.", replyId);
246 SendResponse retMessage = null;
248 retMessage = localHandler.execute(sendMessage);
249 } catch (Exception e) {
250 logger.warn("Exception occured whilst sending message:{}", e.getMessage());
252 if (!evaluateResponse(retMessage)) {
255 if (replyId != null && retMessage != null) {
256 logger.debug("Adding chatId {}, replyId {} and messageId {}", chatId, replyId,
257 retMessage.message().messageId());
258 localHandler.addMessageId(chatId, replyId, retMessage.message().messageId());
265 @RuleAction(label = "send a message", description = "Send a Telegram using the Telegram API.")
266 public boolean sendTelegram(@ActionInput(name = "chatId") @Nullable Long chatId,
267 @ActionInput(name = "message") @Nullable String message,
268 @ActionInput(name = "args") @Nullable Object... args) {
269 if (message == null) {
272 return sendTelegram(chatId, String.format(message, args));
275 @RuleAction(label = "send a message", description = "Send a Telegram using the Telegram API.")
276 public boolean sendTelegram(@ActionInput(name = "message") @Nullable String message,
277 @ActionInput(name = "args") @Nullable Object... args) {
278 TelegramHandler localHandler = handler;
279 if (localHandler != null) {
280 for (Long chatId : localHandler.getReceiverChatIds()) {
281 if (!sendTelegram(chatId, message, args)) {
289 @RuleAction(label = "send a photo", description = "Send a picture using the Telegram API.")
290 public boolean sendTelegramPhoto(@ActionInput(name = "chatId") @Nullable Long chatId,
291 @ActionInput(name = "photoURL") @Nullable String photoURL,
292 @ActionInput(name = "caption") @Nullable String caption) {
293 return sendTelegramPhoto(chatId, photoURL, caption, null, null);
296 @RuleAction(label = "send a photo", description = "Send a picture using the Telegram API.")
297 public boolean sendTelegramPhoto(@ActionInput(name = "chatId") @Nullable Long chatId,
298 @ActionInput(name = "photoURL") @Nullable String photoURL,
299 @ActionInput(name = "caption") @Nullable String caption,
300 @ActionInput(name = "username") @Nullable String username,
301 @ActionInput(name = "password") @Nullable String password) {
302 if (photoURL == null) {
303 logger.warn("Photo URL not defined; unable to retrieve photo for sending.");
306 if (chatId == null) {
307 logger.warn("chatId not defined; action skipped.");
310 String lowercasePhotoUrl = photoURL.toLowerCase();
311 TelegramHandler localHandler = handler;
312 if (localHandler != null) {
313 final SendPhoto sendPhoto;
314 if (lowercasePhotoUrl.startsWith("http")) {
315 logger.debug("Http based URL for photo provided.");
316 HttpClient client = localHandler.getClient();
317 if (client == null) {
320 Request request = client.newRequest(photoURL).method(HttpMethod.GET).timeout(30, TimeUnit.SECONDS);
321 if (username != null && password != null) {
322 AuthenticationStore auth = client.getAuthenticationStore();
323 URI uri = URI.create(photoURL);
324 auth.addAuthenticationResult(
325 new BasicResult(HttpHeader.AUTHORIZATION, uri, "Basic " + Base64.getEncoder()
326 .encodeToString((username + ":" + password).getBytes(StandardCharsets.UTF_8))));
329 // API has 10mb limit to jpg file size, without this it can only accept 2mb
330 FutureResponseListener listener = new FutureResponseListener(request, 10 * 1024 * 1024);
331 request.send(listener);
332 ContentResponse contentResponse = listener.get();
333 if (contentResponse.getStatus() == 200) {
334 byte[] fileContent = contentResponse.getContent();
335 sendPhoto = new SendPhoto(chatId, fileContent);
337 logger.warn("Download from {} failed with status: {}", photoURL, contentResponse.getStatus());
338 sendTelegram(chatId, caption + ":Download failed with status " + contentResponse.getStatus());
341 } catch (InterruptedException | ExecutionException e) {
342 logger.warn("Download from {} failed with exception: {}", photoURL, e.getMessage());
345 } else if (lowercasePhotoUrl.startsWith("file:")
346 || PHOTO_EXTENSIONS.stream().anyMatch(lowercasePhotoUrl::endsWith)) {
347 logger.debug("Read file from local file system: {}", photoURL);
348 String temp = photoURL;
349 if (!lowercasePhotoUrl.startsWith("file:")) {
350 temp = "file://" + photoURL;
353 sendPhoto = new SendPhoto(chatId, Path.of(new URL(temp).getPath()).toFile());
354 } catch (MalformedURLException e) {
355 logger.warn("Malformed URL: {}", photoURL);
359 logger.debug("Base64 image provided; converting to binary.");
360 final String photoB64Data;
361 if (photoURL.startsWith("data:")) { // support data URI scheme
362 String[] photoURLParts = photoURL.split(",");
363 if (photoURLParts.length > 1) {
364 photoB64Data = photoURLParts[1];
366 logger.warn("The provided base64 string is not a valid data URI scheme");
370 photoB64Data = photoURL;
372 InputStream is = Base64.getDecoder()
373 .wrap(new ByteArrayInputStream(photoB64Data.getBytes(StandardCharsets.UTF_8)));
375 byte[] photoBytes = is.readAllBytes();
376 sendPhoto = new SendPhoto(chatId, photoBytes);
377 } catch (IOException e) {
378 logger.warn("Malformed base64 string: {}", e.getMessage());
382 if (caption != null) {
383 sendPhoto.caption(caption);
385 if (localHandler.getParseMode() != null) {
386 sendPhoto.parseMode(localHandler.getParseMode());
388 return evaluateResponse(localHandler.execute(sendPhoto));
393 @RuleAction(label = "send a photo", description = "Send a Picture using the Telegram API.")
394 public boolean sendTelegramPhoto(@ActionInput(name = "photoURL") @Nullable String photoURL,
395 @ActionInput(name = "caption") @Nullable String caption,
396 @ActionInput(name = "username") @Nullable String username,
397 @ActionInput(name = "password") @Nullable String password) {
398 TelegramHandler localHandler = handler;
399 if (localHandler != null) {
400 for (Long chatId : localHandler.getReceiverChatIds()) {
401 if (!sendTelegramPhoto(chatId, photoURL, caption, username, password)) {
409 @RuleAction(label = "send a photo", description = "Send a Picture using the Telegram API.")
410 public boolean sendTelegramPhoto(@ActionInput(name = "photoURL") @Nullable String photoURL,
411 @ActionInput(name = "caption") @Nullable String caption) {
412 return sendTelegramPhoto(photoURL, caption, null, null);
415 @RuleAction(label = "send animation", description = "Send an Animation using the Telegram API.")
416 public boolean sendTelegramAnimation(@ActionInput(name = "animationURL") @Nullable String animationURL,
417 @ActionInput(name = "caption") @Nullable String caption) {
418 TelegramHandler localHandler = handler;
419 if (localHandler != null) {
420 for (Long chatId : localHandler.getReceiverChatIds()) {
421 if (!sendTelegramAnimation(chatId, animationURL, caption)) {
429 @RuleAction(label = "send animation", description = "Send an Animation using the Telegram API.")
430 public boolean sendTelegramAnimation(@ActionInput(name = "chatId") @Nullable Long chatId,
431 @ActionInput(name = "animationURL") @Nullable String animationURL,
432 @ActionInput(name = "caption") @Nullable String caption) {
433 if (animationURL == null) {
434 logger.warn("Animation URL not defined; unable to retrieve video for sending.");
437 if (chatId == null) {
438 logger.warn("chatId not defined; action skipped.");
441 TelegramHandler localHandler = handler;
442 if (localHandler != null) {
443 final SendAnimation sendAnimation;
444 if (animationURL.toLowerCase().startsWith("http")) {
445 // load image from url
446 logger.debug("Animation URL provided.");
447 HttpClient client = localHandler.getClient();
448 if (client == null) {
451 Request request = client.newRequest(animationURL).method(HttpMethod.GET).timeout(30, TimeUnit.SECONDS);
453 // 50mb limit to file size
454 FutureResponseListener listener = new FutureResponseListener(request, 50 * 1024 * 1024);
455 request.send(listener);
456 ContentResponse contentResponse = listener.get();
457 if (contentResponse.getStatus() == 200) {
458 byte[] fileContent = contentResponse.getContent();
459 sendAnimation = new SendAnimation(chatId, fileContent);
461 logger.warn("Download from {} failed with status: {}", animationURL,
462 contentResponse.getStatus());
463 sendTelegram(chatId, caption + ":Download failed with status " + contentResponse.getStatus());
466 } catch (InterruptedException | ExecutionException e) {
467 logger.warn("Download from {} failed with exception: {}", animationURL, e.getMessage());
471 String temp = animationURL;
472 if (!animationURL.toLowerCase().startsWith("file:")) {
473 temp = "file://" + animationURL;
475 // Load video from local file system
476 logger.debug("Read file from local file system: {}", animationURL);
478 sendAnimation = new SendAnimation(chatId, Path.of(new URL(temp).getPath()).toFile());
479 } catch (MalformedURLException e) {
480 logger.warn("Malformed URL, should start with http or file: {}", animationURL);
484 if (caption != null) {
485 sendAnimation.caption(caption);
487 if (localHandler.getParseMode() != null) {
488 sendAnimation.parseMode(localHandler.getParseMode());
490 return evaluateResponse(localHandler.execute(sendAnimation));
495 @RuleAction(label = "send video", description = "Send a Video using the Telegram API.")
496 public boolean sendTelegramVideo(@ActionInput(name = "videoURL") @Nullable String videoURL,
497 @ActionInput(name = "caption") @Nullable String caption) {
498 TelegramHandler localHandler = handler;
499 if (localHandler != null) {
500 for (Long chatId : localHandler.getReceiverChatIds()) {
501 if (!sendTelegramVideo(chatId, videoURL, caption)) {
509 @RuleAction(label = "send video", description = "Send a Video using the Telegram API.")
510 public boolean sendTelegramVideo(@ActionInput(name = "chatId") @Nullable Long chatId,
511 @ActionInput(name = "videoURL") @Nullable String videoURL,
512 @ActionInput(name = "caption") @Nullable String caption) {
513 final SendVideo sendVideo;
514 if (videoURL == null) {
515 logger.warn("Video URL not defined; unable to retrieve video for sending.");
518 if (chatId == null) {
519 logger.warn("chatId not defined; action skipped.");
522 TelegramHandler localHandler = handler;
523 if (localHandler != null) {
524 if (videoURL.toLowerCase().startsWith("http")) {
525 logger.debug("Video http://URL provided.");
526 HttpClient client = localHandler.getClient();
527 if (client == null) {
530 Request request = client.newRequest(videoURL).method(HttpMethod.GET).timeout(30, TimeUnit.SECONDS);
532 // 50mb limit to file size
533 FutureResponseListener listener = new FutureResponseListener(request, 50 * 1024 * 1024);
534 request.send(listener);
535 ContentResponse contentResponse = listener.get();
536 if (contentResponse.getStatus() == 200) {
537 byte[] fileContent = contentResponse.getContent();
538 sendVideo = new SendVideo(chatId, fileContent);
540 logger.warn("Download from {} failed with status: {}", videoURL, contentResponse.getStatus());
541 sendTelegram(chatId, caption + ":Download failed with status " + contentResponse.getStatus());
544 } catch (InterruptedException | ExecutionException e) {
545 logger.warn("Download from {} failed with exception: {}", videoURL, e.getMessage());
549 String temp = videoURL;
550 if (!videoURL.toLowerCase().startsWith("file:")) {
551 temp = "file://" + videoURL;
553 // Load video from local file system with file://path
554 logger.debug("Read file from local file: {}", videoURL);
556 sendVideo = new SendVideo(chatId, Path.of(new URL(temp).getPath()).toFile());
557 } catch (MalformedURLException e) {
558 logger.warn("Malformed URL, should start with http or file: {}", videoURL);
562 if (caption != null) {
563 sendVideo.caption(caption);
565 if (localHandler.getParseMode() != null) {
566 sendVideo.parseMode(localHandler.getParseMode());
568 return evaluateResponse(localHandler.execute(sendVideo));
573 // legacy delegate methods
574 /* APIs without chatId parameter */
575 public static boolean sendTelegram(ThingActions actions, @Nullable String format, @Nullable Object... args) {
576 return ((TelegramActions) actions).sendTelegram(format, args);
579 public static boolean sendTelegramQuery(ThingActions actions, @Nullable String message, @Nullable String replyId,
580 @Nullable String... buttons) {
581 return ((TelegramActions) actions).sendTelegramQuery(message, replyId, buttons);
584 public static boolean sendTelegramPhoto(ThingActions actions, @Nullable String photoURL, @Nullable String caption) {
585 return ((TelegramActions) actions).sendTelegramPhoto(photoURL, caption, null, null);
588 public static boolean sendTelegramPhoto(ThingActions actions, @Nullable String photoURL, @Nullable String caption,
589 @Nullable String username, @Nullable String password) {
590 return ((TelegramActions) actions).sendTelegramPhoto(photoURL, caption, username, password);
593 public static boolean sendTelegramAnimation(ThingActions actions, @Nullable String animationURL,
594 @Nullable String caption) {
595 return ((TelegramActions) actions).sendTelegramVideo(animationURL, caption);
598 public static boolean sendTelegramVideo(ThingActions actions, @Nullable String videoURL, @Nullable String caption) {
599 return ((TelegramActions) actions).sendTelegramVideo(videoURL, caption);
602 public static boolean sendTelegramAnswer(ThingActions actions, @Nullable String replyId, @Nullable String message) {
603 return ((TelegramActions) actions).sendTelegramAnswer(replyId, message);
606 /* APIs with chatId parameter */
608 public static boolean sendTelegram(ThingActions actions, @Nullable Long chatId, @Nullable String format,
609 @Nullable Object... args) {
610 return ((TelegramActions) actions).sendTelegram(chatId, format, args);
613 public static boolean sendTelegramQuery(ThingActions actions, @Nullable Long chatId, @Nullable String message,
614 @Nullable String replyId, @Nullable String... buttons) {
615 return ((TelegramActions) actions).sendTelegramQuery(chatId, message, replyId, buttons);
618 public static boolean sendTelegramPhoto(ThingActions actions, @Nullable Long chatId, @Nullable String photoURL,
619 @Nullable String caption) {
620 return ((TelegramActions) actions).sendTelegramPhoto(chatId, photoURL, caption, null, null);
623 public static boolean sendTelegramPhoto(ThingActions actions, @Nullable Long chatId, @Nullable String photoURL,
624 @Nullable String caption, @Nullable String username, @Nullable String password) {
625 return ((TelegramActions) actions).sendTelegramPhoto(chatId, photoURL, caption, username, password);
628 public static boolean sendTelegramAnimation(ThingActions actions, @Nullable Long chatId,
629 @Nullable String animationURL, @Nullable String caption) {
630 return ((TelegramActions) actions).sendTelegramVideo(chatId, animationURL, caption);
633 public static boolean sendTelegramVideo(ThingActions actions, @Nullable Long chatId, @Nullable String videoURL,
634 @Nullable String caption) {
635 return ((TelegramActions) actions).sendTelegramVideo(chatId, videoURL, caption);
638 public static boolean sendTelegramAnswer(ThingActions actions, @Nullable Long chatId, @Nullable String replyId,
639 @Nullable String message) {
640 return ((TelegramActions) actions).sendTelegramAnswer(chatId, replyId, message);
643 public static boolean sendTelegramAnswer(ThingActions actions, @Nullable String chatId, @Nullable String replyId,
644 @Nullable String message) {
645 if (actions instanceof TelegramActions) {
646 if (chatId == null) {
649 return ((TelegramActions) actions).sendTelegramAnswer(Long.valueOf(chatId), replyId, message);
651 throw new IllegalArgumentException("Actions is not an instance of TelegramActions");
656 public void setThingHandler(@Nullable ThingHandler handler) {
657 this.handler = (TelegramHandler) handler;
661 public @Nullable ThingHandler getThingHandler() {