2 * Copyright (c) 2010-2020 Contributors to the openHAB project
4 * See the NOTICE file(s) distributed with this work for additional
7 * This program and the accompanying materials are made available under the
8 * terms of the Eclipse Public License 2.0 which is available at
9 * http://www.eclipse.org/legal/epl-2.0
11 * SPDX-License-Identifier: EPL-2.0
13 package org.openhab.binding.lgwebos.action;
15 import java.awt.image.BufferedImage;
16 import java.io.ByteArrayOutputStream;
17 import java.io.IOException;
18 import java.io.OutputStream;
19 import java.lang.reflect.Method;
20 import java.lang.reflect.Proxy;
22 import java.nio.charset.StandardCharsets;
23 import java.util.Base64;
24 import java.util.Collections;
25 import java.util.List;
26 import java.util.Optional;
27 import java.util.stream.Collectors;
28 import java.util.stream.Stream;
30 import javax.imageio.ImageIO;
32 import org.eclipse.jdt.annotation.NonNullByDefault;
33 import org.eclipse.jdt.annotation.Nullable;
34 import org.openhab.binding.lgwebos.internal.handler.LGWebOSHandler;
35 import org.openhab.binding.lgwebos.internal.handler.LGWebOSTVMouseSocket.ButtonType;
36 import org.openhab.binding.lgwebos.internal.handler.LGWebOSTVSocket;
37 import org.openhab.binding.lgwebos.internal.handler.LGWebOSTVSocket.State;
38 import org.openhab.binding.lgwebos.internal.handler.command.ServiceSubscription;
39 import org.openhab.binding.lgwebos.internal.handler.core.AppInfo;
40 import org.openhab.binding.lgwebos.internal.handler.core.ResponseListener;
41 import org.openhab.binding.lgwebos.internal.handler.core.TextInputStatusInfo;
42 import org.openhab.core.automation.annotation.ActionInput;
43 import org.openhab.core.automation.annotation.RuleAction;
44 import org.openhab.core.thing.binding.ThingActions;
45 import org.openhab.core.thing.binding.ThingActionsScope;
46 import org.openhab.core.thing.binding.ThingHandler;
47 import org.slf4j.Logger;
48 import org.slf4j.LoggerFactory;
50 import com.google.gson.JsonObject;
51 import com.google.gson.JsonParseException;
52 import com.google.gson.JsonParser;
55 * The {@link LGWebOSActions} defines the thing actions for the LGwebOS binding.
57 * <b>Note:</b>The static method <b>invokeMethodOf</b> handles the case where
58 * the test <i>actions instanceof LGWebOSActions</i> fails. This test can fail
59 * due to an issue in openHAB core v2.5.0 where the {@link LGWebOSActions} class
60 * can be loaded by a different classloader than the <i>actions</i> instance.
62 * @author Sebastian Prehn - Initial contribution
63 * @author Laurent Garnier - new method invokeMethodOf + interface ILGWebOSActions
65 @ThingActionsScope(name = "lgwebos")
67 public class LGWebOSActions implements ThingActions, ILGWebOSActions {
68 private final Logger logger = LoggerFactory.getLogger(LGWebOSActions.class);
69 private final ResponseListener<TextInputStatusInfo> textInputListener = createTextInputStatusListener();
70 private @Nullable LGWebOSHandler handler;
73 public void setThingHandler(@Nullable ThingHandler handler) {
74 this.handler = (LGWebOSHandler) handler;
78 public @Nullable ThingHandler getThingHandler() {
82 // a NonNull getter for handler
83 private LGWebOSHandler getLGWebOSHandler() {
84 LGWebOSHandler lgWebOSHandler = this.handler;
85 if (lgWebOSHandler == null) {
86 throw new IllegalStateException(
87 "ThingHandler must be set before any action may be invoked on LGWebOSActions.");
89 return lgWebOSHandler;
105 @RuleAction(label = "@text/actionShowToastLabel", description = "@text/actionShowToastDesc")
106 public void showToast(
107 @ActionInput(name = "text", label = "@text/actionShowToastInputTextLabel", description = "@text/actionShowToastInputTextDesc") String text)
109 getConnectedSocket().ifPresent(control -> control.showToast(text, createResponseListener()));
113 @RuleAction(label = "@text/actionShowToastWithIconLabel", description = "@text/actionShowToastWithIconLabel")
114 public void showToast(
115 @ActionInput(name = "icon", label = "@text/actionShowToastInputIconLabel", description = "@text/actionShowToastInputIconDesc") String icon,
116 @ActionInput(name = "text", label = "@text/actionShowToastInputTextLabel", description = "@text/actionShowToastInputTextDesc") String text)
118 BufferedImage bi = ImageIO.read(new URL(icon));
119 try (ByteArrayOutputStream os = new ByteArrayOutputStream(); OutputStream b64 = Base64.getEncoder().wrap(os)) {
120 ImageIO.write(bi, "png", b64);
121 String string = os.toString(StandardCharsets.UTF_8.name());
122 getConnectedSocket().ifPresent(control -> control.showToast(text, string, "png", createResponseListener()));
127 @RuleAction(label = "@text/actionLaunchBrowserLabel", description = "@text/actionLaunchBrowserDesc")
128 public void launchBrowser(
129 @ActionInput(name = "url", label = "@text/actionLaunchBrowserInputUrlLabel", description = "@text/actionLaunchBrowserInputUrlDesc") String url) {
130 getConnectedSocket().ifPresent(control -> control.launchBrowser(url, createResponseListener()));
133 private List<AppInfo> getAppInfos() {
134 LGWebOSHandler lgWebOSHandler = getLGWebOSHandler();
136 if (!this.getConnectedSocket().isPresent()) {
137 return Collections.emptyList();
140 List<AppInfo> appInfos = lgWebOSHandler.getLauncherApplication()
141 .getAppInfos(lgWebOSHandler.getThing().getUID());
142 if (appInfos == null) {
143 logger.warn("No AppInfos found for device with ThingID {}.", lgWebOSHandler.getThing().getUID());
144 return Collections.emptyList();
150 @RuleAction(label = "@text/actionLaunchApplicationLabel", description = "@text/actionLaunchApplicationDesc")
151 public void launchApplication(
152 @ActionInput(name = "appId", label = "@text/actionLaunchApplicationInputAppIDLabel", description = "@text/actionLaunchApplicationInputAppIDDesc") String appId) {
153 Optional<AppInfo> appInfo = getAppInfos().stream().filter(a -> a.getId().equals(appId)).findFirst();
154 if (appInfo.isPresent()) {
156 .ifPresent(control -> control.launchAppWithInfo(appInfo.get(), createResponseListener()));
158 logger.warn("Device with ThingID {} does not support any app with id: {}.",
159 getLGWebOSHandler().getThing().getUID(), appId);
164 @RuleAction(label = "@text/actionLaunchApplicationWithParamsLabel", description = "@text/actionLaunchApplicationWithParamsDesc")
165 public void launchApplication(
166 @ActionInput(name = "appId", label = "@text/actionLaunchApplicationInputAppIDLabel", description = "@text/actionLaunchApplicationInputAppIDDesc") String appId,
167 @ActionInput(name = "params", label = "@text/actionLaunchApplicationInputParamsLabel", description = "@text/actionLaunchApplicationInputParamsDesc") String params) {
169 JsonParser parser = new JsonParser();
170 JsonObject payload = (JsonObject) parser.parse(params);
172 Optional<AppInfo> appInfo = getAppInfos().stream().filter(a -> a.getId().equals(appId)).findFirst();
173 if (appInfo.isPresent()) {
174 getConnectedSocket().ifPresent(
175 control -> control.launchAppWithInfo(appInfo.get(), payload, createResponseListener()));
177 logger.warn("Device with ThingID {} does not support any app with id: {}.",
178 getLGWebOSHandler().getThing().getUID(), appId);
181 } catch (JsonParseException ex) {
182 logger.warn("Parameters value ({}) is not in a valid JSON format. {}", params, ex.getMessage());
188 @RuleAction(label = "@text/actionSendTextLabel", description = "@text/actionSendTextDesc")
189 public void sendText(
190 @ActionInput(name = "text", label = "@text/actionSendTextInputTextLabel", description = "@text/actionSendTextInputTextDesc") String text) {
191 getConnectedSocket().ifPresent(control -> {
192 ServiceSubscription<TextInputStatusInfo> subscription = control.subscribeTextInputStatus(textInputListener);
193 control.sendText(text);
194 control.unsubscribe(subscription);
199 @RuleAction(label = "@text/actionSendButtonLabel", description = "@text/actionSendButtonDesc")
200 public void sendButton(
201 @ActionInput(name = "text", label = "@text/actionSendButtonInputButtonLabel", description = "@text/actionSendButtonInputButtonDesc") String button) {
203 switch (Button.valueOf(button)) {
205 getConnectedSocket().ifPresent(control -> control.executeMouse(s -> s.button(ButtonType.UP)));
208 getConnectedSocket().ifPresent(control -> control.executeMouse(s -> s.button(ButtonType.DOWN)));
211 getConnectedSocket().ifPresent(control -> control.executeMouse(s -> s.button(ButtonType.LEFT)));
214 getConnectedSocket().ifPresent(control -> control.executeMouse(s -> s.button(ButtonType.RIGHT)));
217 getConnectedSocket().ifPresent(control -> control.executeMouse(s -> s.button(ButtonType.BACK)));
220 getConnectedSocket().ifPresent(control -> control.sendDelete());
223 getConnectedSocket().ifPresent(control -> control.sendEnter());
226 getConnectedSocket().ifPresent(control -> control.executeMouse(s -> s.button("HOME")));
229 getConnectedSocket().ifPresent(control -> control.executeMouse(s -> s.click()));
232 } catch (IllegalArgumentException ex) {
233 logger.warn("{} is not a valid value for button - available are: {}", button,
234 Stream.of(Button.values()).map(b -> b.name()).collect(Collectors.joining(", ")));
239 @RuleAction(label = "@text/actionIncreaseChannelLabel", description = "@text/actionIncreaseChannelDesc")
240 public void increaseChannel() {
241 getConnectedSocket().ifPresent(control -> control.channelUp(createResponseListener()));
245 @RuleAction(label = "@text/actionDecreaseChannelLabel", description = "@text/actionDecreaseChannelDesc")
246 public void decreaseChannel() {
247 getConnectedSocket().ifPresent(control -> control.channelDown(createResponseListener()));
251 @RuleAction(label = "@text/actionSendRCButtonLabel", description = "@text/actionSendRCButtonDesc")
252 public void sendRCButton(
253 @ActionInput(name = "text", label = "@text/actionSendRCButtonInputTextLabel", description = "@text/actionSendRCButtonInputTextDesc") String rcButton) {
254 getConnectedSocket().ifPresent(control -> control.executeMouse(s -> s.button(rcButton)));
257 private Optional<LGWebOSTVSocket> getConnectedSocket() {
258 LGWebOSHandler lgWebOSHandler = getLGWebOSHandler();
259 final LGWebOSTVSocket socket = lgWebOSHandler.getSocket();
261 if (socket.getState() != State.REGISTERED) {
262 logger.warn("Device with ThingID {} is currently not connected.", lgWebOSHandler.getThing().getUID());
263 return Optional.empty();
266 return Optional.of(socket);
269 private ResponseListener<TextInputStatusInfo> createTextInputStatusListener() {
270 return new ResponseListener<TextInputStatusInfo>() {
273 public void onError(@Nullable String error) {
274 logger.warn("Response: {}", error);
278 public void onSuccess(@Nullable TextInputStatusInfo info) {
279 logger.debug("Response: {}", info == null ? "OK" : info.getRawData());
284 private <O> ResponseListener<O> createResponseListener() {
285 return new ResponseListener<O>() {
288 public void onError(@Nullable String error) {
289 logger.warn("Response: {}", error);
293 public void onSuccess(@Nullable O object) {
294 logger.debug("Response: {}", object == null ? "OK" : object.toString());
299 // delegation methods for "legacy" rule support
301 private static ILGWebOSActions invokeMethodOf(@Nullable ThingActions actions) {
302 if (actions == null) {
303 throw new IllegalArgumentException("actions cannot be null");
305 if (actions.getClass().getName().equals(LGWebOSActions.class.getName())) {
306 if (actions instanceof ILGWebOSActions) {
307 return (ILGWebOSActions) actions;
309 return (ILGWebOSActions) Proxy.newProxyInstance(ILGWebOSActions.class.getClassLoader(),
310 new Class[] { ILGWebOSActions.class }, (Object proxy, Method method, Object[] args) -> {
311 Method m = actions.getClass().getDeclaredMethod(method.getName(),
312 method.getParameterTypes());
313 return m.invoke(actions, args);
317 throw new IllegalArgumentException("Actions is not an instance of LGWebOSActions");
320 public static void showToast(@Nullable ThingActions actions, String text) throws IOException {
321 invokeMethodOf(actions).showToast(text);
324 public static void showToast(@Nullable ThingActions actions, String icon, String text) throws IOException {
325 invokeMethodOf(actions).showToast(icon, text);
328 public static void launchBrowser(@Nullable ThingActions actions, String url) {
329 invokeMethodOf(actions).launchBrowser(url);
332 public static void launchApplication(@Nullable ThingActions actions, String appId) {
333 invokeMethodOf(actions).launchApplication(appId);
336 public static void launchApplication(@Nullable ThingActions actions, String appId, String param) {
337 invokeMethodOf(actions).launchApplication(appId, param);
340 public static void sendText(@Nullable ThingActions actions, String text) {
341 invokeMethodOf(actions).sendText(text);
344 public static void sendButton(@Nullable ThingActions actions, String button) {
345 invokeMethodOf(actions).sendButton(button);
348 public static void increaseChannel(@Nullable ThingActions actions) {
349 invokeMethodOf(actions).increaseChannel();
352 public static void decreaseChannel(@Nullable ThingActions actions) {
353 invokeMethodOf(actions).decreaseChannel();
356 public static void sendRCButton(@Nullable ThingActions actions, String rcButton) {
357 invokeMethodOf(actions).sendRCButton(rcButton);