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,11 @@
<?xml version="1.0" encoding="UTF-8"?>
<features name="org.openhab.binding.lgwebos-${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-lgwebos" description="LG webOS Binding" version="${project.version}">
<feature>openhab-runtime-base</feature>
<feature>openhab-transport-upnp</feature>
<feature>openhab.tp-httpclient</feature>
<bundle start-level="80">mvn:org.openhab.addons.bundles/org.openhab.binding.lgwebos/${project.version}</bundle>
</feature>
</features>

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.lgwebos.action;
import java.io.IOException;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* The {@link ILGWebOSActions} defines the interface for all thing actions supported by the binding.
*
* @author Laurent Garnier - Initial contribution
*/
@NonNullByDefault
public interface ILGWebOSActions {
public void showToast(String text) throws IOException;
public void showToast(String icon, String text) throws IOException;
public void launchBrowser(String url);
public void launchApplication(String appId);
public void launchApplication(String appId, String params);
public void sendText(String text);
public void sendButton(String button);
public void increaseChannel();
public void decreaseChannel();
public void sendRCButton(String rcButton);
}

View File

@@ -0,0 +1,359 @@
/**
* 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.lgwebos.action;
import java.awt.image.BufferedImage;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.util.Base64;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import javax.imageio.ImageIO;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.lgwebos.internal.handler.LGWebOSHandler;
import org.openhab.binding.lgwebos.internal.handler.LGWebOSTVMouseSocket.ButtonType;
import org.openhab.binding.lgwebos.internal.handler.LGWebOSTVSocket;
import org.openhab.binding.lgwebos.internal.handler.LGWebOSTVSocket.State;
import org.openhab.binding.lgwebos.internal.handler.command.ServiceSubscription;
import org.openhab.binding.lgwebos.internal.handler.core.AppInfo;
import org.openhab.binding.lgwebos.internal.handler.core.ResponseListener;
import org.openhab.binding.lgwebos.internal.handler.core.TextInputStatusInfo;
import org.openhab.core.automation.annotation.ActionInput;
import org.openhab.core.automation.annotation.RuleAction;
import org.openhab.core.thing.binding.ThingActions;
import org.openhab.core.thing.binding.ThingActionsScope;
import org.openhab.core.thing.binding.ThingHandler;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.gson.JsonObject;
import com.google.gson.JsonParseException;
import com.google.gson.JsonParser;
/**
* The {@link LGWebOSActions} defines the thing actions for the LGwebOS binding.
* <p>
* <b>Note:</b>The static method <b>invokeMethodOf</b> handles the case where
* the test <i>actions instanceof LGWebOSActions</i> fails. This test can fail
* due to an issue in openHAB core v2.5.0 where the {@link LGWebOSActions} class
* can be loaded by a different classloader than the <i>actions</i> instance.
*
* @author Sebastian Prehn - Initial contribution
* @author Laurent Garnier - new method invokeMethodOf + interface ILGWebOSActions
*/
@ThingActionsScope(name = "lgwebos")
@NonNullByDefault
public class LGWebOSActions implements ThingActions, ILGWebOSActions {
private final Logger logger = LoggerFactory.getLogger(LGWebOSActions.class);
private final ResponseListener<TextInputStatusInfo> textInputListener = createTextInputStatusListener();
private @Nullable LGWebOSHandler handler;
@Override
public void setThingHandler(@Nullable ThingHandler handler) {
this.handler = (LGWebOSHandler) handler;
}
@Override
public @Nullable ThingHandler getThingHandler() {
return this.handler;
}
// a NonNull getter for handler
private LGWebOSHandler getLGWebOSHandler() {
LGWebOSHandler lgWebOSHandler = this.handler;
if (lgWebOSHandler == null) {
throw new IllegalStateException(
"ThingHandler must be set before any action may be invoked on LGWebOSActions.");
}
return lgWebOSHandler;
}
private enum Button {
UP,
DOWN,
LEFT,
RIGHT,
BACK,
DELETE,
ENTER,
HOME,
OK
}
@Override
@RuleAction(label = "@text/actionShowToastLabel", description = "@text/actionShowToastDesc")
public void showToast(
@ActionInput(name = "text", label = "@text/actionShowToastInputTextLabel", description = "@text/actionShowToastInputTextDesc") String text)
throws IOException {
getConnectedSocket().ifPresent(control -> control.showToast(text, createResponseListener()));
}
@Override
@RuleAction(label = "@text/actionShowToastWithIconLabel", description = "@text/actionShowToastWithIconLabel")
public void showToast(
@ActionInput(name = "icon", label = "@text/actionShowToastInputIconLabel", description = "@text/actionShowToastInputIconDesc") String icon,
@ActionInput(name = "text", label = "@text/actionShowToastInputTextLabel", description = "@text/actionShowToastInputTextDesc") String text)
throws IOException {
BufferedImage bi = ImageIO.read(new URL(icon));
try (ByteArrayOutputStream os = new ByteArrayOutputStream(); OutputStream b64 = Base64.getEncoder().wrap(os)) {
ImageIO.write(bi, "png", b64);
String string = os.toString(StandardCharsets.UTF_8.name());
getConnectedSocket().ifPresent(control -> control.showToast(text, string, "png", createResponseListener()));
}
}
@Override
@RuleAction(label = "@text/actionLaunchBrowserLabel", description = "@text/actionLaunchBrowserDesc")
public void launchBrowser(
@ActionInput(name = "url", label = "@text/actionLaunchBrowserInputUrlLabel", description = "@text/actionLaunchBrowserInputUrlDesc") String url) {
getConnectedSocket().ifPresent(control -> control.launchBrowser(url, createResponseListener()));
}
private List<AppInfo> getAppInfos() {
LGWebOSHandler lgWebOSHandler = getLGWebOSHandler();
if (!this.getConnectedSocket().isPresent()) {
return Collections.emptyList();
}
List<AppInfo> appInfos = lgWebOSHandler.getLauncherApplication()
.getAppInfos(lgWebOSHandler.getThing().getUID());
if (appInfos == null) {
logger.warn("No AppInfos found for device with ThingID {}.", lgWebOSHandler.getThing().getUID());
return Collections.emptyList();
}
return appInfos;
}
@Override
@RuleAction(label = "@text/actionLaunchApplicationLabel", description = "@text/actionLaunchApplicationDesc")
public void launchApplication(
@ActionInput(name = "appId", label = "@text/actionLaunchApplicationInputAppIDLabel", description = "@text/actionLaunchApplicationInputAppIDDesc") String appId) {
Optional<AppInfo> appInfo = getAppInfos().stream().filter(a -> a.getId().equals(appId)).findFirst();
if (appInfo.isPresent()) {
getConnectedSocket()
.ifPresent(control -> control.launchAppWithInfo(appInfo.get(), createResponseListener()));
} else {
logger.warn("Device with ThingID {} does not support any app with id: {}.",
getLGWebOSHandler().getThing().getUID(), appId);
}
}
@Override
@RuleAction(label = "@text/actionLaunchApplicationWithParamsLabel", description = "@text/actionLaunchApplicationWithParamsDesc")
public void launchApplication(
@ActionInput(name = "appId", label = "@text/actionLaunchApplicationInputAppIDLabel", description = "@text/actionLaunchApplicationInputAppIDDesc") String appId,
@ActionInput(name = "params", label = "@text/actionLaunchApplicationInputParamsLabel", description = "@text/actionLaunchApplicationInputParamsDesc") String params) {
try {
JsonParser parser = new JsonParser();
JsonObject payload = (JsonObject) parser.parse(params);
Optional<AppInfo> appInfo = getAppInfos().stream().filter(a -> a.getId().equals(appId)).findFirst();
if (appInfo.isPresent()) {
getConnectedSocket().ifPresent(
control -> control.launchAppWithInfo(appInfo.get(), payload, createResponseListener()));
} else {
logger.warn("Device with ThingID {} does not support any app with id: {}.",
getLGWebOSHandler().getThing().getUID(), appId);
}
} catch (JsonParseException ex) {
logger.warn("Parameters value ({}) is not in a valid JSON format. {}", params, ex.getMessage());
return;
}
}
@Override
@RuleAction(label = "@text/actionSendTextLabel", description = "@text/actionSendTextDesc")
public void sendText(
@ActionInput(name = "text", label = "@text/actionSendTextInputTextLabel", description = "@text/actionSendTextInputTextDesc") String text) {
getConnectedSocket().ifPresent(control -> {
ServiceSubscription<TextInputStatusInfo> subscription = control.subscribeTextInputStatus(textInputListener);
control.sendText(text);
control.unsubscribe(subscription);
});
}
@Override
@RuleAction(label = "@text/actionSendButtonLabel", description = "@text/actionSendButtonDesc")
public void sendButton(
@ActionInput(name = "text", label = "@text/actionSendButtonInputButtonLabel", description = "@text/actionSendButtonInputButtonDesc") String button) {
try {
switch (Button.valueOf(button)) {
case UP:
getConnectedSocket().ifPresent(control -> control.executeMouse(s -> s.button(ButtonType.UP)));
break;
case DOWN:
getConnectedSocket().ifPresent(control -> control.executeMouse(s -> s.button(ButtonType.DOWN)));
break;
case LEFT:
getConnectedSocket().ifPresent(control -> control.executeMouse(s -> s.button(ButtonType.LEFT)));
break;
case RIGHT:
getConnectedSocket().ifPresent(control -> control.executeMouse(s -> s.button(ButtonType.RIGHT)));
break;
case BACK:
getConnectedSocket().ifPresent(control -> control.executeMouse(s -> s.button(ButtonType.BACK)));
break;
case DELETE:
getConnectedSocket().ifPresent(control -> control.sendDelete());
break;
case ENTER:
getConnectedSocket().ifPresent(control -> control.sendEnter());
break;
case HOME:
getConnectedSocket().ifPresent(control -> control.executeMouse(s -> s.button("HOME")));
break;
case OK:
getConnectedSocket().ifPresent(control -> control.executeMouse(s -> s.click()));
break;
}
} catch (IllegalArgumentException ex) {
logger.warn("{} is not a valid value for button - available are: {}", button,
Stream.of(Button.values()).map(b -> b.name()).collect(Collectors.joining(", ")));
}
}
@Override
@RuleAction(label = "@text/actionIncreaseChannelLabel", description = "@text/actionIncreaseChannelDesc")
public void increaseChannel() {
getConnectedSocket().ifPresent(control -> control.channelUp(createResponseListener()));
}
@Override
@RuleAction(label = "@text/actionDecreaseChannelLabel", description = "@text/actionDecreaseChannelDesc")
public void decreaseChannel() {
getConnectedSocket().ifPresent(control -> control.channelDown(createResponseListener()));
}
@Override
@RuleAction(label = "@text/actionSendRCButtonLabel", description = "@text/actionSendRCButtonDesc")
public void sendRCButton(
@ActionInput(name = "text", label = "@text/actionSendRCButtonInputTextLabel", description = "@text/actionSendRCButtonInputTextDesc") String rcButton) {
getConnectedSocket().ifPresent(control -> control.executeMouse(s -> s.button(rcButton)));
}
private Optional<LGWebOSTVSocket> getConnectedSocket() {
LGWebOSHandler lgWebOSHandler = getLGWebOSHandler();
final LGWebOSTVSocket socket = lgWebOSHandler.getSocket();
if (socket.getState() != State.REGISTERED) {
logger.warn("Device with ThingID {} is currently not connected.", lgWebOSHandler.getThing().getUID());
return Optional.empty();
}
return Optional.of(socket);
}
private ResponseListener<TextInputStatusInfo> createTextInputStatusListener() {
return new ResponseListener<TextInputStatusInfo>() {
@Override
public void onError(@Nullable String error) {
logger.warn("Response: {}", error);
}
@Override
public void onSuccess(@Nullable TextInputStatusInfo info) {
logger.debug("Response: {}", info == null ? "OK" : info.getRawData());
}
};
}
private <O> ResponseListener<O> createResponseListener() {
return new ResponseListener<O>() {
@Override
public void onError(@Nullable String error) {
logger.warn("Response: {}", error);
}
@Override
public void onSuccess(@Nullable O object) {
logger.debug("Response: {}", object == null ? "OK" : object.toString());
}
};
}
// delegation methods for "legacy" rule support
private static ILGWebOSActions invokeMethodOf(@Nullable ThingActions actions) {
if (actions == null) {
throw new IllegalArgumentException("actions cannot be null");
}
if (actions.getClass().getName().equals(LGWebOSActions.class.getName())) {
if (actions instanceof ILGWebOSActions) {
return (ILGWebOSActions) actions;
} else {
return (ILGWebOSActions) Proxy.newProxyInstance(ILGWebOSActions.class.getClassLoader(),
new Class[] { ILGWebOSActions.class }, (Object proxy, Method method, Object[] args) -> {
Method m = actions.getClass().getDeclaredMethod(method.getName(),
method.getParameterTypes());
return m.invoke(actions, args);
});
}
}
throw new IllegalArgumentException("Actions is not an instance of LGWebOSActions");
}
public static void showToast(@Nullable ThingActions actions, String text) throws IOException {
invokeMethodOf(actions).showToast(text);
}
public static void showToast(@Nullable ThingActions actions, String icon, String text) throws IOException {
invokeMethodOf(actions).showToast(icon, text);
}
public static void launchBrowser(@Nullable ThingActions actions, String url) {
invokeMethodOf(actions).launchBrowser(url);
}
public static void launchApplication(@Nullable ThingActions actions, String appId) {
invokeMethodOf(actions).launchApplication(appId);
}
public static void launchApplication(@Nullable ThingActions actions, String appId, String param) {
invokeMethodOf(actions).launchApplication(appId, param);
}
public static void sendText(@Nullable ThingActions actions, String text) {
invokeMethodOf(actions).sendText(text);
}
public static void sendButton(@Nullable ThingActions actions, String button) {
invokeMethodOf(actions).sendButton(button);
}
public static void increaseChannel(@Nullable ThingActions actions) {
invokeMethodOf(actions).increaseChannel();
}
public static void decreaseChannel(@Nullable ThingActions actions) {
invokeMethodOf(actions).decreaseChannel();
}
public static void sendRCButton(@Nullable ThingActions actions, String rcButton) {
invokeMethodOf(actions).sendRCButton(rcButton);
}
}

View File

@@ -0,0 +1,103 @@
/**
* 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.lgwebos.internal;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.ConcurrentHashMap;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.lgwebos.internal.handler.LGWebOSHandler;
import org.openhab.binding.lgwebos.internal.handler.command.ServiceSubscription;
import org.openhab.binding.lgwebos.internal.handler.core.ResponseListener;
import org.openhab.core.thing.ThingUID;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* An abstract implementation of ChannelHander which serves as a base class for all concrete instances.
*
* @author Sebastian Prehn - initial contribution
*/
@NonNullByDefault
abstract class BaseChannelHandler<T> implements ChannelHandler {
private final Logger logger = LoggerFactory.getLogger(BaseChannelHandler.class);
private final ResponseListener<T> defaultResponseListener = createResponseListener();
protected <Y> ResponseListener<Y> createResponseListener() {
return new ResponseListener<Y>() {
@Override
public void onError(String error) {
logger.debug("{} received error response: {}", BaseChannelHandler.this.getClass().getSimpleName(),
error);
}
@Override
public void onSuccess(Y object) {
logger.debug("{} received: {}.", BaseChannelHandler.this.getClass().getSimpleName(), object);
}
};
}
// IP to Subscriptions map
private Map<ThingUID, ServiceSubscription<T>> subscriptions = new ConcurrentHashMap<>();
@Override
public void onDeviceReady(String channelId, LGWebOSHandler handler) {
// NOP
}
@Override
public void onDeviceRemoved(String channelId, LGWebOSHandler handler) {
// NOP
}
@Override
public final synchronized void refreshSubscription(String channelId, LGWebOSHandler handler) {
removeAnySubscription(handler);
Optional<ServiceSubscription<T>> listener = getSubscription(channelId, handler);
if (listener.isPresent()) {
logger.debug("Subscribed {} on Thing: {}", this.getClass().getName(), handler.getThing().getUID());
subscriptions.put(handler.getThing().getUID(), listener.get());
}
}
/**
* Creates a subscription instance for this device if subscription is supported.
*
* @param device device to which state changes to subscribe to
* @param channelID channel ID
* @param handler
* @return an {@code Optional} containing the ServiceSubscription, or an empty {@code Optional} if subscription is
* not supported.
*/
protected Optional<ServiceSubscription<T>> getSubscription(String channelId, LGWebOSHandler handler) {
return Optional.empty();
}
@Override
public final synchronized void removeAnySubscription(LGWebOSHandler handler) {
ServiceSubscription<T> l = subscriptions.remove(handler.getThing().getUID());
if (l != null) {
handler.getSocket().unsubscribe(l);
logger.debug("Unsubscribed {} on Thing: {}", this.getClass().getName(), handler.getThing().getUID());
}
}
protected ResponseListener<T> getDefaultResponseListener() {
return defaultResponseListener;
}
}

