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.protocol;
15 import static org.openhab.binding.samsungtv.internal.SamsungTvBindingConstants.*;
17 import java.io.BufferedOutputStream;
18 import java.io.ByteArrayInputStream;
19 import java.io.DataOutputStream;
20 import java.io.IOException;
21 import java.net.Socket;
22 import java.net.URLConnection;
23 import java.nio.ByteBuffer;
24 import java.nio.charset.StandardCharsets;
25 import java.security.KeyManagementException;
26 import java.security.NoSuchAlgorithmException;
27 import java.security.cert.X509Certificate;
28 import java.time.Instant;
29 import java.time.ZoneId;
30 import java.time.format.DateTimeFormatter;
31 import java.util.ArrayList;
32 import java.util.Arrays;
33 import java.util.Collections;
34 import java.util.HashMap;
36 import java.util.Optional;
37 import java.util.concurrent.TimeUnit;
39 import javax.net.ssl.SSLContext;
40 import javax.net.ssl.SSLSocket;
41 import javax.net.ssl.SSLSocketFactory;
42 import javax.net.ssl.TrustManager;
43 import javax.net.ssl.X509TrustManager;
45 import org.eclipse.jdt.annotation.NonNullByDefault;
46 import org.eclipse.jdt.annotation.Nullable;
47 import org.openhab.core.library.types.DecimalType;
48 import org.openhab.core.library.types.OnOffType;
49 import org.openhab.core.library.types.PercentType;
50 import org.openhab.core.library.types.RawType;
51 import org.openhab.core.library.types.StringType;
52 import org.openhab.core.types.State;
53 import org.openhab.core.types.UnDefType;
54 import org.slf4j.Logger;
55 import org.slf4j.LoggerFactory;
57 import com.google.gson.JsonElement;
58 import com.google.gson.JsonSyntaxException;
61 * Websocket class to retrieve artmode status (on o.a. the Frame TV's)
63 * @author Arjan Mels - Initial contribution
64 * @author Nick Waterton - added slideshow handling, upload/download, refactoring
67 class WebSocketArt extends WebSocketBase {
68 private final Logger logger = LoggerFactory.getLogger(WebSocketArt.class);
70 private String host = "";
71 private String className = "";
72 private String slideShowDuration = "off";
73 // Favourites is default
74 private String categoryId = "MY-C0004";
75 private String lastThumbnail = "";
76 private boolean slideshow = false;
77 public byte[] imageBytes = new byte[0];
78 public String fileType = "jpg";
79 private long connection_id_random = 2705890518L;
80 private static final DateTimeFormatter DATEFORMAT = DateTimeFormatter.ofPattern("yyyy:MM:dd HH:mm:ss")
81 .withZone(ZoneId.systemDefault());
82 private Map<String, String> stateMap = Collections.synchronizedMap(new HashMap<>());
83 private @Nullable SSLSocketFactory sslsocketfactory = null;
86 * @param remoteControllerWebSocket
88 WebSocketArt(RemoteControllerWebSocket remoteControllerWebSocket) {
89 super(remoteControllerWebSocket);
90 this.host = remoteControllerWebSocket.host;
91 this.className = this.getClass().getSimpleName();
94 SSLContext sslContext = SSLContext.getInstance("TLS");
95 sslContext.init(null, acceptAlltrustManagers(), null);
96 sslsocketfactory = sslContext.getSocketFactory();
97 } catch (KeyManagementException | NoSuchAlgorithmException e) {
98 logger.debug("{}: sslsocketfactory failed to initialize: {}", host, e.getMessage());
102 @NonNullByDefault({})
103 @SuppressWarnings("unused")
104 private class JSONMessage {
112 String current_content_id;
115 int current_rotation_status;
122 public String getEvent() {
123 return Optional.ofNullable(event).orElse("");
126 public String getStatus() {
127 return Optional.ofNullable(status).orElse("");
130 public int getVersion() {
131 return Optional.ofNullable(version).map(a -> a.replace(".", "")).map(Integer::parseInt).orElse(0);
134 public String getValue() {
135 return Optional.ofNullable(value).orElse(getStatus());
138 public int getIntValue() {
139 return Optional.of(Integer.valueOf(getValue())).orElse(0);
142 public String getCategoryId() {
143 return Optional.ofNullable(category_id).orElse("");
146 public String getContentId() {
147 return Optional.ofNullable(content_id).orElse(getCurrentContentId());
150 public String getCurrentContentId() {
151 return Optional.ofNullable(current_content_id).orElse("");
154 public int getRotationStatus() {
155 return Optional.ofNullable(Integer.valueOf(current_rotation_status)).orElse(0);
158 public String getType() {
159 return Optional.ofNullable(type).orElse("");
162 public String getIsShown() {
163 return Optional.ofNullable(is_shown).orElse("No");
166 public String getFileType() {
167 return Optional.ofNullable(file_type).orElse("");
170 public String getConnInfo() {
171 return Optional.ofNullable(conn_info).orElse("");
174 public String getData() {
175 return Optional.ofNullable(data).orElse("");
184 BinaryData(byte[] data, int off, int len) {
190 public byte[] getBinaryData() {
191 return Optional.ofNullable(data).orElse(new byte[0]);
194 public int getOff() {
195 return Optional.ofNullable(off).orElse(0);
198 public int getLen() {
199 return Optional.ofNullable(len).orElse(0);
203 class ArtmodeSettings {
210 public String getItem() {
211 return Optional.ofNullable(item).orElse("");
214 public int getValue() {
215 return Optional.ofNullable(value).map(a -> Integer.valueOf(a)).orElse(0);
236 public String getContentInfo() {
237 return Optional.ofNullable(contentInfo).orElse("");
240 public String getIp() {
241 return Optional.ofNullable(ip).orElse("");
244 public int getPort() {
245 return Optional.ofNullable(port).map(Integer::parseInt).orElse(0);
248 public String getKey() {
249 return Optional.ofNullable(key).orElse("");
252 public boolean getSecured() {
253 return Optional.ofNullable(secured).orElse(false);
258 String connection_id;
268 Header(int fileLength) {
269 this.fileLength = String.valueOf(fileLength);
272 public int getFileLength() {
273 return Optional.ofNullable(fileLength).map(Integer::parseInt).orElse(0);
276 public String getFileType() {
277 return Optional.ofNullable(fileType).orElse("");
280 public String getFileID() {
281 return Optional.ofNullable(fileID).orElse("");
285 // data is sometimes a json object, sometimes a string representation of a json object for d2d_service_message
291 public String getEvent() {
292 return Optional.ofNullable(event).orElse("");
295 public String getData() {
296 return Optional.ofNullable(data).map(a -> a.getAsString()).orElse("");
299 public void putBinaryData(byte[] arr, int off, int len) {
300 this.binData = new BinaryData(arr, off, len);
303 public BinaryData getBinaryData() {
304 return Optional.ofNullable(binData).orElse(new BinaryData(new byte[0], 0, 0));
309 public void onWebSocketBinary(byte @Nullable [] arr, int off, int len) {
313 super.onWebSocketBinary(arr, off, len);
314 String msg = extractMsg(arr, off, len, true);
315 // offset is start of binary data
316 int offset = ByteBuffer.wrap(arr, off, len).getShort() + off + 2; // 2 = length of Short
318 JSONMessage jsonMsg = remoteControllerWebSocket.gson.fromJson(msg, JSONMessage.class);
319 if (jsonMsg == null) {
322 switch (jsonMsg.getEvent()) {
323 case "d2d_service_message":
324 jsonMsg.putBinaryData(arr, offset, len);
325 handleD2DServiceMessage(jsonMsg);
328 logger.debug("{}: WebSocketArt(binary) Unknown event: {}", host, msg);
330 } catch (JsonSyntaxException e) {
331 logger.warn("{}: {}: Error ({}) in message: {}", host, className, e.getMessage(), msg);
336 public synchronized void onWebSocketText(@Nullable String msgarg) {
337 if (msgarg == null) {
340 String msg = msgarg.replace('\n', ' ');
341 super.onWebSocketText(msg);
343 JSONMessage jsonMsg = remoteControllerWebSocket.gson.fromJson(msg, JSONMessage.class);
344 if (jsonMsg == null) {
347 switch (jsonMsg.getEvent()) {
348 case "ms.channel.connect":
349 logger.debug("{}: Art channel connected", host);
351 case "ms.channel.ready":
352 logger.debug("{}: Art channel ready", host);
354 if (remoteControllerWebSocket.callback.getArtMode2022()) {
355 remoteControllerWebSocket.callback.setArtMode2022(false);
356 remoteControllerWebSocket.callback.setArtModeSupported(true);
357 logger.info("{}: Art Mode has been renabled on Frame TV's >= 2022", host);
360 getArtmodeStatus("get_api_version");
361 getArtmodeStatus("api_version");
362 getArtmodeStatus("get_slideshow_status");
363 getArtmodeStatus("get_auto_rotation_status");
364 getArtmodeStatus("get_current_artwork");
365 getArtmodeStatus("get_color_temperature");
366 getArtmodeStatus("get_current_rotation");
368 case "ms.channel.clientConnect":
369 logger.debug("{}: Another Art client has connected", host);
371 case "ms.channel.clientDisconnect":
372 logger.debug("{}: Other Art client has disconnected", host);
375 case "d2d_service_message":
376 handleD2DServiceMessage(jsonMsg);
379 logger.debug("{}: WebSocketArt Unknown event: {}", host, msg);
381 } catch (JsonSyntaxException e) {
382 logger.warn("{}: {}: Error ({}) in message: {}", host, className, e.getMessage(), msg);
387 * handle D2DServiceMessages
389 * @param jsonMsg JSONMessage
392 private synchronized void handleD2DServiceMessage(JSONMessage jsonMsg) {
393 String msg = jsonMsg.getData();
395 JSONMessage.Data data = remoteControllerWebSocket.gson.fromJson(msg, JSONMessage.Data.class);
397 logger.debug("{}: Empty d2d_service_message event", host);
400 // remove returns and white space for ART_JSON channel
401 valueReceived(ART_JSON, new StringType(msg.trim().replaceAll("\\n|\\\\n", "").replaceAll("\\s{2,}", " ")));
402 switch (data.getEvent()) {
404 logger.debug("{}: ERROR event: {}", host, msg);
407 // old (2021) version is "2.03", new (2022) version is "4.3.4.0"
408 logger.debug("{}: {}: {}", host, data.getEvent(), data.getVersion());
409 if (data.getVersion() >= 4000) {
412 logger.debug("{}: API Version set to: {}", host, getArtApiVersion());
415 case "image_deleted":
416 case "set_artmode_status":
417 case "get_content_list":
418 case "recently_set_updated":
419 case "preview_started":
420 case "preview_stopped":
421 case "favorite_changed":
425 case "get_artmode_settings":
426 logger.debug("{}: {}: {}", host, data.getEvent(), data.getData());
427 msg = data.getData();
428 if (!msg.isBlank()) {
429 JSONMessage.ArtmodeSettings[] artmodeSettings = remoteControllerWebSocket.gson.fromJson(msg,
430 JSONMessage.ArtmodeSettings[].class);
431 if (artmodeSettings != null) {
432 for (JSONMessage.ArtmodeSettings setting : artmodeSettings) {
433 // extract brightness and colour temperature here
434 if ("brightness".equals(setting.getItem())) {
435 valueReceived(ART_BRIGHTNESS, new PercentType(setting.getValue() * 10));
437 if ("color_temperature".equals(setting.getItem())) {
438 valueReceived(ART_COLOR_TEMPERATURE, new DecimalType(setting.getValue()));
444 case "current_rotation_status":
445 case "get_current_rotation":
446 // Landscape = 1, Portrait = 2
447 valueReceived(ART_ORIENTATION, OnOffType.from(data.getRotationStatus() == 2));
449 case "set_brightness":
450 case "brightness_changed":
452 valueReceived(ART_BRIGHTNESS, new PercentType(data.getIntValue() * 10));
454 case "set_color_temperature":
455 case "color_temperature_changed":
456 case "color_temperature":
457 valueReceived(ART_COLOR_TEMPERATURE, new DecimalType(data.getIntValue()));
459 case "get_artmode_status":
460 case "art_mode_changed":
461 case "artmode_status":
462 logger.debug("{}: {}: {}", host, data.getEvent(), data.getValue());
463 if ("off".equals(data.getValue())) {
464 remoteControllerWebSocket.callback.powerUpdated(true, false);
465 remoteControllerWebSocket.callback.currentAppUpdated("");
467 remoteControllerWebSocket.callback.powerUpdated(false, true);
469 if (!remoteControllerWebSocket.noApps()) {
470 remoteControllerWebSocket.updateCurrentApp();
473 case "slideshow_image_changed":
474 case "slideshow_changed":
475 case "get_slideshow_status":
476 case "auto_rotation_changed":
477 case "auto_rotation_image_changed":
478 case "auto_rotation_status":
479 // value (duration) is "off" or "number" where number is duration in minutes
480 // data.type: "shuffleslideshow" or "slideshow"
481 // data.current_content_id: Current art displayed eg "MY_F0005"
482 // data.category_id: category eg 'MY-C0004' ie favouries or my Photos/shelf
483 if (!data.getValue().isBlank()) {
484 slideShowDuration = data.getValue();
485 slideshow = !"off".equals(data.getValue());
487 categoryId = (data.getCategoryId().isBlank()) ? categoryId : data.getCategoryId();
488 if (!data.getContentId().isBlank() && slideshow) {
489 remoteControllerWebSocket.callback.currentAppUpdated(
490 String.format("%s %s %s", data.getType(), slideShowDuration, categoryId));
492 logger.trace("{}: slideshow: {}, {}, {}, {}", host, data.getEvent(), data.getType(),
493 data.getValue(), data.getContentId());
496 if (!data.getCategoryId().isBlank()) {
497 logger.debug("{}: Image added: {}, category: {}", host, data.getContentId(),
498 data.getCategoryId());
501 case "get_current_artwork":
503 case "current_artwork":
504 case "image_selected":
505 // data.content_id: Current art displayed eg "MY_F0005"
506 // data.is_shown: "Yes" or "No"
507 if ("Yes".equals(data.getIsShown())) {
509 remoteControllerWebSocket.callback.currentAppUpdated("artMode");
512 valueReceived(ART_LABEL, new StringType(data.getContentId()));
513 if (remoteControllerWebSocket.callback.handler.isChannelLinked(ART_IMAGE)) {
514 if (data.getEvent().contains("current_artwork") || "Yes".equals(data.getIsShown())) {
515 getThumbnail(data.getContentId());
519 case "get_thumbnail_list":
521 logger.trace("{}: thumbnail: Fetching {}", host, data.getContentId());
523 // upload image (should be 3840x2160 pixels in size)
524 msg = data.getConnInfo();
525 if (!msg.isBlank()) {
526 JSONMessage.Contentinfo contentInfo = remoteControllerWebSocket.gson.fromJson(msg,
527 JSONMessage.Contentinfo.class);
528 if (contentInfo != null) {
529 // NOTE: do not tie up the websocket receive loop for too long, so use the scheduler
530 // upload image, or download thumbnail
531 scheduleSocketOperation(contentInfo, "ready_to_use".equals(data.getEvent()));
534 // <2019 (ish) Frame TV's return thumbnails as binary data
535 receiveThumbnail(jsonMsg.getBinaryData());
538 case "go_to_standby":
539 logger.debug("{}: go_to_standby", host);
540 remoteControllerWebSocket.callback.powerUpdated(false, false);
541 remoteControllerWebSocket.callback.setOffline();
546 logger.debug("{}: wakeup from standby", host);
547 // check artmode status to know complete status before updating
549 getArtmodeStatus((getArtApiVersion() == 0) ? "get_auto_rotation_status" : "get_slideshow_status");
550 getArtmodeStatus("get_current_artwork");
551 getArtmodeStatus("get_color_temperature");
554 logger.debug("{}: Unknown d2d_service_message event: {}", host, msg);
556 } catch (JsonSyntaxException e) {
557 logger.warn("{}: {}: Error ({}) in message: {}", host, className, e.getMessage(), msg);
561 public void valueReceived(String variable, State value) {
562 if (!stateMap.getOrDefault(variable, "").equals(value.toString())) {
563 remoteControllerWebSocket.callback.handler.valueReceived(variable, value);
564 stateMap.put(variable, value.toString());
566 logger.trace("{}: Value '{}' for {} hasn't changed, ignoring update", host, value, variable);
570 public int getArtApiVersion() {
571 return remoteControllerWebSocket.callback.handler.artApiVersion;
574 public void setArtApiVersion(int apiVersion) {
575 remoteControllerWebSocket.callback.handler.artApiVersion = apiVersion;
579 * creates formatted json string for art websocket commands
581 * @param request Array of string requests to format
584 @NonNullByDefault({})
585 class JSONArtModeStatus {
586 public JSONArtModeStatus(String[] request) {
587 Params.Data data = params.new Data();
588 if (request.length == 1) {
589 if (request[0].endsWith("}")) {
590 // full json request/command
591 request = request[0].split(",");
593 // send simple command in request[0]
594 data.request = request[0];
595 params.data = remoteControllerWebSocket.gson.toJson(data);
599 switch (request[0]) {
600 // predefined requests/commands
601 case "set_slideshow_status":
602 case "set_auto_rotation_status":
603 data.request = request[0];
604 data.type = request[1];
605 data.value = request[2];
606 data.category_id = request[3];
607 params.data = remoteControllerWebSocket.gson.toJson(data);
609 case "set_brightness":
610 case "set_color_temperature":
611 data.request = request[0];
612 data.value = request[1];
613 params.data = remoteControllerWebSocket.gson.toJson(data);
615 case "get_thumbnail":
616 data.request = request[0];
617 data.content_id = request[1];
618 data.conn_info = new Conninfo();
619 params.data = remoteControllerWebSocket.gson.toJson(data);
621 case "get_thumbnail_list":
622 connection_id_random++;
623 data.request = request[0];
624 Content_id_list content_id = new Content_id_list();
625 content_id.content_id = request[1];
626 data.content_id_list = new Content_id_list[] { content_id };
627 data.conn_info = new Conninfo();
628 params.data = remoteControllerWebSocket.gson.toJson(data);
631 data.request = request[0];
632 data.content_id = request[1];
634 params.data = remoteControllerWebSocket.gson.toJson(data);
637 RawType image = RawType.valueOf(request[1]);
638 fileType = image.getMimeType().split("/")[1];
639 imageBytes = image.getBytes();
640 data.request = request[0];
641 data.request_id = remoteControllerWebSocket.uuid.toString();
642 data.file_type = fileType;
643 data.conn_info = new Conninfo();
644 data.image_date = DATEFORMAT.format(Instant.now());
645 // data.matte_id = "flexible_polar";
646 // data.portrait_matte_id = "flexible_polar";
647 data.file_size = Long.valueOf(imageBytes.length);
648 params.data = remoteControllerWebSocket.gson.toJson(data);
651 // Just return formatted json (add id if needed)
652 if (Arrays.stream(request).anyMatch(a -> a.contains("\"id\""))) {
653 params.data = String.join(",", request).replace(",}", "}");
655 ArrayList<String> requestList = new ArrayList<>(Arrays.asList(request));
656 requestList.add(requestList.size() - 1,
657 String.format("\"id\":\"%s\"", remoteControllerWebSocket.uuid.toString()));
658 params.data = String.join(",", requestList).replace(",}", "}");
666 String request = "get_artmode_status";
670 Content_id_list[] content_id_list = null;
680 String id = remoteControllerWebSocket.uuid.toString();
684 String event = "art_app_request";
690 String d2d_mode = "socket";
691 // this is a random number usually
692 // long connection_id = 2705890518L;
693 long connection_id = connection_id_random;
695 String id = remoteControllerWebSocket.uuid.toString();
698 class Content_id_list {
702 String method = "ms.channel.emit";
703 Params params = new Params();
706 public void getThumbnail(String content_id) {
707 if (!content_id.equals(lastThumbnail) || "NULL".equals(stateMap.getOrDefault(ART_IMAGE, "NULL"))) {
708 getArtmodeStatus((getArtApiVersion() == 0) ? "get_thumbnail" : "get_thumbnail_list", content_id);
709 lastThumbnail = content_id;
711 logger.trace("{}: NOT getting thumbnail for: {} as it hasn't changed", host, content_id);
716 * Extract header message from binary data
717 * <2019 (ish) Frame TV's return some messages as binary data
718 * First two bytes are a short giving the header length
719 * header is a D2DServiceMessages followed by the binary image data.
721 * Also Extract header information from image downloaded via socket
722 * in which case first four bytes are the header length followed by the binary image data.
724 * @param byte[] payload
725 * @param int offset (usually 0)
727 * @param boolean fromBinMsg true if this was received as a binary message (header length is a Short)
730 public String extractMsg(byte[] payload, int offset, int len, boolean fromBinMsg) {
731 ByteBuffer buf = ByteBuffer.wrap(payload, offset, len);
732 int headerlen = fromBinMsg ? buf.getShort() : buf.getInt();
733 offset += fromBinMsg ? Short.BYTES : Integer.BYTES;
734 String type = fromBinMsg ? "D2DServiceMessages(from binary)" : "image header";
735 String header = new String(payload, offset, headerlen, StandardCharsets.UTF_8);
736 logger.trace("{}: Got {}: {}", host, type, header);
741 * Receive thumbnail from binary data returned by TV in response to get_thumbnail command
742 * <2019 (ish) Frame TV's return thumbnails as binary data.
744 * @param JSONMessage.BinaryData
747 public void receiveThumbnail(JSONMessage.BinaryData binaryData) {
748 extractThumbnail(binaryData.getBinaryData(), binaryData.getLen() - binaryData.getOff());
752 * Return a no-op SSL trust manager which will not verify server or client certificates.
754 private TrustManager[] acceptAlltrustManagers() {
755 return new TrustManager[] { new X509TrustManager() {
757 public void checkClientTrusted(final X509Certificate @Nullable [] chain, final @Nullable String authType) {
761 public void checkServerTrusted(final X509Certificate @Nullable [] chain, final @Nullable String authType) {
765 public X509Certificate @Nullable [] getAcceptedIssuers() {
771 public void scheduleSocketOperation(JSONMessage.Contentinfo contentInfo, boolean upload) {
772 logger.trace("{}: scheduled scheduleSocketOperation()", host);
773 remoteControllerWebSocket.callback.handler.getScheduler().schedule(() -> {
775 uploadImage(contentInfo);
777 downloadThumbnail(contentInfo);
779 }, 50, TimeUnit.MILLISECONDS);
783 * Download thumbnail of current selected image/jpeg from ip+port
785 * @param contentinfo Contentinfo containing ip address and port to download from
788 public void downloadThumbnail(JSONMessage.Contentinfo contentInfo) {
789 logger.trace("{}: thumbnail: downloading from: ip:{}, port:{}, secured:{}", host, contentInfo.getIp(),
790 contentInfo.getPort(), contentInfo.getSecured());
793 if (contentInfo.getSecured()) {
794 if (sslsocketfactory != null) {
795 logger.trace("{}: thumbnail SSL socket connecting", host);
796 socket = sslsocketfactory.createSocket(contentInfo.getIp(), contentInfo.getPort());
798 logger.debug("{}: sslsocketfactory is null", host);
802 socket = new Socket(contentInfo.getIp(), contentInfo.getPort());
804 if (socket != null) {
805 logger.trace("{}: thumbnail socket connected", host);
806 byte[] payload = Optional.ofNullable(socket.getInputStream().readAllBytes()).orElse(new byte[0]);
808 if (payload.length > 0) {
809 String header = extractMsg(payload, 0, payload.length, false);
810 JSONMessage.Header headerData = Optional
811 .ofNullable(remoteControllerWebSocket.gson.fromJson(header, JSONMessage.Header.class))
812 .orElse(new JSONMessage().new Header(0));
813 extractThumbnail(payload, headerData.getFileLength());
815 logger.trace("{}: thumbnail no data received", host);
816 valueReceived(ART_IMAGE, UnDefType.NULL);
819 } catch (IOException e) {
820 logger.warn("{}: Error downloading thumbnail {}", host, e.getMessage());
825 * Extract thumbnail from payload
827 * @param payload byte[] containing binary data and possibly header info
828 * @param fileLength int with image file size
831 public void extractThumbnail(byte[] payload, int fileLength) {
833 byte[] image = new byte[fileLength];
834 ByteBuffer.wrap(image).put(payload, payload.length - fileLength, fileLength);
835 String ftype = Optional
836 .ofNullable(URLConnection.guessContentTypeFromStream(new ByteArrayInputStream(image)))
837 .orElseThrow(() -> new Exception("Unable to determine image type"));
838 valueReceived(ART_IMAGE, new RawType(image, ftype));
839 } catch (Exception e) {
840 if (logger.isTraceEnabled()) {
841 logger.trace("{}: Error extracting thumbnail: ", host, e);
843 logger.warn("{}: Error extracting thumbnail {}", host, e.getMessage());
848 @NonNullByDefault({})
849 @SuppressWarnings("unused")
850 private class JSONHeader {
851 public JSONHeader(int num, int total, long fileLength, String fileType, String secKey) {
854 this.fileLength = fileLength;
855 this.fileType = fileType;
856 this.secKey = secKey;
862 String fileName = "dummy";
865 String version = "0.0.1";
869 * Upload Image from ART_IMAGE/ART_LABEL channel
871 * @param contentinfo Contentinfo containing ip address, port and key to upload to
873 * imageBytes and fileType are class instance variables obtained from the
874 * getArtmodeStatus() command that triggered the upload.
877 public void uploadImage(JSONMessage.Contentinfo contentInfo) {
878 logger.trace("{}: Uploading image to ip:{}, port:{}", host, contentInfo.getIp(), contentInfo.getPort());
881 if (contentInfo.getSecured()) {
882 if (sslsocketfactory != null) {
883 logger.trace("{}: upload SSL socket connecting", host);
884 socket = (SSLSocket) sslsocketfactory.createSocket(contentInfo.getIp(), contentInfo.getPort());
886 logger.debug("{}: sslsocketfactory is null", host);
890 socket = new Socket(contentInfo.getIp(), contentInfo.getPort());
892 if (socket != null) {
893 logger.trace("{}: upload socket connected", host);
894 DataOutputStream dataOutputStream = new DataOutputStream(
895 new BufferedOutputStream(socket.getOutputStream()));
896 String header = remoteControllerWebSocket.gson
897 .toJson(new JSONHeader(0, 1, imageBytes.length, fileType, contentInfo.getKey()));
898 logger.debug("{}: Image header: {}, {} bytes", host, header, header.length());
899 dataOutputStream.writeInt(header.length());
900 dataOutputStream.writeBytes(header);
901 dataOutputStream.write(imageBytes, 0, imageBytes.length);
902 dataOutputStream.flush();
903 logger.debug("{}: wrote Image:{} {} bytes to TV", host, fileType, dataOutputStream.size());
906 } catch (IOException e) {
907 logger.warn("{}: Error writing image to TV {}", host, e.getMessage());
914 * @param command split on ,space or + where
916 * First parameter is shuffleslideshow or slideshow
917 * Second is duration in minutes or off
918 * Third is category where the value is somethng like MY-C0004 = Favourites or MY-C0002 = My Photos.
921 public void setSlideshow(String command) {
922 String[] cmd = command.split("[, +]");
923 if (cmd.length <= 1) {
924 logger.warn("{}: Invalid slideshow command: {}", host, command);
927 String value = ("0".equals(cmd[1])) ? "off" : cmd[1];
928 categoryId = (cmd.length >= 3) ? cmd[2] : categoryId;
929 getArtmodeStatus((getArtApiVersion() == 0) ? "set_auto_rotation_status" : "set_slideshow_status",
930 cmd[0].toLowerCase(), value, categoryId);
934 * Send commands to Frame TV Art websocket channel
936 * @param optionalRequests Array of string requests
939 void getArtmodeStatus(String... optionalRequests) {
940 if (optionalRequests.length == 0) {
941 optionalRequests = new String[] { "get_artmode_status" };
943 if (getArtApiVersion() != 0) {
944 if ("get_brightness".equals(optionalRequests[0])) {
945 optionalRequests = new String[] { "get_artmode_settings" };
947 if ("get_color_temperature".equals(optionalRequests[0])) {
948 optionalRequests = new String[] { "get_artmode_settings" };
951 sendCommand(remoteControllerWebSocket.gson.toJson(new JSONArtModeStatus(optionalRequests)));