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.PercentType;
49 import org.openhab.core.library.types.RawType;
50 import org.openhab.core.library.types.StringType;
51 import org.openhab.core.types.State;
52 import org.openhab.core.types.UnDefType;
53 import org.slf4j.Logger;
54 import org.slf4j.LoggerFactory;
56 import com.google.gson.JsonElement;
57 import com.google.gson.JsonSyntaxException;
60 * Websocket class to retrieve artmode status (on o.a. the Frame TV's)
62 * @author Arjan Mels - Initial contribution
63 * @author Nick Waterton - added slideshow handling, upload/download, refactoring
66 class WebSocketArt extends WebSocketBase {
67 private final Logger logger = LoggerFactory.getLogger(WebSocketArt.class);
69 private String host = "";
70 private String className = "";
71 private String slideShowDuration = "off";
72 // Favourites is default
73 private String categoryId = "MY-C0004";
74 private String lastThumbnail = "";
75 private boolean slideshow = false;
76 public byte[] imageBytes = new byte[0];
77 public String fileType = "jpg";
78 private long connection_id_random = 2705890518L;
79 private static final DateTimeFormatter DATEFORMAT = DateTimeFormatter.ofPattern("yyyy:MM:dd HH:mm:ss")
80 .withZone(ZoneId.systemDefault());
81 private Map<String, String> stateMap = Collections.synchronizedMap(new HashMap<>());
82 private @Nullable SSLSocketFactory sslsocketfactory = null;
85 * @param remoteControllerWebSocket
87 WebSocketArt(RemoteControllerWebSocket remoteControllerWebSocket) {
88 super(remoteControllerWebSocket);
89 this.host = remoteControllerWebSocket.host;
90 this.className = this.getClass().getSimpleName();
93 SSLContext sslContext = SSLContext.getInstance("TLS");
94 sslContext.init(null, acceptAlltrustManagers(), null);
95 sslsocketfactory = sslContext.getSocketFactory();
96 } catch (KeyManagementException | NoSuchAlgorithmException e) {
97 logger.debug("{}: sslsocketfactory failed to initialize: {}", host, e.getMessage());
101 @NonNullByDefault({})
102 @SuppressWarnings("unused")
103 private class JSONMessage {
111 String current_content_id;
120 public String getEvent() {
121 return Optional.ofNullable(event).orElse("");
124 public String getStatus() {
125 return Optional.ofNullable(status).orElse("");
128 public int getVersion() {
129 return Optional.ofNullable(version).map(a -> a.replace(".", "")).map(Integer::parseInt).orElse(0);
132 public String getValue() {
133 return Optional.ofNullable(value).orElse(getStatus());
136 public int getIntValue() {
137 return Optional.of(Integer.valueOf(getValue())).orElse(0);
140 public String getCategoryId() {
141 return Optional.ofNullable(category_id).orElse("");
144 public String getContentId() {
145 return Optional.ofNullable(content_id).orElse(getCurrentContentId());
148 public String getCurrentContentId() {
149 return Optional.ofNullable(current_content_id).orElse("");
152 public String getType() {
153 return Optional.ofNullable(type).orElse("");
156 public String getIsShown() {
157 return Optional.ofNullable(is_shown).orElse("No");
160 public String getFileType() {
161 return Optional.ofNullable(file_type).orElse("");
164 public String getConnInfo() {
165 return Optional.ofNullable(conn_info).orElse("");
168 public String getData() {
169 return Optional.ofNullable(data).orElse("");
178 BinaryData(byte[] data, int off, int len) {
184 public byte[] getBinaryData() {
185 return Optional.ofNullable(data).orElse(new byte[0]);
188 public int getOff() {
189 return Optional.ofNullable(off).orElse(0);
192 public int getLen() {
193 return Optional.ofNullable(len).orElse(0);
197 class ArtmodeSettings {
204 public String getItem() {
205 return Optional.ofNullable(item).orElse("");
208 public int getValue() {
209 return Optional.ofNullable(value).map(a -> Integer.valueOf(a)).orElse(0);
230 public String getContentInfo() {
231 return Optional.ofNullable(contentInfo).orElse("");
234 public String getIp() {
235 return Optional.ofNullable(ip).orElse("");
238 public int getPort() {
239 return Optional.ofNullable(port).map(Integer::parseInt).orElse(0);
242 public String getKey() {
243 return Optional.ofNullable(key).orElse("");
246 public boolean getSecured() {
247 return Optional.ofNullable(secured).orElse(false);
252 String connection_id;
262 Header(int fileLength) {
263 this.fileLength = String.valueOf(fileLength);
266 public int getFileLength() {
267 return Optional.ofNullable(fileLength).map(Integer::parseInt).orElse(0);
270 public String getFileType() {
271 return Optional.ofNullable(fileType).orElse("");
274 public String getFileID() {
275 return Optional.ofNullable(fileID).orElse("");
279 // data is sometimes a json object, sometimes a string representation of a json object for d2d_service_message
285 public String getEvent() {
286 return Optional.ofNullable(event).orElse("");
289 public String getData() {
290 return Optional.ofNullable(data).map(a -> a.getAsString()).orElse("");
293 public void putBinaryData(byte[] arr, int off, int len) {
294 this.binData = new BinaryData(arr, off, len);
297 public BinaryData getBinaryData() {
298 return Optional.ofNullable(binData).orElse(new BinaryData(new byte[0], 0, 0));
303 public void onWebSocketBinary(byte @Nullable [] arr, int off, int len) {
307 super.onWebSocketBinary(arr, off, len);
308 String msg = extractMsg(arr, off, len, true);
309 // offset is start of binary data
310 int offset = ByteBuffer.wrap(arr, off, len).getShort() + off + 2; // 2 = length of Short
312 JSONMessage jsonMsg = remoteControllerWebSocket.gson.fromJson(msg, JSONMessage.class);
313 if (jsonMsg == null) {
316 switch (jsonMsg.getEvent()) {
317 case "d2d_service_message":
318 jsonMsg.putBinaryData(arr, offset, len);
319 handleD2DServiceMessage(jsonMsg);
322 logger.debug("{}: WebSocketArt(binary) Unknown event: {}", host, msg);
324 } catch (JsonSyntaxException e) {
325 logger.warn("{}: {}: Error ({}) in message: {}", host, className, e.getMessage(), msg);
330 public synchronized void onWebSocketText(@Nullable String msgarg) {
331 if (msgarg == null) {
334 String msg = msgarg.replace('\n', ' ');
335 super.onWebSocketText(msg);
337 JSONMessage jsonMsg = remoteControllerWebSocket.gson.fromJson(msg, JSONMessage.class);
338 if (jsonMsg == null) {
341 switch (jsonMsg.getEvent()) {
342 case "ms.channel.connect":
343 logger.debug("{}: Art channel connected", host);
345 case "ms.channel.ready":
346 logger.debug("{}: Art channel ready", host);
348 if (remoteControllerWebSocket.callback.getArtMode2022()) {
349 remoteControllerWebSocket.callback.setArtMode2022(false);
350 remoteControllerWebSocket.callback.setArtModeSupported(true);
351 logger.info("{}: Art Mode has been renabled on Frame TV's >= 2022", host);
354 getArtmodeStatus("get_api_version");
355 getArtmodeStatus("api_version");
356 getArtmodeStatus("get_slideshow_status");
357 getArtmodeStatus("get_auto_rotation_status");
358 getArtmodeStatus("get_current_artwork");
359 getArtmodeStatus("get_color_temperature");
361 case "ms.channel.clientConnect":
362 logger.debug("{}: Another Art client has connected", host);
364 case "ms.channel.clientDisconnect":
365 logger.debug("{}: Other Art client has disconnected", host);
368 case "d2d_service_message":
369 handleD2DServiceMessage(jsonMsg);
372 logger.debug("{}: WebSocketArt Unknown event: {}", host, msg);
374 } catch (JsonSyntaxException e) {
375 logger.warn("{}: {}: Error ({}) in message: {}", host, className, e.getMessage(), msg);
380 * handle D2DServiceMessages
382 * @param jsonMsg JSONMessage
385 private synchronized void handleD2DServiceMessage(JSONMessage jsonMsg) {
386 String msg = jsonMsg.getData();
388 JSONMessage.Data data = remoteControllerWebSocket.gson.fromJson(msg, JSONMessage.Data.class);
390 logger.debug("{}: Empty d2d_service_message event", host);
393 // remove returns and white space for ART_JSON channel
394 valueReceived(ART_JSON, new StringType(msg.trim().replaceAll("\\n|\\\\n", "").replaceAll("\\s{2,}", " ")));
395 switch (data.getEvent()) {
397 logger.debug("{}: ERROR event: {}", host, msg);
400 // old (2021) version is "2.03", new (2022) version is "4.3.4.0"
401 logger.debug("{}: {}: {}", host, data.getEvent(), data.getVersion());
402 if (data.getVersion() >= 4000) {
405 logger.debug("{}: API Version set to: {}", host, getArtApiVersion());
408 case "image_deleted":
409 case "set_artmode_status":
410 case "get_content_list":
411 case "recently_set_updated":
412 case "preview_started":
413 case "preview_stopped":
414 case "favorite_changed":
418 case "get_artmode_settings":
419 logger.debug("{}: {}: {}", host, data.getEvent(), data.getData());
420 msg = data.getData();
421 if (!msg.isBlank()) {
422 JSONMessage.ArtmodeSettings[] artmodeSettings = remoteControllerWebSocket.gson.fromJson(msg,
423 JSONMessage.ArtmodeSettings[].class);
424 if (artmodeSettings != null) {
425 for (JSONMessage.ArtmodeSettings setting : artmodeSettings) {
426 // extract brightness and colour temperature here
427 if ("brightness".equals(setting.getItem())) {
428 valueReceived(ART_BRIGHTNESS, new PercentType(setting.getValue() * 10));
430 if ("color_temperature".equals(setting.getItem())) {
431 valueReceived(ART_COLOR_TEMPERATURE, new DecimalType(setting.getValue()));
437 case "set_brightness":
438 case "brightness_changed":
440 valueReceived(ART_BRIGHTNESS, new PercentType(data.getIntValue() * 10));
442 case "set_color_temperature":
443 case "color_temperature_changed":
444 case "color_temperature":
445 valueReceived(ART_COLOR_TEMPERATURE, new DecimalType(data.getIntValue()));
447 case "get_artmode_status":
448 case "art_mode_changed":
449 case "artmode_status":
450 logger.debug("{}: {}: {}", host, data.getEvent(), data.getValue());
451 if ("off".equals(data.getValue())) {
452 remoteControllerWebSocket.callback.powerUpdated(true, false);
453 remoteControllerWebSocket.callback.currentAppUpdated("");
455 remoteControllerWebSocket.callback.powerUpdated(false, true);
457 if (!remoteControllerWebSocket.noApps()) {
458 remoteControllerWebSocket.updateCurrentApp();
461 case "slideshow_image_changed":
462 case "slideshow_changed":
463 case "get_slideshow_status":
464 case "auto_rotation_changed":
465 case "auto_rotation_image_changed":
466 case "auto_rotation_status":
467 // value (duration) is "off" or "number" where number is duration in minutes
468 // data.type: "shuffleslideshow" or "slideshow"
469 // data.current_content_id: Current art displayed eg "MY_F0005"
470 // data.category_id: category eg 'MY-C0004' ie favouries or my Photos/shelf
471 if (!data.getValue().isBlank()) {
472 slideShowDuration = data.getValue();
473 slideshow = !"off".equals(data.getValue());
475 categoryId = (data.getCategoryId().isBlank()) ? categoryId : data.getCategoryId();
476 if (!data.getContentId().isBlank() && slideshow) {
477 remoteControllerWebSocket.callback.currentAppUpdated(
478 String.format("%s %s %s", data.getType(), slideShowDuration, categoryId));
480 logger.trace("{}: slideshow: {}, {}, {}, {}", host, data.getEvent(), data.getType(),
481 data.getValue(), data.getContentId());
484 if (!data.getCategoryId().isBlank()) {
485 logger.debug("{}: Image added: {}, category: {}", host, data.getContentId(),
486 data.getCategoryId());
489 case "get_current_artwork":
491 case "current_artwork":
492 case "image_selected":
493 // data.content_id: Current art displayed eg "MY_F0005"
494 // data.is_shown: "Yes" or "No"
495 if ("Yes".equals(data.getIsShown())) {
497 remoteControllerWebSocket.callback.currentAppUpdated("artMode");
500 valueReceived(ART_LABEL, new StringType(data.getContentId()));
501 if (remoteControllerWebSocket.callback.handler.isChannelLinked(ART_IMAGE)) {
502 if (data.getEvent().contains("current_artwork") || "Yes".equals(data.getIsShown())) {
503 getThumbnail(data.getContentId());
507 case "get_thumbnail_list":
509 logger.trace("{}: thumbnail: Fetching {}", host, data.getContentId());
511 // upload image (should be 3840x2160 pixels in size)
512 msg = data.getConnInfo();
513 if (!msg.isBlank()) {
514 JSONMessage.Contentinfo contentInfo = remoteControllerWebSocket.gson.fromJson(msg,
515 JSONMessage.Contentinfo.class);
516 if (contentInfo != null) {
517 // NOTE: do not tie up the websocket receive loop for too long, so use the scheduler
518 // upload image, or download thumbnail
519 scheduleSocketOperation(contentInfo, "ready_to_use".equals(data.getEvent()));
522 // <2019 (ish) Frame TV's return thumbnails as binary data
523 receiveThumbnail(jsonMsg.getBinaryData());
526 case "go_to_standby":
527 logger.debug("{}: go_to_standby", host);
528 remoteControllerWebSocket.callback.powerUpdated(false, false);
529 remoteControllerWebSocket.callback.setOffline();
534 logger.debug("{}: wakeup from standby", host);
535 // check artmode status to know complete status before updating
537 getArtmodeStatus((getArtApiVersion() == 0) ? "get_auto_rotation_status" : "get_slideshow_status");
538 getArtmodeStatus("get_current_artwork");
539 getArtmodeStatus("get_color_temperature");
542 logger.debug("{}: Unknown d2d_service_message event: {}", host, msg);
544 } catch (JsonSyntaxException e) {
545 logger.warn("{}: {}: Error ({}) in message: {}", host, className, e.getMessage(), msg);
549 public void valueReceived(String variable, State value) {
550 if (!stateMap.getOrDefault(variable, "").equals(value.toString())) {
551 remoteControllerWebSocket.callback.handler.valueReceived(variable, value);
552 stateMap.put(variable, value.toString());
554 logger.trace("{}: Value '{}' for {} hasn't changed, ignoring update", host, value, variable);
558 public int getArtApiVersion() {
559 return remoteControllerWebSocket.callback.handler.artApiVersion;
562 public void setArtApiVersion(int apiVersion) {
563 remoteControllerWebSocket.callback.handler.artApiVersion = apiVersion;
567 * creates formatted json string for art websocket commands
569 * @param request Array of string requests to format
572 @NonNullByDefault({})
573 class JSONArtModeStatus {
574 public JSONArtModeStatus(String[] request) {
575 Params.Data data = params.new Data();
576 if (request.length == 1) {
577 if (request[0].endsWith("}")) {
578 // full json request/command
579 request = request[0].split(",");
581 // send simple command in request[0]
582 data.request = request[0];
583 params.data = remoteControllerWebSocket.gson.toJson(data);
587 switch (request[0]) {
588 // predefined requests/commands
589 case "set_slideshow_status":
590 case "set_auto_rotation_status":
591 data.request = request[0];
592 data.type = request[1];
593 data.value = request[2];
594 data.category_id = request[3];
595 params.data = remoteControllerWebSocket.gson.toJson(data);
597 case "set_brightness":
598 case "set_color_temperature":
599 data.request = request[0];
600 data.value = request[1];
601 params.data = remoteControllerWebSocket.gson.toJson(data);
603 case "get_thumbnail":
604 data.request = request[0];
605 data.content_id = request[1];
606 data.conn_info = new Conninfo();
607 params.data = remoteControllerWebSocket.gson.toJson(data);
609 case "get_thumbnail_list":
610 connection_id_random++;
611 data.request = request[0];
612 Content_id_list content_id = new Content_id_list();
613 content_id.content_id = request[1];
614 data.content_id_list = new Content_id_list[] { content_id };
615 data.conn_info = new Conninfo();
616 params.data = remoteControllerWebSocket.gson.toJson(data);
619 data.request = request[0];
620 data.content_id = request[1];
622 params.data = remoteControllerWebSocket.gson.toJson(data);
625 RawType image = RawType.valueOf(request[1]);
626 fileType = image.getMimeType().split("/")[1];
627 imageBytes = image.getBytes();
628 data.request = request[0];
629 data.request_id = remoteControllerWebSocket.uuid.toString();
630 data.file_type = fileType;
631 data.conn_info = new Conninfo();
632 data.image_date = DATEFORMAT.format(Instant.now());
633 // data.matte_id = "flexible_polar";
634 // data.portrait_matte_id = "flexible_polar";
635 data.file_size = Long.valueOf(imageBytes.length);
636 params.data = remoteControllerWebSocket.gson.toJson(data);
639 // Just return formatted json (add id if needed)
640 if (Arrays.stream(request).anyMatch(a -> a.contains("\"id\""))) {
641 params.data = String.join(",", request).replace(",}", "}");
643 ArrayList<String> requestList = new ArrayList<>(Arrays.asList(request));
644 requestList.add(requestList.size() - 1,
645 String.format("\"id\":\"%s\"", remoteControllerWebSocket.uuid.toString()));
646 params.data = String.join(",", requestList).replace(",}", "}");
654 String request = "get_artmode_status";
658 Content_id_list[] content_id_list = null;
668 String id = remoteControllerWebSocket.uuid.toString();
672 String event = "art_app_request";
678 String d2d_mode = "socket";
679 // this is a random number usually
680 // long connection_id = 2705890518L;
681 long connection_id = connection_id_random;
683 String id = remoteControllerWebSocket.uuid.toString();
686 class Content_id_list {
690 String method = "ms.channel.emit";
691 Params params = new Params();
694 public void getThumbnail(String content_id) {
695 if (!content_id.equals(lastThumbnail) || "NULL".equals(stateMap.getOrDefault(ART_IMAGE, "NULL"))) {
696 getArtmodeStatus((getArtApiVersion() == 0) ? "get_thumbnail" : "get_thumbnail_list", content_id);
697 lastThumbnail = content_id;
699 logger.trace("{}: NOT getting thumbnail for: {} as it hasn't changed", host, content_id);
704 * Extract header message from binary data
705 * <2019 (ish) Frame TV's return some messages as binary data
706 * First two bytes are a short giving the header length
707 * header is a D2DServiceMessages followed by the binary image data.
709 * Also Extract header information from image downloaded via socket
710 * in which case first four bytes are the header length followed by the binary image data.
712 * @param byte[] payload
713 * @param int offset (usually 0)
715 * @param boolean fromBinMsg true if this was received as a binary message (header length is a Short)
718 public String extractMsg(byte[] payload, int offset, int len, boolean fromBinMsg) {
719 ByteBuffer buf = ByteBuffer.wrap(payload, offset, len);
720 int headerlen = fromBinMsg ? buf.getShort() : buf.getInt();
721 offset += fromBinMsg ? Short.BYTES : Integer.BYTES;
722 String type = fromBinMsg ? "D2DServiceMessages(from binary)" : "image header";
723 String header = new String(payload, offset, headerlen, StandardCharsets.UTF_8);
724 logger.trace("{}: Got {}: {}", host, type, header);
729 * Receive thumbnail from binary data returned by TV in response to get_thumbnail command
730 * <2019 (ish) Frame TV's return thumbnails as binary data.
732 * @param JSONMessage.BinaryData
735 public void receiveThumbnail(JSONMessage.BinaryData binaryData) {
736 extractThumbnail(binaryData.getBinaryData(), binaryData.getLen() - binaryData.getOff());
740 * Return a no-op SSL trust manager which will not verify server or client certificates.
742 private TrustManager[] acceptAlltrustManagers() {
743 return new TrustManager[] { new X509TrustManager() {
745 public void checkClientTrusted(final X509Certificate @Nullable [] chain, final @Nullable String authType) {
749 public void checkServerTrusted(final X509Certificate @Nullable [] chain, final @Nullable String authType) {
753 public X509Certificate @Nullable [] getAcceptedIssuers() {
759 public void scheduleSocketOperation(JSONMessage.Contentinfo contentInfo, boolean upload) {
760 logger.trace("{}: scheduled scheduleSocketOperation()", host);
761 remoteControllerWebSocket.callback.handler.getScheduler().schedule(() -> {
763 uploadImage(contentInfo);
765 downloadThumbnail(contentInfo);
767 }, 50, TimeUnit.MILLISECONDS);
771 * Download thumbnail of current selected image/jpeg from ip+port
773 * @param contentinfo Contentinfo containing ip address and port to download from
776 public void downloadThumbnail(JSONMessage.Contentinfo contentInfo) {
777 logger.trace("{}: thumbnail: downloading from: ip:{}, port:{}, secured:{}", host, contentInfo.getIp(),
778 contentInfo.getPort(), contentInfo.getSecured());
781 if (contentInfo.getSecured()) {
782 if (sslsocketfactory != null) {
783 logger.trace("{}: thumbnail SSL socket connecting", host);
784 socket = sslsocketfactory.createSocket(contentInfo.getIp(), contentInfo.getPort());
786 logger.debug("{}: sslsocketfactory is null", host);
790 socket = new Socket(contentInfo.getIp(), contentInfo.getPort());
792 if (socket != null) {
793 logger.trace("{}: thumbnail socket connected", host);
794 byte[] payload = Optional.ofNullable(socket.getInputStream().readAllBytes()).orElse(new byte[0]);
796 if (payload.length > 0) {
797 String header = extractMsg(payload, 0, payload.length, false);
798 JSONMessage.Header headerData = Optional
799 .ofNullable(remoteControllerWebSocket.gson.fromJson(header, JSONMessage.Header.class))
800 .orElse(new JSONMessage().new Header(0));
801 extractThumbnail(payload, headerData.getFileLength());
803 logger.trace("{}: thumbnail no data received", host);
804 valueReceived(ART_IMAGE, UnDefType.NULL);
807 } catch (IOException e) {
808 logger.warn("{}: Error downloading thumbnail {}", host, e.getMessage());
813 * Extract thumbnail from payload
815 * @param payload byte[] containing binary data and possibly header info
816 * @param fileLength int with image file size
819 public void extractThumbnail(byte[] payload, int fileLength) {
821 byte[] image = new byte[fileLength];
822 ByteBuffer.wrap(image).put(payload, payload.length - fileLength, fileLength);
823 String ftype = Optional
824 .ofNullable(URLConnection.guessContentTypeFromStream(new ByteArrayInputStream(image)))
825 .orElseThrow(() -> new Exception("Unable to determine image type"));
826 valueReceived(ART_IMAGE, new RawType(image, ftype));
827 } catch (Exception e) {
828 if (logger.isTraceEnabled()) {
829 logger.trace("{}: Error extracting thumbnail: ", host, e);
831 logger.warn("{}: Error extracting thumbnail {}", host, e.getMessage());
836 @NonNullByDefault({})
837 @SuppressWarnings("unused")
838 private class JSONHeader {
839 public JSONHeader(int num, int total, long fileLength, String fileType, String secKey) {
842 this.fileLength = fileLength;
843 this.fileType = fileType;
844 this.secKey = secKey;
850 String fileName = "dummy";
853 String version = "0.0.1";
857 * Upload Image from ART_IMAGE/ART_LABEL channel
859 * @param contentinfo Contentinfo containing ip address, port and key to upload to
861 * imageBytes and fileType are class instance variables obtained from the
862 * getArtmodeStatus() command that triggered the upload.
865 public void uploadImage(JSONMessage.Contentinfo contentInfo) {
866 logger.trace("{}: Uploading image to ip:{}, port:{}", host, contentInfo.getIp(), contentInfo.getPort());
869 if (contentInfo.getSecured()) {
870 if (sslsocketfactory != null) {
871 logger.trace("{}: upload SSL socket connecting", host);
872 socket = (SSLSocket) sslsocketfactory.createSocket(contentInfo.getIp(), contentInfo.getPort());
874 logger.debug("{}: sslsocketfactory is null", host);
878 socket = new Socket(contentInfo.getIp(), contentInfo.getPort());
880 if (socket != null) {
881 logger.trace("{}: upload socket connected", host);
882 DataOutputStream dataOutputStream = new DataOutputStream(
883 new BufferedOutputStream(socket.getOutputStream()));
884 String header = remoteControllerWebSocket.gson
885 .toJson(new JSONHeader(0, 1, imageBytes.length, fileType, contentInfo.getKey()));
886 logger.debug("{}: Image header: {}, {} bytes", host, header, header.length());
887 dataOutputStream.writeInt(header.length());
888 dataOutputStream.writeBytes(header);
889 dataOutputStream.write(imageBytes, 0, imageBytes.length);
890 dataOutputStream.flush();
891 logger.debug("{}: wrote Image:{} {} bytes to TV", host, fileType, dataOutputStream.size());
894 } catch (IOException e) {
895 logger.warn("{}: Error writing image to TV {}", host, e.getMessage());
902 * @param command split on ,space or + where
904 * First parameter is shuffleslideshow or slideshow
905 * Second is duration in minutes or off
906 * Third is category where the value is somethng like MY-C0004 = Favourites or MY-C0002 = My Photos.
909 public void setSlideshow(String command) {
910 String[] cmd = command.split("[, +]");
911 if (cmd.length <= 1) {
912 logger.warn("{}: Invalid slideshow command: {}", host, command);
915 String value = ("0".equals(cmd[1])) ? "off" : cmd[1];
916 categoryId = (cmd.length >= 3) ? cmd[2] : categoryId;
917 getArtmodeStatus((getArtApiVersion() == 0) ? "set_auto_rotation_status" : "set_slideshow_status",
918 cmd[0].toLowerCase(), value, categoryId);
922 * Send commands to Frame TV Art websocket channel
924 * @param optionalRequests Array of string requests
927 void getArtmodeStatus(String... optionalRequests) {
928 if (optionalRequests.length == 0) {
929 optionalRequests = new String[] { "get_artmode_status" };
931 if (getArtApiVersion() != 0) {
932 if ("get_brightness".equals(optionalRequests[0])) {
933 optionalRequests = new String[] { "get_artmode_settings" };
935 if ("get_color_temperature".equals(optionalRequests[0])) {
936 optionalRequests = new String[] { "get_artmode_settings" };
939 sendCommand(remoteControllerWebSocket.gson.toJson(new JSONArtModeStatus(optionalRequests)));