View File

@@ -0,0 +1,71 @@
/**
* 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.lgwebos.internal;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.lgwebos.internal.handler.LGWebOSHandler;
import org.openhab.core.types.Command;
/**
* Channel Handler mediates between connect sdk device state changes and openhab channel events.
*
* @author Sebastian Prehn - initial contribution
*/
@NonNullByDefault
public interface ChannelHandler {
/**
* This method will be called whenever a command is received for this handler.
* All implementations provide custom logic here.
*
* @param channelId must not be <code>null</code>
* @param handler must not be <code>null</code>
* @param command must not be <code>null</code>
*/
void onReceiveCommand(String channelId, LGWebOSHandler handler, Command command);
/**
* Handle underlying subscription status if device changes online state, capabilities or channel gets linked or
* unlinked.
*
* Implementation first removes any subscription via removeAnySubscription and subsequently establishes any required
* subscription on this device channel handler.
*
* @param channelId must not be <code>null</code>
* @param handler must not be <code>null</code>
*/
void refreshSubscription(String channelId, LGWebOSHandler handler);
/**
* Removes subscriptions if there are any.
*
* @param handler must not be <code>null</code>
*/
void removeAnySubscription(LGWebOSHandler handler);
/**
* Callback method whenever a device disappears.
*
* @param channelId must not be <code>null</code>
* @param handler must not be <code>null</code>
*/
void onDeviceRemoved(String channelId, LGWebOSHandler handler);
/**
* Callback method whenever a device is discovered and ready to operate.
*
* @param channelId must not be <code>null</code>
* @param handler must not be <code>null</code>
*/
void onDeviceReady(String channelId, LGWebOSHandler handler);
}

View File

@@ -0,0 +1,71 @@
/**
* 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.lgwebos.internal;
import java.util.Collections;
import java.util.Set;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.jupnp.model.types.ServiceType;
import org.openhab.core.thing.ThingTypeUID;
/**
* This class defines common constants, which are used across the whole binding.
*
* @author Sebastian Prehn - Initial contribution
*/
@NonNullByDefault
public class LGWebOSBindingConstants {
public static final String BINDING_ID = "lgwebos";
public static final ThingTypeUID THING_TYPE_WEBOSTV = new ThingTypeUID(BINDING_ID, "WebOSTV");
public static final Set<ThingTypeUID> SUPPORTED_THING_TYPES_UIDS = Collections.singleton(THING_TYPE_WEBOSTV);
public static final ServiceType UPNP_SERVICE_TYPE = new ServiceType("lge-com", "webos-second-screen", 1);
/*
* Config names must match property names in
* - WebOSConfiguration
* - parameter names in OH-INF/config/config.xml
* - property names in OH-INF/thing/thing-types.xml
*/
public static final String CONFIG_HOST = "host";
public static final String CONFIG_KEY = "key";
public static final String CONFIG_MAC_ADDRESS = "macAddress";
/*
* Property names must match property names in
* - property names in OH-INF/thing/thing-types.xml
*/
public static final String PROPERTY_DEVICE_ID = "deviceId";
public static final String PROPERTY_DEVICE_OS = "deviceOS";
public static final String PROPERTY_DEVICE_OS_VERSION = "deviceOSVersion";
public static final String PROPERTY_DEVICE_OS_RELEASE_VERSION = "deviceOSReleaseVersion";
public static final String PROPERTY_LAST_CONNECTED = "lastConnected";
/*
* List of all Channel ids.
* Values have to match ids in thing-types.xml
*/
public static final String CHANNEL_VOLUME = "volume";
public static final String CHANNEL_POWER = "power";
public static final String CHANNEL_MUTE = "mute";
public static final String CHANNEL_CHANNEL = "channel";
public static final String CHANNEL_TOAST = "toast";
public static final String CHANNEL_MEDIA_PLAYER = "mediaPlayer";
public static final String CHANNEL_MEDIA_STOP = "mediaStop";
public static final String CHANNEL_APP_LAUNCHER = "appLauncher";
public static final String CHANNEL_RCBUTTON = "rcButton";
}

View File

@@ -0,0 +1,104 @@
/**
* 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.lgwebos.internal;
import static org.openhab.binding.lgwebos.internal.LGWebOSBindingConstants.*;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.eclipse.jetty.websocket.client.WebSocketClient;
import org.openhab.binding.lgwebos.internal.handler.LGWebOSHandler;
import org.openhab.core.io.net.http.WebSocketFactory;
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.ComponentContext;
import org.osgi.service.component.annotations.Activate;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Reference;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The {@link LGWebOSHandlerFactory} is responsible for creating things and thing
* handlers.
*
* @author Sebastian Prehn - initial contribution
*/
@NonNullByDefault
@Component(service = ThingHandlerFactory.class, configurationPid = "binding.lgwebos")
public class LGWebOSHandlerFactory extends BaseThingHandlerFactory {
private final Logger logger = LoggerFactory.getLogger(LGWebOSHandlerFactory.class);
private final WebSocketClient webSocketClient;
private final LGWebOSStateDescriptionOptionProvider stateDescriptionProvider;
@Activate
public LGWebOSHandlerFactory(final @Reference WebSocketFactory webSocketFactory,
final @Reference LGWebOSStateDescriptionOptionProvider stateDescriptionProvider) {
/*
* Cannot use openHAB's shared web socket client (webSocketFactory.getCommonWebSocketClient()) as we have to
* change client settings.
*/
this.webSocketClient = webSocketFactory.createWebSocketClient("lgwebos");
this.stateDescriptionProvider = stateDescriptionProvider;
}
@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(THING_TYPE_WEBOSTV)) {
return new LGWebOSHandler(thing, webSocketClient, stateDescriptionProvider);
}
return null;
}
@Override
protected void activate(ComponentContext componentContext) {
super.activate(componentContext);
// LGWebOS TVs only support WEAK cipher suites, thus not using SSL.
// SslContextFactory sslContextFactory = new SslContextFactory(true);
// sslContextFactory.addExcludeProtocols("tls/1.3");
// reduce timeout from default 15sec
this.webSocketClient.setConnectTimeout(1000);
// channel and app listing are json docs up to 3MB
this.webSocketClient.getPolicy().setMaxTextMessageSize(3 * 1024 * 1024);
// since this is not using openHAB's shared web socket client we need to start and stop
try {
this.webSocketClient.start();
} catch (Exception e) {
logger.warn("Unable to to start websocket client.", e);
}
}
@Override
protected void deactivate(ComponentContext componentContext) {
super.deactivate(componentContext);
try {
this.webSocketClient.stop();
} catch (Exception e) {
logger.warn("Unable to to stop websocket client.", e);
}
}
}

View File

@@ -0,0 +1,41 @@
/**
* 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.lgwebos.internal;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.core.thing.binding.BaseDynamicStateDescriptionProvider;
import org.openhab.core.thing.i18n.ChannelTypeI18nLocalizationService;
import org.openhab.core.thing.type.DynamicStateDescriptionProvider;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Reference;
/**
* Dynamic provider of state options while leaving other state description fields as original.
*
* @author Laurent Garnier - Initial contribution
*/
@Component(service = { DynamicStateDescriptionProvider.class, LGWebOSStateDescriptionOptionProvider.class })
@NonNullByDefault
public class LGWebOSStateDescriptionOptionProvider extends BaseDynamicStateDescriptionProvider {
@Reference
protected void setChannelTypeI18nLocalizationService(
final ChannelTypeI18nLocalizationService channelTypeI18nLocalizationService) {
this.channelTypeI18nLocalizationService = channelTypeI18nLocalizationService;
}
protected void unsetChannelTypeI18nLocalizationService(
final ChannelTypeI18nLocalizationService channelTypeI18nLocalizationService) {
this.channelTypeI18nLocalizationService = null;
}
}

View File

@@ -0,0 +1,144 @@
/**
* 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.lgwebos.internal;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.lgwebos.internal.handler.LGWebOSHandler;
import org.openhab.binding.lgwebos.internal.handler.command.ServiceSubscription;
import org.openhab.binding.lgwebos.internal.handler.core.AppInfo;
import org.openhab.binding.lgwebos.internal.handler.core.LaunchSession;
import org.openhab.binding.lgwebos.internal.handler.core.ResponseListener;
import org.openhab.core.library.types.StringType;
import org.openhab.core.thing.ThingUID;
import org.openhab.core.types.Command;
import org.openhab.core.types.RefreshType;
import org.openhab.core.types.StateOption;
import org.openhab.core.types.UnDefType;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Provides ability to launch an application on the TV.
*
* @author Sebastian Prehn - Initial contribution
*/
@NonNullByDefault
public class LauncherApplication extends BaseChannelHandler<AppInfo> {
private final Logger logger = LoggerFactory.getLogger(LauncherApplication.class);
private final Map<ThingUID, @Nullable List<AppInfo>> applicationListCache = new HashMap<>();
private final ResponseListener<LaunchSession> launchSessionResponseListener = createResponseListener();
@Override
public void onDeviceReady(String channelId, LGWebOSHandler handler) {
super.onDeviceReady(channelId, handler);
handler.getSocket().getAppList(new ResponseListener<List<AppInfo>>() {
@Override
public void onError(String error) {
logger.warn("Error requesting application list: {}.", error);
}
@Override
@NonNullByDefault({})
public void onSuccess(List<AppInfo> appInfos) {
if (logger.isDebugEnabled()) {
for (AppInfo a : appInfos) {
logger.debug("AppInfo {} - {}", a.getId(), a.getName());
}
}
applicationListCache.put(handler.getThing().getUID(), appInfos);
List<StateOption> options = new ArrayList<>();
for (AppInfo appInfo : appInfos) {
options.add(new StateOption(appInfo.getId(), appInfo.getName()));
}
handler.setOptions(channelId, options);
}
});
}
@Override
public void onDeviceRemoved(String channelId, LGWebOSHandler handler) {
super.onDeviceRemoved(channelId, handler);
applicationListCache.remove(handler.getThing().getUID());
}
@Override
public void onReceiveCommand(String channelId, LGWebOSHandler handler, Command command) {
if (RefreshType.REFRESH == command) {
handler.getSocket().getRunningApp(createResponseListener(channelId, handler));
return;
}
final String value = command.toString();
List<AppInfo> appInfos = applicationListCache.get(handler.getThing().getUID());
if (appInfos == null) {
logger.warn("No application list cached for this device {}, ignoring command.",
handler.getThing().getUID());
} else {
Optional<AppInfo> appInfo = appInfos.stream().filter(a -> a.getId().equals(value)).findFirst();
if (appInfo.isPresent()) {
handler.getSocket().launchAppWithInfo(appInfo.get(), launchSessionResponseListener);
} else {
logger.warn("TV does not support any app with id: {}.", value);
}
}
}
@Override
protected Optional<ServiceSubscription<AppInfo>> getSubscription(String channelId, LGWebOSHandler handler) {
return Optional.of(handler.getSocket().subscribeRunningApp(createResponseListener(channelId, handler)));
}
private ResponseListener<AppInfo> createResponseListener(String channelId, LGWebOSHandler handler) {
return new ResponseListener<AppInfo>() {
@Override
public void onError(@Nullable String error) {
logger.debug("Error in retrieving application: {}.", error);
}
@Override
public void onSuccess(@Nullable AppInfo appInfo) {
if (appInfo == null || appInfo.getId().isEmpty()) {
handler.postUpdate(channelId, UnDefType.UNDEF);
} else {
handler.postUpdate(channelId, new StringType(appInfo.getId()));
}
}
};
}
public @Nullable List<AppInfo> getAppInfos(ThingUID key) {
return applicationListCache.get(key);
}
public List<String> reportApplications(ThingUID thingUID) {
List<String> report = new ArrayList<>();
List<AppInfo> appInfos = applicationListCache.get(thingUID);
if (appInfos != null) {
for (AppInfo a : appInfos) {
report.add(a.getId() + " : " + a.getName());
}
}
return report;
}
}

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.lgwebos.internal;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.lgwebos.internal.handler.LGWebOSHandler;
import org.openhab.binding.lgwebos.internal.handler.core.CommandConfirmation;
import org.openhab.core.library.types.PlayPauseType;
import org.openhab.core.library.types.RewindFastforwardType;
import org.openhab.core.types.Command;
import org.openhab.core.types.RefreshType;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Handles commands of a Player Item.
*
*
* @author Sebastian Prehn - Initial contribution
*/
@NonNullByDefault
public class MediaControlPlayer extends BaseChannelHandler<CommandConfirmation> {
private final Logger logger = LoggerFactory.getLogger(MediaControlPlayer.class);
@Override
public void onReceiveCommand(String channelId, LGWebOSHandler handler, Command command) {
if (RefreshType.REFRESH == command) {
// nothing to do
} else if (PlayPauseType.PLAY == command) {
handler.getSocket().play(getDefaultResponseListener());
} else if (PlayPauseType.PAUSE == command) {
handler.getSocket().pause(getDefaultResponseListener());
} else if (RewindFastforwardType.FASTFORWARD == command) {
handler.getSocket().fastForward(getDefaultResponseListener());
} else if (RewindFastforwardType.REWIND == command) {
handler.getSocket().rewind(getDefaultResponseListener());
} else {
logger.info("Only accept PlayPauseType, RewindFastforwardType, RefreshType. Type was {}.",
command.getClass());
}
}
// TODO: playstatesubscription
}

View File

@@ -0,0 +1,36 @@
/**
* 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.lgwebos.internal;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.lgwebos.internal.handler.LGWebOSHandler;
import org.openhab.binding.lgwebos.internal.handler.core.CommandConfirmation;
import org.openhab.core.types.Command;
import org.openhab.core.types.RefreshType;
/**
* Handles Media Control Command Stop.
*
* @author Sebastian Prehn - Initial contribution
*/
@NonNullByDefault
public class MediaControlStop extends BaseChannelHandler<CommandConfirmation> {
@Override
public void onReceiveCommand(String channelId, LGWebOSHandler handler, Command command) {
if (RefreshType.REFRESH == command) {
return;
}
handler.getSocket().stop(getDefaultResponseListener());
}
}

View File

