2 * Copyright (c) 2010-2024 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.samsungtv.internal.service;
15 import static org.openhab.binding.samsungtv.internal.SamsungTvBindingConstants.*;
16 import static org.openhab.binding.samsungtv.internal.config.SamsungTvConfiguration.*;
18 import java.io.ByteArrayInputStream;
19 import java.io.IOException;
20 import java.io.InputStream;
22 import java.net.URISyntaxException;
23 import java.net.http.HttpClient;
24 import java.net.http.HttpRequest;
25 import java.net.http.HttpResponse;
26 import java.net.http.HttpResponse.BodyHandler;
27 import java.net.http.HttpResponse.BodySubscriber;
28 import java.net.http.HttpResponse.ResponseInfo;
29 import java.nio.ByteBuffer;
30 import java.nio.charset.StandardCharsets;
31 import java.time.Duration;
32 import java.util.ArrayList;
33 import java.util.Arrays;
34 import java.util.Collections;
35 import java.util.HashMap;
36 import java.util.List;
38 import java.util.Optional;
39 import java.util.Properties;
40 import java.util.concurrent.CompletableFuture;
41 import java.util.concurrent.CompletionStage;
42 import java.util.concurrent.CountDownLatch;
43 import java.util.concurrent.ExecutionException;
44 import java.util.concurrent.Flow.Subscription;
45 import java.util.concurrent.TimeUnit;
46 import java.util.concurrent.TimeoutException;
47 import java.util.stream.Collectors;
48 import java.util.stream.IntStream;
50 import org.eclipse.jdt.annotation.NonNullByDefault;
51 import org.eclipse.jdt.annotation.Nullable;
52 import org.eclipse.jetty.http.HttpMethod;
53 import org.openhab.binding.samsungtv.internal.handler.SamsungTvHandler;
54 import org.openhab.binding.samsungtv.internal.service.api.SamsungTvService;
55 import org.openhab.core.io.net.http.HttpUtil;
56 import org.openhab.core.library.types.DecimalType;
57 import org.openhab.core.library.types.StringType;
58 import org.openhab.core.types.Command;
59 import org.openhab.core.types.RefreshType;
60 import org.slf4j.Logger;
61 import org.slf4j.LoggerFactory;
63 import com.google.gson.Gson;
64 import com.google.gson.JsonArray;
65 import com.google.gson.JsonElement;
66 import com.google.gson.JsonObject;
67 import com.google.gson.JsonSyntaxException;
68 import com.google.gson.annotations.SerializedName;
71 * The {@link SmartThingsApiService} is responsible for handling the Smartthings cloud interface
74 * @author Nick Waterton - Initial contribution
77 public class SmartThingsApiService implements SamsungTvService {
79 public static final String SERVICE_NAME = "SmartthingsApi";
80 private static final List<String> SUPPORTED_CHANNELS = Arrays.asList(SOURCE_NAME, SOURCE_ID);
81 private static final List<String> REFRESH_CHANNELS = Arrays.asList(CHANNEL, CHANNEL_NAME, SOURCE_NAME, SOURCE_ID);
83 private static final String SMARTTHINGS_URL = "api.smartthings.com";
84 // Path for the information endpoint note the final /
85 private static final String API_ENDPOINT_V1 = "/v1/";
86 // private static final String INPUT_SOURCE = "/components/main/capabilities/mediaInputSource/status";
87 // private static final String CURRENT_CHANNEL = "/components/main/capabilities/tvChannel/status";
88 private static final String COMPONENTS = "/components/main/status";
89 private static final String DEVICES = "devices";
90 private static final String COMMAND = "/commands";
92 private final Logger logger = LoggerFactory.getLogger(SmartThingsApiService.class);
94 private String host = "";
95 private String apiKey = "";
96 private String deviceId = "";
97 private int RATE_LIMIT = 1000;
98 private int TIMEOUT = 1000; // connection timeout in ms
99 private long prevUpdate = 0;
100 private boolean online = false;
101 private int errorCount = 0;
102 private int MAX_ERRORS = 100;
104 private final SamsungTvHandler handler;
106 private Optional<TvValues> tvInfo = Optional.empty();
107 private boolean subscriptionRunning = false;
108 private Optional<BodyHandlerWrapper> handlerWrapper = Optional.empty();
109 private Optional<STSubscription> subscription = Optional.empty();
111 private Map<String, Object> stateMap = Collections.synchronizedMap(new HashMap<>());
113 public SmartThingsApiService(String host, SamsungTvHandler handler) {
114 this.handler = handler;
116 this.apiKey = handler.configuration.getSmartThingsApiKey();
117 this.deviceId = handler.configuration.getSmartThingsDeviceId();
118 logger.debug("{}: Creating a Samsung TV Smartthings Api service", host);
122 public String getServiceName() {
127 public List<String> getSupportedChannelNames(boolean refresh) {
129 if (subscriptionRunning) {
130 return Arrays.asList();
132 return REFRESH_CHANNELS;
134 logger.trace("{}: getSupportedChannelNames: {}", host, SUPPORTED_CHANNELS);
135 return SUPPORTED_CHANNELS;
138 // Description of tvValues
139 @NonNullByDefault({})
141 class MediaInputSource {
142 ValuesList supportedInputSources;
143 ValuesListMap supportedInputSourcesMap;
149 Values tvChannelName;
151 public String getChannelNum() {
152 return Optional.ofNullable(tvChannel).map(a -> a.value).orElse("");
166 class ValuesListMap {
170 public String[] getInputList() {
171 return Optional.ofNullable(value).map(a -> Arrays.stream(a).map(b -> b.getId()).toArray(String[]::new))
172 .orElse(new String[0]);
180 public String getId() {
181 return Optional.ofNullable(id).orElse("");
190 public String getDeviceId() {
191 return Optional.ofNullable(deviceId).orElse("");
194 public String getName() {
195 return Optional.ofNullable(name).orElse("");
198 public String getLabel() {
199 return Optional.ofNullable(label).orElse("");
215 @SerializedName(value = "samsungvd.mediaInputSource", alternate = { "mediaInputSource" })
216 MediaInputSource mediaInputSource;
221 public void updateSupportedInputSources(String[] values) {
222 mediaInputSource.supportedInputSources.value = values;
225 public Items[] getItems() {
226 return Optional.ofNullable(items).orElse(new Items[0]);
229 public String[] getSources() {
230 return Optional.ofNullable(mediaInputSource).map(a -> a.supportedInputSources).map(a -> a.value)
231 .orElseGet(() -> getSourcesFromMap());
234 public String[] getSourcesFromMap() {
235 return Optional.ofNullable(mediaInputSource).map(a -> a.supportedInputSourcesMap).map(a -> a.getInputList())
236 .orElse(new String[0]);
239 public String getSourcesString() {
240 return Arrays.asList(getSources()).stream().collect(Collectors.joining(","));
243 public String getInputSource() {
244 return Optional.ofNullable(mediaInputSource).map(a -> a.inputSource).map(a -> a.value).orElse("");
247 public int getInputSourceId() {
248 return IntStream.range(0, getSources().length).filter(i -> getSources()[i].equals(getInputSource()))
249 .findFirst().orElse(-1);
252 public Number getTvChannel() {
253 return Optional.ofNullable(tvChannel).map(a -> a.getChannelNum()).map(j -> parseTVChannel(j)).orElse(-1f);
256 public String getTvChannelName() {
257 return Optional.ofNullable(tvChannel).map(a -> a.tvChannelName).map(a -> a.value).orElse("");
260 public boolean isError() {
261 return Optional.ofNullable(error).isPresent();
264 public String getError() {
265 String code = Optional.ofNullable(error).map(a -> a.code).orElse("");
266 String message = Optional.ofNullable(error).map(a -> a.message).orElse("");
267 return String.format("%s, %s", code, message);
271 @NonNullByDefault({})
273 public JSONContent(String capability, String action, String value) {
274 Command command = new Command();
275 command.capability = capability;
276 command.command = action;
277 command.arguments = new String[] { value };
278 commands = new Command[] { command };
282 String component = "main";
291 @NonNullByDefault({})
292 class JSONSubscriptionFilter {
293 public JSONSubscriptionFilter(String deviceId) {
294 SubscriptionFilter sub = new SubscriptionFilter();
295 sub.value = new String[] { deviceId };
296 subscriptionFilters = new SubscriptionFilter[] { sub };
299 class SubscriptionFilter {
300 String type = "DEVICEIDS";
304 SubscriptionFilter[] subscriptionFilters;
305 String name = "OpenHAB Subscription";
308 @NonNullByDefault({})
309 class STSubscription {
311 String subscriptionId;
312 String registrationUrl;
315 SubscriptionFilters[] subscriptionFilters;
317 class SubscriptionFilters {
322 public String getSubscriptionId() {
323 return Optional.ofNullable(subscriptionId).orElse("");
326 public String getregistrationUrl() {
327 return Optional.ofNullable(registrationUrl).orElse("");
331 @NonNullByDefault({})
336 DeviceEvent deviceEvent;
337 Optional<TvValues> tvInfo = Optional.empty();
347 String capability; // example "sec.diagnosticsInformation"
348 String attribute; // example "dumpType"
349 JsonElement value; // example "id" or can be an array
353 String subscriptionName;
356 // Array of supportedInputSourcesMap
360 public String getId() {
361 return Optional.ofNullable(id).orElse("");
364 public String getName() {
365 return Optional.ofNullable(name).orElse("");
369 public String toString() {
370 return Map.of("id", getId(), "name", getName()).toString();
374 public String getCapability() {
375 return Optional.ofNullable(capability).orElse("");
378 public String getAttribute() {
379 return Optional.ofNullable(attribute).orElse("");
382 public String getValueType() {
383 return Optional.ofNullable(valueType).orElse("");
386 public List<?> getValuesAsList() throws JsonSyntaxException {
387 if ("array".equals(getValueType())) {
388 JsonArray resultArray = Optional.ofNullable((JsonArray) value.getAsJsonArray())
389 .orElse(new JsonArray());
391 if (resultArray.get(0) instanceof JsonObject) {
392 // Only for Array of supportedInputSourcesMap
393 ValuesList[] values = new Gson().fromJson(resultArray, ValuesList[].class);
394 List<ValuesList> result = Optional.ofNullable(values).map(a -> Arrays.asList(a))
395 .orElse(new ArrayList<ValuesList>());
396 return Optional.ofNullable(result).orElse(List.of());
398 List<String> result = new Gson().fromJson(resultArray, ArrayList.class);
399 return Optional.ofNullable(result).orElse(List.of());
401 } catch (IllegalStateException e) {
407 public String getValue() {
408 if ("string".equals(getValueType())) {
409 return Optional.ofNullable((String) value.getAsString()).orElse("");
415 public void setTvInfo(Optional<TvValues> tvInfo) {
416 this.tvInfo = tvInfo;
419 public boolean getCapabilityAttribute(String capability, String attribute) {
420 return Optional.ofNullable(deviceEvent).map(a -> a.getCapability()).filter(a -> a.equals(capability))
422 && Optional.ofNullable(deviceEvent).map(a -> a.getAttribute()).filter(a -> a.equals(attribute))
426 public String getSwitch() {
427 if (getCapabilityAttribute("switch", "switch")) {
428 return Optional.ofNullable(deviceEvent).map(a -> a.getValue()).orElse("");
433 public String getInputSource() {
434 if (getCapabilityAttribute("mediaInputSource", "inputSource")
435 || getCapabilityAttribute("samsungvd.mediaInputSource", "inputSource")) {
436 return Optional.ofNullable(deviceEvent).map(a -> a.getValue()).orElse("");
441 public String[] getInputSourceList() {
442 if (getCapabilityAttribute("mediaInputSource", "supportedInputSources")) {
443 return deviceEvent.getValuesAsList().toArray(String[]::new);
445 return new String[0];
448 public List<?> getInputSourceMapList() {
449 if (getCapabilityAttribute("samsungvd.mediaInputSource", "supportedInputSourcesMap")) {
450 return deviceEvent.getValuesAsList();
455 public int getInputSourceId() {
456 return this.tvInfo.map(t -> IntStream.range(0, t.getSources().length)
457 .filter(i -> t.getSources()[i].equals(getInputSource())).findFirst().orElse(-1)).orElse(-1);
460 public Number getTvChannel() {
461 if (getCapabilityAttribute("tvChannel", "tvChannel")) {
462 return Optional.ofNullable(deviceEvent).map(a -> a.getValue()).map(j -> parseTVChannel(j)).orElse(-1f);
467 public String getTvChannelName() {
468 if (getCapabilityAttribute("tvChannel", "tvChannelName")) {
469 return Optional.ofNullable(deviceEvent).map(a -> a.getValue()).orElse("");
475 public static Number parseTVChannel(@Nullable String channel) {
477 return (channel == null || channel.isBlank()) ? -1f
479 channel.replaceAll("\\D+", ".").replaceFirst("^\\D*((\\d+\\.\\d+)|(\\d+)).*", "$1"));
480 } catch (NumberFormatException ignore) {
485 public void updateTV() {
486 if (!tvInfo.isPresent()) {
488 tvInfo.ifPresent(t -> {
489 updateState(CHANNEL_NAME, t.getTvChannelName());
490 updateState(CHANNEL, t.getTvChannel());
491 updateState(SOURCE_NAME, t.getInputSource());
492 updateState(SOURCE_ID, t.getInputSourceId());
498 * Smartthings API HTTP interface
499 * Currently rate limited to 350 requests/minute
501 * @param method the method "GET" or "POST"
502 * @param uri as a URI
503 * @param content to POST (or null)
506 public Optional<String> sendUrl(HttpMethod method, URI uri, @Nullable InputStream content) throws IOException {
507 // need to add header "Authorization":"Bearer " + apiKey;
508 Properties headers = new Properties();
509 headers.put("Authorization", "Bearer " + this.apiKey);
510 logger.trace("{}: Sending {}", host, uri.toURL().toString());
511 Optional<String> response = Optional.ofNullable(HttpUtil.executeUrl(method.toString(), uri.toURL().toString(),
512 headers, content, "application/json", TIMEOUT));
513 if (!response.isPresent()) {
514 throw new IOException("No Data");
516 response.ifPresent(r -> logger.trace("{}: Got response: {}", host, r));
517 response.filter(r -> !r.startsWith("{")).ifPresent(r -> logger.debug("{}: Got response: {}", host, r));
522 * Smartthings API HTTP getter
523 * Currently rate limited to 350 requests/minute
525 * @param value the query to send
528 public synchronized Optional<TvValues> fetchTVProperties(String value) {
529 if (apiKey.isBlank()) {
530 return Optional.empty();
532 Optional<TvValues> tvValues = Optional.empty();
534 String api = API_ENDPOINT_V1 + ((deviceId.isBlank()) ? "" : "devices/") + deviceId + value;
535 URI uri = new URI("https", null, SMARTTHINGS_URL, 443, api, null, null);
536 Optional<String> response = sendUrl(HttpMethod.GET, uri, null);
537 tvValues = response.map(r -> new Gson().fromJson(r, TvValues.class));
538 if (!tvValues.isPresent()) {
539 throw new IOException("No Data - is DeviceID correct?");
541 tvValues.filter(t -> t.isError()).ifPresent(t -> logger.debug("{}: Error: {}", host, t.getError()));
543 } catch (JsonSyntaxException | URISyntaxException | IOException e) {
544 logger.debug("{}: Cannot connect to Smartthings Cloud: {}", host, e.getMessage());
545 if (errorCount++ > MAX_ERRORS) {
546 logger.warn("{}: Too many connection errors, disabling SmartThings", host);
554 * Smartthings API HTTP setter
555 * Currently rate limited to 350 requests/minute
557 * @param capability eg mediaInputSource
558 * @param command eg setInputSource
559 * @param value from acceptible list eg HDMI1, digitalTv, AM etc
560 * @return boolean true if successful
562 public synchronized boolean setTVProperties(String capability, String command, String value) {
563 if (apiKey.isBlank() || deviceId.isBlank()) {
566 Optional<String> response = Optional.empty();
568 String contentString = new Gson().toJson(new JSONContent(capability, command, value));
569 logger.trace("{}: content: {}", host, contentString);
570 InputStream content = new ByteArrayInputStream(contentString.getBytes());
571 String api = API_ENDPOINT_V1 + "devices/" + deviceId + COMMAND;
572 URI uri = new URI("https", null, SMARTTHINGS_URL, 443, api, null, null);
573 response = sendUrl(HttpMethod.POST, uri, content);
574 } catch (JsonSyntaxException | URISyntaxException | IOException e) {
575 logger.debug("{}: Send Command to Smartthings Cloud failed: {}", host, e.getMessage());
577 return response.map(r -> r.contains("ACCEPTED") || r.contains("COMPLETED")).orElse(false);
581 * Smartthings API Subscription
582 * Retrieves the Smartthings API Subscription from a remote service, performing an API call
586 public synchronized Optional<STSubscription> smartthingsSubscription() {
587 if (apiKey.isBlank() || deviceId.isBlank()) {
588 return Optional.empty();
590 Optional<STSubscription> stSub = Optional.empty();
592 logger.info("{}: SSE Creating Smartthings Subscription", host);
593 String contentString = new Gson().toJson(new JSONSubscriptionFilter(deviceId));
594 logger.trace("{}: subscription: {}", host, contentString);
595 InputStream subscriptionFilter = new ByteArrayInputStream(contentString.getBytes());
596 URI uri = new URI("https", null, SMARTTHINGS_URL, 443, "/subscriptions", null, null);
597 Optional<String> response = sendUrl(HttpMethod.POST, uri, subscriptionFilter);
598 stSub = response.map(r -> new Gson().fromJson(r, STSubscription.class));
599 if (!stSub.isPresent()) {
600 throw new IOException("No Data - is DeviceID correct?");
602 } catch (JsonSyntaxException | URISyntaxException | IOException e) {
603 logger.warn("{}: SSE Subscription to Smartthings Cloud failed: {}", host, e.getMessage());
608 public synchronized void startSSE() {
609 if (!subscriptionRunning) {
610 logger.trace("{}: SSE Starting job", host);
611 subscription = smartthingsSubscription();
612 logger.trace("{}: SSE got subscription ID: {}", host,
613 subscription.map(a -> a.getSubscriptionId()).orElse("None"));
614 if (!subscription.map(a -> a.getSubscriptionId()).orElse("").isBlank()) {
620 public void stopSSE() {
621 handlerWrapper.ifPresent(a -> {
623 logger.trace("{}: SSE Stopping job", host);
624 handlerWrapper = Optional.empty();
625 subscriptionRunning = false;
630 * SubscriberWrapper needed to make async SSE stream cancelable
633 @NonNullByDefault({})
634 private static class SubscriberWrapper implements BodySubscriber<Void> {
635 private final CountDownLatch latch;
636 private final BodySubscriber<Void> subscriber;
637 private Subscription subscription;
639 private SubscriberWrapper(BodySubscriber<Void> subscriber, CountDownLatch latch) {
640 this.subscriber = subscriber;
645 public CompletionStage<Void> getBody() {
646 return subscriber.getBody();
650 public void onSubscribe(Subscription subscription) {
651 subscriber.onSubscribe(subscription);
652 this.subscription = subscription;
657 public void onNext(List<ByteBuffer> item) {
658 subscriber.onNext(item);
662 public void onError(Throwable throwable) {
663 subscriber.onError(throwable);
667 public void onComplete() {
668 subscriber.onComplete();
671 public void cancel() {
672 subscription.cancel();
676 @NonNullByDefault({})
677 private static class BodyHandlerWrapper implements BodyHandler<Void> {
678 private final CountDownLatch latch = new CountDownLatch(1);
679 private final BodyHandler<Void> handler;
680 private SubscriberWrapper subscriberWrapper;
681 private int statusCode = -1;
683 private BodyHandlerWrapper(BodyHandler<Void> handler) {
684 this.handler = handler;
688 public BodySubscriber<Void> apply(ResponseInfo responseInfo) {
689 subscriberWrapper = new SubscriberWrapper(handler.apply(responseInfo), latch);
690 this.statusCode = responseInfo.statusCode();
691 return subscriberWrapper;
694 public void waitForEvent(boolean cancel) {
696 CompletableFuture.runAsync(() -> {
700 subscriberWrapper.cancel();
702 } catch (InterruptedException ignore) {
704 }).get(2, TimeUnit.SECONDS);
705 } catch (InterruptedException | ExecutionException | TimeoutException ignore) {
709 public int getStatusCode() {
714 public void cancel() {
719 public void receiveSSEEvents() {
720 subscription.ifPresent(sub -> {
723 URI uri = new URI(sub.getregistrationUrl());
724 HttpClient client = HttpClient.newHttpClient();
725 HttpRequest request = HttpRequest.newBuilder(uri).timeout(Duration.ofSeconds(2)).GET()
726 .header("Authorization", "Bearer " + this.apiKey).build();
727 handlerWrapper = Optional.ofNullable(
728 new BodyHandlerWrapper(HttpResponse.BodyHandlers.ofByteArrayConsumer(b -> processSSEEvent(b))));
729 handlerWrapper.ifPresent(h -> {
730 client.sendAsync(request, h);
732 logger.debug("{}: SSE job {}", host, checkResponseCode() ? "Started" : "Failed");
733 } catch (URISyntaxException e) {
734 logger.warn("{}: SSE URI Exception: {}", host, e.getMessage());
739 boolean checkResponseCode() {
740 int respCode = handlerWrapper.map(a -> a.getStatusCode()).orElse(-1);
741 logger.trace("{}: SSE GOT Response Code: {}", host, respCode);
742 subscriptionRunning = (respCode == 200);
743 return subscriptionRunning;
746 Map<String, String> bytesToMap(byte[] bytes) {
747 String s = new String(bytes, StandardCharsets.UTF_8);
748 // logger.trace("{}: SSE received: {}", host, s);
749 Map<String, String> properties = new HashMap<String, String>();
750 String[] pairs = s.split("\r?\n");
751 for (String pair : pairs) {
752 String[] kv = pair.split(":", 2);
753 properties.put(kv[0].trim(), kv[1].trim());
755 logger.trace("{}: SSE received: {}", host, properties);
760 synchronized void processSSEEvent(Optional<byte[]> bytes) {
761 bytes.ifPresent(b -> {
762 Map<String, String> properties = bytesToMap(b);
763 String rawData = properties.getOrDefault("data", "none");
764 String event = properties.getOrDefault("event", "none");
765 // logger.trace("{}: SSE Decoding event: {}", host, event);
767 case "CONTROL_EVENT":
768 subscriptionRunning = "welcome".equals(rawData);
769 if (!subscriptionRunning) {
770 logger.trace("{}: SSE Subscription ended", host);
777 Optional<STSSEData> data = Optional.ofNullable(new Gson().fromJson(rawData, STSSEData.class));
778 data.ifPresentOrElse(d -> {
780 String[] inputList = d.getInputSourceList();
781 if (inputList.length > 0) {
782 logger.trace("{}: SSE Got input source list: {}", host, Arrays.asList(inputList));
783 tvInfo.ifPresent(a -> a.updateSupportedInputSources(inputList));
785 String inputSource = d.getInputSource();
786 if (!inputSource.isBlank()) {
787 updateState(SOURCE_NAME, inputSource);
788 int sourceId = d.getInputSourceId();
789 logger.trace("{}: SSE Got input source: {} ID: {}", host, inputSource, sourceId);
790 updateState(SOURCE_ID, sourceId);
792 Number tvChannel = d.getTvChannel();
793 if (tvChannel.intValue() != -1) {
794 logger.trace("{}: SSE Got TV Channel: {}", host, tvChannel);
795 updateState(CHANNEL, tvChannel);
797 String tvChannelName = d.getTvChannelName();
798 if (!tvChannelName.isBlank()) {
799 logger.trace("{}: SSE Got TV Channel Name: {}", host, tvChannelName);
800 updateState(CHANNEL_NAME, tvChannelName);
802 String Power = d.getSwitch();
803 if (!Power.isBlank()) {
804 logger.debug("{}: SSE Got TV Power: {}", host, Power);
805 if ("on".equals(Power)) {
806 // handler.putOnline(); // ignore on event for now
808 // handler.setOffline(); // ignore off event for now
811 }, () -> logger.warn("{}: SSE Received NULL data", host));
812 } catch (JsonSyntaxException e) {
813 logger.warn("{}: SmartThingsApiService: Error ({}) in message: {}", host, e.getMessage(),
818 logger.trace("{}: SSE not handling event: {}", host, event);
824 private boolean updateDeviceID(TvValues.Items item) {
825 this.deviceId = item.getDeviceId();
826 logger.debug("{}: found {} device, adding device id {}", host, item.getName(), deviceId);
827 handler.putConfig(SMARTTHINGS_DEVICEID, deviceId);
832 public boolean fetchdata() {
833 if (System.currentTimeMillis() >= prevUpdate + RATE_LIMIT) {
834 if (deviceId.isBlank()) {
835 tvInfo = fetchTVProperties(DEVICES);
836 boolean found = false;
837 if (tvInfo.isPresent()) {
838 TvValues t = tvInfo.get();
839 switch (t.getItems().length) {
842 logger.warn("{}: No devices found - please add your TV to the Smartthings app", host);
845 found = Arrays.asList(t.getItems()).stream().filter(a -> "Samsung TV".equals(a.getName()))
846 .map(a -> updateDeviceID(a)).findFirst().orElse(false);
849 logger.warn("{}: No device Id selected, please enter one of the following:", host);
850 Arrays.asList(t.getItems()).stream().forEach(a -> logger.info("{}: '{}' : {}({})", host,
851 a.getDeviceId(), a.getName(), a.getLabel()));
861 tvInfo = fetchTVProperties(COMPONENTS);
862 prevUpdate = System.currentTimeMillis();
864 return (tvInfo.isPresent());
868 public void start() {
881 public void clearCache() {
887 public boolean isUpnp() {
892 public boolean checkConnection() {
897 public boolean handleCommand(String channel, Command command) {
898 logger.trace("{}: Received channel: {}, command: {}", host, channel, command);
899 if (!checkConnection()) {
900 logger.trace("{}: Smartthings offline", host);
905 return tvInfo.map(t -> {
906 boolean result = false;
907 if (command == RefreshType.REFRESH) {
910 updateState(CHANNEL_NAME, t.getTvChannelName());
913 updateState(CHANNEL, t.getTvChannel());
917 updateState(SOURCE_NAME, t.getInputSource());
918 updateState(SOURCE_ID, t.getInputSourceId());
928 if (command instanceof DecimalType commandAsDecimalType) {
929 int val = commandAsDecimalType.intValue();
930 if (val >= 0 && val < t.getSources().length) {
931 result = setSourceName(t.getSources()[val]);
933 logger.warn("{}: Invalid source ID: {}, acceptable: 0..{}", host, command,
934 t.getSources().length);
939 if (command instanceof StringType) {
940 if (t.getSourcesString().contains(command.toString()) || t.getSourcesString().isBlank()) {
941 result = setSourceName(command.toString());
943 logger.warn("{}: Invalid source Name: {}, acceptable: {}", host, command,
944 t.getSourcesString());
949 logger.warn("{}: Samsung TV doesn't support transmitting for channel '{}'", host, channel);
952 logger.warn("{}: Smartthings: wrong command type {} channel {}", host, command, channel);
960 private void updateState(String channel, Object value) {
961 if (!stateMap.getOrDefault(channel, "None").equals(value)) {
965 handler.valueReceived(channel, new DecimalType((Number) value));
968 handler.valueReceived(channel, new StringType((String) value));
971 stateMap.put(channel, value);
973 logger.trace("{}: Value '{}' for {} hasn't changed, ignoring update", host, value, channel);
977 private boolean setSourceName(String value) {
978 return setTVProperties("mediaInputSource", "setInputSource", value);