Skip to content

Commit 20ace64

Browse files
authored
[samsungtv] Frame TV Fixes, Improvements and New Channels (openhab#11895)
* [samsungtv] add certificate trust Signed-off-by: Nick Waterton <n.waterton@outlook.com>
1 parent 367f8c4 commit 20ace64

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

43 files changed

+5710
-1860
lines changed

bundles/org.openhab.binding.samsungtv/NOTICE

100644100755
File mode changed.

bundles/org.openhab.binding.samsungtv/README.md

100644100755
+636-56
Large diffs are not rendered by default.

bundles/org.openhab.binding.samsungtv/pom.xml

100644100755
File mode changed.

bundles/org.openhab.binding.samsungtv/src/main/feature/feature.xml

100644100755
File mode changed.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
/**
2+
* Copyright (c) 2010-2024 Contributors to the openHAB project
3+
*
4+
* See the NOTICE file(s) distributed with this work for additional
5+
* information.
6+
*
7+
* This program and the accompanying materials are made available under the
8+
* terms of the Eclipse Public License 2.0 which is available at
9+
* http://www.eclipse.org/legal/epl-2.0
10+
*
11+
* SPDX-License-Identifier: EPL-2.0
12+
*/
13+
package org.openhab.binding.samsungtv.internal;
14+
15+
import java.io.File;
16+
import java.io.IOException;
17+
import java.nio.file.Files;
18+
import java.nio.file.Path;
19+
import java.nio.file.Paths;
20+
import java.util.List;
21+
import java.util.stream.Collectors;
22+
23+
import org.eclipse.jdt.annotation.NonNullByDefault;
24+
import org.openhab.binding.samsungtv.internal.protocol.RemoteControllerWebSocket;
25+
import org.openhab.core.OpenHAB;
26+
import org.openhab.core.service.WatchService;
27+
import org.osgi.service.component.annotations.Component;
28+
import org.slf4j.Logger;
29+
import org.slf4j.LoggerFactory;
30+
31+
/**
32+
* The {@link SamsungTvAppWatchService} provides a list of apps for >2020 Samsung TV's
33+
* File should be in json format
34+
*
35+
* @author Nick Waterton - Initial contribution
36+
* @author Nick Waterton - Refactored to new WatchService
37+
*/
38+
@Component(service = SamsungTvAppWatchService.class)
39+
@NonNullByDefault
40+
public class SamsungTvAppWatchService implements WatchService.WatchEventListener {
41+
private static final String APPS_PATH = OpenHAB.getConfigFolder() + File.separator + "services";
42+
private static final String APPS_FILE = "samsungtv.cfg";
43+
44+
private final Logger logger = LoggerFactory.getLogger(SamsungTvAppWatchService.class);
45+
private final RemoteControllerWebSocket remoteControllerWebSocket;
46+
private String host = "";
47+
private boolean started = false;
48+
int count = 0;
49+
50+
public SamsungTvAppWatchService(String host, RemoteControllerWebSocket remoteControllerWebSocket) {
51+
this.host = host;
52+
this.remoteControllerWebSocket = remoteControllerWebSocket;
53+
}
54+
55+
public void start() {
56+
File file = new File(APPS_PATH, APPS_FILE);
57+
if (file.exists() && !getStarted()) {
58+
logger.info("{}: Starting Apps File monitoring service", host);
59+
started = true;
60+
readFileApps();
61+
} else if (count++ == 0) {
62+
logger.warn("{}: cannot start Apps File monitoring service, file {} does not exist", host, file.toString());
63+
remoteControllerWebSocket.addKnownAppIds();
64+
}
65+
}
66+
67+
public boolean getStarted() {
68+
return started;
69+
}
70+
71+
/**
72+
* Check file path for existance
73+
*
74+
*/
75+
public boolean checkFileDir() {
76+
File file = new File(APPS_PATH, APPS_FILE);
77+
return file.exists();
78+
}
79+
80+
public void readFileApps() {
81+
processWatchEvent(WatchService.Kind.MODIFY, Paths.get(APPS_PATH, APPS_FILE));
82+
}
83+
84+
public boolean watchSubDirectories() {
85+
return false;
86+
}
87+
88+
@Override
89+
public void processWatchEvent(WatchService.Kind kind, Path path) {
90+
if (path.endsWith(APPS_FILE) && kind != WatchService.Kind.DELETE) {
91+
logger.debug("{}: Updating Apps list from FILE {}", host, path);
92+
try {
93+
@SuppressWarnings("null")
94+
List<String> allLines = Files.lines(path).filter(line -> !line.trim().startsWith("#"))
95+
.collect(Collectors.toList());
96+
logger.debug("{}: Updated Apps list, {} apps in list", host, allLines.size());
97+
remoteControllerWebSocket.updateAppList(allLines);
98+
} catch (IOException e) {
99+
logger.debug("{}: Cannot read apps file: {}", host, e.getMessage());
100+
}
101+
}
102+
}
103+
}

bundles/org.openhab.binding.samsungtv/src/main/java/org/openhab/binding/samsungtv/internal/SamsungTvBindingConstants.java

100644100755
+9
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
*
2222
* @author Pauli Anttila - Initial contribution
2323
* @author Arjan Mels - Added constants for websocket based remote controller
24+
* @author Nick Waterton - Added artMode channels
2425
*/
2526
@NonNullByDefault
2627
public class SamsungTvBindingConstants {
@@ -33,6 +34,7 @@ public class SamsungTvBindingConstants {
3334
public static final String KEY_CODE = "keyCode";
3435
public static final String POWER = "power";
3536
public static final String ART_MODE = "artMode";
37+
public static final String SET_ART_MODE = "setArtMode";
3638
public static final String SOURCE_APP = "sourceApp";
3739

3840
// List of all media renderer thing channel id's
@@ -51,4 +53,11 @@ public class SamsungTvBindingConstants {
5153
public static final String CHANNEL_NAME = "channelName";
5254
public static final String BROWSER_URL = "url";
5355
public static final String STOP_BROWSER = "stopBrowser";
56+
57+
// List of all artMode channels (Frame TV's only)
58+
public static final String ART_IMAGE = "artImage";
59+
public static final String ART_LABEL = "artLabel";
60+
public static final String ART_JSON = "artJson";
61+
public static final String ART_BRIGHTNESS = "artBrightness";
62+
public static final String ART_COLOR_TEMPERATURE = "artColorTemperature";
5463
}

bundles/org.openhab.binding.samsungtv/src/main/java/org/openhab/binding/samsungtv/internal/SamsungTvHandlerFactory.java

100644100755
File mode changed.

bundles/org.openhab.binding.samsungtv/src/main/java/org/openhab/binding/samsungtv/internal/SamsungTvTlsTrustManagerProvider.java

100644100755
File mode changed.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
/**
2+
* Copyright (c) 2010-2024 Contributors to the openHAB project
3+
*
4+
* See the NOTICE file(s) distributed with this work for additional
5+
* information.
6+
*
7+
* This program and the accompanying materials are made available under the
8+
* terms of the Eclipse Public License 2.0 which is available at
9+
* http://www.eclipse.org/legal/epl-2.0
10+
*
11+
* SPDX-License-Identifier: EPL-2.0
12+
*/
13+
package org.openhab.binding.samsungtv.internal;
14+
15+
import java.io.IOException;
16+
import java.io.StringReader;
17+
import java.util.Base64;
18+
import java.util.Optional;
19+
20+
import javax.xml.parsers.DocumentBuilderFactory;
21+
import javax.xml.parsers.ParserConfigurationException;
22+
23+
import org.eclipse.jdt.annotation.NonNullByDefault;
24+
import org.eclipse.jdt.annotation.Nullable;
25+
import org.jupnp.model.meta.RemoteDevice;
26+
import org.openhab.core.types.Command;
27+
import org.slf4j.Logger;
28+
import org.slf4j.LoggerFactory;
29+
import org.w3c.dom.Document;
30+
import org.xml.sax.InputSource;
31+
import org.xml.sax.SAXException;
32+
33+
/**
34+
* The {@link Utils} is a collection of static utilities
35+
*
36+
* @author Nick Waterton - Initial contribution
37+
*/
38+
@NonNullByDefault
39+
public class Utils {
40+
private static final Logger LOGGER = LoggerFactory.getLogger(Utils.class);
41+
public static DocumentBuilderFactory factory = getDocumentBuilder();
42+
43+
private static DocumentBuilderFactory getDocumentBuilder() {
44+
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
45+
try {
46+
// see https://cheatsheetseries.owasp.org/cheatsheets/XML_External_Entity_Prevention_Cheat_Sheet.html
47+
factory.setFeature("http://xml.org/sax/features/external-general-entities", false);
48+
factory.setFeature("http://xml.org/sax/features/external-parameter-entities", false);
49+
factory.setFeature("http://apache.org/xml/features/nonvalidating/load-external-dtd", false);
50+
factory.setXIncludeAware(false);
51+
factory.setExpandEntityReferences(false);
52+
} catch (ParserConfigurationException e) {
53+
LOGGER.debug("XMLParser Configuration Error: {}", e.getMessage());
54+
}
55+
return Optional.ofNullable(factory).orElse(DocumentBuilderFactory.newInstance());
56+
}
57+
58+
/**
59+
* Build {@link Document} from {@link String} which contains XML content.
60+
*
61+
* @param xml
62+
* {@link String} which contains XML content.
63+
* @return {@link Optional Document} or empty if convert has failed.
64+
*/
65+
public static Optional<Document> loadXMLFromString(String xml, String host) {
66+
try {
67+
return Optional.ofNullable(factory.newDocumentBuilder().parse(new InputSource(new StringReader(xml))));
68+
} catch (ParserConfigurationException | SAXException | IOException e) {
69+
LOGGER.debug("{}: Error loading XML: {}", host, e.getMessage());
70+
}
71+
return Optional.empty();
72+
}
73+
74+
public static boolean isSoundChannel(String name) {
75+
return (name.contains("Volume") || name.contains("Mute"));
76+
}
77+
78+
public static String b64encode(String str) {
79+
return Base64.getUrlEncoder().encodeToString(str.getBytes());
80+
}
81+
82+
public static String truncCmd(Command command) {
83+
String cmd = command.toString();
84+
return (cmd.length() <= 80) ? cmd : cmd.substring(0, 80) + "...";
85+
}
86+
87+
public static String getModelName(@Nullable RemoteDevice device) {
88+
return Optional.ofNullable(device).map(a -> a.getDetails()).map(a -> a.getModelDetails())
89+
.map(a -> a.getModelName()).orElse("");
90+
}
91+
92+
public static String getManufacturer(@Nullable RemoteDevice device) {
93+
return Optional.ofNullable(device).map(a -> a.getDetails()).map(a -> a.getManufacturerDetails())
94+
.map(a -> a.getManufacturer()).orElse("");
95+
}
96+
97+
public static String getFriendlyName(@Nullable RemoteDevice device) {
98+
return Optional.ofNullable(device).map(a -> a.getDetails()).map(a -> a.getFriendlyName()).orElse("");
99+
}
100+
101+
public static String getUdn(@Nullable RemoteDevice device) {
102+
return Optional.ofNullable(device).map(a -> a.getIdentity()).map(a -> a.getUdn())
103+
.map(a -> a.getIdentifierString()).orElse("");
104+
}
105+
106+
public static String getHost(@Nullable RemoteDevice device) {
107+
return Optional.ofNullable(device).map(a -> a.getIdentity()).map(a -> a.getDescriptorURL())
108+
.map(a -> a.getHost()).orElse("");
109+
}
110+
111+
public static String getType(@Nullable RemoteDevice device) {
112+
return Optional.ofNullable(device).map(a -> a.getType()).map(a -> a.getType()).orElse("");
113+
}
114+
}

bundles/org.openhab.binding.samsungtv/src/main/java/org/openhab/binding/samsungtv/internal/WakeOnLanUtility.java

100644100755
+42-25
Original file line numberDiff line numberDiff line change
@@ -35,32 +35,44 @@
3535
*
3636
* @author Arjan Mels - Initial contribution
3737
* @author Laurent Garnier - Use improvements from the LG webOS binding
38+
* @author Nick Waterton - use single ip address as source per interface
3839
*
3940
*/
4041
@NonNullByDefault
4142
public class WakeOnLanUtility {
4243

4344
private static final Logger LOGGER = LoggerFactory.getLogger(WakeOnLanUtility.class);
44-
private static final Pattern MAC_REGEX = Pattern.compile("(([0-9a-fA-F]{2}[:-]){5}[0-9a-fA-F]{2})");
4545
private static final int CMD_TIMEOUT_MS = 1000;
46+
private static String host = "";
4647

47-
private static final String COMMAND;
48-
static {
49-
String os = System.getProperty("os.name").toLowerCase();
50-
LOGGER.debug("os: {}", os);
51-
if ((os.contains("win"))) {
52-
COMMAND = "arp -a %s";
53-
} else if ((os.contains("mac"))) {
54-
COMMAND = "arp %s";
55-
} else { // linux
56-
if (checkIfLinuxCommandExists("arp")) {
48+
/**
49+
* Get os command to find MAC address
50+
*
51+
* @return os COMMAND
52+
*/
53+
public static String getCommand() {
54+
String os = System.getProperty("os.name");
55+
String COMMAND = "";
56+
if (os != null) {
57+
os = os.toLowerCase();
58+
LOGGER.debug("{}: os: {}", host, os);
59+
if ((os.contains("win"))) {
60+
COMMAND = "arp -a %s";
61+
} else if ((os.contains("mac"))) {
5762
COMMAND = "arp %s";
58-
} else if (checkIfLinuxCommandExists("arping")) { // typically OH provided docker image
59-
COMMAND = "arping -r -c 1 -C 1 %s";
60-
} else {
61-
COMMAND = "";
63+
} else { // linux
64+
if (checkIfLinuxCommandExists("arp")) {
65+
COMMAND = "arp %s";
66+
} else if (checkIfLinuxCommandExists("arping")) { // typically OH provided docker image
67+
COMMAND = "arping -r -c 1 -C 1 %s";
68+
} else {
69+
LOGGER.warn("{}: arping not installed", host);
70+
}
6271
}
72+
} else {
73+
LOGGER.warn("{}: Unable to determine os", host);
6374
}
75+
return COMMAND;
6476
}
6577

6678
/**
@@ -70,11 +82,14 @@ public class WakeOnLanUtility {
7082
* @return MAC address
7183
*/
7284
public static @Nullable String getMACAddress(String hostName) {
85+
host = hostName;
86+
String COMMAND = getCommand();
7387
if (COMMAND.isEmpty()) {
74-
LOGGER.debug("MAC address detection not possible. No command to identify MAC found.");
88+
LOGGER.debug("{}: MAC address detection not possible. No command to identify MAC found.", hostName);
7589
return null;
7690
}
7791

92+
Pattern MAC_REGEX = Pattern.compile("(([0-9a-fA-F]{2}[:-]){5}[0-9a-fA-F]{2})");
7893
String[] cmds = Stream.of(COMMAND.split(" ")).map(arg -> String.format(arg, hostName)).toArray(String[]::new);
7994
String response = ExecUtil.executeCommandLineAndWaitResponse(Duration.ofMillis(CMD_TIMEOUT_MS), cmds);
8095
String macAddress = null;
@@ -91,9 +106,9 @@ public class WakeOnLanUtility {
91106
}
92107
}
93108
if (macAddress != null) {
94-
LOGGER.debug("MAC address of host {} is {}", hostName, macAddress);
109+
LOGGER.debug("{}: MAC address of host {} is {}", hostName, hostName, macAddress);
95110
} else {
96-
LOGGER.debug("Problem executing command {} to retrieve MAC address for {}: {}",
111+
LOGGER.debug("{}: Problem executing command {} to retrieve MAC address for {}: {}", hostName,
97112
String.format(COMMAND, hostName), hostName, response);
98113
}
99114
return macAddress;
@@ -109,23 +124,25 @@ public static void sendWOLPacket(String macAddress) {
109124

110125
try {
111126
Enumeration<NetworkInterface> interfaces = NetworkInterface.getNetworkInterfaces();
112-
while (interfaces.hasMoreElements()) {
127+
while (interfaces != null && interfaces.hasMoreElements()) {
113128
NetworkInterface networkInterface = interfaces.nextElement();
114-
if (networkInterface.isLoopback()) {
115-
continue; // Do not want to use the loopback interface.
129+
if (networkInterface.isLoopback() || !networkInterface.isUp()) {
130+
continue; // Do not want to use the loopback or down interface.
116131
}
117132
for (InterfaceAddress interfaceAddress : networkInterface.getInterfaceAddresses()) {
118133
InetAddress broadcast = interfaceAddress.getBroadcast();
119134
if (broadcast == null) {
120135
continue;
121136
}
122137

138+
InetAddress local = interfaceAddress.getAddress();
123139
DatagramPacket packet = new DatagramPacket(bytes, bytes.length, broadcast, 9);
124140
try (DatagramSocket socket = new DatagramSocket()) {
125141
socket.send(packet);
126-
LOGGER.trace("Sent WOL packet to {} {}", broadcast, macAddress);
142+
LOGGER.trace("Sent WOL packet from {} to {} {}", local, broadcast, macAddress);
143+
break;
127144
} catch (IOException e) {
128-
LOGGER.warn("Problem sending WOL packet to {} {}", broadcast, macAddress);
145+
LOGGER.warn("Problem sending WOL packet from {} to {} {}", local, broadcast, macAddress);
129146
}
130147
}
131148
}
@@ -138,7 +155,7 @@ public static void sendWOLPacket(String macAddress) {
138155
/**
139156
* Create WOL UDP package: 6 bytes 0xff and then 16 times the 6 byte mac address repeated
140157
*
141-
* @param macStr String representation of teh MAC address (either with : or -)
158+
* @param macStr String representation of the MAC address (either with : or -)
142159
* @return byte array with the WOL package
143160
* @throws IllegalArgumentException
144161
*/
@@ -171,7 +188,7 @@ private static boolean checkIfLinuxCommandExists(String cmd) {
171188
try {
172189
return 0 == Runtime.getRuntime().exec(String.format("which %s", cmd)).waitFor();
173190
} catch (InterruptedException | IOException e) {
174-
LOGGER.debug("Error trying to check if command {} exists: {}", cmd, e.getMessage());
191+
LOGGER.debug("{}: Error trying to check if command {} exists: {}", host, cmd, e.getMessage());
175192
}
176193
return false;
177194
}

0 commit comments

Comments
 (0)