@@ -0,0 +1,117 @@
/**
* 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.lgwebos.internal;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.lgwebos.internal.handler.LGWebOSHandler;
import org.openhab.binding.lgwebos.internal.handler.LGWebOSTVSocket.State;
import org.openhab.binding.lgwebos.internal.handler.core.CommandConfirmation;
import org.openhab.core.library.types.OnOffType;
import org.openhab.core.types.Command;
import org.openhab.core.types.RefreshType;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Handles Power Control Command.
* Note: Connect SDK only supports powering OFF for most devices.
*
* @author Sebastian Prehn - Initial contribution
*/
@NonNullByDefault
public class PowerControlPower extends BaseChannelHandler<CommandConfirmation> {
private static final int WOL_PACKET_RETRY_COUNT = 10;
private static final int WOL_PACKET_RETRY_DELAY_MILLIS = 100;
private final Logger logger = LoggerFactory.getLogger(PowerControlPower.class);
private final ConfigProvider configProvider;
private final ScheduledExecutorService scheduler;
public PowerControlPower(ConfigProvider configProvider, ScheduledExecutorService scheduler) {
this.configProvider = configProvider;
this.scheduler = scheduler;
}
@Override
public void onReceiveCommand(String channelId, LGWebOSHandler handler, Command command) {
final State state = handler.getSocket().getState();
if (RefreshType.REFRESH == command) {
handler.postUpdate(channelId, state == State.REGISTERED ? OnOffType.ON : OnOffType.OFF);
} else if (OnOffType.ON == command) {
switch (state) {
case CONNECTING:
case REGISTERING:
logger.debug("Received ON - TV is currently connecting.");
handler.postUpdate(channelId, OnOffType.OFF);
break;
case REGISTERED:
logger.debug("Received ON - TV is already on.");
break;
case DISCONNECTING: // WOL will not stop the shutdown process, but we must not update the state to ON
case DISCONNECTED:
String macAddress = configProvider.getMacAddress();
if (macAddress.isEmpty()) {
logger.debug("Received ON - Turning TV on via API is not supported by LG WebOS TVs. "
+ "You may succeed using wake on lan (WOL). "
+ "Please set the macAddress config value in Thing configuration to enable this.");
handler.postUpdate(channelId, OnOffType.OFF);
} else {
for (int i = 0; i < WOL_PACKET_RETRY_COUNT; i++) {
scheduler.schedule(() -> {
try {
WakeOnLanUtility.sendWOLPacket(macAddress);
} catch (IllegalArgumentException e) {
logger.debug("Failed to send WOL packet: {}", e.getMessage());
}
}, i * WOL_PACKET_RETRY_DELAY_MILLIS, TimeUnit.MILLISECONDS);
}
}
break;
}
} else if (OnOffType.OFF == command) {
switch (state) {
case CONNECTING:
case REGISTERING:
// in both states no message will sent to TV, thus the operation won't have an effect
logger.debug("Received OFF - TV is currently connecting.");
break;
case REGISTERED:
handler.getSocket().powerOff(getDefaultResponseListener());
break;
case DISCONNECTING:
case DISCONNECTED:
logger.debug("Received OFF - TV is already off.");
break;
}
} else {
logger.info("Only accept OnOffType, RefreshType. Type was {}.", command.getClass());
}
}
@Override
public void onDeviceReady(String channelId, LGWebOSHandler handler) {
handler.postUpdate(channelId, OnOffType.ON);
}
@Override
public void onDeviceRemoved(String channelId, LGWebOSHandler handler) {
handler.postUpdate(channelId, OnOffType.OFF);
}
public interface ConfigProvider {
String getMacAddress();
}
}

View File

@@ -0,0 +1,36 @@
/**
* 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.lgwebos.internal;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.lgwebos.internal.handler.LGWebOSHandler;
import org.openhab.binding.lgwebos.internal.handler.core.CommandConfirmation;
import org.openhab.core.types.Command;
import org.openhab.core.types.RefreshType;
/**
* Handles rcButton Control Command. This allows to send IR Remote Control button presses to the TV.
*
* @author Robert Brodt - Initial contribution
*/
@NonNullByDefault
public class RCButtonControl extends BaseChannelHandler<CommandConfirmation> {
@Override
public void onReceiveCommand(String channelId, LGWebOSHandler handler, Command command) {
if (RefreshType.REFRESH == command) {
return;
}
handler.getSocket().sendRCButton(command.toString(), getDefaultResponseListener());
}
}

View File

@@ -0,0 +1,138 @@
/**
* 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.lgwebos.internal;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.lgwebos.internal.handler.LGWebOSHandler;
import org.openhab.binding.lgwebos.internal.handler.command.ServiceSubscription;
import org.openhab.binding.lgwebos.internal.handler.core.ChannelInfo;
import org.openhab.binding.lgwebos.internal.handler.core.CommandConfirmation;
import org.openhab.binding.lgwebos.internal.handler.core.ResponseListener;
import org.openhab.core.library.types.StringType;
import org.openhab.core.thing.ThingUID;
import org.openhab.core.types.Command;
import org.openhab.core.types.RefreshType;
import org.openhab.core.types.StateOption;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Handles TV Control Channel Command.
* Allows to set a channel to an absolute channel number.
*
* @author Sebastian Prehn - Initial contribution
*/
@NonNullByDefault
public class TVControlChannel extends BaseChannelHandler<ChannelInfo> {
private final Logger logger = LoggerFactory.getLogger(TVControlChannel.class);
private final Map<ThingUID, @Nullable List<ChannelInfo>> channelListCache = new HashMap<>();
private final ResponseListener<CommandConfirmation> objResponseListener = createResponseListener();
@Override
public void onDeviceReady(String channelId, LGWebOSHandler handler) {
super.onDeviceReady(channelId, handler);
handler.getSocket().getChannelList(new ResponseListener<List<ChannelInfo>>() {
@Override
public void onError(@Nullable String error) {
logger.warn("error requesting channel list: {}.", error);
}
@Override
@NonNullByDefault({})
public void onSuccess(List<ChannelInfo> channels) {
if (logger.isDebugEnabled()) {
channels.forEach(c -> logger.debug("Channel {} - {}", c.getChannelNumber(), c.getName()));
}
channelListCache.put(handler.getThing().getUID(), channels);
List<StateOption> options = new ArrayList<>();
for (ChannelInfo channel : channels) {
String name = channel.getName() == null ? "" : channel.getName();
options.add(new StateOption(channel.getId(), channel.getChannelNumber() + " - " + name));
}
handler.setOptions(channelId, options);
}
});
}
@Override
public void onDeviceRemoved(String channelId, LGWebOSHandler handler) {
super.onDeviceRemoved(channelId, handler);
channelListCache.remove(handler.getThing().getUID());
}
@Override
public void onReceiveCommand(String channelId, LGWebOSHandler handler, Command command) {
if (RefreshType.REFRESH == command) {
handler.getSocket().getCurrentChannel(createResponseListener(channelId, handler));
return;
}
final String value = command.toString();
List<ChannelInfo> channels = channelListCache.get(handler.getThing().getUID());
if (channels == null) {
logger.warn("No channel list cached for this device {}, ignoring command.",
handler.getThing().getUID().toString());
} else {
Optional<ChannelInfo> channelInfo = channels.stream()
.filter(c -> c.getId().equals(value) || c.getChannelNumber().equals(value)).findFirst();
if (channelInfo.isPresent()) {
handler.getSocket().setChannel(channelInfo.get(), objResponseListener);
} else {
logger.info("TV does not have a channel: {}.", value);
}
}
}
@Override
protected Optional<ServiceSubscription<ChannelInfo>> getSubscription(String channelId, LGWebOSHandler handler) {
return Optional.of(handler.getSocket().subscribeCurrentChannel(createResponseListener(channelId, handler)));
}
private ResponseListener<ChannelInfo> createResponseListener(String channelId, LGWebOSHandler handler) {
return new ResponseListener<ChannelInfo>() {
@Override
public void onError(@Nullable String error) {
logger.debug("Error in retrieving channel: {}.", error);
}
@Override
public void onSuccess(@Nullable ChannelInfo channelInfo) {
if (channelInfo == null) {
return;
}
handler.postUpdate(channelId, new StringType(channelInfo.getId()));
}
};
}
public List<String> reportChannels(ThingUID thingUID) {
List<String> report = new ArrayList<>();
List<ChannelInfo> channels = channelListCache.get(thingUID);
if (channels != null) {
for (ChannelInfo channel : channels) {
String name = channel.getName() == null ? "" : channel.getName();
report.add(channel.getId() + " : " + channel.getChannelNumber() + " - " + name);
}
}
return report;
}
}

View File

@@ -0,0 +1,36 @@
/**
* 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.lgwebos.internal;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.lgwebos.internal.handler.LGWebOSHandler;
import org.openhab.binding.lgwebos.internal.handler.core.CommandConfirmation;
import org.openhab.core.types.Command;
import org.openhab.core.types.RefreshType;
/**
* Handles Toast Control Command. This allows to send messages to the TV screen.
*
* @author Sebastian Prehn - Initial contribution
*/
@NonNullByDefault
public class ToastControlToast extends BaseChannelHandler<CommandConfirmation> {
@Override
public void onReceiveCommand(String channelId, LGWebOSHandler handler, Command command) {
if (RefreshType.REFRESH == command) {
return;
}
handler.getSocket().showToast(command.toString(), getDefaultResponseListener());
}
}

View File

@@ -0,0 +1,73 @@
/**
* 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.lgwebos.internal;
import java.util.Optional;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.lgwebos.internal.handler.LGWebOSHandler;
import org.openhab.binding.lgwebos.internal.handler.command.ServiceSubscription;
import org.openhab.binding.lgwebos.internal.handler.core.CommandConfirmation;
import org.openhab.binding.lgwebos.internal.handler.core.ResponseListener;
import org.openhab.core.library.types.OnOffType;
import org.openhab.core.types.Command;
import org.openhab.core.types.RefreshType;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Handles TV Control Mute Command.
*
* @author Sebastian Prehn - initial contribution
*/
@NonNullByDefault
public class VolumeControlMute extends BaseChannelHandler<Boolean> {
private final Logger logger = LoggerFactory.getLogger(VolumeControlMute.class);
private final ResponseListener<CommandConfirmation> objResponseListener = createResponseListener();
@Override
public void onReceiveCommand(String channelId, LGWebOSHandler handler, Command command) {
if (RefreshType.REFRESH == command) {
handler.getSocket().getMute(createResponseListener(channelId, handler));
} else if (OnOffType.ON == command || OnOffType.OFF == command) {
handler.getSocket().setMute(OnOffType.ON == command, objResponseListener);
} else {
logger.info("Only accept OnOffType, RefreshType. Type was {}.", command.getClass());
}
}
@Override
protected Optional<ServiceSubscription<Boolean>> getSubscription(String channelId, LGWebOSHandler handler) {
return Optional.of(handler.getSocket().subscribeMute(createResponseListener(channelId, handler)));
}
private ResponseListener<Boolean> createResponseListener(String channelId, LGWebOSHandler handler) {
return new ResponseListener<Boolean>() {
@Override
public void onError(@Nullable String error) {
logger.debug("Error in retrieving mute: {}.", error);
}
@Override
public void onSuccess(@Nullable Boolean value) {
if (value == null) {
return;
}
handler.postUpdate(channelId, OnOffType.from(value));
}
};
}
}

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.lgwebos.internal;
import java.util.Optional;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.lgwebos.internal.handler.LGWebOSHandler;
import org.openhab.binding.lgwebos.internal.handler.command.ServiceSubscription;
import org.openhab.binding.lgwebos.internal.handler.core.CommandConfirmation;
import org.openhab.binding.lgwebos.internal.handler.core.ResponseListener;
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.PercentType;
import org.openhab.core.library.types.StringType;
import org.openhab.core.types.Command;
import org.openhab.core.types.RefreshType;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Handles TV Control Volume Commands. Allows to set a volume to an absolute number or increment and decrement the
* volume. If used with On Off type commands it will mute volume when receiving OFF and unmute when receiving ON.
*
* @author Sebastian Prehn - initial contribution
*/
@NonNullByDefault
public class VolumeControlVolume extends BaseChannelHandler<Float> {
private final Logger logger = LoggerFactory.getLogger(VolumeControlVolume.class);
private final ResponseListener<CommandConfirmation> objResponseListener = createResponseListener();
@Override
public void onReceiveCommand(String channelId, LGWebOSHandler handler, Command command) {
final PercentType percent;
if (RefreshType.REFRESH == command) {
handler.getSocket().getVolume(createResponseListener(channelId, handler));
return;
}
if (command instanceof PercentType) {
percent = (PercentType) command;
} else if (command instanceof DecimalType) {
percent = new PercentType(((DecimalType) command).toBigDecimal());
} else if (command instanceof StringType) {
percent = new PercentType(((StringType) command).toString());
} else {
percent = null;
}
if (percent != null) {
handler.getSocket().setVolume(percent.floatValue() / 100.0f, objResponseListener);
} else if (IncreaseDecreaseType.INCREASE == command) {
handler.getSocket().volumeUp(objResponseListener);
} else if (IncreaseDecreaseType.DECREASE == command) {
handler.getSocket().volumeDown(objResponseListener);
} else if (OnOffType.OFF == command || OnOffType.ON == command) {
handler.getSocket().setMute(OnOffType.OFF == command, objResponseListener);
} else {
logger.info("Only accept PercentType, DecimalType, StringType, RefreshType. Type was {}.",
command.getClass());
}
}
@Override
protected Optional<ServiceSubscription<Float>> getSubscription(String channelUID, LGWebOSHandler handler) {
return Optional.of(handler.getSocket().subscribeVolume(createResponseListener(channelUID, handler)));
}
private ResponseListener<Float> createResponseListener(String channelUID, LGWebOSHandler handler) {
return new ResponseListener<Float>() {
@Override
public void onError(@Nullable String error) {
logger.debug("Error in retrieving volume: {}.", error);
}
@Override
public void onSuccess(@Nullable Float value) {
if (value != null && !Float.isNaN(value)) {
handler.postUpdate(channelUID, new PercentType(Math.round(value * 100)));
}
}
};
}
}

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.lgwebos.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 Sebastian Prehn - Modification to getMACAddress
*
*/
@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 the 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,107 @@
/**
* 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.lgwebos.internal.console;
import java.util.Arrays;
import java.util.List;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.lgwebos.internal.handler.LGWebOSHandler;
import org.openhab.core.io.console.Console;
import org.openhab.core.io.console.extensions.AbstractConsoleCommandExtension;
import org.openhab.core.io.console.extensions.ConsoleCommandExtension;
import org.openhab.core.thing.Thing;
import org.openhab.core.thing.ThingRegistry;
import org.openhab.core.thing.ThingUID;
import org.openhab.core.thing.binding.ThingHandler;
import org.osgi.service.component.annotations.Activate;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Reference;
/**
* The {@link LGWebOSCommandExtension} is responsible for handling console commands
*
* @author Laurent Garnier - Initial contribution
*/
@NonNullByDefault
@Component(service = ConsoleCommandExtension.class)
public class LGWebOSCommandExtension extends AbstractConsoleCommandExtension {
private static final String APPLICATIONS = "applications";
private static final String CHANNELS = "channels";
private static final String ACCESS_KEY = "accesskey";
private final ThingRegistry thingRegistry;
@Activate
public LGWebOSCommandExtension(final @Reference ThingRegistry thingRegistry) {
super("lgwebos", "Interact with the LG webOS binding.");
this.thingRegistry = thingRegistry;
}
@Override
public void execute(String[] args, Console console) {
if (args.length == 2) {
Thing thing = null;
try {
ThingUID thingUID = new ThingUID(args[0]);
thing = thingRegistry.get(thingUID);
} catch (IllegalArgumentException e) {
thing = null;
}
ThingHandler thingHandler = null;
LGWebOSHandler handler = null;
if (thing != null) {
thingHandler = thing.getHandler();
if (thingHandler instanceof LGWebOSHandler) {
handler = (LGWebOSHandler) thingHandler;
}
}
if (thing == null) {
console.println("Bad thing id '" + args[0] + "'");
printUsage(console);
} else if (thingHandler == null) {
console.println("No handler initialized for the thing id '" + args[0] + "'");
printUsage(console);
} else if (handler == null) {
console.println("'" + args[0] + "' is not a LG webOS thing id");
printUsage(console);
} else {
switch (args[1]) {
case APPLICATIONS:
handler.reportApplications().forEach(console::println);
break;
case CHANNELS:
handler.reportChannels().forEach(console::println);
break;
case ACCESS_KEY:
console.println("Your access key is " + handler.getKey());
break;
default:
printUsage(console);
break;
}
}
} else {
printUsage(console);
}
}
@Override
public List<String> getUsages() {
return Arrays.asList(new String[] { buildCommandUsage("<thingUID> " + APPLICATIONS, "list applications"),
buildCommandUsage("<thingUID> " + CHANNELS, "list channels"),
buildCommandUsage("<thingUID> " + ACCESS_KEY, "show the access key") });
}
}

