added migrated 2.x add-ons
Signed-off-by: Kai Kreuzer <kai@openhab.org>
This commit is contained in:
@@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<features name="org.openhab.binding.samsungtv-${project.version}" xmlns="http://karaf.apache.org/xmlns/features/v1.4.0">
|
||||
<repository>mvn:org.openhab.core.features.karaf/org.openhab.core.features.karaf.openhab-core/${ohc.version}/xml/features</repository>
|
||||
|
||||
<feature name="openhab-binding-samsungtv" description="Samsung TV Binding" version="${project.version}">
|
||||
<feature>openhab-runtime-base</feature>
|
||||
<feature>openhab-transport-upnp</feature>
|
||||
<bundle start-level="80">mvn:org.openhab.addons.bundles/org.openhab.binding.samsungtv/${project.version}</bundle>
|
||||
</feature>
|
||||
</features>
|
||||
@@ -0,0 +1,54 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 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 org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.openhab.core.thing.ThingTypeUID;
|
||||
|
||||
/**
|
||||
* The {@link SamsungTvBinding} class defines common constants, which are used
|
||||
* across the whole binding.
|
||||
*
|
||||
* @author Pauli Anttila - Initial contribution
|
||||
* @author Arjan Mels - Added constants for websocket based remote controller
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class SamsungTvBindingConstants {
|
||||
|
||||
public static final String BINDING_ID = "samsungtv";
|
||||
|
||||
public static final ThingTypeUID SAMSUNG_TV_THING_TYPE = new ThingTypeUID(BINDING_ID, "tv");
|
||||
|
||||
// List of all remote controller thing channel id's
|
||||
public static final String KEY_CODE = "keyCode";
|
||||
public static final String POWER = "power";
|
||||
public static final String ART_MODE = "artMode";
|
||||
public static final String SOURCE_APP = "sourceApp";
|
||||
|
||||
// List of all media renderer thing channel id's
|
||||
public static final String VOLUME = "volume";
|
||||
public static final String MUTE = "mute";
|
||||
public static final String BRIGHTNESS = "brightness";
|
||||
public static final String CONTRAST = "contrast";
|
||||
public static final String SHARPNESS = "sharpness";
|
||||
public static final String COLOR_TEMPERATURE = "colorTemperature";
|
||||
|
||||
// List of all main TV server thing channel id's
|
||||
public static final String SOURCE_NAME = "sourceName";
|
||||
public static final String SOURCE_ID = "sourceId";
|
||||
public static final String CHANNEL = "channel";
|
||||
public static final String PROGRAM_TITLE = "programTitle";
|
||||
public static final String CHANNEL_NAME = "channelName";
|
||||
public static final String BROWSER_URL = "url";
|
||||
public static final String STOP_BROWSER = "stopBrowser";
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 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 static org.openhab.binding.samsungtv.internal.SamsungTvBindingConstants.SAMSUNG_TV_THING_TYPE;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.Set;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.jupnp.UpnpService;
|
||||
import org.openhab.binding.samsungtv.internal.handler.SamsungTvHandler;
|
||||
import org.openhab.core.config.discovery.DiscoveryServiceRegistry;
|
||||
import org.openhab.core.io.net.http.WebSocketFactory;
|
||||
import org.openhab.core.io.transport.upnp.UpnpIOService;
|
||||
import org.openhab.core.thing.Thing;
|
||||
import org.openhab.core.thing.ThingTypeUID;
|
||||
import org.openhab.core.thing.binding.BaseThingHandlerFactory;
|
||||
import org.openhab.core.thing.binding.ThingHandler;
|
||||
import org.openhab.core.thing.binding.ThingHandlerFactory;
|
||||
import org.osgi.service.component.annotations.Component;
|
||||
import org.osgi.service.component.annotations.Reference;
|
||||
|
||||
/**
|
||||
* The {@link SamsungTvHandlerFactory} is responsible for creating things and
|
||||
* thing handlers.
|
||||
*
|
||||
* @author Pauli Anttila - Initial contribution
|
||||
* @author Arjan Mels - Added Component annotation
|
||||
*/
|
||||
@NonNullByDefault
|
||||
@Component(service = ThingHandlerFactory.class, configurationPid = "binding.samsungtv")
|
||||
public class SamsungTvHandlerFactory extends BaseThingHandlerFactory {
|
||||
|
||||
private static final Set<ThingTypeUID> SUPPORTED_THING_TYPES_UIDS = Collections.singleton(SAMSUNG_TV_THING_TYPE);
|
||||
|
||||
private @NonNullByDefault({}) UpnpIOService upnpIOService;
|
||||
private @NonNullByDefault({}) DiscoveryServiceRegistry discoveryServiceRegistry;
|
||||
private @NonNullByDefault({}) UpnpService upnpService;
|
||||
|
||||
@Reference
|
||||
private @NonNullByDefault({}) WebSocketFactory webSocketFactory;
|
||||
|
||||
@Override
|
||||
public boolean supportsThingType(ThingTypeUID thingTypeUID) {
|
||||
return SUPPORTED_THING_TYPES_UIDS.contains(thingTypeUID);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected @Nullable ThingHandler createHandler(Thing thing) {
|
||||
ThingTypeUID thingTypeUID = thing.getThingTypeUID();
|
||||
|
||||
if (thingTypeUID.equals(SAMSUNG_TV_THING_TYPE)) {
|
||||
return new SamsungTvHandler(thing, upnpIOService, discoveryServiceRegistry, upnpService, webSocketFactory);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
@Reference
|
||||
protected void setUpnpIOService(UpnpIOService upnpIOService) {
|
||||
this.upnpIOService = upnpIOService;
|
||||
}
|
||||
|
||||
protected void unsetUpnpIOService(UpnpIOService upnpIOService) {
|
||||
this.upnpIOService = null;
|
||||
}
|
||||
|
||||
@Reference
|
||||
protected void setDiscoveryServiceRegistry(DiscoveryServiceRegistry discoveryServiceRegistry) {
|
||||
this.discoveryServiceRegistry = discoveryServiceRegistry;
|
||||
}
|
||||
|
||||
protected void unsetDiscoveryServiceRegistry(DiscoveryServiceRegistry discoveryServiceRegistry) {
|
||||
this.discoveryServiceRegistry = null;
|
||||
}
|
||||
|
||||
@Reference
|
||||
protected void setUpnpService(UpnpService upnpService) {
|
||||
this.upnpService = upnpService;
|
||||
}
|
||||
|
||||
protected void unsetUpnpService(UpnpService upnpService) {
|
||||
this.upnpService = null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 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 javax.net.ssl.X509ExtendedTrustManager;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.openhab.core.io.net.http.TlsTrustManagerProvider;
|
||||
import org.openhab.core.io.net.http.TrustAllTrustMananger;
|
||||
import org.osgi.service.component.annotations.Component;
|
||||
|
||||
/**
|
||||
* Provides a TrustManager to allow secure websocket connections to any TV (=server)
|
||||
*
|
||||
* @author Arjan Mels - Initial Contribution
|
||||
*/
|
||||
@Component
|
||||
@NonNullByDefault
|
||||
public class SamsungTvTlsTrustManagerProvider implements TlsTrustManagerProvider {
|
||||
|
||||
@Override
|
||||
public String getHostName() {
|
||||
return "SmartViewSDK";
|
||||
}
|
||||
|
||||
@Override
|
||||
public X509ExtendedTrustManager getTrustManager() {
|
||||
return TrustAllTrustMananger.getInstance();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,174 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 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.net.DatagramPacket;
|
||||
import java.net.DatagramSocket;
|
||||
import java.net.InetAddress;
|
||||
import java.net.InterfaceAddress;
|
||||
import java.net.NetworkInterface;
|
||||
import java.util.Enumeration;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.openhab.core.io.net.exec.ExecUtil;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* Class with utility functions to support Wake On Lan (WOL)
|
||||
*
|
||||
* @author Arjan Mels - Initial contribution
|
||||
* @author Laurent Garnier - Use improvements from the LG webOS binding
|
||||
*
|
||||
*/
|
||||
@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 final String COMMAND;
|
||||
static {
|
||||
String os = System.getProperty("os.name").toLowerCase();
|
||||
LOGGER.debug("os: {}", os);
|
||||
if ((os.indexOf("win") >= 0)) {
|
||||
COMMAND = "arp -a %s";
|
||||
} else if ((os.indexOf("mac") >= 0)) {
|
||||
COMMAND = "arp %s";
|
||||
} 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 {
|
||||
COMMAND = "";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get MAC address for host
|
||||
*
|
||||
* @param hostName Host Name (or IP address) of host to retrieve MAC address for
|
||||
* @return MAC address
|
||||
*/
|
||||
public static @Nullable String getMACAddress(String hostName) {
|
||||
if (COMMAND.isEmpty()) {
|
||||
LOGGER.debug("MAC address detection not possible. No command to identify MAC found.");
|
||||
return null;
|
||||
}
|
||||
|
||||
String cmd = String.format(COMMAND, hostName);
|
||||
String response = ExecUtil.executeCommandLineAndWaitResponse(cmd, CMD_TIMEOUT_MS);
|
||||
Matcher matcher = MAC_REGEX.matcher(response);
|
||||
String macAddress = null;
|
||||
|
||||
while (matcher.find()) {
|
||||
String group = matcher.group();
|
||||
|
||||
if (group.length() == 17) {
|
||||
macAddress = group;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (macAddress != null) {
|
||||
LOGGER.debug("MAC address of host {} is {}", hostName, macAddress);
|
||||
} else {
|
||||
LOGGER.debug("Problem executing command {} to retrieve MAC address for {}: {}", cmd, hostName, response);
|
||||
}
|
||||
return macAddress;
|
||||
}
|
||||
|
||||
/**
|
||||
* Send single WOL (Wake On Lan) package on all interfaces
|
||||
*
|
||||
* @macAddress MAC address to send WOL package to
|
||||
*/
|
||||
public static void sendWOLPacket(String macAddress) {
|
||||
byte[] bytes = getWOLPackage(macAddress);
|
||||
|
||||
try {
|
||||
Enumeration<NetworkInterface> interfaces = NetworkInterface.getNetworkInterfaces();
|
||||
while (interfaces.hasMoreElements()) {
|
||||
NetworkInterface networkInterface = interfaces.nextElement();
|
||||
if (networkInterface.isLoopback()) {
|
||||
continue; // Do not want to use the loopback interface.
|
||||
}
|
||||
for (InterfaceAddress interfaceAddress : networkInterface.getInterfaceAddresses()) {
|
||||
InetAddress broadcast = interfaceAddress.getBroadcast();
|
||||
if (broadcast == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
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);
|
||||
} catch (IOException e) {
|
||||
LOGGER.warn("Problem sending WOL packet to {} {}", broadcast, macAddress);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} catch (IOException e) {
|
||||
LOGGER.warn("Problem with interface while sending WOL packet to {}", 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 -)
|
||||
* @return byte array with the WOL package
|
||||
* @throws IllegalArgumentException
|
||||
*/
|
||||
private static byte[] getWOLPackage(String macStr) throws IllegalArgumentException {
|
||||
byte[] macBytes = new byte[6];
|
||||
String[] hex = macStr.split("(\\:|\\-)");
|
||||
if (hex.length != 6) {
|
||||
throw new IllegalArgumentException("Invalid MAC address.");
|
||||
}
|
||||
try {
|
||||
for (int i = 0; i < 6; i++) {
|
||||
macBytes[i] = (byte) Integer.parseInt(hex[i], 16);
|
||||
}
|
||||
} catch (NumberFormatException e) {
|
||||
throw new IllegalArgumentException("Invalid hex digit in MAC address.");
|
||||
}
|
||||
|
||||
byte[] bytes = new byte[6 + 16 * macBytes.length];
|
||||
for (int i = 0; i < 6; i++) {
|
||||
bytes[i] = (byte) 0xff;
|
||||
}
|
||||
for (int i = 6; i < bytes.length; i += macBytes.length) {
|
||||
System.arraycopy(macBytes, 0, bytes, i, macBytes.length);
|
||||
}
|
||||
|
||||
return bytes;
|
||||
}
|
||||
|
||||
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());
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 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.config;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.openhab.binding.samsungtv.internal.handler.SamsungTvHandler;
|
||||
|
||||
/**
|
||||
* Configuration class for {@link SamsungTvHandler}.
|
||||
*
|
||||
* @author Pauli Anttila - Initial contribution
|
||||
* @author Arjan Mels - Added MAC Address
|
||||
*/
|
||||
@NonNullByDefault({})
|
||||
public class SamsungTvConfiguration {
|
||||
public static final String PROTOCOL = "protocol";
|
||||
public static final String PROTOCOL_NONE = "None";
|
||||
public static final String PROTOCOL_LEGACY = "Legacy";
|
||||
public static final String PROTOCOL_WEBSOCKET = "WebSocket";
|
||||
public static final String PROTOCOL_SECUREWEBSOCKET = "SecureWebSocket";
|
||||
public static final String HOST_NAME = "hostName";
|
||||
public static final String PORT = "port";
|
||||
public static final String MAC_ADDRESS = "macAddress";
|
||||
public static final String REFRESH_INTERVAL = "refreshInterval";
|
||||
public static final String WEBSOCKET_TOKEN = "webSocketToken";
|
||||
public static final int PORT_DEFAULT_LEGACY = 55000;
|
||||
public static final int PORT_DEFAULT_WEBSOCKET = 8001;
|
||||
public static final int PORT_DEFAULT_SECUREWEBSOCKET = 8002;
|
||||
|
||||
public String protocol;
|
||||
public String hostName;
|
||||
public String macAddress;
|
||||
public int port;
|
||||
public int refreshInterval;
|
||||
public String websocketToken;
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 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.discovery;
|
||||
|
||||
import static org.openhab.binding.samsungtv.internal.SamsungTvBindingConstants.SAMSUNG_TV_THING_TYPE;
|
||||
import static org.openhab.binding.samsungtv.internal.config.SamsungTvConfiguration.HOST_NAME;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.jupnp.model.meta.RemoteDevice;
|
||||
import org.openhab.core.config.discovery.DiscoveryResult;
|
||||
import org.openhab.core.config.discovery.DiscoveryResultBuilder;
|
||||
import org.openhab.core.config.discovery.upnp.UpnpDiscoveryParticipant;
|
||||
import org.openhab.core.thing.ThingTypeUID;
|
||||
import org.openhab.core.thing.ThingUID;
|
||||
import org.osgi.service.component.annotations.Component;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* The {@link SamsungTvDiscoveryParticipant} is responsible for processing the
|
||||
* results of searched UPnP devices
|
||||
*
|
||||
* @author Pauli Anttila - Initial contribution
|
||||
* @author Arjan Mels - Changed to upnp.UpnpDiscoveryParticipant
|
||||
*/
|
||||
@NonNullByDefault
|
||||
@Component(immediate = true)
|
||||
public class SamsungTvDiscoveryParticipant implements UpnpDiscoveryParticipant {
|
||||
private final Logger logger = LoggerFactory.getLogger(SamsungTvDiscoveryParticipant.class);
|
||||
|
||||
@Override
|
||||
public Set<ThingTypeUID> getSupportedThingTypeUIDs() {
|
||||
return Collections.singleton(SAMSUNG_TV_THING_TYPE);
|
||||
}
|
||||
|
||||
@Override
|
||||
public @Nullable DiscoveryResult createResult(RemoteDevice device) {
|
||||
ThingUID uid = getThingUID(device);
|
||||
if (uid != null) {
|
||||
Map<String, Object> properties = new HashMap<>();
|
||||
properties.put(HOST_NAME, device.getIdentity().getDescriptorURL().getHost());
|
||||
|
||||
DiscoveryResult result = DiscoveryResultBuilder.create(uid).withProperties(properties)
|
||||
.withRepresentationProperty(HOST_NAME).withLabel(getLabel(device)).build();
|
||||
|
||||
logger.debug("Created a DiscoveryResult for device '{}' with UDN '{}' and properties: {}",
|
||||
device.getDetails().getModelDetails().getModelName(),
|
||||
device.getIdentity().getUdn().getIdentifierString(), properties);
|
||||
return result;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private String getLabel(RemoteDevice device) {
|
||||
String label = "Samsung TV";
|
||||
try {
|
||||
label = device.getDetails().getFriendlyName();
|
||||
} catch (Exception e) {
|
||||
// ignore and use the default label
|
||||
}
|
||||
return label;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @Nullable ThingUID getThingUID(RemoteDevice device) {
|
||||
if (device.getDetails() != null && device.getDetails().getManufacturerDetails() != null) {
|
||||
String manufacturer = device.getDetails().getManufacturerDetails().getManufacturer();
|
||||
|
||||
if (manufacturer != null && manufacturer.toUpperCase().contains("SAMSUNG ELECTRONICS")) {
|
||||
// One Samsung TV contains several UPnP devices.
|
||||
// Create unique Samsung TV thing for every MediaRenderer
|
||||
// device and ignore rest of the UPnP devices.
|
||||
|
||||
if (device.getType() != null && "MediaRenderer".equals(device.getType().getType())) {
|
||||
// UDN shouldn't contain '-' characters.
|
||||
String udn = device.getIdentity().getUdn().getIdentifierString().replace("-", "_");
|
||||
|
||||
if (logger.isDebugEnabled()) {
|
||||
String modelName = device.getDetails().getModelDetails().getModelName();
|
||||
String friendlyName = device.getDetails().getFriendlyName();
|
||||
logger.debug("Retrieved Thing UID for a Samsung TV '{}' model '{}' thing with UDN '{}'",
|
||||
friendlyName, modelName, udn);
|
||||
}
|
||||
|
||||
return new ThingUID(SAMSUNG_TV_THING_TYPE, udn);
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,448 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 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.handler;
|
||||
|
||||
import static org.openhab.binding.samsungtv.internal.SamsungTvBindingConstants.*;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.CopyOnWriteArraySet;
|
||||
import java.util.concurrent.ScheduledFuture;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.jupnp.UpnpService;
|
||||
import org.jupnp.model.meta.Device;
|
||||
import org.jupnp.model.meta.RemoteDevice;
|
||||
import org.openhab.binding.samsungtv.internal.WakeOnLanUtility;
|
||||
import org.openhab.binding.samsungtv.internal.config.SamsungTvConfiguration;
|
||||
import org.openhab.binding.samsungtv.internal.service.RemoteControllerService;
|
||||
import org.openhab.binding.samsungtv.internal.service.ServiceFactory;
|
||||
import org.openhab.binding.samsungtv.internal.service.api.EventListener;
|
||||
import org.openhab.binding.samsungtv.internal.service.api.SamsungTvService;
|
||||
import org.openhab.core.config.discovery.DiscoveryListener;
|
||||
import org.openhab.core.config.discovery.DiscoveryResult;
|
||||
import org.openhab.core.config.discovery.DiscoveryService;
|
||||
import org.openhab.core.config.discovery.DiscoveryServiceRegistry;
|
||||
import org.openhab.core.io.net.http.WebSocketFactory;
|
||||
import org.openhab.core.io.transport.upnp.UpnpIOService;
|
||||
import org.openhab.core.library.types.OnOffType;
|
||||
import org.openhab.core.library.types.StringType;
|
||||
import org.openhab.core.thing.ChannelUID;
|
||||
import org.openhab.core.thing.Thing;
|
||||
import org.openhab.core.thing.ThingStatus;
|
||||
import org.openhab.core.thing.ThingStatusDetail;
|
||||
import org.openhab.core.thing.ThingTypeUID;
|
||||
import org.openhab.core.thing.ThingUID;
|
||||
import org.openhab.core.thing.binding.BaseThingHandler;
|
||||
import org.openhab.core.types.Command;
|
||||
import org.openhab.core.types.RefreshType;
|
||||
import org.openhab.core.types.State;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* The {@link SamsungTvHandler} is responsible for handling commands, which are
|
||||
* sent to one of the channels.
|
||||
*
|
||||
* @author Pauli Anttila - Initial contribution
|
||||
* @author Martin van Wingerden - Some changes for non-UPnP configured devices
|
||||
* @author Arjan Mels - Remove RegistryListener, manually create RemoteService in all circumstances, add sending of WOL
|
||||
* package to power on TV
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class SamsungTvHandler extends BaseThingHandler implements DiscoveryListener, EventListener {
|
||||
|
||||
private static final int WOL_PACKET_RETRY_COUNT = 10;
|
||||
private static final int WOL_SERVICE_CHECK_COUNT = 30;
|
||||
|
||||
private final Logger logger = LoggerFactory.getLogger(SamsungTvHandler.class);
|
||||
|
||||
private final UpnpIOService upnpIOService;
|
||||
private final DiscoveryServiceRegistry discoveryServiceRegistry;
|
||||
private final UpnpService upnpService;
|
||||
private final WebSocketFactory webSocketFactory;
|
||||
|
||||
private SamsungTvConfiguration configuration;
|
||||
|
||||
private @Nullable ThingUID upnpThingUID = null;
|
||||
|
||||
/* Samsung TV services */
|
||||
private final Set<SamsungTvService> services = new CopyOnWriteArraySet<>();
|
||||
|
||||
/* Store powerState to be able to restore upon new link */
|
||||
private boolean powerState = false;
|
||||
|
||||
/* Store if art mode is supported to be able to skip switching power state to ON during initialization */
|
||||
boolean artModeIsSupported = false;
|
||||
|
||||
private @Nullable ScheduledFuture<?> pollingJob;
|
||||
|
||||
public SamsungTvHandler(Thing thing, UpnpIOService upnpIOService, DiscoveryServiceRegistry discoveryServiceRegistry,
|
||||
UpnpService upnpService, WebSocketFactory webSocketFactory) {
|
||||
super(thing);
|
||||
|
||||
logger.debug("Create a Samsung TV Handler for thing '{}'", getThing().getUID());
|
||||
|
||||
this.upnpIOService = upnpIOService;
|
||||
this.upnpService = upnpService;
|
||||
this.discoveryServiceRegistry = discoveryServiceRegistry;
|
||||
this.webSocketFactory = webSocketFactory;
|
||||
this.configuration = getConfigAs(SamsungTvConfiguration.class);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleCommand(ChannelUID channelUID, Command command) {
|
||||
logger.debug("Received channel: {}, command: {}", channelUID, command);
|
||||
|
||||
String channel = channelUID.getId();
|
||||
|
||||
// if power on command try WOL for good measure:
|
||||
if ((channel.equals(POWER) || channel.equals(ART_MODE)) && OnOffType.ON.equals(command)) {
|
||||
sendWOLandResendCommand(channel, command);
|
||||
}
|
||||
|
||||
// Delegate command to correct service
|
||||
for (SamsungTvService service : services) {
|
||||
for (String s : service.getSupportedChannelNames()) {
|
||||
if (channel.equals(s)) {
|
||||
service.handleCommand(channel, command);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
logger.warn("Channel '{}' not supported", channelUID);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void channelLinked(ChannelUID channelUID) {
|
||||
logger.trace("channelLinked: {}", channelUID);
|
||||
|
||||
updateState(POWER, getPowerState() ? OnOffType.ON : OnOffType.OFF);
|
||||
|
||||
for (SamsungTvService service : services) {
|
||||
service.clearCache();
|
||||
}
|
||||
}
|
||||
|
||||
private synchronized void setPowerState(boolean state) {
|
||||
powerState = state;
|
||||
}
|
||||
|
||||
private synchronized boolean getPowerState() {
|
||||
return powerState;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void initialize() {
|
||||
updateStatus(ThingStatus.UNKNOWN);
|
||||
|
||||
logger.debug("Initializing Samsung TV handler for uid '{}'", getThing().getUID());
|
||||
|
||||
configuration = getConfigAs(SamsungTvConfiguration.class);
|
||||
|
||||
discoveryServiceRegistry.addDiscoveryListener(this);
|
||||
|
||||
checkAndCreateServices();
|
||||
|
||||
logger.debug("Start refresh task, interval={}", configuration.refreshInterval);
|
||||
pollingJob = scheduler.scheduleWithFixedDelay(this::poll, 0, configuration.refreshInterval,
|
||||
TimeUnit.MILLISECONDS);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void dispose() {
|
||||
logger.debug("Disposing SamsungTvHandler");
|
||||
|
||||
if (pollingJob != null) {
|
||||
if (!pollingJob.isCancelled()) {
|
||||
pollingJob.cancel(true);
|
||||
}
|
||||
pollingJob = null;
|
||||
}
|
||||
|
||||
discoveryServiceRegistry.removeDiscoveryListener(this);
|
||||
shutdown();
|
||||
putOffline();
|
||||
}
|
||||
|
||||
private void shutdown() {
|
||||
logger.debug("Shutdown all Samsung services");
|
||||
for (SamsungTvService service : services) {
|
||||
stopService(service);
|
||||
}
|
||||
services.clear();
|
||||
}
|
||||
|
||||
private synchronized void putOnline() {
|
||||
setPowerState(true);
|
||||
updateStatus(ThingStatus.ONLINE);
|
||||
|
||||
if (!artModeIsSupported) {
|
||||
updateState(POWER, OnOffType.ON);
|
||||
}
|
||||
}
|
||||
|
||||
private synchronized void putOffline() {
|
||||
setPowerState(false);
|
||||
updateStatus(ThingStatus.OFFLINE);
|
||||
updateState(ART_MODE, OnOffType.OFF);
|
||||
updateState(POWER, OnOffType.OFF);
|
||||
updateState(SOURCE_APP, new StringType(""));
|
||||
}
|
||||
|
||||
private void poll() {
|
||||
for (SamsungTvService service : services) {
|
||||
for (String channel : service.getSupportedChannelNames()) {
|
||||
if (isLinked(channel)) {
|
||||
// Avoid redundant REFRESH commands when 2 channels are linked to the same UPnP action request
|
||||
if ((channel.equals(SOURCE_ID) && isLinked(SOURCE_NAME))
|
||||
|| (channel.equals(CHANNEL_NAME) && isLinked(PROGRAM_TITLE))) {
|
||||
continue;
|
||||
}
|
||||
service.handleCommand(channel, RefreshType.REFRESH);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized void valueReceived(String variable, State value) {
|
||||
logger.debug("Received value '{}':'{}' for thing '{}'", variable, value, this.getThing().getUID());
|
||||
|
||||
if (POWER.equals(variable)) {
|
||||
setPowerState(OnOffType.ON.equals(value));
|
||||
} else if (ART_MODE.equals(variable)) {
|
||||
artModeIsSupported = true;
|
||||
}
|
||||
updateState(variable, value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void reportError(ThingStatusDetail statusDetail, @Nullable String message, @Nullable Throwable e) {
|
||||
logger.debug("Error was reported: {}", message, e);
|
||||
updateStatus(ThingStatus.OFFLINE, statusDetail, message);
|
||||
}
|
||||
|
||||
/**
|
||||
* One Samsung TV contains several UPnP devices. Samsung TV is discovered by
|
||||
* Media Renderer UPnP device. This function tries to find another UPnP
|
||||
* devices related to same Samsung TV and create handler for those.
|
||||
*/
|
||||
private void checkAndCreateServices() {
|
||||
logger.debug("Check and create missing UPnP services");
|
||||
|
||||
for (Device<?, ?, ?> device : upnpService.getRegistry().getDevices()) {
|
||||
createService((RemoteDevice) device);
|
||||
}
|
||||
|
||||
checkCreateManualConnection();
|
||||
}
|
||||
|
||||
private synchronized void createService(RemoteDevice device) {
|
||||
if (configuration.hostName != null
|
||||
&& configuration.hostName.equals(device.getIdentity().getDescriptorURL().getHost())) {
|
||||
String modelName = device.getDetails().getModelDetails().getModelName();
|
||||
String udn = device.getIdentity().getUdn().getIdentifierString();
|
||||
String type = device.getType().getType();
|
||||
|
||||
SamsungTvService existingService = findServiceInstance(type);
|
||||
|
||||
if (existingService == null || !existingService.isUpnp()) {
|
||||
SamsungTvService newService = ServiceFactory.createService(type, upnpIOService, udn,
|
||||
configuration.hostName, configuration.port);
|
||||
|
||||
if (newService != null) {
|
||||
if (existingService != null) {
|
||||
stopService(existingService);
|
||||
startService(newService);
|
||||
logger.debug("Restarting service in UPnP mode for: {}, {} ({})", modelName, type, udn);
|
||||
} else {
|
||||
startService(newService);
|
||||
logger.debug("Started service for: {}, {} ({})", modelName, type, udn);
|
||||
}
|
||||
} else {
|
||||
logger.trace("Skipping unknown UPnP service: {}, {} ({})", modelName, type, udn);
|
||||
}
|
||||
} else {
|
||||
logger.debug("Service rediscovered, clearing caches: {}, {} ({})", modelName, type, udn);
|
||||
existingService.clearCache();
|
||||
}
|
||||
putOnline();
|
||||
}
|
||||
}
|
||||
|
||||
private @Nullable SamsungTvService findServiceInstance(String serviceName) {
|
||||
Class<? extends SamsungTvService> cl = ServiceFactory.getClassByServiceName(serviceName);
|
||||
|
||||
for (SamsungTvService service : services) {
|
||||
if (service.getClass() == cl) {
|
||||
return service;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private synchronized void checkCreateManualConnection() {
|
||||
try {
|
||||
// create remote service manually if it does not yet exist
|
||||
|
||||
RemoteControllerService service = (RemoteControllerService) findServiceInstance(
|
||||
RemoteControllerService.SERVICE_NAME);
|
||||
if (service == null) {
|
||||
service = RemoteControllerService.createNonUpnpService(configuration.hostName, configuration.port);
|
||||
startService(service);
|
||||
} else {
|
||||
// open connection again if needed
|
||||
if (!service.checkConnection()) {
|
||||
service.start();
|
||||
}
|
||||
}
|
||||
} catch (RuntimeException e) {
|
||||
logger.warn("Catching all exceptions because otherwise the thread would silently fail", e);
|
||||
}
|
||||
}
|
||||
|
||||
private synchronized void startService(SamsungTvService service) {
|
||||
service.addEventListener(this);
|
||||
service.start();
|
||||
services.add(service);
|
||||
}
|
||||
|
||||
private synchronized void stopService(SamsungTvService service) {
|
||||
service.stop();
|
||||
service.removeEventListener(this);
|
||||
services.remove(service);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void thingDiscovered(DiscoveryService source, DiscoveryResult result) {
|
||||
if (configuration.hostName != null
|
||||
&& configuration.hostName.equals(result.getProperties().get(SamsungTvConfiguration.HOST_NAME))) {
|
||||
logger.debug("thingDiscovered: {}, {}", result.getProperties().get(SamsungTvConfiguration.HOST_NAME),
|
||||
result);
|
||||
|
||||
/* Check if configuration should be updated */
|
||||
if (configuration.macAddress == null || configuration.macAddress.trim().isEmpty()) {
|
||||
String macAddress = WakeOnLanUtility.getMACAddress(configuration.hostName);
|
||||
if (macAddress != null) {
|
||||
putConfig(SamsungTvConfiguration.MAC_ADDRESS, macAddress);
|
||||
logger.debug("thingDiscovered, macAddress: {}", macAddress);
|
||||
}
|
||||
}
|
||||
if (SamsungTvConfiguration.PROTOCOL_NONE.equals(configuration.protocol)) {
|
||||
Map<String, Object> properties = RemoteControllerService.discover(configuration.hostName);
|
||||
for (Map.Entry<String, Object> property : properties.entrySet()) {
|
||||
putConfig(property.getKey(), property.getValue());
|
||||
logger.debug("thingDiscovered, {}: {}", property.getKey(), property.getValue());
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* SamsungTV discovery services creates thing UID from UPnP UDN.
|
||||
* When thing is generated manually, thing UID may not match UPnP UDN, so store it for later use (e.g.
|
||||
* thingRemoved).
|
||||
*/
|
||||
upnpThingUID = result.getThingUID();
|
||||
logger.debug("thingDiscovered, thingUID={}, discoveredUID={}", this.getThing().getUID(), upnpThingUID);
|
||||
checkAndCreateServices();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void thingRemoved(DiscoveryService source, ThingUID thingUID) {
|
||||
if (thingUID.equals(upnpThingUID)) {
|
||||
logger.debug("Thing Removed: {}", thingUID);
|
||||
shutdown();
|
||||
putOffline();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public @Nullable Collection<ThingUID> removeOlderResults(DiscoveryService source, long timestamp,
|
||||
@Nullable Collection<ThingTypeUID> thingTypeUIDs, @Nullable ThingUID bridgeUID) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
/**
|
||||
* Send multiple WOL packets spaced with 100ms intervals and resend command
|
||||
*
|
||||
* @param channel Channel to resend command on
|
||||
* @param command Command to resend
|
||||
*/
|
||||
private void sendWOLandResendCommand(String channel, Command command) {
|
||||
if (configuration.macAddress == null || configuration.macAddress.isEmpty()) {
|
||||
logger.warn("Cannot send WOL packet to {} MAC address unknown", configuration.hostName);
|
||||
return;
|
||||
} else {
|
||||
logger.info("Send WOL packet to {} ({})", configuration.hostName, configuration.macAddress);
|
||||
|
||||
// send max 10 WOL packets with 100ms intervals
|
||||
scheduler.schedule(new Runnable() {
|
||||
int count = 0;
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
count++;
|
||||
if (count < WOL_PACKET_RETRY_COUNT) {
|
||||
WakeOnLanUtility.sendWOLPacket(configuration.macAddress);
|
||||
scheduler.schedule(this, 100, TimeUnit.MILLISECONDS);
|
||||
}
|
||||
}
|
||||
}, 1, TimeUnit.MILLISECONDS);
|
||||
|
||||
// after RemoteService up again to ensure state is properly set
|
||||
scheduler.schedule(new Runnable() {
|
||||
int count = 0;
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
count++;
|
||||
if (count < WOL_SERVICE_CHECK_COUNT) {
|
||||
RemoteControllerService service = (RemoteControllerService) findServiceInstance(
|
||||
RemoteControllerService.SERVICE_NAME);
|
||||
if (service != null) {
|
||||
logger.info("Service found after {} attempts: resend command {} to channel {}", count,
|
||||
command, channel);
|
||||
service.handleCommand(channel, command);
|
||||
} else {
|
||||
scheduler.schedule(this, 1000, TimeUnit.MILLISECONDS);
|
||||
}
|
||||
} else {
|
||||
logger.info("Service NOT found after {} attempts", count);
|
||||
}
|
||||
}
|
||||
}, 1000, TimeUnit.MILLISECONDS);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void putConfig(@Nullable String key, @Nullable Object value) {
|
||||
getConfig().put(key, value);
|
||||
configuration = getConfigAs(SamsungTvConfiguration.class);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object getConfig(@Nullable String key) {
|
||||
return getConfig().get(key);
|
||||
}
|
||||
|
||||
@Override
|
||||
public WebSocketFactory getWebSocketFactory() {
|
||||
return webSocketFactory;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,292 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 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.protocol;
|
||||
|
||||
/**
|
||||
* The {@link KeyCode} presents all available key codes of Samsung TV.
|
||||
*
|
||||
* @see <a
|
||||
* href="http://wiki.samygo.tv/index.php5/D-Series_Key_Codes">http://wiki.samygo.tv/index.php5/D-Series_Key_Codes
|
||||
* </a>
|
||||
*
|
||||
*
|
||||
* @author Pauli Anttila - Initial contribution
|
||||
*/
|
||||
public enum KeyCode {
|
||||
|
||||
KEY_0,
|
||||
KEY_1,
|
||||
KEY_2,
|
||||
KEY_3,
|
||||
KEY_4,
|
||||
KEY_5,
|
||||
KEY_6,
|
||||
KEY_7,
|
||||
KEY_8,
|
||||
KEY_9,
|
||||
KEY_11,
|
||||
KEY_12,
|
||||
KEY_3SPEED,
|
||||
KEY_4_3,
|
||||
KEY_16_9,
|
||||
KEY_AD,
|
||||
KEY_ADDDEL,
|
||||
KEY_ALT_MHP,
|
||||
KEY_ANGLE,
|
||||
KEY_ANTENA,
|
||||
KEY_ANYNET,
|
||||
KEY_ANYVIEW,
|
||||
KEY_APP_LIST,
|
||||
KEY_ASPECT,
|
||||
KEY_AUTO_ARC_ANTENNA_AIR,
|
||||
KEY_AUTO_ARC_ANTENNA_CABLE,
|
||||
KEY_AUTO_ARC_ANTENNA_SATELLITE,
|
||||
KEY_AUTO_ARC_ANYNET_AUTO_START,
|
||||
KEY_AUTO_ARC_ANYNET_MODE_OK,
|
||||
KEY_AUTO_ARC_AUTOCOLOR_FAIL,
|
||||
KEY_AUTO_ARC_AUTOCOLOR_SUCCESS,
|
||||
KEY_AUTO_ARC_CAPTION_ENG,
|
||||
KEY_AUTO_ARC_CAPTION_KOR,
|
||||
KEY_AUTO_ARC_CAPTION_OFF,
|
||||
KEY_AUTO_ARC_CAPTION_ON,
|
||||
KEY_AUTO_ARC_C_FORCE_AGING,
|
||||
KEY_AUTO_ARC_JACK_IDENT,
|
||||
KEY_AUTO_ARC_LNA_OFF,
|
||||
KEY_AUTO_ARC_LNA_ON,
|
||||
KEY_AUTO_ARC_PIP_CH_CHANGE,
|
||||
KEY_AUTO_ARC_PIP_DOUBLE,
|
||||
KEY_AUTO_ARC_PIP_LARGE,
|
||||
KEY_AUTO_ARC_PIP_LEFT_BOTTOM,
|
||||
KEY_AUTO_ARC_PIP_LEFT_TOP,
|
||||
KEY_AUTO_ARC_PIP_RIGHT_BOTTOM,
|
||||
KEY_AUTO_ARC_PIP_RIGHT_TOP,
|
||||
KEY_AUTO_ARC_PIP_SMALL,
|
||||
KEY_AUTO_ARC_PIP_SOURCE_CHANGE,
|
||||
KEY_AUTO_ARC_PIP_WIDE,
|
||||
KEY_AUTO_ARC_RESET,
|
||||
KEY_AUTO_ARC_USBJACK_INSPECT,
|
||||
KEY_AUTO_FORMAT,
|
||||
KEY_AUTO_PROGRAM,
|
||||
KEY_AV1,
|
||||
KEY_AV2,
|
||||
KEY_AV3,
|
||||
KEY_BACK_MHP,
|
||||
KEY_BOOKMARK,
|
||||
KEY_CALLER_ID,
|
||||
KEY_CAPTION,
|
||||
KEY_CATV_MODE,
|
||||
KEY_CHDOWN,
|
||||
KEY_CHUP,
|
||||
KEY_CH_LIST,
|
||||
KEY_CLEAR,
|
||||
KEY_CLOCK_DISPLAY,
|
||||
KEY_COMPONENT1,
|
||||
KEY_COMPONENT2,
|
||||
KEY_CONTENTS,
|
||||
KEY_CONVERGENCE,
|
||||
KEY_CONVERT_AUDIO_MAINSUB,
|
||||
KEY_CUSTOM,
|
||||
KEY_CYAN,
|
||||
KEY_BLUE(KEY_CYAN),
|
||||
KEY_DEVICE_CONNECT,
|
||||
KEY_DISC_MENU,
|
||||
KEY_DMA,
|
||||
KEY_DNET,
|
||||
KEY_DNIE,
|
||||
KEY_DNSE,
|
||||
KEY_DOOR,
|
||||
KEY_DOWN,
|
||||
KEY_DSS_MODE,
|
||||
KEY_DTV,
|
||||
KEY_DTV_LINK,
|
||||
KEY_DTV_SIGNAL,
|
||||
KEY_DVD_MODE,
|
||||
KEY_DVI,
|
||||
KEY_DVR,
|
||||
KEY_DVR_MENU,
|
||||
KEY_DYNAMIC,
|
||||
KEY_ENTER,
|
||||
KEY_ENTERTAINMENT,
|
||||
KEY_ESAVING,
|
||||
KEY_EXIT,
|
||||
KEY_EXT1,
|
||||
KEY_EXT2,
|
||||
KEY_EXT3,
|
||||
KEY_EXT4,
|
||||
KEY_EXT5,
|
||||
KEY_EXT6,
|
||||
KEY_EXT7,
|
||||
KEY_EXT8,
|
||||
KEY_EXT9,
|
||||
KEY_EXT10,
|
||||
KEY_EXT11,
|
||||
KEY_EXT12,
|
||||
KEY_EXT13,
|
||||
KEY_EXT14,
|
||||
KEY_EXT15,
|
||||
KEY_EXT16,
|
||||
KEY_EXT17,
|
||||
KEY_EXT18,
|
||||
KEY_EXT19,
|
||||
KEY_EXT20,
|
||||
KEY_EXT21,
|
||||
KEY_EXT22,
|
||||
KEY_EXT23,
|
||||
KEY_EXT24,
|
||||
KEY_EXT25,
|
||||
KEY_EXT26,
|
||||
KEY_EXT27,
|
||||
KEY_EXT28,
|
||||
KEY_EXT29,
|
||||
KEY_EXT30,
|
||||
KEY_EXT31,
|
||||
KEY_EXT32,
|
||||
KEY_EXT33,
|
||||
KEY_EXT34,
|
||||
KEY_EXT35,
|
||||
KEY_EXT36,
|
||||
KEY_EXT37,
|
||||
KEY_EXT38,
|
||||
KEY_EXT39,
|
||||
KEY_EXT40,
|
||||
KEY_EXT41,
|
||||
KEY_FACTORY,
|
||||
KEY_FAVCH,
|
||||
KEY_FF,
|
||||
KEY_FM_RADIO,
|
||||
KEY_GAME,
|
||||
KEY_GREEN,
|
||||
KEY_GUIDE,
|
||||
KEY_HDMI,
|
||||
KEY_HDMI1,
|
||||
KEY_HDMI2,
|
||||
KEY_HDMI3,
|
||||
KEY_HDMI4,
|
||||
KEY_HELP,
|
||||
KEY_HOME,
|
||||
KEY_ID_INPUT,
|
||||
KEY_ID_SETUP,
|
||||
KEY_INFO,
|
||||
KEY_INSTANT_REPLAY,
|
||||
KEY_LEFT,
|
||||
KEY_LINK,
|
||||
KEY_LIVE,
|
||||
KEY_MAGIC_BRIGHT,
|
||||
KEY_MAGIC_CHANNEL,
|
||||
KEY_MDC,
|
||||
KEY_MENU,
|
||||
KEY_MIC,
|
||||
KEY_MORE,
|
||||
KEY_MOVIE1,
|
||||
KEY_MS,
|
||||
KEY_MTS,
|
||||
KEY_MUTE,
|
||||
KEY_NINE_SEPERATE,
|
||||
KEY_OPEN,
|
||||
KEY_PANNEL_CHDOWN,
|
||||
KEY_PANNEL_CHUP,
|
||||
KEY_PANNEL_ENTER,
|
||||
KEY_PANNEL_MENU,
|
||||
KEY_PANNEL_POWER,
|
||||
KEY_PANNEL_SOURCE,
|
||||
KEY_PANNEL_VOLDOW,
|
||||
KEY_PANNEL_VOLUP,
|
||||
KEY_PANORAMA,
|
||||
KEY_PAUSE,
|
||||
KEY_PCMODE,
|
||||
KEY_PERPECT_FOCUS,
|
||||
KEY_PICTURE_SIZE,
|
||||
KEY_PIP_CHDOWN,
|
||||
KEY_PIP_CHUP,
|
||||
KEY_PIP_ONOFF,
|
||||
KEY_PIP_SCAN,
|
||||
KEY_PIP_SIZE,
|
||||
KEY_PIP_SWAP,
|
||||
KEY_PLAY,
|
||||
KEY_PLUS100,
|
||||
KEY_PMODE,
|
||||
KEY_POWER,
|
||||
KEY_POWEROFF,
|
||||
KEY_POWERON,
|
||||
KEY_PRECH,
|
||||
KEY_PRINT,
|
||||
KEY_PROGRAM,
|
||||
KEY_QUICK_REPLAY,
|
||||
KEY_REC,
|
||||
KEY_RED,
|
||||
KEY_REPEAT,
|
||||
KEY_RESERVED1,
|
||||
KEY_RETURN,
|
||||
KEY_REWIND,
|
||||
KEY_RIGHT,
|
||||
KEY_RSS,
|
||||
KEY_INTERNET(KEY_RSS),
|
||||
KEY_RSURF,
|
||||
KEY_SCALE,
|
||||
KEY_SEFFECT,
|
||||
KEY_SETUP_CLOCK_TIMER,
|
||||
KEY_SLEEP,
|
||||
KEY_SOUND_MODE,
|
||||
KEY_SOURCE,
|
||||
KEY_SRS,
|
||||
KEY_STANDARD,
|
||||
KEY_STB_MODE,
|
||||
KEY_STILL_PICTURE,
|
||||
KEY_STOP,
|
||||
KEY_SUB_TITLE,
|
||||
KEY_SVIDEO1,
|
||||
KEY_SVIDEO2,
|
||||
KEY_SVIDEO3,
|
||||
KEY_TOOLS,
|
||||
KEY_TOPMENU,
|
||||
KEY_TTX_MIX,
|
||||
KEY_TTX_SUBFACE,
|
||||
KEY_TURBO,
|
||||
KEY_TV,
|
||||
KEY_TV_MODE,
|
||||
KEY_UP,
|
||||
KEY_VCHIP,
|
||||
KEY_VCR_MODE,
|
||||
KEY_VOLDOWN,
|
||||
KEY_VOLUP,
|
||||
KEY_WHEEL_LEFT,
|
||||
KEY_WHEEL_RIGHT,
|
||||
KEY_W_LINK,
|
||||
KEY_YELLOW,
|
||||
KEY_ZOOM1,
|
||||
KEY_ZOOM2,
|
||||
KEY_ZOOM_IN,
|
||||
KEY_ZOOM_MOVE,
|
||||
KEY_ZOOM_OUT;
|
||||
|
||||
private final String value;
|
||||
|
||||
KeyCode() {
|
||||
value = null;
|
||||
}
|
||||
|
||||
KeyCode(String value) {
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
KeyCode(KeyCode otherKey) {
|
||||
this(otherKey.getValue());
|
||||
}
|
||||
|
||||
public String getValue() {
|
||||
if (value == null) {
|
||||
return this.name();
|
||||
}
|
||||
return value;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 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.protocol;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
|
||||
/**
|
||||
* The {@link RemoteController} is the base class for handling remote control keys for the Samsung TV.
|
||||
*
|
||||
* @author Arjan Mels - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public abstract class RemoteController implements AutoCloseable {
|
||||
protected String host;
|
||||
protected int port;
|
||||
protected String appName;
|
||||
protected String uniqueId;
|
||||
|
||||
public RemoteController(String host, int port, @Nullable String appName, @Nullable String uniqueId) {
|
||||
this.host = host;
|
||||
this.port = port;
|
||||
this.appName = appName != null ? appName : "";
|
||||
this.uniqueId = uniqueId != null ? uniqueId : "";
|
||||
}
|
||||
|
||||
public abstract void openConnection() throws RemoteControllerException;
|
||||
|
||||
public abstract boolean isConnected();
|
||||
|
||||
public abstract void sendKey(KeyCode key) throws RemoteControllerException;
|
||||
|
||||
public abstract void sendKeys(List<KeyCode> keys) throws RemoteControllerException;
|
||||
|
||||
@Override
|
||||
public abstract void close() throws RemoteControllerException;
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 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.protocol;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
|
||||
/**
|
||||
* Exception for Samsung TV communication
|
||||
*
|
||||
* @author Pauli Anttila - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class RemoteControllerException extends Exception {
|
||||
|
||||
private static final long serialVersionUID = -5292218577704635666L;
|
||||
|
||||
public RemoteControllerException(String message) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
public RemoteControllerException(String message, Throwable cause) {
|
||||
super(message, cause);
|
||||
}
|
||||
|
||||
public RemoteControllerException(Throwable cause) {
|
||||
super(cause);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,414 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 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.protocol;
|
||||
|
||||
import java.io.BufferedWriter;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.InputStreamReader;
|
||||
import java.io.OutputStreamWriter;
|
||||
import java.io.Reader;
|
||||
import java.io.StringWriter;
|
||||
import java.io.Writer;
|
||||
import java.net.InetSocketAddress;
|
||||
import java.net.Socket;
|
||||
import java.util.Arrays;
|
||||
import java.util.Base64;
|
||||
import java.util.List;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* The {@link RemoteControllerLegacy} is responsible for sending key codes to the
|
||||
* Samsung TV.
|
||||
*
|
||||
* @see <a
|
||||
* href="http://sc0ty.pl/2012/02/samsung-tv-network-remote-control-protocol/">http://sc0ty.pl/2012/02/samsung-tv-
|
||||
* network-remote-control-protocol/</a>
|
||||
*
|
||||
*
|
||||
* @author Pauli Anttila - Initial contribution
|
||||
* @author Arjan Mels - Renamed and reworked to use RemoteController base class, to allow different protocols
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class RemoteControllerLegacy extends RemoteController {
|
||||
|
||||
private static final int CONNECTION_TIMEOUT = 500;
|
||||
|
||||
private final Logger logger = LoggerFactory.getLogger(RemoteControllerLegacy.class);
|
||||
|
||||
// Access granted response
|
||||
private static final char[] ACCESS_GRANTED_RESP = new char[] { 0x64, 0x00, 0x01, 0x00 };
|
||||
|
||||
// User rejected your network remote controller response
|
||||
private static final char[] ACCESS_DENIED_RESP = new char[] { 0x64, 0x00, 0x00, 0x00 };
|
||||
|
||||
// waiting for user to grant or deny access response
|
||||
private static final char[] WAITING_USER_GRANT_RESP = new char[] { 0x0A, 0x00, 0x02, 0x00, 0x00, 0x00 };
|
||||
|
||||
// timeout or cancelled by user response
|
||||
private static final char[] ACCESS_TIMEOUT_RESP = new char[] { 0x65, 0x00 };
|
||||
|
||||
private static final String APP_STRING = "iphone.iapp.samsung";
|
||||
|
||||
private @Nullable Socket socket;
|
||||
private @Nullable InputStreamReader reader;
|
||||
private @Nullable BufferedWriter writer;
|
||||
|
||||
/**
|
||||
* Create and initialize remote controller instance.
|
||||
*
|
||||
* @param host Host name of the Samsung TV.
|
||||
* @param port TCP port of the remote controller protocol.
|
||||
* @param appName Application name used to send key codes.
|
||||
* @param uniqueId Unique Id used to send key codes.
|
||||
*/
|
||||
public RemoteControllerLegacy(String host, int port, @Nullable String appName, @Nullable String uniqueId) {
|
||||
super(host, port, appName, uniqueId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Open Connection to Samsung TV.
|
||||
*
|
||||
* @throws RemoteControllerException
|
||||
*/
|
||||
@Override
|
||||
public void openConnection() throws RemoteControllerException {
|
||||
logger.debug("Open connection to host '{}:{}'", host, port);
|
||||
|
||||
Socket localsocket = new Socket();
|
||||
socket = localsocket;
|
||||
try {
|
||||
socket.connect(new InetSocketAddress(host, port), CONNECTION_TIMEOUT);
|
||||
} catch (IOException e) {
|
||||
logger.debug("Cannot connect to Legacy Remote Controller: {}", e.getMessage());
|
||||
throw new RemoteControllerException("Connection failed", e);
|
||||
}
|
||||
|
||||
InputStream inputStream;
|
||||
try {
|
||||
BufferedWriter localwriter = new BufferedWriter(new OutputStreamWriter(localsocket.getOutputStream()));
|
||||
writer = localwriter;
|
||||
inputStream = localsocket.getInputStream();
|
||||
InputStreamReader localreader = new InputStreamReader(inputStream);
|
||||
reader = localreader;
|
||||
|
||||
logger.debug("Connection successfully opened...querying access");
|
||||
writeInitialInfo(localwriter, localsocket);
|
||||
readInitialInfo(localreader);
|
||||
|
||||
int i;
|
||||
while ((i = inputStream.available()) > 0) {
|
||||
inputStream.skip(i);
|
||||
}
|
||||
} catch (IOException e) {
|
||||
throw new RemoteControllerException(e);
|
||||
}
|
||||
}
|
||||
|
||||
private void writeInitialInfo(Writer writer, Socket socket) throws RemoteControllerException {
|
||||
try {
|
||||
/* @formatter:off
|
||||
*
|
||||
* offset value and description
|
||||
* ------ ---------------------
|
||||
* 0x00 0x00 - datagram type?
|
||||
* 0x01 0x0013 - string length (little endian)
|
||||
* 0x03 "iphone.iapp.samsung" - string content
|
||||
* 0x16 0x0038 - payload size (little endian)
|
||||
* 0x18 payload
|
||||
*
|
||||
* Payload starts with 2 bytes: 0x64 and 0x00, then comes 3 strings
|
||||
* encoded with base64 algorithm. Every string is preceded by
|
||||
* 2-bytes field containing encoded string length.
|
||||
*
|
||||
* These three strings are as follow:
|
||||
*
|
||||
* remote control device IP, unique ID – value to distinguish
|
||||
* controllers, name – it will be displayed as controller name.
|
||||
*
|
||||
* @formatter:on
|
||||
*/
|
||||
|
||||
writer.append((char) 0x00);
|
||||
writeString(writer, APP_STRING);
|
||||
writeString(writer, createRegistrationPayload(socket.getLocalAddress().getHostAddress()));
|
||||
writer.flush();
|
||||
} catch (IOException e) {
|
||||
throw new RemoteControllerException(e);
|
||||
}
|
||||
}
|
||||
|
||||
private void readInitialInfo(Reader reader) throws RemoteControllerException {
|
||||
try {
|
||||
/* @formatter:off
|
||||
*
|
||||
* offset value and description
|
||||
* ------ ---------------------
|
||||
* 0x00 don't know, it it always 0x00 or 0x02
|
||||
* 0x01 0x000c - string length (little endian)
|
||||
* 0x03 "iapp.samsung" - string content
|
||||
* 0x0f 0x0006 - payload size (little endian)
|
||||
* 0x11 payload
|
||||
*
|
||||
* @formatter:on
|
||||
*/
|
||||
|
||||
reader.skip(1);
|
||||
readString(reader);
|
||||
char[] result = readCharArray(reader);
|
||||
|
||||
if (Arrays.equals(result, ACCESS_GRANTED_RESP)) {
|
||||
logger.debug("Access granted");
|
||||
} else if (Arrays.equals(result, ACCESS_DENIED_RESP)) {
|
||||
throw new RemoteControllerException("Access denied");
|
||||
} else if (Arrays.equals(result, ACCESS_TIMEOUT_RESP)) {
|
||||
throw new RemoteControllerException("Registration timed out");
|
||||
} else if (Arrays.equals(result, WAITING_USER_GRANT_RESP)) {
|
||||
throw new RemoteControllerException("Waiting for user to grant access");
|
||||
} else {
|
||||
throw new RemoteControllerException("Unknown response received for access query");
|
||||
}
|
||||
} catch (IOException e) {
|
||||
throw new RemoteControllerException(e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Close connection to Samsung TV.
|
||||
*
|
||||
* @throws RemoteControllerException
|
||||
*/
|
||||
public void closeConnection() throws RemoteControllerException {
|
||||
try {
|
||||
if (socket != null) {
|
||||
socket.close();
|
||||
}
|
||||
} catch (IOException e) {
|
||||
throw new RemoteControllerException(e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Send key code to Samsung TV.
|
||||
*
|
||||
* @param key Key code to send.
|
||||
* @throws RemoteControllerException
|
||||
*/
|
||||
@Override
|
||||
public void sendKey(KeyCode key) throws RemoteControllerException {
|
||||
logger.debug("Try to send command: {}", key);
|
||||
|
||||
if (!isConnected()) {
|
||||
openConnection();
|
||||
}
|
||||
|
||||
try {
|
||||
sendKeyData(key);
|
||||
} catch (RemoteControllerException e) {
|
||||
logger.debug("Couldn't send command", e);
|
||||
logger.debug("Retry one time...");
|
||||
|
||||
closeConnection();
|
||||
openConnection();
|
||||
|
||||
sendKeyData(key);
|
||||
}
|
||||
|
||||
logger.debug("Command successfully sent");
|
||||
}
|
||||
|
||||
/**
|
||||
* Send sequence of key codes to Samsung TV.
|
||||
*
|
||||
* @param keys List of key codes to send.
|
||||
* @throws RemoteControllerException
|
||||
*/
|
||||
@Override
|
||||
public void sendKeys(List<KeyCode> keys) throws RemoteControllerException {
|
||||
sendKeys(keys, 300);
|
||||
}
|
||||
|
||||
/**
|
||||
* Send sequence of key codes to Samsung TV.
|
||||
*
|
||||
* @param keys List of key codes to send.
|
||||
* @param sleepInMs Sleep between key code sending in milliseconds.
|
||||
* @throws RemoteControllerException
|
||||
*/
|
||||
public void sendKeys(List<KeyCode> keys, int sleepInMs) throws RemoteControllerException {
|
||||
logger.debug("Try to send sequence of commands: {}", keys);
|
||||
|
||||
if (!isConnected()) {
|
||||
openConnection();
|
||||
}
|
||||
|
||||
for (int i = 0; i < keys.size(); i++) {
|
||||
KeyCode key = keys.get(i);
|
||||
try {
|
||||
sendKeyData(key);
|
||||
} catch (RemoteControllerException e) {
|
||||
logger.debug("Couldn't send command", e);
|
||||
logger.debug("Retry one time...");
|
||||
|
||||
closeConnection();
|
||||
openConnection();
|
||||
|
||||
sendKeyData(key);
|
||||
}
|
||||
|
||||
if ((keys.size() - 1) != i) {
|
||||
// Sleep a while between commands
|
||||
try {
|
||||
Thread.sleep(sleepInMs);
|
||||
} catch (InterruptedException e) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
logger.debug("Command(s) successfully sent");
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isConnected() {
|
||||
return socket != null && !socket.isClosed() && socket != null && socket.isConnected();
|
||||
}
|
||||
|
||||
private String createRegistrationPayload(String ip) throws IOException {
|
||||
/*
|
||||
* Payload starts with 2 bytes: 0x64 and 0x00, then comes 3 strings
|
||||
* encoded with base64 algorithm. Every string is preceded by 2-bytes
|
||||
* field containing encoded string length.
|
||||
*
|
||||
* These three strings are as follow:
|
||||
*
|
||||
* remote control device IP, unique ID – value to distinguish
|
||||
* controllers, name – it will be displayed as controller name.
|
||||
*/
|
||||
|
||||
StringWriter w = new StringWriter();
|
||||
w.append((char) 0x64);
|
||||
w.append((char) 0x00);
|
||||
writeBase64String(w, ip);
|
||||
writeBase64String(w, uniqueId);
|
||||
writeBase64String(w, appName);
|
||||
w.flush();
|
||||
return w.toString();
|
||||
}
|
||||
|
||||
private void writeString(Writer writer, String str) throws IOException {
|
||||
int len = str.length();
|
||||
byte low = (byte) (len & 0xFF);
|
||||
byte high = (byte) ((len >> 8) & 0xFF);
|
||||
|
||||
writer.append((char) (low));
|
||||
writer.append((char) (high));
|
||||
writer.append(str);
|
||||
}
|
||||
|
||||
private void writeBase64String(Writer writer, String str) throws IOException {
|
||||
String tmp = Base64.getEncoder().encodeToString(str.getBytes());
|
||||
writeString(writer, tmp);
|
||||
}
|
||||
|
||||
private String readString(Reader reader) throws IOException {
|
||||
char[] buf = readCharArray(reader);
|
||||
return new String(buf);
|
||||
}
|
||||
|
||||
private char[] readCharArray(Reader reader) throws IOException {
|
||||
byte low = (byte) reader.read();
|
||||
byte high = (byte) reader.read();
|
||||
int len = (high << 8) + low;
|
||||
|
||||
if (len > 0) {
|
||||
char[] buffer = new char[len];
|
||||
reader.read(buffer);
|
||||
return buffer;
|
||||
} else {
|
||||
return new char[] {};
|
||||
}
|
||||
}
|
||||
|
||||
private void sendKeyData(KeyCode key) throws RemoteControllerException {
|
||||
logger.debug("Sending key code {}", key.getValue());
|
||||
|
||||
Writer localwriter = writer;
|
||||
Reader localreader = reader;
|
||||
if (localwriter == null || localreader == null) {
|
||||
return;
|
||||
}
|
||||
/* @formatter:off
|
||||
*
|
||||
* offset value and description
|
||||
* ------ ---------------------
|
||||
* 0x00 always 0x00
|
||||
* 0x01 0x0013 - string length (little endian)
|
||||
* 0x03 "iphone.iapp.samsung" - string content
|
||||
* 0x16 0x0011 - payload size (little endian)
|
||||
* 0x18 payload
|
||||
*
|
||||
* @formatter:on
|
||||
*/
|
||||
try {
|
||||
localwriter.append((char) 0x00);
|
||||
writeString(localwriter, APP_STRING);
|
||||
writeString(localwriter, createKeyDataPayload(key));
|
||||
localwriter.flush();
|
||||
|
||||
/*
|
||||
* Read response. Response is pretty useless, because TV seems to
|
||||
* send same response in both ok and error situation.
|
||||
*/
|
||||
localreader.skip(1);
|
||||
readString(localreader);
|
||||
readCharArray(localreader);
|
||||
} catch (IOException e) {
|
||||
throw new RemoteControllerException(e);
|
||||
}
|
||||
}
|
||||
|
||||
private String createKeyDataPayload(KeyCode key) throws IOException {
|
||||
/* @formatter:off
|
||||
*
|
||||
* Payload:
|
||||
*
|
||||
* offset value and description
|
||||
* ------ ---------------------
|
||||
* 0x18 three 0x00 bytes
|
||||
* 0x1b 0x000c - key code size (little endian)
|
||||
* 0x1d key code encoded as base64 string
|
||||
*
|
||||
* @formatter:on
|
||||
*/
|
||||
|
||||
StringWriter writer = new StringWriter();
|
||||
writer.append((char) 0x00);
|
||||
writer.append((char) 0x00);
|
||||
writer.append((char) 0x00);
|
||||
writeBase64String(writer, key.getValue());
|
||||
writer.flush();
|
||||
return writer.toString();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() throws RemoteControllerException {
|
||||
if (isConnected()) {
|
||||
closeConnection();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,382 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 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.protocol;
|
||||
|
||||
import java.net.URI;
|
||||
import java.net.URISyntaxException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Base64;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.eclipse.jetty.util.StringUtil;
|
||||
import org.eclipse.jetty.util.component.LifeCycle;
|
||||
import org.eclipse.jetty.util.component.LifeCycle.Listener;
|
||||
import org.eclipse.jetty.websocket.client.WebSocketClient;
|
||||
import org.openhab.binding.samsungtv.internal.config.SamsungTvConfiguration;
|
||||
import org.openhab.core.io.net.http.WebSocketFactory;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import com.google.gson.Gson;
|
||||
|
||||
/**
|
||||
* The {@link RemoteControllerWebSocket} is responsible for sending key codes to the
|
||||
* Samsung TV via the websocket protocol (for newer TV's).
|
||||
*
|
||||
* @author Arjan Mels - Initial contribution
|
||||
* @author Arjan Mels - Moved websocket inner classes to standalone classes
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class RemoteControllerWebSocket extends RemoteController implements Listener {
|
||||
|
||||
private final Logger logger = LoggerFactory.getLogger(RemoteControllerWebSocket.class);
|
||||
|
||||
private static final String WS_ENDPOINT_REMOTE_CONTROL = "/api/v2/channels/samsung.remote.control";
|
||||
private static final String WS_ENDPOINT_ART = "/api/v2/channels/com.samsung.art-app";
|
||||
private static final String WS_ENDPOINT_V2 = "/api/v2";
|
||||
|
||||
// WebSocket helper classes
|
||||
private final WebSocketRemote webSocketRemote;
|
||||
private final WebSocketArt webSocketArt;
|
||||
private final WebSocketV2 webSocketV2;
|
||||
|
||||
// JSON parser class. Also used by WebSocket handlers.
|
||||
final Gson gson = new Gson();
|
||||
|
||||
// Callback class. Also used by WebSocket handlers.
|
||||
final RemoteControllerWebsocketCallback callback;
|
||||
|
||||
// Websocket client class shared by WebSocket handlers.
|
||||
final WebSocketClient client;
|
||||
|
||||
// temporary storage for source app. Will be used as value for the sourceApp channel when information is complete.
|
||||
// Also used by Websocket handlers.
|
||||
@Nullable
|
||||
String currentSourceApp = null;
|
||||
|
||||
// last app in the apps list: used to detect when status information is complete in WebSocketV2.
|
||||
@Nullable
|
||||
String lastApp = null;
|
||||
|
||||
// timeout for status information search
|
||||
private static final long UPDATE_CURRENT_APP_TIMEOUT = 5000;
|
||||
private long previousUpdateCurrentApp = 0;
|
||||
|
||||
// UUID used for data exchange via websockets
|
||||
final UUID uuid = UUID.randomUUID();
|
||||
|
||||
// Description of Apps
|
||||
@NonNullByDefault()
|
||||
class App {
|
||||
String appId;
|
||||
String name;
|
||||
int type;
|
||||
|
||||
App(String appId, String name, int type) {
|
||||
this.appId = appId;
|
||||
this.name = name;
|
||||
this.type = type;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return this.name;
|
||||
}
|
||||
}
|
||||
|
||||
// Map of all available apps
|
||||
Map<String, App> apps = new LinkedHashMap<>();
|
||||
|
||||
/**
|
||||
* Create and initialize remote controller instance.
|
||||
*
|
||||
* @param host Host name of the Samsung TV.
|
||||
* @param port TCP port of the remote controller protocol.
|
||||
* @param appName Application name used to send key codes.
|
||||
* @param uniqueId Unique Id used to send key codes.
|
||||
* @param remoteControllerWebsocketCallback callback
|
||||
* @throws RemoteControllerException
|
||||
*/
|
||||
public RemoteControllerWebSocket(String host, int port, String appName, String uniqueId,
|
||||
RemoteControllerWebsocketCallback remoteControllerWebsocketCallback) throws RemoteControllerException {
|
||||
super(host, port, appName, uniqueId);
|
||||
|
||||
this.callback = remoteControllerWebsocketCallback;
|
||||
|
||||
WebSocketFactory webSocketFactory = remoteControllerWebsocketCallback.getWebSocketFactory();
|
||||
if (webSocketFactory == null) {
|
||||
throw new RemoteControllerException("No WebSocketFactory available");
|
||||
}
|
||||
|
||||
client = webSocketFactory.createWebSocketClient("samsungtv");
|
||||
|
||||
client.addLifeCycleListener(this);
|
||||
|
||||
webSocketRemote = new WebSocketRemote(this);
|
||||
webSocketArt = new WebSocketArt(this);
|
||||
webSocketV2 = new WebSocketV2(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isConnected() {
|
||||
return webSocketRemote.isConnected();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void openConnection() throws RemoteControllerException {
|
||||
logger.trace("openConnection()");
|
||||
|
||||
if (!(client.isStarted() || client.isStarting())) {
|
||||
logger.debug("RemoteControllerWebSocket start Client");
|
||||
try {
|
||||
client.start();
|
||||
client.setMaxBinaryMessageBufferSize(1000000);
|
||||
// websocket connect will be done in lifetime handler
|
||||
return;
|
||||
} catch (Exception e) {
|
||||
logger.warn("Cannot connect to websocket remote control interface: {}", e.getMessage(), e);
|
||||
throw new RemoteControllerException(e);
|
||||
}
|
||||
}
|
||||
connectWebSockets();
|
||||
}
|
||||
|
||||
private void connectWebSockets() {
|
||||
logger.trace("connectWebSockets()");
|
||||
|
||||
String encodedAppName = Base64.getUrlEncoder().encodeToString(appName.getBytes());
|
||||
|
||||
String protocol;
|
||||
|
||||
if (SamsungTvConfiguration.PROTOCOL_SECUREWEBSOCKET
|
||||
.equals(callback.getConfig(SamsungTvConfiguration.PROTOCOL))) {
|
||||
protocol = "wss";
|
||||
} else {
|
||||
protocol = "ws";
|
||||
}
|
||||
|
||||
try {
|
||||
String token = (String) callback.getConfig(SamsungTvConfiguration.WEBSOCKET_TOKEN);
|
||||
webSocketRemote.connect(new URI(protocol, null, host, port, WS_ENDPOINT_REMOTE_CONTROL,
|
||||
"name=" + encodedAppName + (StringUtil.isNotBlank(token) ? "&token=" + token : ""), null));
|
||||
} catch (RemoteControllerException | URISyntaxException e) {
|
||||
logger.warn("Problem connecting to remote websocket", e);
|
||||
}
|
||||
|
||||
try {
|
||||
webSocketArt.connect(new URI(protocol, null, host, port, WS_ENDPOINT_ART, "name=" + encodedAppName, null));
|
||||
} catch (RemoteControllerException | URISyntaxException e) {
|
||||
logger.warn("Problem connecting to artmode websocket", e);
|
||||
}
|
||||
|
||||
try {
|
||||
webSocketV2.connect(new URI(protocol, null, host, port, WS_ENDPOINT_V2, "name=" + encodedAppName, null));
|
||||
} catch (RemoteControllerException | URISyntaxException e) {
|
||||
logger.warn("Problem connecting to V2 websocket", e);
|
||||
}
|
||||
}
|
||||
|
||||
private void closeConnection() throws RemoteControllerException {
|
||||
logger.debug("RemoteControllerWebSocket closeConnection");
|
||||
|
||||
try {
|
||||
webSocketRemote.close();
|
||||
webSocketArt.close();
|
||||
webSocketV2.close();
|
||||
client.stop();
|
||||
} catch (Exception e) {
|
||||
throw new RemoteControllerException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() throws RemoteControllerException {
|
||||
logger.debug("RemoteControllerWebSocket close");
|
||||
closeConnection();
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve app status for all apps. In the WebSocketv2 handler the currently running app will be determined
|
||||
*/
|
||||
void updateCurrentApp() {
|
||||
if (webSocketV2.isNotConnected()) {
|
||||
logger.warn("Cannot retrieve current app webSocketV2 is not connected");
|
||||
return;
|
||||
}
|
||||
|
||||
// update still running and not timed out
|
||||
if (lastApp != null && System.currentTimeMillis() < previousUpdateCurrentApp + UPDATE_CURRENT_APP_TIMEOUT) {
|
||||
return;
|
||||
}
|
||||
|
||||
lastApp = null;
|
||||
previousUpdateCurrentApp = System.currentTimeMillis();
|
||||
|
||||
currentSourceApp = null;
|
||||
|
||||
// retrieve last app (don't merge with next loop as this might run asynchronously
|
||||
for (App app : apps.values()) {
|
||||
lastApp = app.appId;
|
||||
}
|
||||
|
||||
for (App app : apps.values()) {
|
||||
webSocketV2.getAppStatus(app.appId);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Send key code to Samsung TV.
|
||||
*
|
||||
* @param key Key code to send.
|
||||
* @throws RemoteControllerException
|
||||
*/
|
||||
@Override
|
||||
public void sendKey(KeyCode key) throws RemoteControllerException {
|
||||
sendKey(key, false);
|
||||
}
|
||||
|
||||
public void sendKeyPress(KeyCode key) throws RemoteControllerException {
|
||||
sendKey(key, true);
|
||||
}
|
||||
|
||||
public void sendKey(KeyCode key, boolean press) throws RemoteControllerException {
|
||||
logger.debug("Try to send command: {}", key);
|
||||
|
||||
if (!isConnected()) {
|
||||
openConnection();
|
||||
}
|
||||
|
||||
try {
|
||||
sendKeyData(key, press);
|
||||
} catch (RemoteControllerException e) {
|
||||
logger.debug("Couldn't send command", e);
|
||||
logger.debug("Retry one time...");
|
||||
|
||||
closeConnection();
|
||||
openConnection();
|
||||
|
||||
sendKeyData(key, press);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Send sequence of key codes to Samsung TV.
|
||||
*
|
||||
* @param keys List of key codes to send.
|
||||
* @throws RemoteControllerException
|
||||
*/
|
||||
@Override
|
||||
public void sendKeys(List<KeyCode> keys) throws RemoteControllerException {
|
||||
sendKeys(keys, 300);
|
||||
}
|
||||
|
||||
/**
|
||||
* Send sequence of key codes to Samsung TV.
|
||||
*
|
||||
* @param keys List of key codes to send.
|
||||
* @param sleepInMs Sleep between key code sending in milliseconds.
|
||||
* @throws RemoteControllerException
|
||||
*/
|
||||
public void sendKeys(List<KeyCode> keys, int sleepInMs) throws RemoteControllerException {
|
||||
logger.debug("Try to send sequence of commands: {}", keys);
|
||||
|
||||
if (!isConnected()) {
|
||||
openConnection();
|
||||
}
|
||||
|
||||
for (int i = 0; i < keys.size(); i++) {
|
||||
KeyCode key = keys.get(i);
|
||||
try {
|
||||
sendKeyData(key, false);
|
||||
} catch (RemoteControllerException e) {
|
||||
logger.debug("Couldn't send command", e);
|
||||
logger.debug("Retry one time...");
|
||||
|
||||
closeConnection();
|
||||
openConnection();
|
||||
|
||||
sendKeyData(key, false);
|
||||
}
|
||||
|
||||
if ((keys.size() - 1) != i) {
|
||||
// Sleep a while between commands
|
||||
try {
|
||||
Thread.sleep(sleepInMs);
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
logger.debug("Command(s) successfully sent");
|
||||
}
|
||||
|
||||
private void sendKeyData(KeyCode key, boolean press) throws RemoteControllerException {
|
||||
webSocketRemote.sendKeyData(press, key.toString());
|
||||
}
|
||||
|
||||
public void sendSourceApp(String app) {
|
||||
String appName = app;
|
||||
App appVal = apps.get(app);
|
||||
boolean deepLink = false;
|
||||
appName = appVal.appId;
|
||||
deepLink = appVal.type == 2;
|
||||
|
||||
webSocketRemote.sendSourceApp(appName, deepLink);
|
||||
}
|
||||
|
||||
public void sendUrl(String url) {
|
||||
String processedUrl = url.replace("/", "\\/");
|
||||
webSocketRemote.sendSourceApp("org.tizen.browser", false, processedUrl);
|
||||
}
|
||||
|
||||
public List<String> getAppList() {
|
||||
ArrayList<String> appList = new ArrayList<>();
|
||||
for (App app : apps.values()) {
|
||||
appList.add(app.name);
|
||||
}
|
||||
return appList;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void lifeCycleStarted(@Nullable LifeCycle arg0) {
|
||||
logger.trace("WebSocketClient started");
|
||||
connectWebSockets();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void lifeCycleFailure(@Nullable LifeCycle arg0, @Nullable Throwable throwable) {
|
||||
logger.warn("WebSocketClient failure: {}", throwable != null ? throwable.toString() : null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void lifeCycleStarting(@Nullable LifeCycle arg0) {
|
||||
logger.trace("WebSocketClient starting");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void lifeCycleStopped(@Nullable LifeCycle arg0) {
|
||||
logger.trace("WebSocketClient stopped");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void lifeCycleStopping(@Nullable LifeCycle arg0) {
|
||||
logger.trace("WebSocketClient stopping");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 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.protocol;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.openhab.core.io.net.http.WebSocketFactory;
|
||||
|
||||
/**
|
||||
* Callback from the websocket remote controller
|
||||
*
|
||||
* @author Arjan Mels - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public interface RemoteControllerWebsocketCallback {
|
||||
|
||||
void appsUpdated(List<String> apps);
|
||||
|
||||
void currentAppUpdated(@Nullable String app);
|
||||
|
||||
void powerUpdated(boolean on, boolean artmode);
|
||||
|
||||
void connectionError(@Nullable Throwable error);
|
||||
|
||||
void putConfig(String token, Object object);
|
||||
|
||||
@Nullable
|
||||
Object getConfig(String token);
|
||||
|
||||
@Nullable
|
||||
WebSocketFactory getWebSocketFactory();
|
||||
}
|
||||
@@ -0,0 +1,163 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 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.protocol;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import com.google.gson.JsonElement;
|
||||
import com.google.gson.JsonSyntaxException;
|
||||
|
||||
/**
|
||||
* Websocket class to retrieve artmode status (on o.a. the Frame TV's)
|
||||
*
|
||||
* @author Arjan Mels - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
class WebSocketArt extends WebSocketBase {
|
||||
private final Logger logger = LoggerFactory.getLogger(WebSocketArt.class);
|
||||
|
||||
/**
|
||||
* @param remoteControllerWebSocket
|
||||
*/
|
||||
WebSocketArt(RemoteControllerWebSocket remoteControllerWebSocket) {
|
||||
super(remoteControllerWebSocket);
|
||||
}
|
||||
|
||||
@NonNullByDefault({})
|
||||
private static class JSONMessage {
|
||||
String event;
|
||||
|
||||
@NonNullByDefault({})
|
||||
static class Data {
|
||||
String event;
|
||||
String status;
|
||||
String value;
|
||||
}
|
||||
|
||||
// data is sometimes a json object, sometimes a string representation of a json object for d2d_service_message
|
||||
@Nullable
|
||||
JsonElement data;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onWebSocketText(@Nullable String msgarg) {
|
||||
if (msgarg == null) {
|
||||
return;
|
||||
}
|
||||
String msg = msgarg.replace('\n', ' ');
|
||||
super.onWebSocketText(msg);
|
||||
try {
|
||||
JSONMessage jsonMsg = remoteControllerWebSocket.gson.fromJson(msg, JSONMessage.class);
|
||||
|
||||
switch (jsonMsg.event) {
|
||||
case "ms.channel.connect":
|
||||
logger.debug("Art channel connected");
|
||||
break;
|
||||
case "ms.channel.ready":
|
||||
logger.debug("Art channel ready");
|
||||
getArtmodeStatus();
|
||||
break;
|
||||
case "ms.channel.clientConnect":
|
||||
logger.debug("Art client connected");
|
||||
break;
|
||||
case "ms.channel.clientDisconnect":
|
||||
logger.debug("Art client disconnected");
|
||||
break;
|
||||
|
||||
case "d2d_service_message":
|
||||
if (jsonMsg.data != null) {
|
||||
handleD2DServiceMessage(jsonMsg.data.getAsString());
|
||||
} else {
|
||||
logger.debug("Empty d2d_service_message event: {}", msg);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
logger.debug("WebSocketArt Unknown event: {}", msg);
|
||||
}
|
||||
} catch (JsonSyntaxException e) {
|
||||
logger.warn("{}: Error ({}) in message: {}", this.getClass().getSimpleName(), e.getMessage(), msg, e);
|
||||
}
|
||||
}
|
||||
|
||||
private void handleD2DServiceMessage(String msg) {
|
||||
JSONMessage.Data data = remoteControllerWebSocket.gson.fromJson(msg, JSONMessage.Data.class);
|
||||
if (data.event == null) {
|
||||
logger.debug("Unknown d2d_service_message event: {}", msg);
|
||||
return;
|
||||
} else {
|
||||
switch (data.event) {
|
||||
case "art_mode_changed":
|
||||
logger.debug("art_mode_changed: {}", data.status);
|
||||
if ("on".equals(data.status)) {
|
||||
remoteControllerWebSocket.callback.powerUpdated(false, true);
|
||||
} else {
|
||||
remoteControllerWebSocket.callback.powerUpdated(true, false);
|
||||
}
|
||||
remoteControllerWebSocket.updateCurrentApp();
|
||||
break;
|
||||
case "artmode_status":
|
||||
logger.debug("artmode_status: {}", data.value);
|
||||
if ("on".equals(data.value)) {
|
||||
remoteControllerWebSocket.callback.powerUpdated(false, true);
|
||||
} else {
|
||||
remoteControllerWebSocket.callback.powerUpdated(true, false);
|
||||
}
|
||||
remoteControllerWebSocket.updateCurrentApp();
|
||||
break;
|
||||
case "go_to_standby":
|
||||
logger.debug("go_to_standby");
|
||||
remoteControllerWebSocket.callback.powerUpdated(false, false);
|
||||
break;
|
||||
case "wakeup":
|
||||
logger.debug("wakeup");
|
||||
// check artmode status to know complete status before updating
|
||||
getArtmodeStatus();
|
||||
break;
|
||||
default:
|
||||
logger.debug("Unknown d2d_service_message event: {}", msg);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@NonNullByDefault({})
|
||||
class JSONArtModeStatus {
|
||||
public JSONArtModeStatus() {
|
||||
Params.Data data = params.new Data();
|
||||
data.id = remoteControllerWebSocket.uuid.toString();
|
||||
params.data = remoteControllerWebSocket.gson.toJson(data);
|
||||
}
|
||||
|
||||
@NonNullByDefault({})
|
||||
class Params {
|
||||
@NonNullByDefault({})
|
||||
class Data {
|
||||
String request = "get_artmode_status";
|
||||
String id;
|
||||
}
|
||||
|
||||
String event = "art_app_request";
|
||||
String to = "host";
|
||||
String data;
|
||||
}
|
||||
|
||||
String method = "ms.channel.emit";
|
||||
Params params = new Params();
|
||||
}
|
||||
|
||||
void getArtmodeStatus() {
|
||||
sendCommand(remoteControllerWebSocket.gson.toJson(new JSONArtModeStatus()));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,131 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 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.protocol;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.URI;
|
||||
import java.util.concurrent.Future;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.eclipse.jetty.websocket.api.Session;
|
||||
import org.eclipse.jetty.websocket.api.WebSocketAdapter;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* Websocket base class
|
||||
*
|
||||
* @author Arjan Mels - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
class WebSocketBase extends WebSocketAdapter {
|
||||
private final Logger logger = LoggerFactory.getLogger(WebSocketBase.class);
|
||||
/**
|
||||
*
|
||||
*/
|
||||
final RemoteControllerWebSocket remoteControllerWebSocket;
|
||||
|
||||
private @Nullable Future<?> sessionFuture;
|
||||
|
||||
/**
|
||||
* @param remoteControllerWebSocket
|
||||
*/
|
||||
WebSocketBase(RemoteControllerWebSocket remoteControllerWebSocket) {
|
||||
this.remoteControllerWebSocket = remoteControllerWebSocket;
|
||||
}
|
||||
|
||||
boolean isConnecting = false;
|
||||
|
||||
@Override
|
||||
public void onWebSocketClose(int statusCode, @Nullable String reason) {
|
||||
logger.debug("{} connection closed: {} - {}", this.getClass().getSimpleName(), statusCode, reason);
|
||||
super.onWebSocketClose(statusCode, reason);
|
||||
isConnecting = false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onWebSocketError(@Nullable Throwable error) {
|
||||
if (logger.isTraceEnabled()) {
|
||||
logger.trace("{} connection error", this.getClass().getSimpleName(), error);
|
||||
} else {
|
||||
logger.debug("{} connection error", this.getClass().getSimpleName());
|
||||
}
|
||||
super.onWebSocketError(error);
|
||||
isConnecting = false;
|
||||
}
|
||||
|
||||
void connect(URI uri) throws RemoteControllerException {
|
||||
if (isConnecting || isConnected()) {
|
||||
logger.trace("{} already connecting or connected", this.getClass().getSimpleName());
|
||||
return;
|
||||
}
|
||||
|
||||
logger.debug("{} connecting to: {}", this.getClass().getSimpleName(), uri);
|
||||
isConnecting = true;
|
||||
|
||||
try {
|
||||
sessionFuture = remoteControllerWebSocket.client.connect(this, uri);
|
||||
logger.trace("Connecting session Future: {}", sessionFuture);
|
||||
} catch (IOException | IllegalStateException e) {
|
||||
throw new RemoteControllerException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onWebSocketConnect(@Nullable Session session) {
|
||||
logger.debug("{} connection established: {}", this.getClass().getSimpleName(),
|
||||
session != null ? session.getRemoteAddress().getHostString() : "");
|
||||
super.onWebSocketConnect(session);
|
||||
|
||||
isConnecting = false;
|
||||
}
|
||||
|
||||
void close() {
|
||||
logger.debug("{} connection close requested", this.getClass().getSimpleName());
|
||||
|
||||
Session session = getSession();
|
||||
if (session != null) {
|
||||
session.close();
|
||||
}
|
||||
|
||||
final Future<?> sessionFuture = this.sessionFuture;
|
||||
logger.trace("Closing session Future: {}", sessionFuture);
|
||||
if (sessionFuture != null && !sessionFuture.isDone()) {
|
||||
sessionFuture.cancel(true);
|
||||
}
|
||||
}
|
||||
|
||||
void sendCommand(String cmd) {
|
||||
try {
|
||||
if (isConnected()) {
|
||||
getRemote().sendString(cmd);
|
||||
logger.trace("{}: sendCommand: {}", this.getClass().getSimpleName(), cmd);
|
||||
} else {
|
||||
logger.warn("{} sending command while socket not connected: {}", this.getClass().getSimpleName(), cmd);
|
||||
// retry opening connection just in case
|
||||
remoteControllerWebSocket.openConnection();
|
||||
|
||||
getRemote().sendString(cmd);
|
||||
logger.trace("{}: sendCommand: {}", this.getClass().getSimpleName(), cmd);
|
||||
}
|
||||
} catch (IOException | RemoteControllerException e) {
|
||||
logger.warn("{}: cannot send command", this.getClass().getSimpleName(), e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onWebSocketText(@Nullable String str) {
|
||||
logger.trace("{}: onWebSocketText: {}", this.getClass().getSimpleName(), str);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,226 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 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.protocol;
|
||||
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.openhab.binding.samsungtv.internal.config.SamsungTvConfiguration;
|
||||
import org.openhab.binding.samsungtv.internal.protocol.RemoteControllerWebSocket.App;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import com.google.gson.JsonSyntaxException;
|
||||
|
||||
/**
|
||||
* Websocket class for remote control
|
||||
*
|
||||
* @author Arjan Mels - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
class WebSocketRemote extends WebSocketBase {
|
||||
private final Logger logger = LoggerFactory.getLogger(WebSocketRemote.class);
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
@NonNullByDefault({})
|
||||
private static class JSONMessage {
|
||||
String event;
|
||||
|
||||
@NonNullByDefault({})
|
||||
static class App {
|
||||
String appId;
|
||||
String name;
|
||||
int app_type;
|
||||
}
|
||||
|
||||
@NonNullByDefault({})
|
||||
static class Data {
|
||||
String update_type;
|
||||
App[] data;
|
||||
|
||||
String id;
|
||||
String token;
|
||||
}
|
||||
|
||||
Data data;
|
||||
|
||||
@NonNullByDefault({})
|
||||
static class Params {
|
||||
String params;
|
||||
|
||||
@NonNullByDefault({})
|
||||
static class Data {
|
||||
String appId;
|
||||
}
|
||||
|
||||
Data data;
|
||||
}
|
||||
|
||||
Params params;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param remoteControllerWebSocket
|
||||
*/
|
||||
WebSocketRemote(RemoteControllerWebSocket remoteControllerWebSocket) {
|
||||
super(remoteControllerWebSocket);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onWebSocketError(@Nullable Throwable error) {
|
||||
super.onWebSocketError(error);
|
||||
remoteControllerWebSocket.callback.connectionError(error);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onWebSocketText(@Nullable String msgarg) {
|
||||
if (msgarg == null) {
|
||||
return;
|
||||
}
|
||||
String msg = msgarg.replace('\n', ' ');
|
||||
super.onWebSocketText(msg);
|
||||
try {
|
||||
JSONMessage jsonMsg = remoteControllerWebSocket.gson.fromJson(msg, JSONMessage.class);
|
||||
switch (jsonMsg.event) {
|
||||
case "ms.channel.connect":
|
||||
logger.debug("Remote channel connected. Token = {}", jsonMsg.data.token);
|
||||
if (jsonMsg.data.token != null) {
|
||||
this.remoteControllerWebSocket.callback.putConfig(SamsungTvConfiguration.WEBSOCKET_TOKEN,
|
||||
jsonMsg.data.token);
|
||||
// try opening additional websockets
|
||||
try {
|
||||
this.remoteControllerWebSocket.openConnection();
|
||||
} catch (RemoteControllerException e) {
|
||||
logger.warn("{}: Error ({})", this.getClass().getSimpleName(), e.getMessage());
|
||||
}
|
||||
}
|
||||
getApps();
|
||||
break;
|
||||
case "ms.channel.clientConnect":
|
||||
logger.debug("Remote client connected");
|
||||
break;
|
||||
case "ms.channel.clientDisconnect":
|
||||
logger.debug("Remote client disconnected");
|
||||
break;
|
||||
case "ed.edenTV.update":
|
||||
logger.debug("edenTV update: {}", jsonMsg.data.update_type);
|
||||
remoteControllerWebSocket.updateCurrentApp();
|
||||
break;
|
||||
case "ed.apps.launch":
|
||||
logger.debug("App launched: {}", jsonMsg.params.data.appId);
|
||||
break;
|
||||
case "ed.installedApp.get":
|
||||
handleInstalledApps(jsonMsg);
|
||||
break;
|
||||
default:
|
||||
logger.debug("WebSocketRemote Unknown event: {}", msg);
|
||||
|
||||
}
|
||||
} catch (JsonSyntaxException e) {
|
||||
logger.warn("{}: Error ({}) in message: {}", this.getClass().getSimpleName(), e.getMessage(), msg, e);
|
||||
}
|
||||
}
|
||||
|
||||
private void handleInstalledApps(JSONMessage jsonMsg) {
|
||||
remoteControllerWebSocket.apps.clear();
|
||||
|
||||
for (JSONMessage.App jsonApp : jsonMsg.data.data) {
|
||||
App app = remoteControllerWebSocket.new App(jsonApp.appId, jsonApp.name, jsonApp.app_type);
|
||||
remoteControllerWebSocket.apps.put(app.name, app);
|
||||
}
|
||||
if (logger.isDebugEnabled()) {
|
||||
logger.debug("Installed Apps: {}", remoteControllerWebSocket.apps.entrySet().stream()
|
||||
.map(entry -> entry.getValue().appId + " = " + entry.getKey()).collect(Collectors.joining(", ")));
|
||||
}
|
||||
|
||||
remoteControllerWebSocket.updateCurrentApp();
|
||||
}
|
||||
|
||||
@NonNullByDefault({})
|
||||
static class JSONAppInfo {
|
||||
@NonNullByDefault({})
|
||||
static class Params {
|
||||
String event = "ed.installedApp.get";
|
||||
String to = "host";
|
||||
}
|
||||
|
||||
String method = "ms.channel.emit";
|
||||
Params params = new Params();
|
||||
}
|
||||
|
||||
void getApps() {
|
||||
sendCommand(remoteControllerWebSocket.gson.toJson(new JSONAppInfo()));
|
||||
}
|
||||
|
||||
@NonNullByDefault({})
|
||||
static class JSONSourceApp {
|
||||
public JSONSourceApp(String appName, boolean deepLink) {
|
||||
this(appName, deepLink, null);
|
||||
}
|
||||
|
||||
public JSONSourceApp(String appName, boolean deepLink, String metaTag) {
|
||||
params.data.appId = appName;
|
||||
params.data.action_type = deepLink ? "DEEP_LINK" : "NATIVE_LAUNCH";
|
||||
params.data.metaTag = metaTag;
|
||||
}
|
||||
|
||||
@NonNullByDefault({})
|
||||
static class Params {
|
||||
@NonNullByDefault({})
|
||||
static class Data {
|
||||
String appId;
|
||||
String action_type;
|
||||
String metaTag;
|
||||
}
|
||||
|
||||
String event = "ed.apps.launch";
|
||||
String to = "host";
|
||||
Data data = new Data();
|
||||
}
|
||||
|
||||
String method = "ms.channel.emit";
|
||||
Params params = new Params();
|
||||
}
|
||||
|
||||
public void sendSourceApp(String appName, boolean deepLink) {
|
||||
sendCommand(remoteControllerWebSocket.gson.toJson(new JSONSourceApp(appName, deepLink)));
|
||||
}
|
||||
|
||||
public void sendSourceApp(String appName, boolean deepLink, String metaTag) {
|
||||
sendCommand(remoteControllerWebSocket.gson.toJson(new JSONSourceApp(appName, deepLink, metaTag)));
|
||||
}
|
||||
|
||||
@NonNullByDefault({})
|
||||
static class JSONRemoteControl {
|
||||
public JSONRemoteControl(boolean press, String key) {
|
||||
params.Cmd = press ? "Press" : "Click";
|
||||
params.DataOfCmd = key;
|
||||
}
|
||||
|
||||
@NonNullByDefault({})
|
||||
static class Params {
|
||||
String Cmd;
|
||||
String DataOfCmd;
|
||||
String Option = "false";
|
||||
String TypeOfRemote = "SendRemoteKey";
|
||||
}
|
||||
|
||||
String method = "ms.remote.control";
|
||||
Params params = new Params();
|
||||
}
|
||||
|
||||
void sendKeyData(boolean press, String key) {
|
||||
sendCommand(remoteControllerWebSocket.gson.toJson(new JSONRemoteControl(press, key)));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,148 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 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.protocol;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import com.google.gson.JsonSyntaxException;
|
||||
|
||||
/**
|
||||
* Websocket class to retrieve app status
|
||||
*
|
||||
* @author Arjan Mels - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
class WebSocketV2 extends WebSocketBase {
|
||||
private final Logger logger = LoggerFactory.getLogger(WebSocketV2.class);
|
||||
|
||||
WebSocketV2(RemoteControllerWebSocket remoteControllerWebSocket) {
|
||||
super(remoteControllerWebSocket);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
@NonNullByDefault({})
|
||||
private static class JSONMessage {
|
||||
String event;
|
||||
|
||||
@NonNullByDefault({})
|
||||
static class Result {
|
||||
String id;
|
||||
String name;
|
||||
String visible;
|
||||
}
|
||||
|
||||
@NonNullByDefault({})
|
||||
static class Data {
|
||||
String id;
|
||||
String token;
|
||||
}
|
||||
|
||||
@NonNullByDefault({})
|
||||
static class Error {
|
||||
String code;
|
||||
String details;
|
||||
String message;
|
||||
String status;
|
||||
}
|
||||
|
||||
Result result;
|
||||
Data data;
|
||||
Error error;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onWebSocketText(@Nullable String msgarg) {
|
||||
if (msgarg == null) {
|
||||
return;
|
||||
}
|
||||
String msg = msgarg.replace('\n', ' ');
|
||||
super.onWebSocketText(msg);
|
||||
try {
|
||||
JSONMessage jsonMsg = this.remoteControllerWebSocket.gson.fromJson(msg, JSONMessage.class);
|
||||
|
||||
if (jsonMsg.result != null) {
|
||||
handleResult(jsonMsg);
|
||||
return;
|
||||
}
|
||||
if (jsonMsg.error != null) {
|
||||
logger.debug("WebSocketV2 Error received: {}", msg);
|
||||
return;
|
||||
}
|
||||
if (jsonMsg.event == null) {
|
||||
logger.debug("WebSocketV2 Unknown response format: {}", msg);
|
||||
return;
|
||||
}
|
||||
|
||||
switch (jsonMsg.event) {
|
||||
case "ms.channel.connect":
|
||||
logger.debug("V2 channel connected. Token = {}", jsonMsg.data.token);
|
||||
|
||||
// update is requested from ed.installedApp.get event: small risk that this websocket is not
|
||||
// yet connected
|
||||
break;
|
||||
case "ms.channel.clientConnect":
|
||||
logger.debug("V2 client connected");
|
||||
break;
|
||||
case "ms.channel.clientDisconnect":
|
||||
logger.debug("V2 client disconnected");
|
||||
break;
|
||||
default:
|
||||
logger.debug("V2 Unknown event: {}", msg);
|
||||
}
|
||||
} catch (JsonSyntaxException e) {
|
||||
logger.warn("{}: Error ({}) in message: {}", this.getClass().getSimpleName(), e.getMessage(), msg, e);
|
||||
}
|
||||
}
|
||||
|
||||
private void handleResult(JSONMessage jsonMsg) {
|
||||
if ((remoteControllerWebSocket.currentSourceApp == null
|
||||
|| remoteControllerWebSocket.currentSourceApp.trim().isEmpty())
|
||||
&& "true".equals(jsonMsg.result.visible)) {
|
||||
logger.debug("Running app: {} = {}", jsonMsg.result.id, jsonMsg.result.name);
|
||||
remoteControllerWebSocket.currentSourceApp = jsonMsg.result.name;
|
||||
remoteControllerWebSocket.callback.currentAppUpdated(remoteControllerWebSocket.currentSourceApp);
|
||||
}
|
||||
|
||||
if (remoteControllerWebSocket.lastApp != null && remoteControllerWebSocket.lastApp.equals(jsonMsg.result.id)) {
|
||||
if (remoteControllerWebSocket.currentSourceApp == null
|
||||
|| remoteControllerWebSocket.currentSourceApp.trim().isEmpty()) {
|
||||
remoteControllerWebSocket.callback.currentAppUpdated("");
|
||||
}
|
||||
remoteControllerWebSocket.lastApp = null;
|
||||
}
|
||||
}
|
||||
|
||||
@NonNullByDefault({})
|
||||
static class JSONAppStatus {
|
||||
public JSONAppStatus(String id) {
|
||||
this.id = id;
|
||||
params.id = id;
|
||||
}
|
||||
|
||||
@NonNullByDefault({})
|
||||
static class Params {
|
||||
String id;
|
||||
}
|
||||
|
||||
String method = "ms.application.get";
|
||||
String id;
|
||||
Params params = new Params();
|
||||
}
|
||||
|
||||
void getAppStatus(String id) {
|
||||
sendCommand(remoteControllerWebSocket.gson.toJson(new JSONAppStatus(id)));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 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.service;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.openhab.core.library.types.DecimalType;
|
||||
import org.openhab.core.library.types.IncreaseDecreaseType;
|
||||
import org.openhab.core.library.types.OnOffType;
|
||||
import org.openhab.core.library.types.OpenClosedType;
|
||||
import org.openhab.core.library.types.UpDownType;
|
||||
import org.openhab.core.types.Command;
|
||||
|
||||
/**
|
||||
* The {@link DataConverters} provides utils for converting openHAB commands to
|
||||
* Samsung TV specific values.
|
||||
*
|
||||
* @author Pauli Anttila - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class DataConverters {
|
||||
|
||||
/**
|
||||
* Convert openHAB command to int.
|
||||
*
|
||||
* @param command
|
||||
* @param min
|
||||
* @param max
|
||||
* @param currentValue
|
||||
* @return
|
||||
*/
|
||||
public static int convertCommandToIntValue(Command command, int min, int max, int currentValue) {
|
||||
if (command instanceof IncreaseDecreaseType || command instanceof DecimalType) {
|
||||
int value;
|
||||
if (command instanceof IncreaseDecreaseType && command == IncreaseDecreaseType.INCREASE) {
|
||||
value = Math.min(max, currentValue + 1);
|
||||
} else if (command instanceof IncreaseDecreaseType && command == IncreaseDecreaseType.DECREASE) {
|
||||
value = Math.max(min, currentValue - 1);
|
||||
} else if (command instanceof DecimalType) {
|
||||
value = ((DecimalType) command).intValue();
|
||||
} else {
|
||||
throw new NumberFormatException("Command '" + command + "' not supported");
|
||||
}
|
||||
|
||||
return value;
|
||||
|
||||
} else {
|
||||
throw new NumberFormatException("Command '" + command + "' not supported");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert openHAB command to boolean.
|
||||
*
|
||||
* @param command
|
||||
* @return
|
||||
*/
|
||||
public static boolean convertCommandToBooleanValue(Command command) {
|
||||
if (command instanceof OnOffType || command instanceof OpenClosedType || command instanceof UpDownType) {
|
||||
boolean newValue;
|
||||
|
||||
if (command.equals(OnOffType.ON) || command.equals(UpDownType.UP) || command.equals(OpenClosedType.OPEN)) {
|
||||
newValue = true;
|
||||
} else if (command.equals(OnOffType.OFF) || command.equals(UpDownType.DOWN)
|
||||
|| command.equals(OpenClosedType.CLOSED)) {
|
||||
newValue = false;
|
||||
} else {
|
||||
throw new NumberFormatException("Command '" + command + "' not supported");
|
||||
}
|
||||
|
||||
return newValue;
|
||||
|
||||
} else {
|
||||
throw new NumberFormatException("Command '" + command + "' not supported for channel");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,334 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 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.service;
|
||||
|
||||
import static org.openhab.binding.samsungtv.internal.SamsungTvBindingConstants.*;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.CopyOnWriteArraySet;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.openhab.binding.samsungtv.internal.service.api.EventListener;
|
||||
import org.openhab.binding.samsungtv.internal.service.api.SamsungTvService;
|
||||
import org.openhab.core.io.transport.upnp.UpnpIOParticipant;
|
||||
import org.openhab.core.io.transport.upnp.UpnpIOService;
|
||||
import org.openhab.core.library.types.DecimalType;
|
||||
import org.openhab.core.library.types.StringType;
|
||||
import org.openhab.core.types.Command;
|
||||
import org.openhab.core.types.RefreshType;
|
||||
import org.openhab.core.types.UnDefType;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.w3c.dom.Document;
|
||||
import org.w3c.dom.Element;
|
||||
import org.w3c.dom.NodeList;
|
||||
|
||||
/**
|
||||
* The {@link MainTVServerService} is responsible for handling MainTVServer
|
||||
* commands.
|
||||
*
|
||||
* @author Pauli Anttila - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class MainTVServerService implements UpnpIOParticipant, SamsungTvService {
|
||||
|
||||
public static final String SERVICE_NAME = "MainTVServer2";
|
||||
private static final List<String> SUPPORTED_CHANNELS = Arrays.asList(CHANNEL_NAME, CHANNEL, SOURCE_NAME, SOURCE_ID,
|
||||
PROGRAM_TITLE, BROWSER_URL, STOP_BROWSER);
|
||||
|
||||
private final Logger logger = LoggerFactory.getLogger(MainTVServerService.class);
|
||||
|
||||
private final UpnpIOService service;
|
||||
|
||||
private final String udn;
|
||||
|
||||
private Map<String, String> stateMap = Collections.synchronizedMap(new HashMap<>());
|
||||
|
||||
private Set<EventListener> listeners = new CopyOnWriteArraySet<>();
|
||||
|
||||
private boolean started;
|
||||
|
||||
public MainTVServerService(UpnpIOService upnpIOService, String udn) {
|
||||
logger.debug("Creating a Samsung TV MainTVServer service");
|
||||
this.service = upnpIOService;
|
||||
this.udn = udn;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> getSupportedChannelNames() {
|
||||
return SUPPORTED_CHANNELS;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addEventListener(EventListener listener) {
|
||||
listeners.add(listener);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void removeEventListener(EventListener listener) {
|
||||
listeners.remove(listener);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void start() {
|
||||
service.registerParticipant(this);
|
||||
started = true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void stop() {
|
||||
service.unregisterParticipant(this);
|
||||
started = false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void clearCache() {
|
||||
stateMap.clear();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isUpnp() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleCommand(String channel, Command command) {
|
||||
logger.debug("Received channel: {}, command: {}", channel, command);
|
||||
|
||||
if (!started) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (command == RefreshType.REFRESH) {
|
||||
if (isRegistered()) {
|
||||
switch (channel) {
|
||||
case CHANNEL:
|
||||
updateResourceState("MainTVAgent2", "GetCurrentMainTVChannel", null);
|
||||
break;
|
||||
case SOURCE_NAME:
|
||||
case SOURCE_ID:
|
||||
updateResourceState("MainTVAgent2", "GetCurrentExternalSource", null);
|
||||
break;
|
||||
case PROGRAM_TITLE:
|
||||
case CHANNEL_NAME:
|
||||
updateResourceState("MainTVAgent2", "GetCurrentContentRecognition", null);
|
||||
break;
|
||||
case BROWSER_URL:
|
||||
updateResourceState("MainTVAgent2", "GetCurrentBrowserURL", null);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
switch (channel) {
|
||||
case SOURCE_NAME:
|
||||
setSourceName(command);
|
||||
// Clear value on cache to force update
|
||||
stateMap.put("CurrentExternalSource", "");
|
||||
break;
|
||||
case BROWSER_URL:
|
||||
setBrowserUrl(command);
|
||||
// Clear value on cache to force update
|
||||
stateMap.put("BrowserURL", "");
|
||||
break;
|
||||
case STOP_BROWSER:
|
||||
stopBrowser(command);
|
||||
break;
|
||||
default:
|
||||
logger.warn("Samsung TV doesn't support transmitting for channel '{}'", channel);
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isRegistered() {
|
||||
return service.isRegistered(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getUDN() {
|
||||
return udn;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onServiceSubscribed(@Nullable String service, boolean succeeded) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onValueReceived(@Nullable String variable, @Nullable String value, @Nullable String service) {
|
||||
if (variable == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
String oldValue = stateMap.get(variable);
|
||||
if ((value == null && oldValue == null) || (value != null && value.equals(oldValue))) {
|
||||
logger.trace("Value '{}' for {} hasn't changed, ignoring update", value, variable);
|
||||
return;
|
||||
}
|
||||
|
||||
stateMap.put(variable, (value != null) ? value : "");
|
||||
|
||||
for (EventListener listener : listeners) {
|
||||
|
||||
switch (variable) {
|
||||
case "ProgramTitle":
|
||||
listener.valueReceived(PROGRAM_TITLE, (value != null) ? new StringType(value) : UnDefType.UNDEF);
|
||||
break;
|
||||
case "ChannelName":
|
||||
listener.valueReceived(CHANNEL_NAME, (value != null) ? new StringType(value) : UnDefType.UNDEF);
|
||||
break;
|
||||
case "CurrentExternalSource":
|
||||
listener.valueReceived(SOURCE_NAME, (value != null) ? new StringType(value) : UnDefType.UNDEF);
|
||||
break;
|
||||
case "CurrentChannel":
|
||||
String currentChannel = (value != null) ? parseCurrentChannel(value) : null;
|
||||
listener.valueReceived(CHANNEL,
|
||||
currentChannel != null ? new DecimalType(currentChannel) : UnDefType.UNDEF);
|
||||
break;
|
||||
case "ID":
|
||||
listener.valueReceived(SOURCE_ID, (value != null) ? new DecimalType(value) : UnDefType.UNDEF);
|
||||
break;
|
||||
case "BrowserURL":
|
||||
listener.valueReceived(BROWSER_URL, (value != null) ? new StringType(value) : UnDefType.UNDEF);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected Map<String, String> updateResourceState(String serviceId, String actionId,
|
||||
@Nullable Map<String, String> inputs) {
|
||||
Map<String, String> result = service.invokeAction(this, serviceId, actionId, inputs);
|
||||
|
||||
for (String variable : result.keySet()) {
|
||||
onValueReceived(variable, result.get(variable), serviceId);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private void setSourceName(Command command) {
|
||||
Map<String, String> result = updateResourceState("MainTVAgent2", "GetSourceList", null);
|
||||
|
||||
String source = command.toString();
|
||||
String id = null;
|
||||
|
||||
if (result.get("Result").equals("OK")) {
|
||||
String xml = result.get("SourceList");
|
||||
|
||||
Map<String, String> list = parseSourceList(xml);
|
||||
if (list != null) {
|
||||
id = list.get(source);
|
||||
}
|
||||
} else {
|
||||
logger.warn("Source list query failed, result='{}'", result.get("Result"));
|
||||
}
|
||||
|
||||
if (source != null && id != null) {
|
||||
result = updateResourceState("MainTVAgent2", "SetMainTVSource",
|
||||
SamsungTvUtils.buildHashMap("Source", source, "ID", id, "UiID", "0"));
|
||||
|
||||
if (result.get("Result").equals("OK")) {
|
||||
logger.debug("Command successfully executed");
|
||||
} else {
|
||||
logger.warn("Command execution failed, result='{}'", result.get("Result"));
|
||||
}
|
||||
} else {
|
||||
logger.warn("Source id for '{}' couldn't be found", command.toString());
|
||||
}
|
||||
}
|
||||
|
||||
private void setBrowserUrl(Command command) {
|
||||
Map<String, String> result = updateResourceState("MainTVAgent2", "RunBrowser",
|
||||
SamsungTvUtils.buildHashMap("BrowserURL", command.toString()));
|
||||
|
||||
if (result.get("Result").equals("OK")) {
|
||||
logger.debug("Command successfully executed");
|
||||
} else {
|
||||
logger.warn("Command execution failed, result='{}'", result.get("Result"));
|
||||
}
|
||||
}
|
||||
|
||||
private void stopBrowser(Command command) {
|
||||
Map<String, String> result = updateResourceState("MainTVAgent2", "StopBrowser", null);
|
||||
|
||||
if (result.get("Result").equals("OK")) {
|
||||
logger.debug("Command successfully executed");
|
||||
} else {
|
||||
logger.warn("Command execution failed, result='{}'", result.get("Result"));
|
||||
}
|
||||
}
|
||||
|
||||
private @Nullable String parseCurrentChannel(@Nullable String xml) {
|
||||
String majorCh = null;
|
||||
|
||||
if (xml != null) {
|
||||
Document dom = SamsungTvUtils.loadXMLFromString(xml);
|
||||
|
||||
if (dom != null) {
|
||||
NodeList nodeList = dom.getDocumentElement().getElementsByTagName("MajorCh");
|
||||
|
||||
if (nodeList != null) {
|
||||
majorCh = nodeList.item(0).getFirstChild().getNodeValue();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return majorCh;
|
||||
}
|
||||
|
||||
private Map<String, String> parseSourceList(String xml) {
|
||||
Map<String, String> list = new HashMap<>();
|
||||
|
||||
Document dom = SamsungTvUtils.loadXMLFromString(xml);
|
||||
|
||||
if (dom != null) {
|
||||
NodeList nodeList = dom.getDocumentElement().getElementsByTagName("Source");
|
||||
|
||||
if (nodeList != null) {
|
||||
for (int i = 0; i < nodeList.getLength(); i++) {
|
||||
|
||||
String sourceType = null;
|
||||
String id = null;
|
||||
|
||||
Element element = (Element) nodeList.item(i);
|
||||
NodeList l = element.getElementsByTagName("SourceType");
|
||||
if (l != null && l.getLength() > 0) {
|
||||
sourceType = l.item(0).getFirstChild().getNodeValue();
|
||||
}
|
||||
l = element.getElementsByTagName("ID");
|
||||
if (l != null && l.getLength() > 0) {
|
||||
id = l.item(0).getFirstChild().getNodeValue();
|
||||
}
|
||||
|
||||
if (sourceType != null && id != null) {
|
||||
list.put(sourceType, id);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return list;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStatusChanged(boolean status) {
|
||||
logger.debug("onStatusChanged: status={}", status);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,347 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 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.service;
|
||||
|
||||
import static org.openhab.binding.samsungtv.internal.SamsungTvBindingConstants.*;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.CopyOnWriteArraySet;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.openhab.binding.samsungtv.internal.service.api.EventListener;
|
||||
import org.openhab.binding.samsungtv.internal.service.api.SamsungTvService;
|
||||
import org.openhab.core.io.transport.upnp.UpnpIOParticipant;
|
||||
import org.openhab.core.io.transport.upnp.UpnpIOService;
|
||||
import org.openhab.core.library.types.DecimalType;
|
||||
import org.openhab.core.library.types.OnOffType;
|
||||
import org.openhab.core.library.types.PercentType;
|
||||
import org.openhab.core.types.Command;
|
||||
import org.openhab.core.types.RefreshType;
|
||||
import org.openhab.core.types.State;
|
||||
import org.openhab.core.types.UnDefType;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* The {@link MediaRendererService} is responsible for handling MediaRenderer
|
||||
* commands.
|
||||
*
|
||||
* @author Pauli Anttila - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class MediaRendererService implements UpnpIOParticipant, SamsungTvService {
|
||||
|
||||
public static final String SERVICE_NAME = "MediaRenderer";
|
||||
private static final List<String> SUPPORTED_CHANNELS = Arrays.asList(VOLUME, MUTE, BRIGHTNESS, CONTRAST, SHARPNESS,
|
||||
COLOR_TEMPERATURE);
|
||||
|
||||
private final Logger logger = LoggerFactory.getLogger(MediaRendererService.class);
|
||||
|
||||
private final UpnpIOService service;
|
||||
|
||||
private final String udn;
|
||||
|
||||
private Map<String, String> stateMap = Collections.synchronizedMap(new HashMap<>());
|
||||
|
||||
private Set<EventListener> listeners = new CopyOnWriteArraySet<>();
|
||||
|
||||
private boolean started;
|
||||
|
||||
public MediaRendererService(UpnpIOService upnpIOService, String udn) {
|
||||
logger.debug("Creating a Samsung TV MediaRenderer service");
|
||||
this.service = upnpIOService;
|
||||
this.udn = udn;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> getSupportedChannelNames() {
|
||||
return SUPPORTED_CHANNELS;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addEventListener(EventListener listener) {
|
||||
listeners.add(listener);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void removeEventListener(EventListener listener) {
|
||||
listeners.remove(listener);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void start() {
|
||||
service.registerParticipant(this);
|
||||
started = true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void stop() {
|
||||
service.unregisterParticipant(this);
|
||||
started = false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void clearCache() {
|
||||
stateMap.clear();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isUpnp() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleCommand(String channel, Command command) {
|
||||
logger.debug("Received channel: {}, command: {}", channel, command);
|
||||
|
||||
if (!started) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (command == RefreshType.REFRESH) {
|
||||
if (isRegistered()) {
|
||||
switch (channel) {
|
||||
case VOLUME:
|
||||
updateResourceState("RenderingControl", "GetVolume",
|
||||
SamsungTvUtils.buildHashMap("InstanceID", "0", "Channel", "Master"));
|
||||
break;
|
||||
case MUTE:
|
||||
updateResourceState("RenderingControl", "GetMute",
|
||||
SamsungTvUtils.buildHashMap("InstanceID", "0", "Channel", "Master"));
|
||||
break;
|
||||
case BRIGHTNESS:
|
||||
updateResourceState("RenderingControl", "GetBrightness",
|
||||
SamsungTvUtils.buildHashMap("InstanceID", "0"));
|
||||
break;
|
||||
case CONTRAST:
|
||||
updateResourceState("RenderingControl", "GetContrast",
|
||||
SamsungTvUtils.buildHashMap("InstanceID", "0"));
|
||||
break;
|
||||
case SHARPNESS:
|
||||
updateResourceState("RenderingControl", "GetSharpness",
|
||||
SamsungTvUtils.buildHashMap("InstanceID", "0"));
|
||||
break;
|
||||
case COLOR_TEMPERATURE:
|
||||
updateResourceState("RenderingControl", "GetColorTemperature",
|
||||
SamsungTvUtils.buildHashMap("InstanceID", "0"));
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
switch (channel) {
|
||||
case VOLUME:
|
||||
setVolume(command);
|
||||
break;
|
||||
case MUTE:
|
||||
setMute(command);
|
||||
break;
|
||||
case BRIGHTNESS:
|
||||
setBrightness(command);
|
||||
break;
|
||||
case CONTRAST:
|
||||
setContrast(command);
|
||||
break;
|
||||
case SHARPNESS:
|
||||
setSharpness(command);
|
||||
break;
|
||||
case COLOR_TEMPERATURE:
|
||||
setColorTemperature(command);
|
||||
break;
|
||||
default:
|
||||
logger.warn("Samsung TV doesn't support transmitting for channel '{}'", channel);
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isRegistered() {
|
||||
return service.isRegistered(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getUDN() {
|
||||
return udn;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onServiceSubscribed(@Nullable String service, boolean succeeded) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onValueReceived(@Nullable String variable, @Nullable String value, @Nullable String service) {
|
||||
if (variable == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
String oldValue = stateMap.get(variable);
|
||||
if ((value == null && oldValue == null) || (value != null && value.equals(oldValue))) {
|
||||
logger.trace("Value '{}' for {} hasn't changed, ignoring update", value, variable);
|
||||
return;
|
||||
}
|
||||
|
||||
stateMap.put(variable, (value != null) ? value : "");
|
||||
|
||||
for (EventListener listener : listeners) {
|
||||
switch (variable) {
|
||||
case "CurrentVolume":
|
||||
listener.valueReceived(VOLUME, (value != null) ? new PercentType(value) : UnDefType.UNDEF);
|
||||
break;
|
||||
|
||||
case "CurrentMute":
|
||||
State newState = UnDefType.UNDEF;
|
||||
if (value != null) {
|
||||
newState = value.equals("true") ? OnOffType.ON : OnOffType.OFF;
|
||||
}
|
||||
listener.valueReceived(MUTE, newState);
|
||||
break;
|
||||
|
||||
case "CurrentBrightness":
|
||||
listener.valueReceived(BRIGHTNESS, (value != null) ? new PercentType(value) : UnDefType.UNDEF);
|
||||
break;
|
||||
|
||||
case "CurrentContrast":
|
||||
listener.valueReceived(CONTRAST, (value != null) ? new PercentType(value) : UnDefType.UNDEF);
|
||||
break;
|
||||
|
||||
case "CurrentSharpness":
|
||||
listener.valueReceived(SHARPNESS, (value != null) ? new PercentType(value) : UnDefType.UNDEF);
|
||||
break;
|
||||
|
||||
case "CurrentColorTemperature":
|
||||
listener.valueReceived(COLOR_TEMPERATURE,
|
||||
(value != null) ? new DecimalType(value) : UnDefType.UNDEF);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected Map<String, String> updateResourceState(String serviceId, String actionId, Map<String, String> inputs) {
|
||||
Map<String, String> result = service.invokeAction(this, serviceId, actionId, inputs);
|
||||
|
||||
for (String variable : result.keySet()) {
|
||||
onValueReceived(variable, result.get(variable), serviceId);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private void setVolume(Command command) {
|
||||
int newValue;
|
||||
|
||||
try {
|
||||
newValue = DataConverters.convertCommandToIntValue(command, 0, 100,
|
||||
Integer.valueOf(stateMap.get("CurrentVolume")));
|
||||
} catch (NumberFormatException e) {
|
||||
throw new NumberFormatException("Command '" + command + "' not supported");
|
||||
}
|
||||
|
||||
updateResourceState("RenderingControl", "SetVolume", SamsungTvUtils.buildHashMap("InstanceID", "0", "Channel",
|
||||
"Master", "DesiredVolume", Integer.toString(newValue)));
|
||||
|
||||
updateResourceState("RenderingControl", "GetVolume",
|
||||
SamsungTvUtils.buildHashMap("InstanceID", "0", "Channel", "Master"));
|
||||
}
|
||||
|
||||
private void setMute(Command command) {
|
||||
boolean newValue;
|
||||
|
||||
try {
|
||||
newValue = DataConverters.convertCommandToBooleanValue(command);
|
||||
} catch (NumberFormatException e) {
|
||||
throw new NumberFormatException("Command '" + command + "' not supported");
|
||||
}
|
||||
|
||||
updateResourceState("RenderingControl", "SetMute", SamsungTvUtils.buildHashMap("InstanceID", "0", "Channel",
|
||||
"Master", "DesiredMute", Boolean.toString(newValue)));
|
||||
|
||||
updateResourceState("RenderingControl", "GetMute",
|
||||
SamsungTvUtils.buildHashMap("InstanceID", "0", "Channel", "Master"));
|
||||
}
|
||||
|
||||
private void setBrightness(Command command) {
|
||||
int newValue;
|
||||
|
||||
try {
|
||||
newValue = DataConverters.convertCommandToIntValue(command, 0, 100,
|
||||
Integer.valueOf(stateMap.get("CurrentBrightness")));
|
||||
} catch (NumberFormatException e) {
|
||||
throw new NumberFormatException("Command '" + command + "' not supported");
|
||||
}
|
||||
|
||||
updateResourceState("RenderingControl", "SetBrightness",
|
||||
SamsungTvUtils.buildHashMap("InstanceID", "0", "DesiredBrightness", Integer.toString(newValue)));
|
||||
|
||||
updateResourceState("RenderingControl", "GetBrightness", SamsungTvUtils.buildHashMap("InstanceID", "0"));
|
||||
}
|
||||
|
||||
private void setContrast(Command command) {
|
||||
int newValue;
|
||||
|
||||
try {
|
||||
newValue = DataConverters.convertCommandToIntValue(command, 0, 100,
|
||||
Integer.valueOf(stateMap.get("CurrentContrast")));
|
||||
} catch (NumberFormatException e) {
|
||||
throw new NumberFormatException("Command '" + command + "' not supported");
|
||||
}
|
||||
|
||||
updateResourceState("RenderingControl", "SetContrast",
|
||||
SamsungTvUtils.buildHashMap("InstanceID", "0", "DesiredContrast", Integer.toString(newValue)));
|
||||
|
||||
updateResourceState("RenderingControl", "GetContrast", SamsungTvUtils.buildHashMap("InstanceID", "0"));
|
||||
}
|
||||
|
||||
private void setSharpness(Command command) {
|
||||
int newValue;
|
||||
|
||||
try {
|
||||
newValue = DataConverters.convertCommandToIntValue(command, 0, 100,
|
||||
Integer.valueOf(stateMap.get("CurrentSharpness")));
|
||||
} catch (NumberFormatException e) {
|
||||
throw new NumberFormatException("Command '" + command + "' not supported");
|
||||
}
|
||||
|
||||
updateResourceState("RenderingControl", "SetSharpness",
|
||||
SamsungTvUtils.buildHashMap("InstanceID", "0", "DesiredSharpness", Integer.toString(newValue)));
|
||||
|
||||
updateResourceState("RenderingControl", "GetSharpness", SamsungTvUtils.buildHashMap("InstanceID", "0"));
|
||||
}
|
||||
|
||||
private void setColorTemperature(Command command) {
|
||||
int newValue;
|
||||
|
||||
try {
|
||||
newValue = DataConverters.convertCommandToIntValue(command, 0, 4,
|
||||
Integer.valueOf(stateMap.get("CurrentColorTemperature")));
|
||||
} catch (NumberFormatException e) {
|
||||
throw new NumberFormatException("Command '" + command + "' not supported");
|
||||
}
|
||||
|
||||
updateResourceState("RenderingControl", "SetColorTemperature",
|
||||
SamsungTvUtils.buildHashMap("InstanceID", "0", "DesiredColorTemperature", Integer.toString(newValue)));
|
||||
|
||||
updateResourceState("RenderingControl", "GetColorTemperature", SamsungTvUtils.buildHashMap("InstanceID", "0"));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStatusChanged(boolean status) {
|
||||
logger.debug("onStatusChanged: status={}", status);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,524 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 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.service;
|
||||
|
||||
import static org.openhab.binding.samsungtv.internal.SamsungTvBindingConstants.*;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStreamReader;
|
||||
import java.net.URI;
|
||||
import java.net.URISyntaxException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.CopyOnWriteArraySet;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.openhab.binding.samsungtv.internal.config.SamsungTvConfiguration;
|
||||
import org.openhab.binding.samsungtv.internal.protocol.KeyCode;
|
||||
import org.openhab.binding.samsungtv.internal.protocol.RemoteController;
|
||||
import org.openhab.binding.samsungtv.internal.protocol.RemoteControllerException;
|
||||
import org.openhab.binding.samsungtv.internal.protocol.RemoteControllerLegacy;
|
||||
import org.openhab.binding.samsungtv.internal.protocol.RemoteControllerWebSocket;
|
||||
import org.openhab.binding.samsungtv.internal.protocol.RemoteControllerWebsocketCallback;
|
||||
import org.openhab.binding.samsungtv.internal.service.api.EventListener;
|
||||
import org.openhab.binding.samsungtv.internal.service.api.SamsungTvService;
|
||||
import org.openhab.core.io.net.http.WebSocketFactory;
|
||||
import org.openhab.core.library.types.DecimalType;
|
||||
import org.openhab.core.library.types.OnOffType;
|
||||
import org.openhab.core.library.types.StringType;
|
||||
import org.openhab.core.library.types.UpDownType;
|
||||
import org.openhab.core.thing.ThingStatusDetail;
|
||||
import org.openhab.core.types.Command;
|
||||
import org.openhab.core.types.RefreshType;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import com.google.gson.Gson;
|
||||
|
||||
/**
|
||||
* The {@link RemoteControllerService} is responsible for handling remote
|
||||
* controller commands.
|
||||
*
|
||||
* @author Pauli Anttila - Initial contribution
|
||||
* @author Martin van Wingerden - Some changes for manually configured devices
|
||||
* @author Arjan Mels - Implemented websocket interface for recent TVs
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class RemoteControllerService implements SamsungTvService, RemoteControllerWebsocketCallback {
|
||||
|
||||
private final Logger logger = LoggerFactory.getLogger(RemoteControllerService.class);
|
||||
|
||||
public static final String SERVICE_NAME = "RemoteControlReceiver";
|
||||
|
||||
private final List<String> supportedCommandsUpnp = Arrays.asList(KEY_CODE, POWER, CHANNEL);
|
||||
private final List<String> supportedCommandsNonUpnp = Arrays.asList(KEY_CODE, VOLUME, MUTE, POWER, CHANNEL);
|
||||
private final List<String> extraSupportedCommandsWebSocket = Arrays.asList(BROWSER_URL, SOURCE_APP, ART_MODE);
|
||||
|
||||
private String host;
|
||||
private int port;
|
||||
private boolean upnp;
|
||||
|
||||
boolean power = true;
|
||||
boolean artMode = false;
|
||||
|
||||
private boolean artModeSupported = false;
|
||||
|
||||
private Set<EventListener> listeners = new CopyOnWriteArraySet<>();
|
||||
|
||||
private @Nullable RemoteController remoteController = null;
|
||||
|
||||
/** Path for the information endpoint (note the final slash!) */
|
||||
private static final String WS_ENDPOINT_V2 = "/api/v2/";
|
||||
|
||||
/** Description of the json returned for the information endpoint */
|
||||
@NonNullByDefault({})
|
||||
static class TVProperties {
|
||||
@NonNullByDefault({})
|
||||
static class Device {
|
||||
boolean FrameTVSupport;
|
||||
boolean GamePadSupport;
|
||||
boolean ImeSyncedSupport;
|
||||
String OS;
|
||||
boolean TokenAuthSupport;
|
||||
boolean VoiceSupport;
|
||||
String countryCode;
|
||||
String description;
|
||||
String firmwareVersion;
|
||||
String modelName;
|
||||
String name;
|
||||
String networkType;
|
||||
String resolution;
|
||||
}
|
||||
|
||||
Device device;
|
||||
String isSupport;
|
||||
}
|
||||
|
||||
/**
|
||||
* Discover the type of remote control service the TV supports.
|
||||
*
|
||||
* @param hostname
|
||||
* @return map with properties containing at least the protocol and port
|
||||
*/
|
||||
public static Map<String, Object> discover(String hostname) {
|
||||
Map<String, Object> result = new HashMap<>();
|
||||
|
||||
try {
|
||||
RemoteControllerLegacy remoteController = new RemoteControllerLegacy(hostname,
|
||||
SamsungTvConfiguration.PORT_DEFAULT_LEGACY, "openHAB", "openHAB");
|
||||
remoteController.openConnection();
|
||||
remoteController.close();
|
||||
result.put(SamsungTvConfiguration.PROTOCOL, SamsungTvConfiguration.PROTOCOL_LEGACY);
|
||||
result.put(SamsungTvConfiguration.PORT, SamsungTvConfiguration.PORT_DEFAULT_LEGACY);
|
||||
return result;
|
||||
} catch (RemoteControllerException e) {
|
||||
// ignore error
|
||||
}
|
||||
|
||||
URI uri;
|
||||
try {
|
||||
uri = new URI("http", null, hostname, SamsungTvConfiguration.PORT_DEFAULT_WEBSOCKET, WS_ENDPOINT_V2, null,
|
||||
null);
|
||||
InputStreamReader reader = new InputStreamReader(uri.toURL().openStream());
|
||||
TVProperties properties = new Gson().fromJson(reader, TVProperties.class);
|
||||
|
||||
if (properties.device.TokenAuthSupport) {
|
||||
result.put(SamsungTvConfiguration.PROTOCOL, SamsungTvConfiguration.PROTOCOL_SECUREWEBSOCKET);
|
||||
result.put(SamsungTvConfiguration.PORT, SamsungTvConfiguration.PORT_DEFAULT_SECUREWEBSOCKET);
|
||||
} else {
|
||||
result.put(SamsungTvConfiguration.PROTOCOL, SamsungTvConfiguration.PROTOCOL_WEBSOCKET);
|
||||
result.put(SamsungTvConfiguration.PORT, SamsungTvConfiguration.PORT_DEFAULT_WEBSOCKET);
|
||||
}
|
||||
} catch (URISyntaxException | IOException e) {
|
||||
LoggerFactory.getLogger(RemoteControllerService.class).debug("Cannot retrieve info from TV", e);
|
||||
result.put(SamsungTvConfiguration.PROTOCOL, SamsungTvConfiguration.PROTOCOL_NONE);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private RemoteControllerService(String host, int port, boolean upnp) {
|
||||
logger.debug("Creating a Samsung TV RemoteController service: {}", upnp);
|
||||
this.upnp = upnp;
|
||||
this.host = host;
|
||||
this.port = port;
|
||||
}
|
||||
|
||||
static RemoteControllerService createUpnpService(String host, int port) {
|
||||
return new RemoteControllerService(host, port, true);
|
||||
}
|
||||
|
||||
public static RemoteControllerService createNonUpnpService(String host, int port) {
|
||||
return new RemoteControllerService(host, port, false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> getSupportedChannelNames() {
|
||||
List<String> supported = upnp ? supportedCommandsUpnp : supportedCommandsNonUpnp;
|
||||
if (remoteController instanceof RemoteControllerWebSocket) {
|
||||
supported = new ArrayList<>(supported);
|
||||
supported.addAll(extraSupportedCommandsWebSocket);
|
||||
}
|
||||
logger.debug("getSupportedChannelNames: {}", supported);
|
||||
return supported;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addEventListener(EventListener listener) {
|
||||
listeners.add(listener);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void removeEventListener(EventListener listener) {
|
||||
listeners.remove(listener);
|
||||
}
|
||||
|
||||
public boolean checkConnection() {
|
||||
if (remoteController != null) {
|
||||
return remoteController.isConnected();
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void start() {
|
||||
if (remoteController != null) {
|
||||
try {
|
||||
remoteController.openConnection();
|
||||
} catch (RemoteControllerException e) {
|
||||
logger.warn("Cannot open remote interface ({})", e.getMessage());
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
String protocol = (String) getConfig(SamsungTvConfiguration.PROTOCOL);
|
||||
logger.info("Using {} interface", protocol);
|
||||
|
||||
if (SamsungTvConfiguration.PROTOCOL_LEGACY.equals(protocol)) {
|
||||
remoteController = new RemoteControllerLegacy(host, port, "openHAB", "openHAB");
|
||||
} else if (SamsungTvConfiguration.PROTOCOL_WEBSOCKET.equals(protocol)
|
||||
|| SamsungTvConfiguration.PROTOCOL_SECUREWEBSOCKET.equals(protocol)) {
|
||||
try {
|
||||
remoteController = new RemoteControllerWebSocket(host, port, "openHAB", "openHAB", this);
|
||||
} catch (RemoteControllerException e) {
|
||||
reportError("Cannot connect to remote control service", e);
|
||||
}
|
||||
} else {
|
||||
remoteController = null;
|
||||
return;
|
||||
}
|
||||
|
||||
if (remoteController != null) {
|
||||
try {
|
||||
remoteController.openConnection();
|
||||
} catch (RemoteControllerException e) {
|
||||
reportError("Cannot connect to remote control service", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void stop() {
|
||||
if (remoteController != null) {
|
||||
try {
|
||||
remoteController.close();
|
||||
} catch (RemoteControllerException ignore) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void clearCache() {
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isUpnp() {
|
||||
return upnp;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleCommand(String channel, Command command) {
|
||||
logger.debug("Received channel: {}, command: {}", channel, command);
|
||||
if (command == RefreshType.REFRESH) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (remoteController == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
KeyCode key = null;
|
||||
|
||||
if (remoteController instanceof RemoteControllerWebSocket) {
|
||||
RemoteControllerWebSocket remoteControllerWebSocket = (RemoteControllerWebSocket) remoteController;
|
||||
switch (channel) {
|
||||
case BROWSER_URL:
|
||||
if (command instanceof StringType) {
|
||||
remoteControllerWebSocket.sendUrl(command.toString());
|
||||
} else {
|
||||
logger.warn("Remote control: unsupported command type {} for channel {}", command, channel);
|
||||
}
|
||||
return;
|
||||
case SOURCE_APP:
|
||||
if (command instanceof StringType) {
|
||||
remoteControllerWebSocket.sendSourceApp(command.toString());
|
||||
} else {
|
||||
logger.warn("Remote control: unsupported command type {} for channel {}", command, channel);
|
||||
}
|
||||
return;
|
||||
case POWER:
|
||||
if (command instanceof OnOffType) {
|
||||
// websocket uses KEY_POWER
|
||||
// send key only to toggle state
|
||||
if (OnOffType.ON.equals(command) != power) {
|
||||
sendKeyCode(KeyCode.KEY_POWER);
|
||||
}
|
||||
} else {
|
||||
logger.warn("Remote control: unsupported command type {} for channel {}", command, channel);
|
||||
}
|
||||
return;
|
||||
case ART_MODE:
|
||||
if (command instanceof OnOffType) {
|
||||
// websocket uses KEY_POWER
|
||||
// send key only to toggle state when power = off
|
||||
if (!power) {
|
||||
if (OnOffType.ON.equals(command)) {
|
||||
if (!artMode) {
|
||||
sendKeyCode(KeyCode.KEY_POWER);
|
||||
}
|
||||
} else {
|
||||
sendKeyCodePress(KeyCode.KEY_POWER);
|
||||
// really switch off
|
||||
}
|
||||
} else {
|
||||
// switch TV off
|
||||
sendKeyCode(KeyCode.KEY_POWER);
|
||||
// switch TV to art mode
|
||||
sendKeyCode(KeyCode.KEY_POWER);
|
||||
}
|
||||
} else {
|
||||
logger.warn("Remote control: unsupported command type {} for channel {}", command, channel);
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
switch (channel) {
|
||||
case KEY_CODE:
|
||||
if (command instanceof StringType) {
|
||||
try {
|
||||
key = KeyCode.valueOf(command.toString().toUpperCase());
|
||||
} catch (IllegalArgumentException e) {
|
||||
try {
|
||||
key = KeyCode.valueOf("KEY_" + command.toString().toUpperCase());
|
||||
} catch (IllegalArgumentException e2) {
|
||||
// do nothing, error message is logged later
|
||||
}
|
||||
}
|
||||
|
||||
if (key != null) {
|
||||
sendKeyCode(key);
|
||||
} else {
|
||||
logger.warn("Remote control: Command '{}' not supported for channel '{}'", command, channel);
|
||||
}
|
||||
} else {
|
||||
logger.warn("Remote control: unsupported command type {} for channel {}", command, channel);
|
||||
}
|
||||
return;
|
||||
|
||||
case POWER:
|
||||
if (command instanceof OnOffType) {
|
||||
// legacy controller uses KEY_POWERON/OFF
|
||||
if (command.equals(OnOffType.ON)) {
|
||||
sendKeyCode(KeyCode.KEY_POWERON);
|
||||
} else {
|
||||
sendKeyCode(KeyCode.KEY_POWEROFF);
|
||||
}
|
||||
} else {
|
||||
logger.warn("Remote control: unsupported command type {} for channel {}", command, channel);
|
||||
}
|
||||
return;
|
||||
|
||||
case MUTE:
|
||||
sendKeyCode(KeyCode.KEY_MUTE);
|
||||
return;
|
||||
|
||||
case VOLUME:
|
||||
if (command instanceof UpDownType) {
|
||||
if (command.equals(UpDownType.UP)) {
|
||||
sendKeyCode(KeyCode.KEY_VOLUP);
|
||||
} else {
|
||||
sendKeyCode(KeyCode.KEY_VOLDOWN);
|
||||
}
|
||||
} else {
|
||||
logger.warn("Remote control: unsupported command type {} for channel {}", command, channel);
|
||||
}
|
||||
return;
|
||||
|
||||
case CHANNEL:
|
||||
if (command instanceof DecimalType) {
|
||||
int val = ((DecimalType) command).intValue();
|
||||
int num4 = val / 1000 % 10;
|
||||
int num3 = val / 100 % 10;
|
||||
int num2 = val / 10 % 10;
|
||||
int num1 = val % 10;
|
||||
|
||||
List<KeyCode> commands = new ArrayList<>();
|
||||
|
||||
if (num4 > 0) {
|
||||
commands.add(KeyCode.valueOf("KEY_" + num4));
|
||||
}
|
||||
if (num4 > 0 || num3 > 0) {
|
||||
commands.add(KeyCode.valueOf("KEY_" + num3));
|
||||
}
|
||||
if (num4 > 0 || num3 > 0 || num2 > 0) {
|
||||
commands.add(KeyCode.valueOf("KEY_" + num2));
|
||||
}
|
||||
commands.add(KeyCode.valueOf("KEY_" + num1));
|
||||
commands.add(KeyCode.KEY_ENTER);
|
||||
sendKeyCodes(commands);
|
||||
} else {
|
||||
logger.warn("Remote control: unsupported command type {} for channel {}", command, channel);
|
||||
}
|
||||
return;
|
||||
default:
|
||||
logger.warn("Remote control: unsupported channel: {}", channel);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a command to Samsung TV device.
|
||||
*
|
||||
* @param key Button code to send
|
||||
*/
|
||||
private void sendKeyCode(KeyCode key) {
|
||||
try {
|
||||
if (remoteController != null) {
|
||||
remoteController.sendKey(key);
|
||||
}
|
||||
} catch (RemoteControllerException e) {
|
||||
reportError(String.format("Could not send command to device on %s:%d", host, port), e);
|
||||
}
|
||||
}
|
||||
|
||||
private void sendKeyCodePress(KeyCode key) {
|
||||
try {
|
||||
if (remoteController != null && remoteController instanceof RemoteControllerWebSocket) {
|
||||
((RemoteControllerWebSocket) remoteController).sendKeyPress(key);
|
||||
}
|
||||
} catch (RemoteControllerException e) {
|
||||
reportError(String.format("Could not send command to device on %s:%d", host, port), e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a sequence of command to Samsung TV device.
|
||||
*
|
||||
* @param keys List of button codes to send
|
||||
*/
|
||||
private void sendKeyCodes(final List<KeyCode> keys) {
|
||||
try {
|
||||
if (remoteController != null) {
|
||||
remoteController.sendKeys(keys);
|
||||
}
|
||||
} catch (RemoteControllerException e) {
|
||||
reportError(String.format("Could not send command to device on %s:%d", host, port), e);
|
||||
}
|
||||
}
|
||||
|
||||
private void reportError(String message, RemoteControllerException e) {
|
||||
reportError(ThingStatusDetail.COMMUNICATION_ERROR, message, e);
|
||||
}
|
||||
|
||||
private void reportError(ThingStatusDetail statusDetail, String message, RemoteControllerException e) {
|
||||
for (EventListener listener : listeners) {
|
||||
listener.reportError(statusDetail, message, e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void appsUpdated(List<String> apps) {
|
||||
// do nothing
|
||||
}
|
||||
|
||||
@Override
|
||||
public void currentAppUpdated(@Nullable String app) {
|
||||
for (EventListener listener : listeners) {
|
||||
listener.valueReceived(SOURCE_APP, new StringType(app));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void powerUpdated(boolean on, boolean artmode) {
|
||||
artModeSupported = true;
|
||||
power = on;
|
||||
this.artMode = artmode;
|
||||
|
||||
for (EventListener listener : listeners) {
|
||||
// order of state updates is important to prevent extraneous transitions in overall state
|
||||
if (on) {
|
||||
listener.valueReceived(POWER, on ? OnOffType.ON : OnOffType.OFF);
|
||||
listener.valueReceived(ART_MODE, artmode ? OnOffType.ON : OnOffType.OFF);
|
||||
} else {
|
||||
listener.valueReceived(ART_MODE, artmode ? OnOffType.ON : OnOffType.OFF);
|
||||
listener.valueReceived(POWER, on ? OnOffType.ON : OnOffType.OFF);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void connectionError(@Nullable Throwable error) {
|
||||
logger.debug("Connection error: {}", error != null ? error.getMessage() : "");
|
||||
try {
|
||||
if (remoteController != null) {
|
||||
remoteController.close();
|
||||
}
|
||||
} catch (RemoteControllerException e) {
|
||||
logger.debug("Error in connection close: {}", e.getMessage());
|
||||
}
|
||||
remoteController = null;
|
||||
}
|
||||
|
||||
public boolean isArtModeSupported() {
|
||||
return artModeSupported;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void putConfig(String key, Object value) {
|
||||
for (EventListener listener : listeners) {
|
||||
listener.putConfig(key, value);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public @Nullable Object getConfig(String key) {
|
||||
for (EventListener listener : listeners) {
|
||||
return listener.getConfig(key);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @Nullable WebSocketFactory getWebSocketFactory() {
|
||||
for (EventListener listener : listeners) {
|
||||
return listener.getWebSocketFactory();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 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.service;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.StringReader;
|
||||
import java.util.HashMap;
|
||||
|
||||
import javax.xml.parsers.DocumentBuilder;
|
||||
import javax.xml.parsers.DocumentBuilderFactory;
|
||||
import javax.xml.parsers.ParserConfigurationException;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.w3c.dom.Document;
|
||||
import org.xml.sax.InputSource;
|
||||
import org.xml.sax.SAXException;
|
||||
|
||||
/**
|
||||
* The {@link SamsungTvUtils} provides some utilities for internal use.
|
||||
*
|
||||
* @author Pauli Anttila - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class SamsungTvUtils {
|
||||
|
||||
/**
|
||||
* Build {@link String} type {@link HashMap} from variable number of
|
||||
* {@link String}s.
|
||||
*
|
||||
* @param data
|
||||
* Variable number of {@link String} parameters which will be
|
||||
* added to hash map.
|
||||
*/
|
||||
public static HashMap<String, String> buildHashMap(String... data) {
|
||||
HashMap<String, String> result = new HashMap<>();
|
||||
|
||||
if (data.length % 2 != 0) {
|
||||
throw new IllegalArgumentException("Odd number of arguments");
|
||||
}
|
||||
String key = null;
|
||||
Integer step = -1;
|
||||
|
||||
for (String value : data) {
|
||||
step++;
|
||||
switch (step % 2) {
|
||||
case 0:
|
||||
if (value == null) {
|
||||
throw new IllegalArgumentException("Null key value");
|
||||
}
|
||||
key = value;
|
||||
continue;
|
||||
case 1:
|
||||
if (key != null) {
|
||||
result.put(key, value);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build {@link Document} from {@link String} which contains XML content.
|
||||
*
|
||||
* @param xml
|
||||
* {@link String} which contains XML content.
|
||||
* @return {@link Document} or null if convert has failed.
|
||||
*/
|
||||
public static @Nullable Document loadXMLFromString(String xml) {
|
||||
try {
|
||||
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
|
||||
DocumentBuilder builder = factory.newDocumentBuilder();
|
||||
InputSource is = new InputSource(new StringReader(xml));
|
||||
return builder.parse(is);
|
||||
|
||||
} catch (ParserConfigurationException | SAXException | IOException e) {
|
||||
// Silently ignore exception and return null.
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 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.service;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.openhab.binding.samsungtv.internal.service.api.SamsungTvService;
|
||||
import org.openhab.core.io.transport.upnp.UpnpIOService;
|
||||
|
||||
/**
|
||||
* The {@link ServiceFactory} is helper class for creating Samsung TV related
|
||||
* services.
|
||||
*
|
||||
* @author Pauli Anttila - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class ServiceFactory {
|
||||
|
||||
@SuppressWarnings("serial")
|
||||
private static final Map<String, Class<? extends SamsungTvService>> SERVICEMAP = Collections
|
||||
.unmodifiableMap(new HashMap<String, Class<? extends SamsungTvService>>() {
|
||||
{
|
||||
put(MainTVServerService.SERVICE_NAME, MainTVServerService.class);
|
||||
put(MediaRendererService.SERVICE_NAME, MediaRendererService.class);
|
||||
put(RemoteControllerService.SERVICE_NAME, RemoteControllerService.class);
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Create Samsung TV service.
|
||||
*
|
||||
* @param type
|
||||
* @param upnpIOService
|
||||
* @param udn
|
||||
* @param host
|
||||
* @param port
|
||||
* @return
|
||||
*/
|
||||
public static @Nullable SamsungTvService createService(String type, UpnpIOService upnpIOService, String udn,
|
||||
String host, int port) {
|
||||
SamsungTvService service = null;
|
||||
|
||||
switch (type) {
|
||||
case MainTVServerService.SERVICE_NAME:
|
||||
service = new MainTVServerService(upnpIOService, udn);
|
||||
break;
|
||||
case MediaRendererService.SERVICE_NAME:
|
||||
service = new MediaRendererService(upnpIOService, udn);
|
||||
break;
|
||||
// will not be created automatically
|
||||
case RemoteControllerService.SERVICE_NAME:
|
||||
service = RemoteControllerService.createUpnpService(host, port);
|
||||
break;
|
||||
|
||||
}
|
||||
|
||||
return service;
|
||||
}
|
||||
|
||||
/**
|
||||
* Procedure to query amount of supported services.
|
||||
*
|
||||
* @return Amount of supported services
|
||||
*/
|
||||
public static int getServiceCount() {
|
||||
return SERVICEMAP.size();
|
||||
}
|
||||
|
||||
/**
|
||||
* Procedure to get service class by service name.
|
||||
*
|
||||
* @param serviceName Name of the service
|
||||
* @return Class of the service
|
||||
*/
|
||||
public static Class<? extends SamsungTvService> getClassByServiceName(String serviceName) {
|
||||
return SERVICEMAP.get(serviceName);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 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.service.api;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.openhab.core.io.net.http.WebSocketFactory;
|
||||
import org.openhab.core.thing.ThingStatusDetail;
|
||||
import org.openhab.core.types.State;
|
||||
|
||||
/**
|
||||
* Interface for receiving data from Samsung TV services.
|
||||
*
|
||||
* @author Pauli Anttila - Initial contribution
|
||||
* @author Arjan Mels - Added methods to put/get configuration
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public interface EventListener {
|
||||
/**
|
||||
* Invoked when value is received from the TV.
|
||||
*
|
||||
* @param variable Name of the variable.
|
||||
* @param value Value of the variable value.
|
||||
*/
|
||||
void valueReceived(String variable, State value);
|
||||
|
||||
/**
|
||||
* Report an error to this event listener
|
||||
*
|
||||
* @param statusDetail hint about the actual underlying problem
|
||||
* @param message of the error
|
||||
* @param e exception that might have occurred
|
||||
*/
|
||||
void reportError(ThingStatusDetail statusDetail, String message, Throwable e);
|
||||
|
||||
/**
|
||||
* Get configuration item
|
||||
*
|
||||
* @param key key of configuration item
|
||||
* @param value value of key
|
||||
*/
|
||||
void putConfig(String key, Object value);
|
||||
|
||||
/**
|
||||
* Put configuration item
|
||||
*
|
||||
* @param key key of configuration item
|
||||
* @return value of key
|
||||
*/
|
||||
Object getConfig(String key);
|
||||
|
||||
/**
|
||||
* Get WebSocket Factory
|
||||
*
|
||||
* @return WebSocket Factory
|
||||
*/
|
||||
WebSocketFactory getWebSocketFactory();
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 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.service.api;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.openhab.core.types.Command;
|
||||
|
||||
/**
|
||||
* Interface for Samsung TV services.
|
||||
*
|
||||
* @author Pauli Anttila - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public interface SamsungTvService {
|
||||
|
||||
/**
|
||||
* Procedure to get list of supported channel names.
|
||||
*
|
||||
* @return List of supported
|
||||
*/
|
||||
List<String> getSupportedChannelNames();
|
||||
|
||||
/**
|
||||
* Procedure for sending command.
|
||||
*
|
||||
* @param channel the channel to which the command applies
|
||||
* @param command the command to be handled
|
||||
*/
|
||||
void handleCommand(String channel, Command command);
|
||||
|
||||
/**
|
||||
* Procedure for register event listener.
|
||||
*
|
||||
* @param listener
|
||||
* Event listener instance to handle events.
|
||||
*/
|
||||
void addEventListener(EventListener listener);
|
||||
|
||||
/**
|
||||
* Procedure for remove event listener.
|
||||
*
|
||||
* @param listener
|
||||
* Event listener instance to remove.
|
||||
*/
|
||||
void removeEventListener(EventListener listener);
|
||||
|
||||
/**
|
||||
* Procedure for starting service.
|
||||
*
|
||||
*/
|
||||
void start();
|
||||
|
||||
/**
|
||||
* Procedure for stopping service.
|
||||
*
|
||||
*/
|
||||
void stop();
|
||||
|
||||
/**
|
||||
* Procedure for clearing internal caches.
|
||||
*
|
||||
*/
|
||||
void clearCache();
|
||||
|
||||
/**
|
||||
* Is this an UPnP configured service
|
||||
*
|
||||
* @return whether this service is an UPnP configured / discovered service
|
||||
*/
|
||||
boolean isUpnp();
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<binding:binding id="samsungtv" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xmlns:binding="https://openhab.org/schemas/binding/v1.0.0"
|
||||
xsi:schemaLocation="https://openhab.org/schemas/binding/v1.0.0 https://openhab.org/schemas/binding-1.0.0.xsd">
|
||||
|
||||
<name>Samsung TV Binding</name>
|
||||
<description>This is the binding for Samsung TV. Binding should support all Samsung TV C (2010), D (2011) and E (2012)
|
||||
models</description>
|
||||
<author>Pauli Anttila</author>
|
||||
|
||||
</binding:binding>
|
||||
@@ -0,0 +1,46 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<config-description:config-descriptions
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xmlns:config-description="https://openhab.org/schemas/config-description/v1.0.0"
|
||||
xsi:schemaLocation="https://openhab.org/schemas/config-description/v1.0.0 https://openhab.org/schemas/config-description-1.0.0.xsd">
|
||||
|
||||
<config-description uri="thing-type:samsungtv:tv">
|
||||
<parameter name="hostName" type="text" required="true">
|
||||
<label>Host Name</label>
|
||||
<description>Network address of the Samsung TV.</description>
|
||||
<context>network-address</context>
|
||||
</parameter>
|
||||
<parameter name="port" type="integer" min="1" max="65535">
|
||||
<label>TCP Port</label>
|
||||
<description>TCP port of the Samsung TV.</description>
|
||||
<default>55000</default>
|
||||
</parameter>
|
||||
<parameter name="macAddress" type="text">
|
||||
<label>MAC Address</label>
|
||||
<description>MAC Address of the Samsung TV.</description>
|
||||
</parameter>
|
||||
<parameter name="refreshInterval" type="integer" unit="ms">
|
||||
<label>Refresh Interval</label>
|
||||
<description>States how often a refresh shall occur in milliseconds.
|
||||
</description>
|
||||
<default>1000</default>
|
||||
</parameter>
|
||||
<parameter name="protocol" type="text" required="true">
|
||||
<label>Remote Control Protocol</label>
|
||||
<description>The type of remote control protocol. This depends on the age of the TV.</description>
|
||||
<options>
|
||||
<option value="None">None</option>
|
||||
<option value="Legacy">Legacy (Before 2014)</option>
|
||||
<option value="WebSocket">Websocket (2016 and later TV's)</option>
|
||||
<option value="SecureWebSocket">Secure websocket (2016 and later TV's)</option>
|
||||
</options>
|
||||
<default>None</default>
|
||||
</parameter>
|
||||
<parameter name="webSocketToken" type="text" readOnly="true">
|
||||
<label>Websocket Token</label>
|
||||
<description>Security token for secure websocket connection</description>
|
||||
<advanced>true</advanced>
|
||||
</parameter>
|
||||
</config-description>
|
||||
|
||||
</config-description:config-descriptions>
|
||||
@@ -0,0 +1,47 @@
|
||||
# binding
|
||||
binding.samsungtv.name = Samsung TV Binding
|
||||
binding.samsungtv.description = Dieses Binding integriert Samsung TV Geräte, wodurch diese gesteuert werden können.
|
||||
|
||||
# thing types
|
||||
thing-type.samsungtv.tv.label = Samsung TV
|
||||
thing-type.samsungtv.tv.description = Dient zur Steuerung des Gerätes und liefert Daten wie z.B. Infos über den aktuellen Kanal oder die laufende Sendung.
|
||||
|
||||
# thing types config
|
||||
thing-type.config.samsungtv.tv.hostName.label = IP-Adresse
|
||||
thing-type.config.samsungtv.tv.hostName.description = Lokale IP-Adresse oder Hostname des Samsung TV.
|
||||
thing-type.config.samsungtv.tv.port.label = Port
|
||||
thing-type.config.samsungtv.tv.port.description = Port des Samsung TV.
|
||||
thing-type.config.samsungtv.tv.refreshInterval.label = Abfrageintervall
|
||||
thing-type.config.samsungtv.tv.refreshInterval.description = Intervall zur Abfrage des Samsung TV (in Millisekunden).
|
||||
|
||||
# channel types
|
||||
channel-type.samsungtv.volume.label = Lautstärke
|
||||
channel-type.samsungtv.volume.description = Ermöglicht die Steuerung der Lautstärke.
|
||||
channel-type.samsungtv.mute.label = Stumm schalten
|
||||
channel-type.samsungtv.mute.description = Ermöglicht die Lautstärke auf stumm zu schalten.
|
||||
channel-type.samsungtv.brightness.label = Helligkeit
|
||||
channel-type.samsungtv.brightness.description = Ermöglicht die Steuerung der Helligkeit.
|
||||
channel-type.samsungtv.contrast.label = Kontrast
|
||||
channel-type.samsungtv.contrast.description = Ermöglicht die Steuerung des Kontrastes.
|
||||
channel-type.samsungtv.sharpness.label = Schärfe
|
||||
channel-type.samsungtv.sharpness.description = Ermöglicht die Steuerung der Schärfe.
|
||||
channel-type.samsungtv.colortemperature.label = Farbtemperatur
|
||||
channel-type.samsungtv.colortemperature.description = Ermöglicht die Steuerung der Farbtemperatur. Von 0 bis 4.
|
||||
channel-type.samsungtv.sourcename.label = Source
|
||||
channel-type.samsungtv.sourcename.description = Zeigt die Quelle des eingehenden Signals an.
|
||||
channel-type.samsungtv.sourceid.label = Source ID
|
||||
channel-type.samsungtv.sourceid.description = Zeigt die ID der Quelle des eingehenden Signals an.
|
||||
channel-type.samsungtv.channel.label = Kanalnummer
|
||||
channel-type.samsungtv.channel.description = Zeigt die Nummer des aktuellen Kanals an.
|
||||
channel-type.samsungtv.programtitle.label = Titel
|
||||
channel-type.samsungtv.programtitle.description = Zeigt den Titel des aktuellen Films an.
|
||||
channel-type.samsungtv.channelname.label = Kanal
|
||||
channel-type.samsungtv.channelname.description = Zeigt den Namen des aktuellen Kanals an.
|
||||
channel-type.samsungtv.url.label = URL
|
||||
channel-type.samsungtv.url.description = Ermöglicht das Abspielen einer URL im Browser des Gerätes.
|
||||
channel-type.samsungtv.stopbrowser.label = Stop Browser
|
||||
channel-type.samsungtv.stopbrowser.description = Ermöglicht das Stoppen des Browsers des Gerätes.
|
||||
channel-type.samsungtv.power.label = Power
|
||||
channel-type.samsungtv.power.description = Ermöglicht das An- und Ausschalten des Gerätes.
|
||||
channel-type.samsungtv.keycode.label = Tastendruck
|
||||
channel-type.samsungtv.keycode.description = Ermöglicht das Senden eines Eingabebefehls an das Gerät.
|
||||
@@ -0,0 +1,368 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<thing:thing-descriptions bindingId="samsungtv"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
|
||||
xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
|
||||
|
||||
<channel-type id="volume">
|
||||
<item-type>Dimmer</item-type>
|
||||
<label>Volume</label>
|
||||
<description>Volume level of the TV.</description>
|
||||
<category>SoundVolume</category>
|
||||
</channel-type>
|
||||
|
||||
<channel-type id="mute">
|
||||
<item-type>Switch</item-type>
|
||||
<label>Mute</label>
|
||||
<description>Mute state of the TV.</description>
|
||||
</channel-type>
|
||||
|
||||
<channel-type id="brightness" advanced="true">
|
||||
<item-type>Dimmer</item-type>
|
||||
<label>Brightness</label>
|
||||
<description>Brightness of the TV picture.</description>
|
||||
</channel-type>
|
||||
|
||||
<channel-type id="contrast" advanced="true">
|
||||
<item-type>Dimmer</item-type>
|
||||
<label>Contrast</label>
|
||||
<description>Contrast of the TV picture.</description>
|
||||
</channel-type>
|
||||
|
||||
<channel-type id="sharpness" advanced="true">
|
||||
<item-type>Dimmer</item-type>
|
||||
<label>Sharpness</label>
|
||||
<description>Sharpness of the TV picture.</description>
|
||||
</channel-type>
|
||||
|
||||
<channel-type id="colortemperature" advanced="true">
|
||||
<item-type>Number</item-type>
|
||||
<label>Color Temperature</label>
|
||||
<description>Color temperature of the TV picture. Minimum value is 0 and
|
||||
maximum 4.
|
||||
</description>
|
||||
<state min="0" max="4"/>
|
||||
</channel-type>
|
||||
|
||||
<channel-type id="sourcename">
|
||||
<item-type>String</item-type>
|
||||
<label>Source Name</label>
|
||||
<description>Name of the current source.</description>
|
||||
</channel-type>
|
||||
|
||||
<channel-type id="sourceid" advanced="true">
|
||||
<item-type>Number</item-type>
|
||||
<label>Source ID</label>
|
||||
<description>Id of the current source.</description>
|
||||
<state readOnly="true"/>
|
||||
</channel-type>
|
||||
|
||||
<channel-type id="channel">
|
||||
<item-type>Number</item-type>
|
||||
<label>Channel</label>
|
||||
<description>Selected TV channel number.</description>
|
||||
</channel-type>
|
||||
|
||||
<channel-type id="programtitle">
|
||||
<item-type>String</item-type>
|
||||
<label>Program Title</label>
|
||||
<description>Program title of the current channel.</description>
|
||||
<state readOnly="true"/>
|
||||
</channel-type>
|
||||
|
||||
<channel-type id="channelname">
|
||||
<item-type>String</item-type>
|
||||
<label>Channel Name</label>
|
||||
<description>Name of the current TV channel.</description>
|
||||
<state readOnly="true"/>
|
||||
</channel-type>
|
||||
|
||||
<channel-type id="url" advanced="true">
|
||||
<item-type>String</item-type>
|
||||
<label>Browser URL</label>
|
||||
<description>Start TV web browser and go the given web page.</description>
|
||||
</channel-type>
|
||||
|
||||
<channel-type id="stopbrowser" advanced="true">
|
||||
<item-type>Switch</item-type>
|
||||
<label>Stop Browser</label>
|
||||
<description>Stop TV's web browser and go back to TV mode.</description>
|
||||
</channel-type>
|
||||
|
||||
<channel-type id="power">
|
||||
<item-type>Switch</item-type>
|
||||
<label>Power</label>
|
||||
<description>TV power. Some of the Samsung TV models doesn't allow to set
|
||||
Power ON remotely.
|
||||
</description>
|
||||
</channel-type>
|
||||
|
||||
<channel-type id="artmode">
|
||||
<item-type>Switch</item-type>
|
||||
<label>Art Mode</label>
|
||||
<description>TV Art Mode.</description>
|
||||
</channel-type>
|
||||
|
||||
<channel-type id="sourceapp" advanced="true">
|
||||
<item-type>String</item-type>
|
||||
<label>Application</label>
|
||||
<description>Application currently running</description>
|
||||
</channel-type>
|
||||
|
||||
<channel-type id="keycode">
|
||||
<item-type>String</item-type>
|
||||
<label>Key Code</label>
|
||||
<description>The key code channel emulates the infrared remote controller and
|
||||
allows to send virtual button presses.
|
||||
</description>
|
||||
<state>
|
||||
<options>
|
||||
<option value="KEY_0">KEY_0</option>
|
||||
<option value="KEY_1">KEY_1</option>
|
||||
<option value="KEY_2">KEY_2</option>
|
||||
<option value="KEY_3">KEY_3</option>
|
||||
<option value="KEY_4">KEY_4</option>
|
||||
<option value="KEY_5">KEY_5</option>
|
||||
<option value="KEY_6">KEY_6</option>
|
||||
<option value="KEY_7">KEY_7</option>
|
||||
<option value="KEY_8">KEY_8</option>
|
||||
<option value="KEY_9">KEY_9</option>
|
||||
<option value="KEY_11">KEY_11</option>
|
||||
<option value="KEY_12">KEY_12</option>
|
||||
<option value="KEY_3SPEED">KEY_3SPEED</option>
|
||||
<option value="KEY_4_3">KEY_4_3</option>
|
||||
<option value="KEY_16_9">KEY_16_9</option>
|
||||
<option value="KEY_AD">KEY_AD</option>
|
||||
<option value="KEY_ADDDEL">KEY_ADDDEL</option>
|
||||
<option value="KEY_ALT_MHP">KEY_ALT_MHP</option>
|
||||
<option value="KEY_ANGLE">KEY_ANGLE</option>
|
||||
<option value="KEY_ANTENA">KEY_ANTENA</option>
|
||||
<option value="KEY_ANYNET">KEY_ANYNET</option>
|
||||
<option value="KEY_ANYVIEW">KEY_ANYVIEW</option>
|
||||
<option value="KEY_APP_LIST">KEY_APP_LIST</option>
|
||||
<option value="KEY_ASPECT">KEY_ASPECT</option>
|
||||
<option value="KEY_AUTO_ARC_ANTENNA_AIR">KEY_AUTO_ARC_ANTENNA_AIR</option>
|
||||
<option value="KEY_AUTO_ARC_ANTENNA_CABLE">KEY_AUTO_ARC_ANTENNA_CABLE</option>
|
||||
<option value="KEY_AUTO_ARC_ANTENNA_SATELLITE">KEY_AUTO_ARC_ANTENNA_SATELLITE</option>
|
||||
<option value="KEY_AUTO_ARC_ANYNET_AUTO_START">KEY_AUTO_ARC_ANYNET_AUTO_START</option>
|
||||
<option value="KEY_AUTO_ARC_ANYNET_MODE_OK">KEY_AUTO_ARC_ANYNET_MODE_OK</option>
|
||||
<option value="KEY_AUTO_ARC_AUTOCOLOR_FAIL">KEY_AUTO_ARC_AUTOCOLOR_FAIL</option>
|
||||
<option value="KEY_AUTO_ARC_AUTOCOLOR_SUCCESS">KEY_AUTO_ARC_AUTOCOLOR_SUCCESS</option>
|
||||
<option value="KEY_AUTO_ARC_CAPTION_ENG">KEY_AUTO_ARC_CAPTION_ENG</option>
|
||||
<option value="KEY_AUTO_ARC_CAPTION_KOR">KEY_AUTO_ARC_CAPTION_KOR</option>
|
||||
<option value="KEY_AUTO_ARC_CAPTION_OFF">KEY_AUTO_ARC_CAPTION_OFF</option>
|
||||
<option value="KEY_AUTO_ARC_CAPTION_ON">KEY_AUTO_ARC_CAPTION_ON</option>
|
||||
<option value="KEY_AUTO_ARC_C_FORCE_AGING">KEY_AUTO_ARC_C_FORCE_AGING</option>
|
||||
<option value="KEY_AUTO_ARC_JACK_IDENT">KEY_AUTO_ARC_JACK_IDENT</option>
|
||||
<option value="KEY_AUTO_ARC_LNA_OFF">KEY_AUTO_ARC_LNA_OFF</option>
|
||||
<option value="KEY_AUTO_ARC_LNA_ON">KEY_AUTO_ARC_LNA_ON</option>
|
||||
<option value="KEY_AUTO_ARC_PIP_CH_CHANGE">KEY_AUTO_ARC_PIP_CH_CHANGE</option>
|
||||
<option value="KEY_AUTO_ARC_PIP_DOUBLE">KEY_AUTO_ARC_PIP_DOUBLE</option>
|
||||
<option value="KEY_AUTO_ARC_PIP_LARGE">KEY_AUTO_ARC_PIP_LARGE</option>
|
||||
<option value="KEY_AUTO_ARC_PIP_LEFT_BOTTOM">KEY_AUTO_ARC_PIP_LEFT_BOTTOM</option>
|
||||
<option value="KEY_AUTO_ARC_PIP_LEFT_TOP">KEY_AUTO_ARC_PIP_LEFT_TOP</option>
|
||||
<option value="KEY_AUTO_ARC_PIP_RIGHT_BOTTOM">KEY_AUTO_ARC_PIP_RIGHT_BOTTOM</option>
|
||||
<option value="KEY_AUTO_ARC_PIP_RIGHT_TOP">KEY_AUTO_ARC_PIP_RIGHT_TOP</option>
|
||||
<option value="KEY_AUTO_ARC_PIP_SMALL">KEY_AUTO_ARC_PIP_SMALL</option>
|
||||
<option value="KEY_AUTO_ARC_PIP_SOURCE_CHANGE">KEY_AUTO_ARC_PIP_SOURCE_CHANGE</option>
|
||||
<option value="KEY_AUTO_ARC_PIP_WIDE">KEY_AUTO_ARC_PIP_WIDE</option>
|
||||
<option value="KEY_AUTO_ARC_RESET">KEY_AUTO_ARC_RESET</option>
|
||||
<option value="KEY_AUTO_ARC_USBJACK_INSPECT">KEY_AUTO_ARC_USBJACK_INSPECT</option>
|
||||
<option value="KEY_AUTO_FORMAT">KEY_AUTO_FORMAT</option>
|
||||
<option value="KEY_AUTO_PROGRAM">KEY_AUTO_PROGRAM</option>
|
||||
<option value="KEY_AV1">KEY_AV1</option>
|
||||
<option value="KEY_AV2">KEY_AV2</option>
|
||||
<option value="KEY_AV3">KEY_AV3</option>
|
||||
<option value="KEY_BACK_MHP">KEY_BACK_MHP</option>
|
||||
<option value="KEY_BOOKMARK">KEY_BOOKMARK</option>
|
||||
<option value="KEY_CALLER_ID">KEY_CALLER_ID</option>
|
||||
<option value="KEY_CAPTION">KEY_CAPTION</option>
|
||||
<option value="KEY_CATV_MODE">KEY_CATV_MODE</option>
|
||||
<option value="KEY_CHDOWN">KEY_CHDOWN</option>
|
||||
<option value="KEY_CHUP">KEY_CHUP</option>
|
||||
<option value="KEY_CH_LIST">KEY_CH_LIST</option>
|
||||
<option value="KEY_CLEAR">KEY_CLEAR</option>
|
||||
<option value="KEY_CLOCK_DISPLAY">KEY_CLOCK_DISPLAY</option>
|
||||
<option value="KEY_COMPONENT1">KEY_COMPONENT1</option>
|
||||
<option value="KEY_COMPONENT2">KEY_COMPONENT2</option>
|
||||
<option value="KEY_CONTENTS">KEY_CONTENTS</option>
|
||||
<option value="KEY_CONVERGENCE">KEY_CONVERGENCE</option>
|
||||
<option value="KEY_CONVERT_AUDIO_MAINSUB">KEY_CONVERT_AUDIO_MAINSUB</option>
|
||||
<option value="KEY_CUSTOM">KEY_CUSTOM</option>
|
||||
<option value="KEY_CYAN">KEY_CYAN</option>
|
||||
<option value="KEY_BLUE">KEY_BLUE</option>
|
||||
<option value="KEY_DEVICE_CONNECT">KEY_DEVICE_CONNECT</option>
|
||||
<option value="KEY_DISC_MENU">KEY_DISC_MENU</option>
|
||||
<option value="KEY_DMA">KEY_DMA</option>
|
||||
<option value="KEY_DNET">KEY_DNET</option>
|
||||
<option value="KEY_DNIE">KEY_DNIE</option>
|
||||
<option value="KEY_DNSE">KEY_DNSE</option>
|
||||
<option value="KEY_DOOR">KEY_DOOR</option>
|
||||
<option value="KEY_DOWN">KEY_DOWN</option>
|
||||
<option value="KEY_DSS_MODE">KEY_DSS_MODE</option>
|
||||
<option value="KEY_DTV">KEY_DTV</option>
|
||||
<option value="KEY_DTV_LINK">KEY_DTV_LINK</option>
|
||||
<option value="KEY_DTV_SIGNAL">KEY_DTV_SIGNAL</option>
|
||||
<option value="KEY_DVD_MODE">KEY_DVD_MODE</option>
|
||||
<option value="KEY_DVI">KEY_DVI</option>
|
||||
<option value="KEY_DVR">KEY_DVR</option>
|
||||
<option value="KEY_DVR_MENU">KEY_DVR_MENU</option>
|
||||
<option value="KEY_DYNAMIC">KEY_DYNAMIC</option>
|
||||
<option value="KEY_ENTER">KEY_ENTER</option>
|
||||
<option value="KEY_ENTERTAINMENT">KEY_ENTERTAINMENT</option>
|
||||
<option value="KEY_ESAVING">KEY_ESAVING</option>
|
||||
<option value="KEY_EXIT">KEY_EXIT</option>
|
||||
<option value="KEY_EXT1">KEY_EXT1</option>
|
||||
<option value="KEY_EXT2">KEY_EXT2</option>
|
||||
<option value="KEY_EXT3">KEY_EXT3</option>
|
||||
<option value="KEY_EXT4">KEY_EXT4</option>
|
||||
<option value="KEY_EXT5">KEY_EXT5</option>
|
||||
<option value="KEY_EXT6">KEY_EXT6</option>
|
||||
<option value="KEY_EXT7">KEY_EXT7</option>
|
||||
<option value="KEY_EXT8">KEY_EXT8</option>
|
||||
<option value="KEY_EXT9">KEY_EXT9</option>
|
||||
<option value="KEY_EXT10">KEY_EXT10</option>
|
||||
<option value="KEY_EXT11">KEY_EXT11</option>
|
||||
<option value="KEY_EXT12">KEY_EXT12</option>
|
||||
<option value="KEY_EXT13">KEY_EXT13</option>
|
||||
<option value="KEY_EXT14">KEY_EXT14</option>
|
||||
<option value="KEY_EXT15">KEY_EXT15</option>
|
||||
<option value="KEY_EXT16">KEY_EXT16</option>
|
||||
<option value="KEY_EXT17">KEY_EXT17</option>
|
||||
<option value="KEY_EXT18">KEY_EXT18</option>
|
||||
<option value="KEY_EXT19">KEY_EXT19</option>
|
||||
<option value="KEY_EXT20">KEY_EXT20</option>
|
||||
<option value="KEY_EXT21">KEY_EXT21</option>
|
||||
<option value="KEY_EXT22">KEY_EXT22</option>
|
||||
<option value="KEY_EXT23">KEY_EXT23</option>
|
||||
<option value="KEY_EXT24">KEY_EXT24</option>
|
||||
<option value="KEY_EXT25">KEY_EXT25</option>
|
||||
<option value="KEY_EXT26">KEY_EXT26</option>
|
||||
<option value="KEY_EXT27">KEY_EXT27</option>
|
||||
<option value="KEY_EXT28">KEY_EXT28</option>
|
||||
<option value="KEY_EXT29">KEY_EXT29</option>
|
||||
<option value="KEY_EXT30">KEY_EXT30</option>
|
||||
<option value="KEY_EXT31">KEY_EXT31</option>
|
||||
<option value="KEY_EXT32">KEY_EXT32</option>
|
||||
<option value="KEY_EXT33">KEY_EXT33</option>
|
||||
<option value="KEY_EXT34">KEY_EXT34</option>
|
||||
<option value="KEY_EXT35">KEY_EXT35</option>
|
||||
<option value="KEY_EXT36">KEY_EXT36</option>
|
||||
<option value="KEY_EXT37">KEY_EXT37</option>
|
||||
<option value="KEY_EXT38">KEY_EXT38</option>
|
||||
<option value="KEY_EXT39">KEY_EXT39</option>
|
||||
<option value="KEY_EXT40">KEY_EXT40</option>
|
||||
<option value="KEY_EXT41">KEY_EXT41</option>
|
||||
<option value="KEY_FACTORY">KEY_FACTORY</option>
|
||||
<option value="KEY_FAVCH">KEY_FAVCH</option>
|
||||
<option value="KEY_FF">KEY_FF</option>
|
||||
<option value="KEY_FM_RADIO">KEY_FM_RADIO</option>
|
||||
<option value="KEY_GAME">KEY_GAME</option>
|
||||
<option value="KEY_GREEN">KEY_GREEN</option>
|
||||
<option value="KEY_GUIDE">KEY_GUIDE</option>
|
||||
<option value="KEY_HDMI">KEY_HDMI</option>
|
||||
<option value="KEY_HDMI1">KEY_HDMI1</option>
|
||||
<option value="KEY_HDMI2">KEY_HDMI2</option>
|
||||
<option value="KEY_HDMI3">KEY_HDMI3</option>
|
||||
<option value="KEY_HDMI4">KEY_HDMI4</option>
|
||||
<option value="KEY_HELP">KEY_HELP</option>
|
||||
<option value="KEY_HOME">KEY_HOME</option>
|
||||
<option value="KEY_ID_INPUT">KEY_ID_INPUT</option>
|
||||
<option value="KEY_ID_SETUP">KEY_ID_SETUP</option>
|
||||
<option value="KEY_INFO">KEY_INFO</option>
|
||||
<option value="KEY_INSTANT_REPLAY">KEY_INSTANT_REPLAY</option>
|
||||
<option value="KEY_LEFT">KEY_LEFT</option>
|
||||
<option value="KEY_LINK">KEY_LINK</option>
|
||||
<option value="KEY_LIVE">KEY_LIVE</option>
|
||||
<option value="KEY_MAGIC_BRIGHT">KEY_MAGIC_BRIGHT</option>
|
||||
<option value="KEY_MAGIC_CHANNEL">KEY_MAGIC_CHANNEL</option>
|
||||
<option value="KEY_MDC">KEY_MDC</option>
|
||||
<option value="KEY_MENU">KEY_MENU</option>
|
||||
<option value="KEY_MIC">KEY_MIC</option>
|
||||
<option value="KEY_MORE">KEY_MORE</option>
|
||||
<option value="KEY_MOVIE1">KEY_MOVIE1</option>
|
||||
<option value="KEY_MS">KEY_MS</option>
|
||||
<option value="KEY_MTS">KEY_MTS</option>
|
||||
<option value="KEY_MUTE">KEY_MUTE</option>
|
||||
<option value="KEY_NINE_SEPERATE">KEY_NINE_SEPERATE</option>
|
||||
<option value="KEY_OPEN">KEY_OPEN</option>
|
||||
<option value="KEY_PANNEL_CHDOWN">KEY_PANNEL_CHDOWN</option>
|
||||
<option value="KEY_PANNEL_CHUP">KEY_PANNEL_CHUP</option>
|
||||
<option value="KEY_PANNEL_ENTER">KEY_PANNEL_ENTER</option>
|
||||
<option value="KEY_PANNEL_MENU">KEY_PANNEL_MENU</option>
|
||||
<option value="KEY_PANNEL_POWER">KEY_PANNEL_POWER</option>
|
||||
<option value="KEY_PANNEL_SOURCE">KEY_PANNEL_SOURCE</option>
|
||||
<option value="KEY_PANNEL_VOLDOW">KEY_PANNEL_VOLDOW</option>
|
||||
<option value="KEY_PANNEL_VOLUP">KEY_PANNEL_VOLUP</option>
|
||||
<option value="KEY_PANORAMA">KEY_PANORAMA</option>
|
||||
<option value="KEY_PAUSE">KEY_PAUSE</option>
|
||||
<option value="KEY_PCMODE">KEY_PCMODE</option>
|
||||
<option value="KEY_PERPECT_FOCUS">KEY_PERPECT_FOCUS</option>
|
||||
<option value="KEY_PICTURE_SIZE">KEY_PICTURE_SIZE</option>
|
||||
<option value="KEY_PIP_CHDOWN">KEY_PIP_CHDOWN</option>
|
||||
<option value="KEY_PIP_CHUP">KEY_PIP_CHUP</option>
|
||||
<option value="KEY_PIP_ONOFF">KEY_PIP_ONOFF</option>
|
||||
<option value="KEY_PIP_SCAN">KEY_PIP_SCAN</option>
|
||||
<option value="KEY_PIP_SIZE">KEY_PIP_SIZE</option>
|
||||
<option value="KEY_PIP_SWAP">KEY_PIP_SWAP</option>
|
||||
<option value="KEY_PLAY">KEY_PLAY</option>
|
||||
<option value="KEY_PLUS100">KEY_PLUS100</option>
|
||||
<option value="KEY_PMODE">KEY_PMODE</option>
|
||||
<option value="KEY_POWER">KEY_POWER</option>
|
||||
<option value="KEY_POWEROFF">KEY_POWEROFF</option>
|
||||
<option value="KEY_POWERON">KEY_POWERON</option>
|
||||
<option value="KEY_PRECH">KEY_PRECH</option>
|
||||
<option value="KEY_PRINT">KEY_PRINT</option>
|
||||
<option value="KEY_PROGRAM">KEY_PROGRAM</option>
|
||||
<option value="KEY_QUICK_REPLAY">KEY_QUICK_REPLAY</option>
|
||||
<option value="KEY_REC">KEY_REC</option>
|
||||
<option value="KEY_RED">KEY_RED</option>
|
||||
<option value="KEY_REPEAT">KEY_REPEAT</option>
|
||||
<option value="KEY_RESERVED1">KEY_RESERVED1</option>
|
||||
<option value="KEY_RETURN">KEY_RETURN</option>
|
||||
<option value="KEY_REWIND">KEY_REWIND</option>
|
||||
<option value="KEY_RIGHT">KEY_RIGHT</option>
|
||||
<option value="KEY_RSS">KEY_RSS</option>
|
||||
<option value="KEY_INTERNET">KEY_INTERNET</option>
|
||||
<option value="KEY_RSURF">KEY_RSURF</option>
|
||||
<option value="KEY_SCALE">KEY_SCALE</option>
|
||||
<option value="KEY_SEFFECT">KEY_SEFFECT</option>
|
||||
<option value="KEY_SETUP_CLOCK_TIMER">KEY_SETUP_CLOCK_TIMER</option>
|
||||
<option value="KEY_SLEEP">KEY_SLEEP</option>
|
||||
<option value="KEY_SOUND_MODE">KEY_SOUND_MODE</option>
|
||||
<option value="KEY_SOURCE">KEY_SOURCE</option>
|
||||
<option value="KEY_SRS">KEY_SRS</option>
|
||||
<option value="KEY_STANDARD">KEY_STANDARD</option>
|
||||
<option value="KEY_STB_MODE">KEY_STB_MODE</option>
|
||||
<option value="KEY_STILL_PICTURE">KEY_STILL_PICTURE</option>
|
||||
<option value="KEY_STOP">KEY_STOP</option>
|
||||
<option value="KEY_SUB_TITLE">KEY_SUB_TITLE</option>
|
||||
<option value="KEY_SVIDEO1">KEY_SVIDEO1</option>
|
||||
<option value="KEY_SVIDEO2">KEY_SVIDEO2</option>
|
||||
<option value="KEY_SVIDEO3">KEY_SVIDEO3</option>
|
||||
<option value="KEY_TOOLS">KEY_TOOLS</option>
|
||||
<option value="KEY_TOPMENU">KEY_TOPMENU</option>
|
||||
<option value="KEY_TTX_MIX">KEY_TTX_MIX</option>
|
||||
<option value="KEY_TTX_SUBFACE">KEY_TTX_SUBFACE</option>
|
||||
<option value="KEY_TURBO">KEY_TURBO</option>
|
||||
<option value="KEY_TV">KEY_TV</option>
|
||||
<option value="KEY_TV_MODE">KEY_TV_MODE</option>
|
||||
<option value="KEY_UP">KEY_UP</option>
|
||||
<option value="KEY_VCHIP">KEY_VCHIP</option>
|
||||
<option value="KEY_VCR_MODE">KEY_VCR_MODE</option>
|
||||
<option value="KEY_VOLDOWN">KEY_VOLDOWN</option>
|
||||
<option value="KEY_VOLUP">KEY_VOLUP</option>
|
||||
<option value="KEY_WHEEL_LEFT">KEY_WHEEL_LEFT</option>
|
||||
<option value="KEY_WHEEL_RIGHT">KEY_WHEEL_RIGHT</option>
|
||||
<option value="KEY_W_LINK">KEY_W_LINK</option>
|
||||
<option value="KEY_YELLOW">KEY_YELLOW</option>
|
||||
<option value="KEY_ZOOM1">KEY_ZOOM1</option>
|
||||
<option value="KEY_ZOOM2">KEY_ZOOM2</option>
|
||||
<option value="KEY_ZOOM_IN">KEY_ZOOM_IN</option>
|
||||
<option value="KEY_ZOOM_MOVE">KEY_ZOOM_MOVE</option>
|
||||
<option value="KEY_ZOOM_OUT">KEY_ZOOM_OUT</option>
|
||||
</options>
|
||||
</state>
|
||||
</channel-type>
|
||||
|
||||
</thing:thing-descriptions>
|
||||
@@ -0,0 +1,36 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<thing:thing-descriptions bindingId="samsungtv"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
|
||||
xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
|
||||
|
||||
<thing-type id="tv">
|
||||
<label>Samsung TV</label>
|
||||
<description>Allows to control Samsung TV</description>
|
||||
|
||||
<channels>
|
||||
<channel id="volume" typeId="volume"/>
|
||||
<channel id="mute" typeId="mute"/>
|
||||
<channel id="brightness" typeId="brightness"/>
|
||||
<channel id="contrast" typeId="contrast"/>
|
||||
<channel id="sharpness" typeId="sharpness"/>
|
||||
<channel id="colorTemperature" typeId="colortemperature"/>
|
||||
<channel id="sourceName" typeId="sourcename"/>
|
||||
<channel id="sourceId" typeId="sourceid"/>
|
||||
<channel id="channel" typeId="channel"/>
|
||||
<channel id="programTitle" typeId="programtitle"/>
|
||||
<channel id="channelName" typeId="channelname"/>
|
||||
<channel id="url" typeId="url"/>
|
||||
<channel id="stopBrowser" typeId="stopbrowser"/>
|
||||
<channel id="keyCode" typeId="keycode"/>
|
||||
<channel id="power" typeId="power"/>
|
||||
<channel id="artMode" typeId="artmode"/>
|
||||
<channel id="sourceApp" typeId="sourceapp"/>
|
||||
</channels>
|
||||
|
||||
<representation-property>hostName</representation-property>
|
||||
|
||||
<config-description-ref uri="thing-type:samsungtv:tv"/>
|
||||
</thing-type>
|
||||
|
||||
</thing:thing-descriptions>
|
||||
Reference in New Issue
Block a user