Skip to content

Commit

Permalink
[samsungtv] Frame TV Fixes, Improvements and New Channels (openhab#11895
Browse files Browse the repository at this point in the history
)

* [samsungtv] add certificate trust

Signed-off-by: Nick Waterton <n.waterton@outlook.com>
Signed-off-by: Paul Smedley <paul@smedley.id.au>
  • Loading branch information
NickWaterton authored and psmedley committed Jun 15, 2024
1 parent d2a4c52 commit 2e7b644
Show file tree
Hide file tree
Showing 43 changed files with 5,710 additions and 1,860 deletions.
Empty file modified bundles/org.openhab.binding.samsungtv/NOTICE
100644 → 100755
Empty file.
692 changes: 636 additions & 56 deletions bundles/org.openhab.binding.samsungtv/README.md
100644 → 100755

Large diffs are not rendered by default.

Empty file modified bundles/org.openhab.binding.samsungtv/pom.xml
100644 → 100755
Empty file.
Empty file.
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
/**
* Copyright (c) 2010-2024 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.samsungtv.internal;

import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.List;
import java.util.stream.Collectors;

import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.samsungtv.internal.protocol.RemoteControllerWebSocket;
import org.openhab.core.OpenHAB;
import org.openhab.core.service.WatchService;
import org.osgi.service.component.annotations.Component;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
* The {@link SamsungTvAppWatchService} provides a list of apps for >2020 Samsung TV's
* File should be in json format
*
* @author Nick Waterton - Initial contribution
* @author Nick Waterton - Refactored to new WatchService
*/
@Component(service = SamsungTvAppWatchService.class)
@NonNullByDefault
public class SamsungTvAppWatchService implements WatchService.WatchEventListener {
private static final String APPS_PATH = OpenHAB.getConfigFolder() + File.separator + "services";
private static final String APPS_FILE = "samsungtv.cfg";

private final Logger logger = LoggerFactory.getLogger(SamsungTvAppWatchService.class);
private final RemoteControllerWebSocket remoteControllerWebSocket;
private String host = "";
private boolean started = false;
int count = 0;

public SamsungTvAppWatchService(String host, RemoteControllerWebSocket remoteControllerWebSocket) {
this.host = host;
this.remoteControllerWebSocket = remoteControllerWebSocket;
}

public void start() {
File file = new File(APPS_PATH, APPS_FILE);
if (file.exists() && !getStarted()) {
logger.info("{}: Starting Apps File monitoring service", host);
started = true;
readFileApps();
} else if (count++ == 0) {
logger.warn("{}: cannot start Apps File monitoring service, file {} does not exist", host, file.toString());
remoteControllerWebSocket.addKnownAppIds();
}
}

public boolean getStarted() {
return started;
}

/**
* Check file path for existance
*
*/
public boolean checkFileDir() {
File file = new File(APPS_PATH, APPS_FILE);
return file.exists();
}

public void readFileApps() {
processWatchEvent(WatchService.Kind.MODIFY, Paths.get(APPS_PATH, APPS_FILE));
}

public boolean watchSubDirectories() {
return false;
}

@Override
public void processWatchEvent(WatchService.Kind kind, Path path) {
if (path.endsWith(APPS_FILE) && kind != WatchService.Kind.DELETE) {
logger.debug("{}: Updating Apps list from FILE {}", host, path);
try {
@SuppressWarnings("null")
List<String> allLines = Files.lines(path).filter(line -> !line.trim().startsWith("#"))
.collect(Collectors.toList());
logger.debug("{}: Updated Apps list, {} apps in list", host, allLines.size());
remoteControllerWebSocket.updateAppList(allLines);
} catch (IOException e) {
logger.debug("{}: Cannot read apps file: {}", host, e.getMessage());
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
*
* @author Pauli Anttila - Initial contribution
* @author Arjan Mels - Added constants for websocket based remote controller
* @author Nick Waterton - Added artMode channels
*/
@NonNullByDefault
public class SamsungTvBindingConstants {
Expand All @@ -33,6 +34,7 @@ public class SamsungTvBindingConstants {
public static final String KEY_CODE = "keyCode";
public static final String POWER = "power";
public static final String ART_MODE = "artMode";
public static final String SET_ART_MODE = "setArtMode";
public static final String SOURCE_APP = "sourceApp";

// List of all media renderer thing channel id's
Expand All @@ -51,4 +53,11 @@ public class SamsungTvBindingConstants {
public static final String CHANNEL_NAME = "channelName";
public static final String BROWSER_URL = "url";
public static final String STOP_BROWSER = "stopBrowser";

// List of all artMode channels (Frame TV's only)
public static final String ART_IMAGE = "artImage";
public static final String ART_LABEL = "artLabel";
public static final String ART_JSON = "artJson";
public static final String ART_BRIGHTNESS = "artBrightness";
public static final String ART_COLOR_TEMPERATURE = "artColorTemperature";
}
Empty file.
Empty file.
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
/**
* Copyright (c) 2010-2024 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.samsungtv.internal;

import java.io.IOException;
import java.io.StringReader;
import java.util.Base64;
import java.util.Optional;

import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;

import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.jupnp.model.meta.RemoteDevice;
import org.openhab.core.types.Command;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.w3c.dom.Document;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;

/**
* The {@link Utils} is a collection of static utilities
*
* @author Nick Waterton - Initial contribution
*/
@NonNullByDefault
public class Utils {
private static final Logger LOGGER = LoggerFactory.getLogger(Utils.class);
public static DocumentBuilderFactory factory = getDocumentBuilder();

private static DocumentBuilderFactory getDocumentBuilder() {
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
try {
// see https://cheatsheetseries.owasp.org/cheatsheets/XML_External_Entity_Prevention_Cheat_Sheet.html
factory.setFeature("http://xml.org/sax/features/external-general-entities", false);
factory.setFeature("http://xml.org/sax/features/external-parameter-entities", false);
factory.setFeature("http://apache.org/xml/features/nonvalidating/load-external-dtd", false);
factory.setXIncludeAware(false);
factory.setExpandEntityReferences(false);
} catch (ParserConfigurationException e) {
LOGGER.debug("XMLParser Configuration Error: {}", e.getMessage());
}
return Optional.ofNullable(factory).orElse(DocumentBuilderFactory.newInstance());
}

/**
* Build {@link Document} from {@link String} which contains XML content.
*
* @param xml
* {@link String} which contains XML content.
* @return {@link Optional Document} or empty if convert has failed.
*/
public static Optional<Document> loadXMLFromString(String xml, String host) {
try {
return Optional.ofNullable(factory.newDocumentBuilder().parse(new InputSource(new StringReader(xml))));
} catch (ParserConfigurationException | SAXException | IOException e) {
LOGGER.debug("{}: Error loading XML: {}", host, e.getMessage());
}
return Optional.empty();
}

public static boolean isSoundChannel(String name) {
return (name.contains("Volume") || name.contains("Mute"));
}

public static String b64encode(String str) {
return Base64.getUrlEncoder().encodeToString(str.getBytes());
}

public static String truncCmd(Command command) {
String cmd = command.toString();
return (cmd.length() <= 80) ? cmd : cmd.substring(0, 80) + "...";
}

public static String getModelName(@Nullable RemoteDevice device) {
return Optional.ofNullable(device).map(a -> a.getDetails()).map(a -> a.getModelDetails())
.map(a -> a.getModelName()).orElse("");
}

public static String getManufacturer(@Nullable RemoteDevice device) {
return Optional.ofNullable(device).map(a -> a.getDetails()).map(a -> a.getManufacturerDetails())
.map(a -> a.getManufacturer()).orElse("");
}

public static String getFriendlyName(@Nullable RemoteDevice device) {
return Optional.ofNullable(device).map(a -> a.getDetails()).map(a -> a.getFriendlyName()).orElse("");
}

public static String getUdn(@Nullable RemoteDevice device) {
return Optional.ofNullable(device).map(a -> a.getIdentity()).map(a -> a.getUdn())
.map(a -> a.getIdentifierString()).orElse("");
}

public static String getHost(@Nullable RemoteDevice device) {
return Optional.ofNullable(device).map(a -> a.getIdentity()).map(a -> a.getDescriptorURL())
.map(a -> a.getHost()).orElse("");
}

public static String getType(@Nullable RemoteDevice device) {
return Optional.ofNullable(device).map(a -> a.getType()).map(a -> a.getType()).orElse("");
}
}
67 changes: 42 additions & 25 deletions ...ding.samsungtv/src/main/java/org/openhab/binding/samsungtv/internal/WakeOnLanUtility.java
100644 → 100755
Original file line number Diff line number Diff line change
Expand Up @@ -35,32 +35,44 @@
*
* @author Arjan Mels - Initial contribution
* @author Laurent Garnier - Use improvements from the LG webOS binding
* @author Nick Waterton - use single ip address as source per interface
*
*/
@NonNullByDefault
public class WakeOnLanUtility {

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

private static final String COMMAND;
static {
String os = System.getProperty("os.name").toLowerCase();
LOGGER.debug("os: {}", os);
if ((os.contains("win"))) {
COMMAND = "arp -a %s";
} else if ((os.contains("mac"))) {
COMMAND = "arp %s";
} else { // linux
if (checkIfLinuxCommandExists("arp")) {
/**
* Get os command to find MAC address
*
* @return os COMMAND
*/
public static String getCommand() {
String os = System.getProperty("os.name");
String COMMAND = "";
if (os != null) {
os = os.toLowerCase();
LOGGER.debug("{}: os: {}", host, os);
if ((os.contains("win"))) {
COMMAND = "arp -a %s";
} else if ((os.contains("mac"))) {
COMMAND = "arp %s";
} else if (checkIfLinuxCommandExists("arping")) { // typically OH provided docker image
COMMAND = "arping -r -c 1 -C 1 %s";
} else {
COMMAND = "";
} else { // linux
if (checkIfLinuxCommandExists("arp")) {
COMMAND = "arp %s";
} else if (checkIfLinuxCommandExists("arping")) { // typically OH provided docker image
COMMAND = "arping -r -c 1 -C 1 %s";
} else {
LOGGER.warn("{}: arping not installed", host);
}
}
} else {
LOGGER.warn("{}: Unable to determine os", host);
}
return COMMAND;
}

/**
Expand All @@ -70,11 +82,14 @@ public class WakeOnLanUtility {
* @return MAC address
*/
public static @Nullable String getMACAddress(String hostName) {
host = hostName;
String COMMAND = getCommand();
if (COMMAND.isEmpty()) {
LOGGER.debug("MAC address detection not possible. No command to identify MAC found.");
LOGGER.debug("{}: MAC address detection not possible. No command to identify MAC found.", hostName);
return null;
}

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

try {
Enumeration<NetworkInterface> interfaces = NetworkInterface.getNetworkInterfaces();
while (interfaces.hasMoreElements()) {
while (interfaces != null && interfaces.hasMoreElements()) {
NetworkInterface networkInterface = interfaces.nextElement();
if (networkInterface.isLoopback()) {
continue; // Do not want to use the loopback interface.
if (networkInterface.isLoopback() || !networkInterface.isUp()) {
continue; // Do not want to use the loopback or down interface.
}
for (InterfaceAddress interfaceAddress : networkInterface.getInterfaceAddresses()) {
InetAddress broadcast = interfaceAddress.getBroadcast();
if (broadcast == null) {
continue;
}

InetAddress local = interfaceAddress.getAddress();
DatagramPacket packet = new DatagramPacket(bytes, bytes.length, broadcast, 9);
try (DatagramSocket socket = new DatagramSocket()) {
socket.send(packet);
LOGGER.trace("Sent WOL packet to {} {}", broadcast, macAddress);
LOGGER.trace("Sent WOL packet from {} to {} {}", local, broadcast, macAddress);
break;
} catch (IOException e) {
LOGGER.warn("Problem sending WOL packet to {} {}", broadcast, macAddress);
LOGGER.warn("Problem sending WOL packet from {} to {} {}", local, broadcast, macAddress);
}
}
}
Expand All @@ -138,7 +155,7 @@ public static void sendWOLPacket(String macAddress) {
/**
* Create WOL UDP package: 6 bytes 0xff and then 16 times the 6 byte mac address repeated
*
* @param macStr String representation of teh MAC address (either with : or -)
* @param macStr String representation of the MAC address (either with : or -)
* @return byte array with the WOL package
* @throws IllegalArgumentException
*/
Expand Down Expand Up @@ -171,7 +188,7 @@ private static boolean checkIfLinuxCommandExists(String cmd) {
try {
return 0 == Runtime.getRuntime().exec(String.format("which %s", cmd)).waitFor();
} catch (InterruptedException | IOException e) {
LOGGER.debug("Error trying to check if command {} exists: {}", cmd, e.getMessage());
LOGGER.debug("{}: Error trying to check if command {} exists: {}", host, cmd, e.getMessage());
}
return false;
}
Expand Down
Loading

0 comments on commit 2e7b644

Please sign in to comment.