View File

@@ -0,0 +1,80 @@
/**
* 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.lgwebos.internal.discovery;
import static org.openhab.binding.lgwebos.internal.LGWebOSBindingConstants.*;
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.Thing;
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;
/**
* This Upnp Discovery participant add the ability to auto discover LG Web OS devices on the network.
* Some users choose to not use upnp. Therefore this can only play an optional role and help discover the device and its
* ip.
*
* @author Sebastian Prehn - Initial contribution
*/
@NonNullByDefault
@Component(service = UpnpDiscoveryParticipant.class, immediate = true, configurationPid = "discovery.lgwebos.upnp")
public class LGWebOSUpnpDiscoveryParticipant implements UpnpDiscoveryParticipant {
private final Logger logger = LoggerFactory.getLogger(LGWebOSUpnpDiscoveryParticipant.class);
@Override
public Set<ThingTypeUID> getSupportedThingTypeUIDs() {
return SUPPORTED_THING_TYPES_UIDS;
}
@Override
public @Nullable DiscoveryResult createResult(RemoteDevice device) {
ThingUID thingUID = getThingUID(device);
if (thingUID == null) {
return null;
}
String modelName = device.getDetails().getModelDetails().getModelName();
if (device.getDetails().getModelDetails().getModelNumber() != null) {
modelName += " " + device.getDetails().getModelDetails().getModelNumber();
}
return DiscoveryResultBuilder.create(thingUID).withLabel(device.getDetails().getFriendlyName())
.withProperty(PROPERTY_DEVICE_ID, device.getIdentity().getUdn().getIdentifierString())
.withProperty(CONFIG_HOST, device.getIdentity().getDescriptorURL().getHost())
.withLabel(device.getDetails().getFriendlyName()).withProperty(Thing.PROPERTY_MODEL_ID, modelName)
.withProperty(Thing.PROPERTY_VENDOR, device.getDetails().getManufacturerDetails().getManufacturer())
.withRepresentationProperty(PROPERTY_DEVICE_ID).withThingType(THING_TYPE_WEBOSTV).build();
}
@Override
public @Nullable ThingUID getThingUID(RemoteDevice device) {
logger.trace("Discovered remote device {}", device);
if (device.findService(UPNP_SERVICE_TYPE) != null) {
logger.debug("Found LG WebOS TV: {}", device);
return new ThingUID(THING_TYPE_WEBOSTV, device.getIdentity().getUdn().getIdentifierString());
}
return null;
}
}

View File

@@ -0,0 +1,58 @@
/**
* 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.lgwebos.internal.handler;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
/**
* The {@link LGWebOSConfiguration} class contains the thing configuration
* parameters for LGWebOS devices
*
* @author Sebastian Prehn - Initial contribution
*/
@NonNullByDefault
public class LGWebOSConfiguration {
@Nullable
String host; // name has to match LGWebOSBindingConstants.CONFIG_HOST
int port = 3000; // 3001 for TLS
@Nullable
String key; // name has to match LGWebOSBindingConstants.CONFIG_KEY
@Nullable
String macAddress; // name has to match LGWebOSBindingConstants.CONFIG_MAC_ADDRESS
public String getHost() {
String h = host;
return h == null ? "" : h;
}
public String getKey() {
String k = key;
return k == null ? "" : k;
}
public int getPort() {
return port;
}
public String getMacAddress() {
String m = macAddress;
return m == null ? "" : m;
}
@Override
public String toString() {
return "WebOSConfiguration [host=" + host + ", port=" + port + ", key.length=" + getKey().length()
+ ", macAddress=" + macAddress + "]";
}
}

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.lgwebos.internal.handler;
import static org.openhab.binding.lgwebos.internal.LGWebOSBindingConstants.*;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.eclipse.jetty.websocket.client.WebSocketClient;
import org.openhab.binding.lgwebos.action.LGWebOSActions;
import org.openhab.binding.lgwebos.internal.ChannelHandler;
import org.openhab.binding.lgwebos.internal.LGWebOSBindingConstants;
import org.openhab.binding.lgwebos.internal.LGWebOSStateDescriptionOptionProvider;
import org.openhab.binding.lgwebos.internal.LauncherApplication;
import org.openhab.binding.lgwebos.internal.MediaControlPlayer;
import org.openhab.binding.lgwebos.internal.MediaControlStop;
import org.openhab.binding.lgwebos.internal.PowerControlPower;
import org.openhab.binding.lgwebos.internal.RCButtonControl;
import org.openhab.binding.lgwebos.internal.TVControlChannel;
import org.openhab.binding.lgwebos.internal.ToastControlToast;
import org.openhab.binding.lgwebos.internal.VolumeControlMute;
import org.openhab.binding.lgwebos.internal.VolumeControlVolume;
import org.openhab.binding.lgwebos.internal.WakeOnLanUtility;
import org.openhab.binding.lgwebos.internal.handler.LGWebOSTVSocket.WebOSTVSocketListener;
import org.openhab.binding.lgwebos.internal.handler.core.AppInfo;
import org.openhab.binding.lgwebos.internal.handler.core.ResponseListener;
import org.openhab.core.config.core.Configuration;
import org.openhab.core.library.types.OnOffType;
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.binding.BaseThingHandler;
import org.openhab.core.thing.binding.ThingHandlerService;
import org.openhab.core.types.Command;
import org.openhab.core.types.State;
import org.openhab.core.types.StateOption;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The {@link LGWebOSHandler} is responsible for handling commands, which are
* sent to one of the channels.
*
* @author Sebastian Prehn - initial contribution
*/
@NonNullByDefault
public class LGWebOSHandler extends BaseThingHandler
implements LGWebOSTVSocket.ConfigProvider, WebOSTVSocketListener, PowerControlPower.ConfigProvider {
/*
* constants for device polling
*/
private static final int RECONNECT_INTERVAL_SECONDS = 10;
private static final int RECONNECT_START_UP_DELAY_SECONDS = 0;
private static final int CHANNEL_SUBSCRIPTION_DELAY_SECONDS = 1;
private static final String APP_ID_LIVETV = "com.webos.app.livetv";
/*
* error messages
*/
private static final String MSG_MISSING_PARAM = "Missing parameter \"host\"";
private final Logger logger = LoggerFactory.getLogger(LGWebOSHandler.class);
// ChannelID to CommandHandler Map
private final Map<String, ChannelHandler> channelHandlers;
private final LauncherApplication appLauncher = new LauncherApplication();
private final WebSocketClient webSocketClient;
private final LGWebOSStateDescriptionOptionProvider stateDescriptionProvider;
private @Nullable LGWebOSTVSocket socket;
private @Nullable ScheduledFuture<?> reconnectJob;
private @Nullable ScheduledFuture<?> keepAliveJob;
private @Nullable ScheduledFuture<?> channelSubscriptionJob;
private @Nullable LGWebOSConfiguration config;
public LGWebOSHandler(Thing thing, WebSocketClient webSocketClient,
LGWebOSStateDescriptionOptionProvider stateDescriptionProvider) {
super(thing);
this.webSocketClient = webSocketClient;
this.stateDescriptionProvider = stateDescriptionProvider;
Map<String, ChannelHandler> handlers = new HashMap<>();
handlers.put(CHANNEL_VOLUME, new VolumeControlVolume());
handlers.put(CHANNEL_POWER, new PowerControlPower(this, scheduler));
handlers.put(CHANNEL_MUTE, new VolumeControlMute());
handlers.put(CHANNEL_CHANNEL, new TVControlChannel());
handlers.put(CHANNEL_APP_LAUNCHER, appLauncher);
handlers.put(CHANNEL_MEDIA_STOP, new MediaControlStop());
handlers.put(CHANNEL_TOAST, new ToastControlToast());
handlers.put(CHANNEL_MEDIA_PLAYER, new MediaControlPlayer());
handlers.put(CHANNEL_RCBUTTON, new RCButtonControl());
channelHandlers = Collections.unmodifiableMap(handlers);
}
private LGWebOSConfiguration getLGWebOSConfig() {
LGWebOSConfiguration c = config;
if (c == null) {
c = getConfigAs(LGWebOSConfiguration.class);
config = c;
}
return c;
}
@Override
public void initialize() {
logger.debug("Initializing handler for thing {}", getThing().getUID());
LGWebOSConfiguration c = getLGWebOSConfig();
logger.trace("Handler initialized with config {}", c);
String host = c.getHost();
if (host.isEmpty()) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, MSG_MISSING_PARAM);
return;
}
LGWebOSTVSocket s = new LGWebOSTVSocket(webSocketClient, this, host, c.getPort(), scheduler);
s.setListener(this);
socket = s;
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.NONE, "TV is off");
startReconnectJob();
}
@Override
public void dispose() {
logger.debug("Disposing handler for thing {}", getThing().getUID());
stopKeepAliveJob();
stopReconnectJob();
stopChannelSubscriptionJob();
LGWebOSTVSocket s = socket;
if (s != null) {
s.setListener(null);
s.disconnect();
}
socket = null;
config = null; // ensure config gets actually refreshed during re-initialization
super.dispose();
}
private void startReconnectJob() {
ScheduledFuture<?> job = reconnectJob;
if (job == null || job.isCancelled()) {
reconnectJob = scheduler.scheduleWithFixedDelay(() -> {
getSocket().disconnect();
getSocket().connect();
}, RECONNECT_START_UP_DELAY_SECONDS, RECONNECT_INTERVAL_SECONDS, TimeUnit.SECONDS);
}
}
private void stopReconnectJob() {
ScheduledFuture<?> job = reconnectJob;
if (job != null && !job.isCancelled()) {
job.cancel(true);
}
reconnectJob = null;
}
/**
* Keep alive ensures that the web socket connection is used and does not time out.
*/
private void startKeepAliveJob() {
ScheduledFuture<?> job = keepAliveJob;
if (job == null || job.isCancelled()) {
// half of idle time out setting
long keepAliveInterval = this.webSocketClient.getMaxIdleTimeout() / 2;
// it is irrelevant which service is queried. Only need to send some packets over the wire
keepAliveJob = scheduler
.scheduleWithFixedDelay(() -> getSocket().getRunningApp(new ResponseListener<AppInfo>() {
@Override
public void onSuccess(AppInfo responseObject) {
// ignore - actual response is not relevant here
}
@Override
public void onError(String message) {
// ignore
}
}), keepAliveInterval, keepAliveInterval, TimeUnit.MILLISECONDS);
}
}
private void stopKeepAliveJob() {
ScheduledFuture<?> job = keepAliveJob;
if (job != null && !job.isCancelled()) {
job.cancel(true);
}
keepAliveJob = null;
}
public LGWebOSTVSocket getSocket() {
LGWebOSTVSocket s = this.socket;
if (s == null) {
throw new IllegalStateException("Component called before it was initialized or already disposed.");
}
return s;
}
public LauncherApplication getLauncherApplication() {
return appLauncher;
}
@Override
public void handleCommand(ChannelUID channelUID, Command command) {
logger.debug("handleCommand({},{})", channelUID, command);
ChannelHandler handler = channelHandlers.get(channelUID.getId());
if (handler == null) {
logger.warn(
"Unable to handle command {}. No handler found for channel {}. This must not happen. Please report as a bug.",
command, channelUID);
return;
}
handler.onReceiveCommand(channelUID.getId(), this, command);
}
@Override
public String getMacAddress() {
return getLGWebOSConfig().getMacAddress();
}
@Override
public String getKey() {
return getLGWebOSConfig().getKey();
}
@Override
public void storeKey(@Nullable String key) {
if (!getKey().equals(key)) {
logger.debug("Store new access Key in the thing configuration");
// store it current configuration and avoiding complete re-initialization via handleConfigurationUpdate
getLGWebOSConfig().key = key;
// persist the configuration change
Configuration configuration = editConfiguration();
configuration.put(LGWebOSBindingConstants.CONFIG_KEY, key);
updateConfiguration(configuration);
}
}
@Override
public void storeProperties(Map<String, String> properties) {
logger.debug("storeProperties {}", properties);
Map<String, String> map = editProperties();
map.putAll(properties);
updateProperties(map);
}
@Override
public void onStateChanged(LGWebOSTVSocket.State state) {
switch (state) {
case DISCONNECTING:
postUpdate(CHANNEL_POWER, OnOffType.OFF);
break;
case DISCONNECTED:
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.NONE, "TV is off");
channelHandlers.forEach((k, v) -> {
v.onDeviceRemoved(k, this);
v.removeAnySubscription(this);
});
stopKeepAliveJob();
startReconnectJob();
break;
case CONNECTING:
stopReconnectJob();
break;
case REGISTERING:
updateStatus(ThingStatus.ONLINE, ThingStatusDetail.NONE,
"Registering - You may need to confirm pairing on TV.");
findMacAddress();
break;
case REGISTERED:
startKeepAliveJob();
updateStatus(ThingStatus.ONLINE, ThingStatusDetail.NONE, "Connected");
channelHandlers.forEach((k, v) -> {
// refresh subscriptions except on channel, which can only be subscribe in livetv app. see
// postUpdate method
if (!CHANNEL_CHANNEL.equals(k)) {
v.refreshSubscription(k, this);
}
v.onDeviceReady(k, this);
});
break;
}
}
@Override
public void onError(String error) {
logger.debug("Connection failed - error: {}", error);
switch (getSocket().getState()) {
case DISCONNECTING:
case DISCONNECTED:
break;
case CONNECTING:
case REGISTERING:
case REGISTERED:
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, "Connection Failed: " + error);
break;
}
}
public void setOptions(String channelId, List<StateOption> options) {
logger.debug("setOptions channelId={} options.size()={}", channelId, options.size());
stateDescriptionProvider.setStateOptions(new ChannelUID(getThing().getUID(), channelId), options);
}
public void postUpdate(String channelId, State state) {
if (isLinked(channelId)) {
updateState(channelId, state);
}
// channel subscription only works when livetv app is started,
// therefore we need to slightly delay the subscription
if (CHANNEL_APP_LAUNCHER.equals(channelId)) {
if (APP_ID_LIVETV.equals(state.toString())) {
scheduleChannelSubscriptionJob();
} else {
stopChannelSubscriptionJob();
}
}
}
private void scheduleChannelSubscriptionJob() {
ScheduledFuture<?> job = channelSubscriptionJob;
if (job == null || job.isCancelled()) {
logger.debug("Schedule channel subscription job");
channelSubscriptionJob = scheduler.schedule(
() -> channelHandlers.get(CHANNEL_CHANNEL).refreshSubscription(CHANNEL_CHANNEL, this),
CHANNEL_SUBSCRIPTION_DELAY_SECONDS, TimeUnit.SECONDS);
}
}
private void stopChannelSubscriptionJob() {
ScheduledFuture<?> job = channelSubscriptionJob;
if (job != null && !job.isCancelled()) {
logger.debug("Stop channel subscription job");
job.cancel(true);
}
channelSubscriptionJob = null;
}
@Override
public Collection<Class<? extends ThingHandlerService>> getServices() {
return Collections.singleton(LGWebOSActions.class);
}
/**
* Make a best effort to automatically detect the MAC address of the TV.
* If this does not work automatically, users can still set it manually in the Thing config.
*/
private void findMacAddress() {
LGWebOSConfiguration c = getLGWebOSConfig();
String host = c.getHost();
if (!host.isEmpty()) {
try {
// validate host, so that no command can be injected
String macAddress = WakeOnLanUtility.getMACAddress(InetAddress.getByName(host).getHostAddress());
if (macAddress != null && !macAddress.equals(c.macAddress)) {
c.macAddress = macAddress;
// persist the configuration change
Configuration configuration = editConfiguration();
configuration.put(LGWebOSBindingConstants.CONFIG_MAC_ADDRESS, macAddress);
updateConfiguration(configuration);
}
} catch (UnknownHostException e) {
logger.debug("Unable to determine MAC address: {}", e.getMessage());
}
}
}
public List<String> reportApplications() {
return appLauncher.reportApplications(getThing().getUID());
}
public List<String> reportChannels() {
return ((TVControlChannel) channelHandlers.get(CHANNEL_CHANNEL)).reportChannels(getThing().getUID());
}
}

