added migrated 2.x add-ons

Signed-off-by: Kai Kreuzer <kai@openhab.org>
This commit is contained in:
Kai Kreuzer
2020-09-21 01:58:32 +02:00
parent bbf1a7fd29
commit 6df6783b60
11662 changed files with 1302875 additions and 11 deletions

View File

@@ -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>

View File

@@ -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";
}

View File

@@ -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;
}
}

View File

@@ -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();
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}

View File

@@ -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);
}
}

View File

@@ -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();
}
}
}

View File

@@ -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");
}
}

View File

@@ -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();
}

View File

@@ -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()));
}
}

View File

@@ -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);
}
}

View File

@@ -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)));
}
}

View File

@@ -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)));
}
}

View File

@@ -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");
}
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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);
}
}

View File

@@ -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();
}

View File

@@ -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();
}

View File

@@ -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>

View File

@@ -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>

View File

@@ -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.

View File

@@ -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>

View File

@@ -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>