Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[samsungtv] Frame TV Fixes, Improvements and New Channels #11895

Merged
merged 9 commits into from
May 21, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
NickWaterton marked this conversation as resolved.
Show resolved Hide resolved
*
* @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";
lsiepel marked this conversation as resolved.
Show resolved Hide resolved
} else if ((os.contains("mac"))) {
COMMAND = "arp %s";
lsiepel marked this conversation as resolved.
Show resolved Hide resolved
} 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);
}
lsiepel marked this conversation as resolved.
Show resolved Hide resolved
}
} 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