View File

@@ -0,0 +1,201 @@
/**
* 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
*/
/*
* WebOSTVKeyboardInput
* Connect SDK
*
* Copyright (c) 2014 LG Electronics.
* Created by Hyun Kook Khang on 19 Jan 2014
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.openhab.binding.lgwebos.internal.handler;
import java.util.ArrayList;
import java.util.List;
import org.openhab.binding.lgwebos.internal.handler.command.ServiceCommand;
import org.openhab.binding.lgwebos.internal.handler.command.ServiceSubscription;
import org.openhab.binding.lgwebos.internal.handler.core.ResponseListener;
import org.openhab.binding.lgwebos.internal.handler.core.TextInputStatusInfo;
import com.google.gson.JsonObject;
/**
* {@link LGWebOSTVKeyboardInput} handles WebOSTV keyboard api.
*
* @author Hyun Kook Khang - Connect SDK initial contribution
* @author Sebastian Prehn - Adoption for openHAB
*/
public class LGWebOSTVKeyboardInput {
private LGWebOSTVSocket service;
private boolean waiting;
private final List<String> toSend;
private static final String KEYBOARD_INPUT = "ssap://com.webos.service.ime/registerRemoteKeyboard";
private static final String ENTER = "ENTER";
private static final String DELETE = "DELETE";
public LGWebOSTVKeyboardInput(LGWebOSTVSocket service) {
this.service = service;
waiting = false;
toSend = new ArrayList<>();
}
public void sendText(String input) {
toSend.add(input);
if (!waiting) { // TODO: use a latch,and send in any case
sendData();
}
}
public void sendEnter() {
sendText(ENTER);
}
public void sendDel() {
if (toSend.isEmpty()) {
toSend.add(DELETE);
if (!waiting) {
sendData();
}
} else {
toSend.remove(toSend.size() - 1);
}
}
private void sendData() {
waiting = true;
String uri;
String typeTest = toSend.get(0);
JsonObject payload = new JsonObject();
if (typeTest.equals(ENTER)) {
toSend.remove(0);
uri = "ssap://com.webos.service.ime/sendEnterKey";
} else if (typeTest.equals(DELETE)) {
uri = "ssap://com.webos.service.ime/deleteCharacters";
int count = 0;
while (!toSend.isEmpty() && toSend.get(0).equals(DELETE)) {
toSend.remove(0);
count++;
}
payload.addProperty("count", count);
} else {
uri = "ssap://com.webos.service.ime/insertText";
StringBuilder sb = new StringBuilder();
while (!toSend.isEmpty() && !(toSend.get(0).equals(DELETE) || toSend.get(0).equals(ENTER))) {
String text = toSend.get(0);
sb.append(text);
toSend.remove(0);
}
payload.addProperty("text", sb.toString());
payload.addProperty("replace", 0);
}
ResponseListener<JsonObject> responseListener = new ResponseListener<JsonObject>() {
@Override
public void onSuccess(JsonObject response) {
waiting = false;
if (!toSend.isEmpty()) {
sendData();
}
}
@Override
public void onError(String error) {
waiting = false;
if (!toSend.isEmpty()) {
sendData();
}
}
};
ServiceCommand<JsonObject> request = new ServiceCommand<>(uri, payload, x -> x, responseListener);
service.sendCommand(request);
}
public ServiceSubscription<TextInputStatusInfo> connect(final ResponseListener<TextInputStatusInfo> listener) {
ServiceSubscription<TextInputStatusInfo> subscription = new ServiceSubscription<>(KEYBOARD_INPUT, null,
rawData -> parseRawKeyboardData(rawData), listener);
service.sendCommand(subscription);
return subscription;
}
private TextInputStatusInfo parseRawKeyboardData(JsonObject rawData) {
boolean focused = false;
String contentType = null;
boolean predictionEnabled = false;
boolean correctionEnabled = false;
boolean autoCapitalization = false;
boolean hiddenText = false;
boolean focusChanged = false;
TextInputStatusInfo keyboard = new TextInputStatusInfo();
keyboard.setRawData(rawData);
if (rawData.has("currentWidget")) {
JsonObject currentWidget = (JsonObject) rawData.get("currentWidget");
focused = currentWidget.get("focus").getAsBoolean();
if (currentWidget.has("contentType")) {
contentType = currentWidget.get("contentType").getAsString();
}
if (currentWidget.has("predictionEnabled")) {
predictionEnabled = currentWidget.get("predictionEnabled").getAsBoolean();
}
if (currentWidget.has("correctionEnabled")) {
correctionEnabled = currentWidget.get("correctionEnabled").getAsBoolean();
}
if (currentWidget.has("autoCapitalization")) {
autoCapitalization = currentWidget.get("autoCapitalization").getAsBoolean();
}
if (currentWidget.has("hiddenText")) {
hiddenText = currentWidget.get("hiddenText").getAsBoolean();
}
}
if (rawData.has("focusChanged")) {
focusChanged = rawData.get("focusChanged").getAsBoolean();
}
keyboard.setFocused(focused);
keyboard.setContentType(contentType);
keyboard.setPredictionEnabled(predictionEnabled);
keyboard.setCorrectionEnabled(correctionEnabled);
keyboard.setAutoCapitalization(autoCapitalization);
keyboard.setHiddenText(hiddenText);
keyboard.setFocusChanged(focusChanged);
return keyboard;
}
}

View File

@@ -0,0 +1,205 @@
/**
* 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.lgwebos.internal.handler;
import java.io.IOException;
import java.net.URI;
import java.util.Optional;
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.annotations.OnWebSocketClose;
import org.eclipse.jetty.websocket.api.annotations.OnWebSocketConnect;
import org.eclipse.jetty.websocket.api.annotations.OnWebSocketError;
import org.eclipse.jetty.websocket.api.annotations.OnWebSocketMessage;
import org.eclipse.jetty.websocket.api.annotations.WebSocket;
import org.eclipse.jetty.websocket.client.WebSocketClient;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* WebSocket implementation to connect to WebOSTV mouse api.
*
* @author Sebastian Prehn - Initial contribution
*
*/
@WebSocket()
@NonNullByDefault
public class LGWebOSTVMouseSocket {
private final Logger logger = LoggerFactory.getLogger(LGWebOSTVMouseSocket.class);
public enum State {
DISCONNECTED,
CONNECTING,
CONNECTED,
DISCONNECTING
}
public enum ButtonType {
HOME,
BACK,
UP,
DOWN,
LEFT,
RIGHT,
}
private State state = State.DISCONNECTED;
private final WebSocketClient client;
private @Nullable Session session;
private @Nullable WebOSTVMouseSocketListener listener;
public LGWebOSTVMouseSocket(WebSocketClient client) {
this.client = client;
}
public State getState() {
return state;
}
private void setState(State state) {
State oldState = this.state;
this.state = state;
Optional.ofNullable(this.listener).ifPresent(l -> l.onStateChanged(oldState, this.state));
}
public interface WebOSTVMouseSocketListener {
public void onStateChanged(State oldState, State newState);
public void onError(String errorMessage);
}
public void setListener(@Nullable WebOSTVMouseSocketListener listener) {
this.listener = listener;
}
public void connect(URI destUri) {
synchronized (this) {
if (state != State.DISCONNECTED) {
logger.debug("Already connecting; not trying to connect again: {}", state);
return;
}
setState(State.CONNECTING);
}
try {
this.client.connect(this, destUri);
logger.debug("Connecting to: {}", destUri);
} catch (IOException e) {
logger.warn("Unable to connect.", e);
setState(State.DISCONNECTED);
}
}
public void disconnect() {
setState(State.DISCONNECTING);
try {
Optional.ofNullable(this.session).ifPresent(s -> s.close());
} catch (Exception e) {
logger.debug("Error connecting to device.", e);
}
setState(State.DISCONNECTED);
}
@OnWebSocketClose
public void onClose(int statusCode, String reason) {
setState(State.DISCONNECTED);
logger.debug("WebSocket Closed - Code: {}, Reason: {}", statusCode, reason);
this.session = null;
}
@OnWebSocketConnect
public void onConnect(Session session) {
logger.debug("WebSocket Connected to: {}", session.getRemoteAddress().getAddress());
this.session = session;
setState(State.CONNECTED);
}
@OnWebSocketMessage
public void onMessage(String message) {
logger.debug("Message [in]: {}", message);
}
@OnWebSocketError
public void onError(Throwable cause) {
Optional.ofNullable(this.listener).ifPresent(l -> l.onError(cause.getMessage()));
logger.debug("Connection Error.", cause);
}
private void sendMessage(String msg) {
Session s = this.session;
try {
if (s != null) {
logger.debug("Message [out]: {}", msg);
s.getRemote().sendString(msg);
} else {
logger.warn("No Connection to TV, skipping [out]: {}", msg);
}
} catch (IOException e) {
logger.error("Unable to send message.", e);
}
}
public void click() {
sendMessage("type:click\n" + "\n");
}
public void button(ButtonType type) {
String keyName;
switch (type) {
case HOME:
keyName = "HOME";
break;
case BACK:
keyName = "BACK";
break;
case UP:
keyName = "UP";
break;
case DOWN:
keyName = "DOWN";
break;
case LEFT:
keyName = "LEFT";
break;
case RIGHT:
keyName = "RIGHT";
break;
default:
keyName = "NONE";
break;
}
button(keyName);
}
public void button(String keyName) {
sendMessage("type:button\n" + "name:" + keyName + "\n" + "\n");
}
public void move(double dx, double dy) {
sendMessage("type:move\n" + "dx:" + dx + "\n" + "dy:" + dy + "\n" + "down:0\n" + "\n");
}
public void move(double dx, double dy, boolean drag) {
sendMessage("type:move\n" + "dx:" + dx + "\n" + "dy:" + dy + "\n" + "down:" + (drag ? 1 : 0) + "\n" + "\n");
}
public void scroll(double dx, double dy) {
sendMessage("type:scroll\n" + "dx:" + dx + "\n" + "dy:" + dy + "\n" + "\n");
}
}

View File

@@ -0,0 +1,951 @@
/**
* 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
*/
/*
* This file is based on:
*
* WebOSTVService
* Connect SDK
*
* Copyright (c) 2014 LG Electronics.
* Created by Hyun Kook Khang on 19 Jan 2014
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.openhab.binding.lgwebos.internal.handler;
import static org.openhab.binding.lgwebos.internal.LGWebOSBindingConstants.*;
import java.io.IOException;
import java.net.ConnectException;
import java.net.SocketTimeoutException;
import java.net.URI;
import java.net.URISyntaxException;
import java.time.Instant;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Optional;
import java.util.concurrent.Future;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import java.util.function.Consumer;
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.annotations.OnWebSocketClose;
import org.eclipse.jetty.websocket.api.annotations.OnWebSocketConnect;
import org.eclipse.jetty.websocket.api.annotations.OnWebSocketError;
import org.eclipse.jetty.websocket.api.annotations.OnWebSocketMessage;
import org.eclipse.jetty.websocket.api.annotations.WebSocket;
import org.eclipse.jetty.websocket.client.WebSocketClient;
import org.openhab.binding.lgwebos.internal.handler.LGWebOSTVMouseSocket.WebOSTVMouseSocketListener;
import org.openhab.binding.lgwebos.internal.handler.command.ServiceCommand;
import org.openhab.binding.lgwebos.internal.handler.command.ServiceSubscription;
import org.openhab.binding.lgwebos.internal.handler.core.AppInfo;
import org.openhab.binding.lgwebos.internal.handler.core.ChannelInfo;
import org.openhab.binding.lgwebos.internal.handler.core.CommandConfirmation;
import org.openhab.binding.lgwebos.internal.handler.core.LaunchSession;
import org.openhab.binding.lgwebos.internal.handler.core.LaunchSession.LaunchSessionType;
import org.openhab.binding.lgwebos.internal.handler.core.Response;
import org.openhab.binding.lgwebos.internal.handler.core.ResponseListener;
import org.openhab.binding.lgwebos.internal.handler.core.TextInputStatusInfo;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.reflect.TypeToken;
/**
* WebSocket to handle the communication with WebOS device.
*
* @author Hyun Kook Khang - Initial contribution
* @author Sebastian Prehn - Web Socket implementation and adoption for openHAB
*/
@WebSocket()
@NonNullByDefault
public class LGWebOSTVSocket {
private static final String FOREGROUND_APP = "ssap://com.webos.applicationManager/getForegroundAppInfo";
// private static final String APP_STATUS = "ssap://com.webos.service.appstatus/getAppStatus";
// private static final String APP_STATE = "ssap://system.launcher/getAppState";
private static final String VOLUME = "ssap://audio/getVolume";
private static final String MUTE = "ssap://audio/getMute";
// private static final String VOLUME_STATUS = "ssap://audio/getStatus";
private static final String CHANNEL_LIST = "ssap://tv/getChannelList";
private static final String CHANNEL = "ssap://tv/getCurrentChannel";
// private static final String PROGRAM = "ssap://tv/getChannelProgramInfo";
// private static final String CURRENT_PROGRAM = "ssap://tv/getChannelCurrentProgramInfo";
// private static final String THREED_STATUS = "ssap://com.webos.service.tv.display/get3DStatus";
private static final int DISCONNECTING_DELAY_SECONDS = 2;
private static final Gson GSON = new GsonBuilder().create();
private final Logger logger = LoggerFactory.getLogger(LGWebOSTVSocket.class);
private final ConfigProvider config;
private final WebSocketClient client;
private final URI destUri;
private final LGWebOSTVKeyboardInput keyboardInput;
private final ScheduledExecutorService scheduler;
public enum State {
DISCONNECTING,
DISCONNECTED,
CONNECTING,
REGISTERING,
REGISTERED
}
private State state = State.DISCONNECTED;
private @Nullable Session session;
private @Nullable Future<?> sessionFuture;
private @Nullable WebOSTVSocketListener listener;
/**
* Requests to which we are awaiting response.
*/
private HashMap<Integer, ServiceCommand<?>> requests = new HashMap<>();
private int nextRequestId = 0;
private @Nullable ScheduledFuture<?> disconnectingJob;
public LGWebOSTVSocket(WebSocketClient client, ConfigProvider config, String host, int port,
ScheduledExecutorService scheduler) {
this.config = config;
this.client = client;
this.keyboardInput = new LGWebOSTVKeyboardInput(this);
try {
this.destUri = new URI("ws://" + host + ":" + port);
} catch (URISyntaxException e) {
throw new IllegalArgumentException("IP address or hostname provided is invalid: " + host);
}
this.scheduler = scheduler;
}
public State getState() {
return state;
}
private void setState(State state) {
logger.debug("setState new {} - current {}", state, this.state);
State oldState = this.state;
if (oldState != state) {
this.state = state;
Optional.ofNullable(this.listener).ifPresent(l -> l.onStateChanged(this.state));
}
}
public void setListener(@Nullable WebOSTVSocketListener listener) {
this.listener = listener;
}
public void clearRequests() {
requests.clear();
}
public void connect() {
try {
sessionFuture = this.client.connect(this, this.destUri);
logger.debug("Connecting to: {}", this.destUri);
} catch (IOException e) {
logger.debug("Unable to connect.", e);
}
}
public void disconnect() {
Optional.ofNullable(this.session).ifPresent(s -> s.close());
Future<?> future = sessionFuture;
if (future != null && !future.isDone()) {
future.cancel(true);
}
stopDisconnectingJob();
setState(State.DISCONNECTED);
}
private void disconnecting() {
logger.debug("disconnecting");
if (state == State.REGISTERED) {
setState(State.DISCONNECTING);
}
}
private void scheduleDisconectingJob() {
ScheduledFuture<?> job = disconnectingJob;
if (job == null || job.isCancelled()) {
logger.debug("Schedule disconecting job");
disconnectingJob = scheduler.schedule(this::disconnecting, DISCONNECTING_DELAY_SECONDS, TimeUnit.SECONDS);
}
}
private void stopDisconnectingJob() {
ScheduledFuture<?> job = disconnectingJob;
if (job != null && !job.isCancelled()) {
logger.debug("Stop disconnecting job");
job.cancel(true);
}
disconnectingJob = null;
}
/*
* WebSocket Callbacks
*/
@OnWebSocketConnect
public void onConnect(Session session) {
logger.debug("WebSocket Connected to: {}", session.getRemoteAddress().getAddress());
this.session = session;
sendHello();
}
@OnWebSocketError
public void onError(Throwable cause) {
logger.trace("Connection Error", cause);
if (cause instanceof SocketTimeoutException && "Connect Timeout".equals(cause.getMessage())) {
// this is expected during connection attempts while TV is off
setState(State.DISCONNECTED);
return;
}
if (cause instanceof ConnectException && "Connection refused".equals(cause.getMessage())) {
// this is expected during TV startup or shutdown
return;
}
Optional.ofNullable(this.listener).ifPresent(l -> l.onError(cause.getMessage()));
}
@OnWebSocketClose
public void onClose(int statusCode, String reason) {
logger.debug("WebSocket Closed - Code: {}, Reason: {}", statusCode, reason);
this.requests.clear();
this.session = null;
setState(State.DISCONNECTED);
}
/*
* WebOS WebSocket API specific Communication
*/
void sendHello() {
setState(State.CONNECTING);
JsonObject packet = new JsonObject();
packet.addProperty("id", nextRequestId());
packet.addProperty("type", "hello");
JsonObject payload = new JsonObject();
payload.addProperty("appId", "org.openhab");
payload.addProperty("appName", "openHAB");
payload.addProperty("appRegion", Locale.getDefault().getDisplayCountry());
packet.add("payload", payload);
// the hello response will not contain id, therefore not registering in requests
sendMessage(packet);
}
void sendRegister() {
setState(State.REGISTERING);
JsonObject packet = new JsonObject();
int id = nextRequestId();
packet.addProperty("id", id);
packet.addProperty("type", "register");
JsonObject manifest = new JsonObject();
manifest.addProperty("manifestVersion", 1);
String[] permissions = { "LAUNCH", "LAUNCH_WEBAPP", "APP_TO_APP", "CONTROL_AUDIO",
"CONTROL_INPUT_MEDIA_PLAYBACK", "CONTROL_POWER", "READ_INSTALLED_APPS", "CONTROL_DISPLAY",
"CONTROL_INPUT_JOYSTICK", "CONTROL_INPUT_MEDIA_RECORDING", "CONTROL_INPUT_TV", "READ_INPUT_DEVICE_LIST",
"READ_NETWORK_STATE", "READ_TV_CHANNEL_LIST", "WRITE_NOTIFICATION_TOAST", "CONTROL_INPUT_TEXT",
"CONTROL_MOUSE_AND_KEYBOARD", "READ_CURRENT_CHANNEL", "READ_RUNNING_APPS" };
manifest.add("permissions", GSON.toJsonTree(permissions));
JsonObject payload = new JsonObject();
String key = config.getKey();
if (!key.isEmpty()) {
payload.addProperty("client-key", key);
}
payload.addProperty("pairingType", "PROMPT"); // PIN, COMBINED
payload.add("manifest", manifest);
packet.add("payload", payload);
ResponseListener<JsonObject> dummyListener = new ResponseListener<JsonObject>() {
@Override
public void onSuccess(@Nullable JsonObject payload) {
// Noting to do here. TV shows PROMPT dialog.
// Waiting for message of type error or registered
}
@Override
public void onError(String message) {
logger.debug("Registration failed with message: {}", message);
disconnect();
}
};
this.requests.put(id, new ServiceSubscription<>("dummy", payload, x -> x, dummyListener));
sendMessage(packet, !key.isEmpty());
}
private int nextRequestId() {
int requestId;
do {
requestId = nextRequestId++;
} while (requests.containsKey(requestId));
return requestId;
}
public void sendCommand(ServiceCommand<?> command) {
switch (state) {
case REGISTERED:
int requestId = nextRequestId();
requests.put(requestId, command);
JsonObject packet = new JsonObject();
packet.addProperty("type", command.getType());
packet.addProperty("id", requestId);
packet.addProperty("uri", command.getTarget());
JsonElement payload = command.getPayload();
if (payload != null) {
packet.add("payload", payload);
}
this.sendMessage(packet);
break;
case CONNECTING:
case REGISTERING:
case DISCONNECTING:
case DISCONNECTED:
logger.debug("Skipping {} command {} for {} in state {}", command.getType(), command,
command.getTarget(), state);
break;
}
}
public void unsubscribe(ServiceSubscription<?> subscription) {
Optional<Entry<Integer, ServiceCommand<?>>> entry = this.requests.entrySet().stream()
.filter(e -> e.getValue().equals(subscription)).findFirst();
if (entry.isPresent()) {
int requestId = entry.get().getKey();
this.requests.remove(requestId);
JsonObject packet = new JsonObject();
packet.addProperty("type", "unsubscribe");
packet.addProperty("id", requestId);
sendMessage(packet);
}
}
private void sendMessage(JsonObject json) {
sendMessage(json, false);
}
private void sendMessage(JsonObject json, boolean checkKey) {
String msg = GSON.toJson(json);
Session s = this.session;
try {
if (s != null) {
if (logger.isTraceEnabled()) {
logger.trace("Message [out]: {}", checkKey ? GSON.toJson(maskKeyInJson(json)) : msg);
}
s.getRemote().sendString(msg);
} else {
logger.warn("No Connection to TV, skipping [out]: {}",
checkKey ? GSON.toJson(maskKeyInJson(json)) : msg);
}
} catch (IOException e) {
logger.warn("Unable to send message.", e);
}
}
private JsonObject maskKeyInJson(JsonObject json) {
if (json.has("payload") && json.getAsJsonObject("payload").has("client-key")) {
JsonObject jsonCopy = json.deepCopy();
JsonObject payload = jsonCopy.getAsJsonObject("payload");
payload.remove("client-key");
payload.addProperty("client-key", "***");
return jsonCopy;
}
return json;
}
@OnWebSocketMessage
public void onMessage(String message) {
Response response = GSON.fromJson(message, Response.class);
JsonElement payload = response.getPayload();
JsonObject jsonPayload = payload == null ? null : payload.getAsJsonObject();
String messageToLog = (jsonPayload != null && jsonPayload.has("client-key")) ? "***" : message;
logger.trace("Message [in]: {}", messageToLog);
ServiceCommand<?> request = null;
if (response.getId() != null) {
request = requests.get(response.getId());
if (request == null) {
logger.warn("Received a response with id {}, for which no request was found. This should not happen.",
response.getId());
} else {
// for subscriptions we want to keep the original
// message, so that we have a reference to the response listener
if (!(request instanceof ServiceSubscription<?>)) {
requests.remove(response.getId());
}
}
}
switch (response.getType()) {
case "response":
if (request == null) {
logger.debug("No matching request found for response message: {}", messageToLog);
break;
}
if (payload == null) {
logger.debug("No payload in response message: {}", messageToLog);
break;
}
try {
request.processResponse(jsonPayload);
} catch (RuntimeException ex) {
// An uncaught runtime exception in @OnWebSocketMessage annotated method will cause the web socket
// implementation to call @OnWebSocketError callback in which we would reset the connection.
// Users have the ability to create miss-configurations in which IllegalArgumentException could be
// thrown
logger.warn("Error while processing message: {} - in response to request: {} - Error Message: {}",
messageToLog, request, ex.getMessage());
}
break;
case "error":
logger.debug("Error: {}", messageToLog);
if (request == null) {
logger.warn("No matching request found for error message: {}", messageToLog);
break;
}
if (payload == null) {
logger.warn("No payload in error message: {}", messageToLog);
break;
}
try {
request.processError(response.getError());
} catch (RuntimeException ex) {
// An uncaught runtime exception in @OnWebSocketMessage annotated method will cause the web socket
// implementation to call @OnWebSocketError callback in which we would reset the connection.
// Users have the ability to create miss-configurations in which IllegalArgumentException could be
// thrown
logger.warn("Error while processing error: {} - in response to request: {} - Error Message: {}",
messageToLog, request, ex.getMessage());
}
break;
case "hello":
if (state != State.CONNECTING) {
logger.debug("Skipping response {}, not in CONNECTING state, state was {}", messageToLog, state);
break;
}
if (jsonPayload == null) {
logger.warn("No payload in error message: {}", messageToLog);
break;
}
Map<String, String> map = new HashMap<>();
map.put(PROPERTY_DEVICE_OS, jsonPayload.get("deviceOS").getAsString());
map.put(PROPERTY_DEVICE_OS_VERSION, jsonPayload.get("deviceOSVersion").getAsString());
map.put(PROPERTY_DEVICE_OS_RELEASE_VERSION, jsonPayload.get("deviceOSReleaseVersion").getAsString());
map.put(PROPERTY_LAST_CONNECTED, Instant.now().toString());
config.storeProperties(map);
sendRegister();
break;
case "registered":
if (state != State.REGISTERING) {
logger.debug("Skipping response {}, not in REGISTERING state, state was {}", messageToLog, state);
break;
}
if (jsonPayload == null) {
logger.warn("No payload in registered message: {}", messageToLog);
break;
}
this.requests.remove(response.getId());
config.storeKey(jsonPayload.get("client-key").getAsString());
setState(State.REGISTERED);
break;
}
}
public interface WebOSTVSocketListener {
public void onStateChanged(State state);
public void onError(String errorMessage);
}
public ServiceSubscription<Boolean> subscribeMute(ResponseListener<Boolean> listener) {
ServiceSubscription<Boolean> request = new ServiceSubscription<>(MUTE, null,
(jsonObj) -> jsonObj.get("mute").getAsBoolean(), listener);
sendCommand(request);
return request;
}
public ServiceCommand<Boolean> getMute(ResponseListener<Boolean> listener) {
ServiceCommand<Boolean> request = new ServiceCommand<>(MUTE, null,
(jsonObj) -> jsonObj.get("mute").getAsBoolean(), listener);
sendCommand(request);
return request;
}
public ServiceSubscription<Float> subscribeVolume(ResponseListener<Float> listener) {
ServiceSubscription<Float> request = new ServiceSubscription<>(VOLUME, null,
jsonObj -> jsonObj.get("volume").getAsInt() >= 0 ? (float) (jsonObj.get("volume").getAsInt() / 100.0)
: Float.NaN,
listener);
sendCommand(request);
return request;
}
public ServiceCommand<Float> getVolume(ResponseListener<Float> listener) {
ServiceCommand<Float> request = new ServiceCommand<>(VOLUME, null,
jsonObj -> jsonObj.get("volume").getAsInt() >= 0 ? (float) (jsonObj.get("volume").getAsInt() / 100.0)
: Float.NaN,
listener);
sendCommand(request);
return request;
}
public void setMute(boolean isMute, ResponseListener<CommandConfirmation> listener) {
String uri = "ssap://audio/setMute";
JsonObject payload = new JsonObject();
payload.addProperty("mute", isMute);
ServiceCommand<CommandConfirmation> request = new ServiceCommand<>(uri, payload,
x -> GSON.fromJson(x, CommandConfirmation.class), listener);
sendCommand(request);
}
public void setVolume(float volume, ResponseListener<CommandConfirmation> listener) {
String uri = "ssap://audio/setVolume";
JsonObject payload = new JsonObject();
int intVolume = Math.round(volume * 100.0f);
payload.addProperty("volume", intVolume);
ServiceCommand<CommandConfirmation> request = new ServiceCommand<>(uri, payload,
x -> GSON.fromJson(x, CommandConfirmation.class), listener);
sendCommand(request);
}
public void volumeUp(ResponseListener<CommandConfirmation> listener) {
String uri = "ssap://audio/volumeUp";
ServiceCommand<CommandConfirmation> request = new ServiceCommand<>(uri, null,
x -> GSON.fromJson(x, CommandConfirmation.class), listener);
sendCommand(request);
}
public void volumeDown(ResponseListener<CommandConfirmation> listener) {
String uri = "ssap://audio/volumeDown";
ServiceCommand<CommandConfirmation> request = new ServiceCommand<>(uri, null,
x -> GSON.fromJson(x, CommandConfirmation.class), listener);
sendCommand(request);
}
public ServiceSubscription<ChannelInfo> subscribeCurrentChannel(ResponseListener<ChannelInfo> listener) {
ServiceSubscription<ChannelInfo> request = new ServiceSubscription<>(CHANNEL, null,
jsonObj -> GSON.fromJson(jsonObj, ChannelInfo.class), listener);
sendCommand(request);
return request;
}
public ServiceCommand<ChannelInfo> getCurrentChannel(ResponseListener<ChannelInfo> listener) {
ServiceCommand<ChannelInfo> request = new ServiceCommand<>(CHANNEL, null,
jsonObj -> GSON.fromJson(jsonObj, ChannelInfo.class), listener);
sendCommand(request);
return request;
}
public void setChannel(ChannelInfo channelInfo, ResponseListener<CommandConfirmation> listener) {
JsonObject payload = new JsonObject();
if (channelInfo.getId() != null) {
payload.addProperty("channelId", channelInfo.getId());
}
if (channelInfo.getChannelNumber() != null) {
payload.addProperty("channelNumber", channelInfo.getChannelNumber());
}
setChannel(payload, listener);
}
private void setChannel(JsonObject payload, ResponseListener<CommandConfirmation> listener) {
String uri = "ssap://tv/openChannel";
ServiceCommand<CommandConfirmation> request = new ServiceCommand<>(uri, payload,
x -> GSON.fromJson(x, CommandConfirmation.class), listener);
sendCommand(request);
}
public void channelUp(ResponseListener<CommandConfirmation> listener) {
String uri = "ssap://tv/channelUp";
ServiceCommand<CommandConfirmation> request = new ServiceCommand<>(uri, null,
x -> GSON.fromJson(x, CommandConfirmation.class), listener);
sendCommand(request);
}
public void channelDown(ResponseListener<CommandConfirmation> listener) {
String uri = "ssap://tv/channelDown";
ServiceCommand<CommandConfirmation> request = new ServiceCommand<>(uri, null,
x -> GSON.fromJson(x, CommandConfirmation.class), listener);
sendCommand(request);
}
public void getChannelList(ResponseListener<List<ChannelInfo>> listener) {
ServiceCommand<List<ChannelInfo>> request = new ServiceCommand<>(CHANNEL_LIST, null,
jsonObj -> GSON.fromJson(jsonObj.get("channelList"), new TypeToken<ArrayList<ChannelInfo>>() {
}.getType()), listener);
sendCommand(request);
}
// TOAST
public void showToast(String message, ResponseListener<CommandConfirmation> listener) {
showToast(message, null, null, listener);
}
public void showToast(String message, @Nullable String iconData, @Nullable String iconExtension,
ResponseListener<CommandConfirmation> listener) {
JsonObject payload = new JsonObject();
payload.addProperty("message", message);
if (iconData != null && iconExtension != null) {
payload.addProperty("iconData", iconData);
payload.addProperty("iconExtension", iconExtension);
}
sendToast(payload, listener);
}
private void sendToast(JsonObject payload, ResponseListener<CommandConfirmation> listener) {
String uri = "ssap://system.notifications/createToast";
ServiceCommand<CommandConfirmation> request = new ServiceCommand<>(uri, payload,
x -> GSON.fromJson(x, CommandConfirmation.class), listener);
sendCommand(request);
}
// POWER
public void powerOff(ResponseListener<CommandConfirmation> listener) {
String uri = "ssap://system/turnOff";
ResponseListener<CommandConfirmation> interceptor = new ResponseListener<CommandConfirmation>() {
@Override
public void onSuccess(CommandConfirmation confirmation) {
if (confirmation.getReturnValue()) {
disconnecting();
}
listener.onSuccess(confirmation);
}
@Override
public void onError(String message) {
listener.onError(message);
}
};
ServiceCommand<CommandConfirmation> request = new ServiceCommand<>(uri, null,
x -> GSON.fromJson(x, CommandConfirmation.class), interceptor);
sendCommand(request);
}
// MEDIA CONTROL
public void play(ResponseListener<CommandConfirmation> listener) {
String uri = "ssap://media.controls/play";
ServiceCommand<CommandConfirmation> request = new ServiceCommand<>(uri, null,
x -> GSON.fromJson(x, CommandConfirmation.class), listener);
sendCommand(request);
}
public void pause(ResponseListener<CommandConfirmation> listener) {
String uri = "ssap://media.controls/pause";
ServiceCommand<CommandConfirmation> request = new ServiceCommand<>(uri, null,
x -> GSON.fromJson(x, CommandConfirmation.class), listener);
sendCommand(request);
}
public void stop(ResponseListener<CommandConfirmation> listener) {
String uri = "ssap://media.controls/stop";
ServiceCommand<CommandConfirmation> request = new ServiceCommand<>(uri, null,
x -> GSON.fromJson(x, CommandConfirmation.class), listener);
sendCommand(request);
}
public void rewind(ResponseListener<CommandConfirmation> listener) {
String uri = "ssap://media.controls/rewind";
ServiceCommand<CommandConfirmation> request = new ServiceCommand<>(uri, null,
x -> GSON.fromJson(x, CommandConfirmation.class), listener);
sendCommand(request);
}
public void fastForward(ResponseListener<CommandConfirmation> listener) {
String uri = "ssap://media.controls/fastForward";
ServiceCommand<CommandConfirmation> request = new ServiceCommand<>(uri, null,
x -> GSON.fromJson(x, CommandConfirmation.class), listener);
sendCommand(request);
}
// APPS
public void getAppList(final ResponseListener<List<AppInfo>> listener) {
String uri = "ssap://com.webos.applicationManager/listApps";
ServiceCommand<List<AppInfo>> request = new ServiceCommand<>(uri, null,
jsonObj -> GSON.fromJson(jsonObj.get("apps"), new TypeToken<ArrayList<AppInfo>>() {
}.getType()), listener);
sendCommand(request);
}
public void launchAppWithInfo(AppInfo appInfo, ResponseListener<LaunchSession> listener) {
launchAppWithInfo(appInfo, null, listener);
}
public void launchAppWithInfo(final AppInfo appInfo, @Nullable JsonObject params,
final ResponseListener<LaunchSession> listener) {
String uri = "ssap://system.launcher/launch";
JsonObject payload = new JsonObject();
final String appId = appInfo.getId();
String contentId = null;
if (params != null) {
contentId = params.get("contentId").getAsString();
}
payload.addProperty("id", appId);
if (contentId != null) {
payload.addProperty("contentId", contentId);
}
if (params != null) {
payload.add("params", params);
}
ServiceCommand<LaunchSession> request = new ServiceCommand<>(uri, payload, obj -> {
LaunchSession launchSession = new LaunchSession();
launchSession.setService(this);
launchSession.setAppId(appId); // note that response uses id to mean appId
if (obj.has("sessionId")) {
launchSession.setSessionId(obj.get("sessionId").getAsString());
launchSession.setSessionType(LaunchSessionType.App);
} else {
launchSession.setSessionType(LaunchSessionType.Unknown);
}
return launchSession;
}, listener);
sendCommand(request);
}
public void launchBrowser(String url, final ResponseListener<LaunchSession> listener) {
String uri = "ssap://system.launcher/open";
JsonObject payload = new JsonObject();
payload.addProperty("target", url);
ServiceCommand<LaunchSession> request = new ServiceCommand<>(uri, payload, obj -> {
LaunchSession launchSession = new LaunchSession();
launchSession.setService(this);
launchSession.setAppId(obj.get("id").getAsString()); // note that response uses id to mean appId
if (obj.has("sessionId")) {
launchSession.setSessionId(obj.get("sessionId").getAsString());
launchSession.setSessionType(LaunchSessionType.App);
} else {
launchSession.setSessionType(LaunchSessionType.Unknown);
}
return launchSession;
}, listener);
sendCommand(request);
}
public void closeLaunchSession(LaunchSession launchSession, ResponseListener<CommandConfirmation> listener) {
LGWebOSTVSocket service = launchSession.getService();
switch (launchSession.getSessionType()) {
case App:
case ExternalInputPicker:
service.closeApp(launchSession, listener);
break;
/*
* If we want to extend support for MediaPlayer or WebAppLauncher at some point, this is how it was handeled
* in connectsdk:
*
* case Media:
* if (service instanceof MediaPlayer) {
* ((MediaPlayer) service).closeMedia(launchSession, listener);
* }
* break;
*
*
* case WebApp:
* if (service instanceof WebAppLauncher) {
* ((WebAppLauncher) service).closeWebApp(launchSession, listener);
* }
* break;
* case Unknown:
*/
default:
listener.onError("This DeviceService does not know ho to close this LaunchSession");
break;
}
}
public void closeApp(LaunchSession launchSession, ResponseListener<CommandConfirmation> listener) {
String uri = "ssap://system.launcher/close";
JsonObject payload = new JsonObject();
payload.addProperty("id", launchSession.getAppId());
payload.addProperty("sessionId", launchSession.getSessionId());
ServiceCommand<CommandConfirmation> request = new ServiceCommand<>(uri, payload,
x -> GSON.fromJson(x, CommandConfirmation.class), listener);
launchSession.getService().sendCommand(request);
}
public ServiceSubscription<AppInfo> subscribeRunningApp(ResponseListener<AppInfo> listener) {
ResponseListener<AppInfo> interceptor = new ResponseListener<AppInfo>() {
@Override
public void onSuccess(AppInfo appInfo) {
if (appInfo.getId().isEmpty()) {
scheduleDisconectingJob();
} else {
stopDisconnectingJob();
if (state == State.DISCONNECTING) {
setState(State.REGISTERED);
}
}
listener.onSuccess(appInfo);
}
@Override
public void onError(String message) {
listener.onError(message);
}
};
ServiceSubscription<AppInfo> request = new ServiceSubscription<>(FOREGROUND_APP, null,
jsonObj -> GSON.fromJson(jsonObj, AppInfo.class), interceptor);
sendCommand(request);
return request;
}
public ServiceCommand<AppInfo> getRunningApp(ResponseListener<AppInfo> listener) {
ServiceCommand<AppInfo> request = new ServiceCommand<>(FOREGROUND_APP, null,
jsonObj -> GSON.fromJson(jsonObj, AppInfo.class), listener);
sendCommand(request);
return request;
}
// KEYBOARD
public ServiceSubscription<TextInputStatusInfo> subscribeTextInputStatus(
ResponseListener<TextInputStatusInfo> listener) {
return keyboardInput.connect(listener);
}
public void sendText(String input) {
keyboardInput.sendText(input);
}
public void sendEnter() {
keyboardInput.sendEnter();
}
public void sendDelete() {
keyboardInput.sendDel();
}
// MOUSE
public void executeMouse(Consumer<LGWebOSTVMouseSocket> onConnected) {
LGWebOSTVMouseSocket mouseSocket = new LGWebOSTVMouseSocket(this.client);
mouseSocket.setListener(new WebOSTVMouseSocketListener() {
@Override
public void onStateChanged(LGWebOSTVMouseSocket.State oldState, LGWebOSTVMouseSocket.State newState) {
switch (newState) {
case CONNECTED:
onConnected.accept(mouseSocket);
mouseSocket.disconnect();
break;
default:
break;
}
}
@Override
public void onError(String errorMessage) {
logger.debug("Error in communication with Mouse Socket: {}", errorMessage);
}
});
String uri = "ssap://com.webos.service.networkinput/getPointerInputSocket";
ResponseListener<JsonObject> listener = new ResponseListener<JsonObject>() {
@Override
public void onSuccess(@Nullable JsonObject jsonObj) {
if (jsonObj != null) {
String socketPath = jsonObj.get("socketPath").getAsString().replace("wss:", "ws:").replace(":3001/",
":3000/");
try {
mouseSocket.connect(new URI(socketPath));
} catch (URISyntaxException e) {
logger.warn("Connect mouse error: {}", e.getMessage());
}
}
}
@Override
public void onError(String error) {
logger.warn("Connect mouse error: {}", error);
}
};
ServiceCommand<JsonObject> request = new ServiceCommand<>(uri, null, x -> x, listener);
sendCommand(request);
}
// Simulate Remote Control Button press
public void sendRCButton(String rcButton, ResponseListener<CommandConfirmation> listener) {
executeMouse(s -> s.button(rcButton));
}
public interface ConfigProvider {
void storeKey(String key);
void storeProperties(Map<String, String> properties);
String getKey();
}
}

View File

@@ -0,0 +1,101 @@
/**
* 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
*/
/*
* This file is based on:
*
* ServiceCommand
* Connect SDK
*
* Copyright (c) 2014 LG Electronics.
* Created by Hyun Kook Khang on 19 Jan 2014
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.openhab.binding.lgwebos.internal.handler.command;
import java.util.function.Function;
import org.openhab.binding.lgwebos.internal.handler.core.ResponseListener;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
/**
* Internal implementation of ServiceCommand for URL-based commands
*
* @author Hyun Kook Khang - Connect SDK initial contribution
* @author Sebastian Prehn - Adoption for openHAB
*/
public class ServiceCommand<T> {
protected enum Type {
request,
subscribe
}
protected Type type;
protected JsonObject payload;
protected String target;
protected Function<JsonObject, T> converter;
ResponseListener<T> responseListener;
public ServiceCommand(String targetURL, JsonObject payload, Function<JsonObject, T> converter,
ResponseListener<T> listener) {
this.target = targetURL;
this.payload = payload;
this.converter = converter;
this.responseListener = listener;
this.type = Type.request;
}
public JsonElement getPayload() {
return payload;
}
public String getType() {
return type.name();
}
public String getTarget() {
return target;
}
public void processResponse(JsonObject response) {
this.getResponseListener().onSuccess(this.converter.apply(response));
}
public void processError(String error) {
this.getResponseListener().onError(error);
}
public ResponseListener<T> getResponseListener() {
return responseListener;
}
@Override
public String toString() {
return "ServiceCommand [type=" + type + ", target=" + target + ", payload=" + payload + "]";
}
}

View File

@@ -0,0 +1,56 @@
/**
* 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
*/
/*
* This file is based on URLServiceSubscription:
*
* Connect SDK
*
* Copyright (c) 2014 LG Electronics.
* Created by Hyun Kook Khang on 19 Jan 2014
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.openhab.binding.lgwebos.internal.handler.command;
import java.util.function.Function;
import org.openhab.binding.lgwebos.internal.handler.core.ResponseListener;
import com.google.gson.JsonObject;
/**
* Internal implementation of ServiceSubscription for URL-based commands.
*
*
* @author Hyun Kook Khang - Connect SDK initial contribution
* @author Sebastian Prehn - Adoption for openHAB
*/
public class ServiceSubscription<T> extends ServiceCommand<T> {
public ServiceSubscription(String uri, JsonObject payload, Function<JsonObject, T> converter,
ResponseListener<T> listener) {
super(uri, payload, converter, listener);
type = Type.subscribe;
}
}

View File

@@ -0,0 +1,105 @@
/**
* 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
*/
/*
* This file is based on:
*
* AppInfo
* Connect SDK
*
* Copyright (c) 2014 LG Electronics.
* Created by Hyun Kook Khang on 19 Jan 2014
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.openhab.binding.lgwebos.internal.handler.core;
import com.google.gson.annotations.SerializedName;
/**
* {@link AppInfo} is a value object to describe an application on WebOSTV.
* The id value is mandatory when starting an application. The name is a human readable friendly name, which is not
* further interpreted by the TV.
*
* @author Hyun Kook Khang - Connect SDK initial contribution
* @author Sebastian Prehn - Adoption for openHAB, made immutable
*/
public class AppInfo {
@SerializedName(value = "id", alternate = "appId")
private String id;
@SerializedName(value = "name", alternate = { "appName", "title" })
private String name;
public AppInfo() {
// no-argument constructor for gson
}
public AppInfo(String id, String name) {
this.id = id;
this.name = name;
}
public String getId() {
return id;
}
public String getName() {
return name;
}
@Override
public String toString() {
return "AppInfo [id=" + id + ", name=" + name + "]";
}
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + ((id == null) ? 0 : id.hashCode());
return result;
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj == null) {
return false;
}
if (getClass() != obj.getClass()) {
return false;
}
AppInfo other = (AppInfo) obj;
if (id == null) {
if (other.id != null) {
return false;
}
} else if (!id.equals(other.id)) {
return false;
}
return true;
}
}

View File

@@ -0,0 +1,109 @@
/**
* 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
*/
/*
* This file is based on:
*
* ChannelInfo
* Connect SDK
*
* Copyright (c) 2014 LG Electronics.
* Created by Hyun Kook Khang on 19 Jan 2014
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.openhab.binding.lgwebos.internal.handler.core;
/**
* {@link ChannelInfo} is a value object to describe a channel on WebOSTV.
* The id value is mandatory when starting an channel. The channelName is a human readable friendly name, which is not
* further interpreted by the TV.
*
* @author Hyun Kook Khang - Connect SDK initial contribution
* @author Sebastian Prehn - Adoption for openHAB, removed minor major number, made immutable
*/
public class ChannelInfo {
private String channelName;
private String channelId;
private String channelNumber;
private String channelType;
public ChannelInfo() {
// no-argument constructor for gson
}
public ChannelInfo(String channelName, String channelId, String channelNumber, String channelType) {
this.channelId = channelId;
this.channelNumber = channelNumber;
this.channelName = channelName;
this.channelType = channelType;
}
public String getName() {
return channelName;
}
public String getId() {
return channelId;
}
public String getChannelNumber() {
return channelNumber;
}
@Override
public String toString() {
return "ChannelInfo [channelId=" + channelId + ", channelNumber=" + channelNumber + ", channelName="
+ channelName + ", channelType=" + channelType + "]";
}
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + ((channelId == null) ? 0 : channelId.hashCode());
return result;
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj == null) {
return false;
}
if (getClass() != obj.getClass()) {
return false;
}
ChannelInfo other = (ChannelInfo) obj;
if (channelId == null) {
if (other.channelId != null) {
return false;
}
} else if (!channelId.equals(other.channelId)) {
return false;
}
return true;
}
}

View File

@@ -0,0 +1,39 @@
/**
* 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.lgwebos.internal.handler.core;
/**
* {@link CommandConfirmation} represents payload in response from TV were it only confirms the result of an operation.
*
* @author Sebastian Prehn - Initial contribution
*/
public class CommandConfirmation {
private boolean returnValue;
public CommandConfirmation() {
// no-argument constructor for gson
}
public CommandConfirmation(boolean returnValue) {
this.returnValue = returnValue;
}
public boolean getReturnValue() {
return returnValue;
}
@Override
public String toString() {
return "CommandConfirmation [returnValue=" + returnValue + "]";
}
}

View File

@@ -0,0 +1,170 @@
/**
* 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
*/
/* This file is based on:
*
* LaunchSession
* Connect SDK
*
* Copyright (c) 2014 LG Electronics.
* Created by Jeffrey Glenn on 07 Mar 2014
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.openhab.binding.lgwebos.internal.handler.core;
import org.openhab.binding.lgwebos.internal.handler.LGWebOSTVSocket;
/**
* {@link LaunchSession} is a value object to describe a session with an application running on WebOSTV.
*
* Any time anything is launched onto a first screen device, there will be important session information that needs to
* be tracked. {@link LaunchSession} tracks this data, and must be retained to perform certain actions within the
* session.
*
* @author Jeffrey Glenn - Connect SDK initial contribution
* @author Sebastian Prehn - Adoption for openHAB
*/
public class LaunchSession {
private String appId;
private String appName;
private String sessionId;
private transient LGWebOSTVSocket socket;
private transient LaunchSessionType sessionType;
/**
* LaunchSession type is used to help DeviceService's know how to close a LaunchSession.
*
*/
public enum LaunchSessionType {
/** Unknown LaunchSession type, may be unable to close this launch session */
Unknown,
/** LaunchSession represents a launched app */
App,
/** LaunchSession represents an external input picker that was launched */
ExternalInputPicker,
/** LaunchSession represents a media app */
Media,
/** LaunchSession represents a web app */
WebApp
}
public LaunchSession() {
}
/**
* Instantiates a LaunchSession object for a given app ID.
*
* @param appId System-specific, unique ID of the app
* @return the launch session
*/
public static LaunchSession launchSessionForAppId(String appId) {
LaunchSession launchSession = new LaunchSession();
launchSession.appId = appId;
return launchSession;
}
/** @return System-specific, unique ID of the app (ex. youtube.leanback.v4, 0000134, hulu) */
public String getAppId() {
return appId;
}
/**
* Sets the system-specific, unique ID of the app (ex. youtube.leanback.v4, 0000134, hulu)
*
* @param appId Id of the app
*/
public void setAppId(String appId) {
this.appId = appId;
}
/** @return User-friendly name of the app (ex. YouTube, Browser, Hulu) */
public String getAppName() {
return appName;
}
/**
* Sets the user-friendly name of the app (ex. YouTube, Browser, Hulu)
*
* @param appName Name of the app
*/
public void setAppName(String appName) {
this.appName = appName;
}
/** @return Unique ID for the session (only provided by certain protocols) */
public String getSessionId() {
return sessionId;
}
/**
* Sets the session id (only provided by certain protocols)
*
* @param sessionId Id of the current session
*/
public void setSessionId(String sessionId) {
this.sessionId = sessionId;
}
/** @return WebOSTVSocket responsible for launching the session. */
public LGWebOSTVSocket getService() {
return socket;
}
/**
* DeviceService responsible for launching the session.
*
* @param service Sets the DeviceService
*/
public void setService(LGWebOSTVSocket service) {
this.socket = service;
}
/**
* @return When closing a LaunchSession, the DeviceService relies on the sessionType to determine the method of
* closing the session.
*/
public LaunchSessionType getSessionType() {
return sessionType;
}
/**
* Sets the LaunchSessionType of this LaunchSession.
*
* @param sessionType The type of LaunchSession
*/
public void setSessionType(LaunchSessionType sessionType) {
this.sessionType = sessionType;
}
/**
* Close the app/media associated with the session.
*
* @param listener the response listener
*/
public void close(ResponseListener<CommandConfirmation> listener) {
socket.closeLaunchSession(this, listener);
}
}

View File

@@ -0,0 +1,55 @@
/**
* 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.lgwebos.internal.handler.core;
import com.google.gson.JsonElement;
/**
* {@link Response} is a value object for a response message from WebOSTV.
*
* @author Sebastian Prehn - Initial contribution
*/
public class Response {
/** Required response type */
private String type;
/** Optional payload */
private JsonElement payload;
/**
* Message ID to which this is a response to.
* This is optional.
*/
private Integer id;
public Response() {
// no-argument constructor for gson
}
/** Optional error message. */
private String error;
public Integer getId() {
return id;
}
public String getType() {
return type;
}
public String getError() {
return error;
}
public JsonElement getPayload() {
return payload;
}
}

View File

@@ -0,0 +1,63 @@
/**
* 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
*/
/* This file is based on:
*
* ResponseListener
* Connect SDK
*
* Copyright (c) 2014 LG Electronics.
* Created by Hyun Kook Khang on 19 Jan 2014
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.openhab.binding.lgwebos.internal.handler.core;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* Generic asynchronous operation response success handler block. If there is any response data to be processed, it will
* be provided via the responseObject parameter.
*
* @author Hyun Kook Khang - Connect SDK initial contribution
* @author Sebastian Prehn - Adoption for openHAB
*/
@NonNullByDefault
public interface ResponseListener<T> {
/**
* Returns the success of the call of type T.
* Contains the output data as a generic object reference.
* This value may be any of a number of types as defined by T in subclasses of ResponseListener.
*
* @param responseObject Response object, can be any number of object types, depending on the
* protocol/capability/etc
*/
void onSuccess(T responseObject);
/**
* Method to return the error message that was generated.
*
*/
void onError(String message);
}

View File

@@ -0,0 +1,178 @@
/**
* 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
*/
/* This file is based on:
*
* TextInputStatusInfo
* Connect SDK
*
* Copyright (c) 2014 LG Electronics.
* Created by Hyun Kook Khang on 19 Jan 2014
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.openhab.binding.lgwebos.internal.handler.core;
import com.google.gson.JsonObject;
/**
* Normalized reference object for information about a text input event.
*
* @author Hyun Kook Khang - Connect SDK initial contribution
* @author Sebastian Prehn - Adoption for openHAB
*/
public class TextInputStatusInfo {
// @cond INTERNAL
public enum TextInputType {
DEFAULT,
URL,
NUMBER,
PHONE_NUMBER,
EMAIL
}
boolean focused = false;
String contentType = null;
boolean predictionEnabled = false;
boolean correctionEnabled = false;
boolean autoCapitalization = false;
boolean hiddenText = false;
boolean focusChanged = false;
JsonObject rawData;
// @endcond
public TextInputStatusInfo() {
}
public boolean isFocused() {
return focused;
}
public void setFocused(boolean focused) {
this.focused = focused;
}
/**
* Gets the type of keyboard that should be displayed to the user.
*
* @return the keyboard type
*/
public TextInputType getTextInputType() {
TextInputType textInputType = TextInputType.DEFAULT;
if (contentType != null) {
if (contentType.equals("number")) {
textInputType = TextInputType.NUMBER;
} else if (contentType.equals("phonenumber")) {
textInputType = TextInputType.PHONE_NUMBER;
} else if (contentType.equals("url")) {
textInputType = TextInputType.URL;
} else if (contentType.equals("email")) {
textInputType = TextInputType.EMAIL;
}
}
return textInputType;
}
/**
* Sets the type of keyboard that should be displayed to the user.
*
* @param textInputType the keyboard type
*/
public void setTextInputType(TextInputType textInputType) {
switch (textInputType) {
case NUMBER:
contentType = "number";
break;
case PHONE_NUMBER:
contentType = "phonenumber";
break;
case URL:
contentType = "url";
break;
case EMAIL:
contentType = "number";
break;
case DEFAULT:
default:
contentType = "email";
break;
}
}
public void setContentType(String contentType) {
this.contentType = contentType;
}
public boolean isPredictionEnabled() {
return predictionEnabled;
}
public void setPredictionEnabled(boolean predictionEnabled) {
this.predictionEnabled = predictionEnabled;
}
public boolean isCorrectionEnabled() {
return correctionEnabled;
}
public void setCorrectionEnabled(boolean correctionEnabled) {
this.correctionEnabled = correctionEnabled;
}
public boolean isAutoCapitalization() {
return autoCapitalization;
}
public void setAutoCapitalization(boolean autoCapitalization) {
this.autoCapitalization = autoCapitalization;
}
public boolean isHiddenText() {
return hiddenText;
}
public void setHiddenText(boolean hiddenText) {
this.hiddenText = hiddenText;
}
/** @return the raw data from the first screen device about the text input status. */
public JsonObject getRawData() {
return rawData;
}
/** @param data the raw data from the first screen device about the text input status. */
public void setRawData(JsonObject data) {
rawData = data;
}
public boolean isFocusChanged() {
return focusChanged;
}
public void setFocusChanged(boolean focusChanged) {
this.focusChanged = focusChanged;
}
}

View File

@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="UTF-8"?>
<binding:binding id="lgwebos" 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>LG webOS Binding</name>
<description>Binding to connect LG's WebOS based smart TVs</description>
<author>Sebastian Prehn</author>
</binding:binding>

View File

@@ -0,0 +1,27 @@
<?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:lgwebos:WebOSTV">
<parameter name="host" type="text" required="true">
<label>Host</label>
<description>Hostname or IP address of TV.</description>
<context>network-address</context>
</parameter>
<parameter name="key" type="text" required="false">
<label>Access Key</label>
<description>Key exchanged with TV after pairing.</description>
<context>password</context>
</parameter>
<parameter name="macAddress" type="text" required="false">
<label>MAC Address</label>
<description>If MAC Address of TV is entered here, the binding will attempt to power on the device via Wake On Lan
(WOL), when it receives command ON on channel power. Accepted value is six groups of two hexadecimal digits,
separated by hyphens or colons, e.g '3c:cd:93:c2:20:e0'.)</description>
</parameter>
</config-description>
</config-description:config-descriptions>

View File

@@ -0,0 +1,45 @@
actionShowToastLabel=Show Toast
actionShowToastDesc=Sends a toast message to a WebOS device with openHAB icon.
actionShowToastInputTextLabel=Text
actionShowToastInputTextDesc=The text to display
actionShowToastWithIconLabel=Show Toast with Icon
actionShowToastWithIconLabel=Sends a toast message to a WebOS device with custom icon.
actionShowToastInputIconLabel=Icon
actionShowToastInputIconDesc=The URL to the icon to display
actionLaunchBrowserLabel=Launch Browser
actionLaunchBrowserDesc=Opens the given URL in the TV's browser application.
actionLaunchBrowserInputUrlLabel=URL
actionLaunchBrowserInputUrlDesc=The URL to open
actionLaunchApplicationLabel=Launch Application
actionLaunchApplicationDesc=Opens the application with given Application ID.
actionLaunchApplicationInputAppIDLabel=Application ID
actionLaunchApplicationInputAppIDDesc=The Application ID
actionLaunchApplicationWithParamsLabel=Launch Application with Parameters
actionLaunchApplicationWithParamsDesc=Opens the application with given Application ID and passes additional parameters.
actionLaunchApplicationInputParamsLabel=JSON Parameters
actionLaunchApplicationInputParamsDesc=The parameters to hand over to the application in JSON format
actionSendTextLabel=Send Text
actionSendTextDesc=Sends a text input to a WebOS device.
actionSendTextInputTextLabel=Text
actionSendTextInputTextDesc=The text to input
actionSendButtonLabel=Send Button
actionSendButtonDesc=Sends a button press event to a WebOS device.
actionSendButtonInputButtonLabel=Button
actionSendButtonInputButtonDesc=Can be one of UP, DOWN, LEFT, RIGHT, BACK, DELETE, ENTER, HOME, or OK
actionIncreaseChannelLabel=Channel Up
actionIncreaseChannelDesc=TV will switch one channel up in the current channel list.
actionDecreaseChannelLabel=Channel Down
actionDecreaseChannelDesc=TV will switch one channel down in the current channel list.
actionSendRCButtonLabel=Remote Control button press
actionSendRCButtonDesc=Simulates pressing of a Remote Control Button.
actionSendRCButtonInputTextLabel=Remote Control button name
actionSendRCButtonInputTextDesc=The Remote Control button name to send to the WebOS device.

View File

@@ -0,0 +1,86 @@
<?xml version="1.0" encoding="UTF-8"?>
<thing:thing-descriptions bindingId="lgwebos"
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="WebOSTV">
<label>WebOS TV</label>
<description>WebOS based smart TV</description>
<channels>
<channel id="power" typeId="powerType"/>
<channel id="mute" typeId="muteType"/>
<channel id="volume" typeId="volumeType"/>
<channel id="channel" typeId="channelType"/>
<channel id="toast" typeId="toastType"/>
<channel id="mediaPlayer" typeId="mediaPlayerType"/>
<channel id="mediaStop" typeId="mediaStopType"/>
<channel id="appLauncher" typeId="appLauncherChannelType"/>
<channel id="rcButton" typeId="rcButtonType"/>
</channels>
<properties>
<property name="deviceId"/>
<property name="lastConnected"/>
<property name="deviceOS"/>
<property name="deviceOSVersion"/>
<property name="deviceOSReleaseVersion"/>
</properties>
<representation-property>deviceId</representation-property>
<config-description-ref uri="thing-type:lgwebos:WebOSTV"/>
</thing-type>
<channel-type id="powerType">
<item-type>Switch</item-type>
<label>Power</label>
<description>Via this binding TV can only be powered off, not on.</description>
</channel-type>
<channel-type id="muteType">
<item-type>Switch</item-type>
<label>Mute</label>
<description>Current Mute Setting</description>
<category>SoundVolume</category>
</channel-type>
<channel-type id="volumeType">
<item-type>Dimmer</item-type>
<label>Volume</label>
<description>Current Volume Setting</description>
<category>SoundVolume</category>
<state min="0" max="100" step="1"></state>
</channel-type>
<channel-type id="channelType">
<item-type>String</item-type>
<label>Channel</label>
<description>Current Channel</description>
</channel-type>
<channel-type id="toastType">
<item-type>String</item-type>
<label>Toast</label>
<description>Send a message onto the TV screen.</description>
</channel-type>
<channel-type id="mediaPlayerType">
<item-type>Player</item-type>
<label>Media Control</label>
<description>Control media (e.g. audio or video) playback</description>
<category>MediaControl</category>
</channel-type>
<channel-type id="mediaStopType">
<item-type>Switch</item-type>
<label>Stop</label>
<description>Stop Playback</description>
</channel-type>
<channel-type id="appLauncherChannelType">
<item-type>String</item-type>
<label>Application</label>
<description>Start application and monitor running applications.</description>
</channel-type>
<channel-type id="rcButtonType">
<item-type>String</item-type>
<label>RCButton</label>
<description>Simulate a Remote Control button press</description>
</channel-type>
</thing:thing-descriptions>

View File

@@ -0,0 +1,3 @@
org.slf4j.simpleLogger.defaultLogLevel=info
org.slf4j.simpleLogger.log.org.openhab.binding.lgwebos=trace
org.slf4j.simpleLogger.log.org.eclipse.jetty.websocket=info