]> git.basschouten.com Git - openhab-addons.git/blob
6cf60a38a28d14de9992ddd657ca94db4036e0db
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2020 Contributors to the openHAB project
3  *
4  * See the NOTICE file(s) distributed with this work for additional
5  * information.
6  *
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
10  *
11  * SPDX-License-Identifier: EPL-2.0
12  */
13 package org.openhab.binding.lgwebos.action;
14
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;
21 import java.net.URL;
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;
29
30 import javax.imageio.ImageIO;
31
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;
49
50 import com.google.gson.JsonObject;
51 import com.google.gson.JsonParseException;
52 import com.google.gson.JsonParser;
53
54 /**
55  * The {@link LGWebOSActions} defines the thing actions for the LGwebOS binding.
56  * <p>
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.
61  *
62  * @author Sebastian Prehn - Initial contribution
63  * @author Laurent Garnier - new method invokeMethodOf + interface ILGWebOSActions
64  */
65 @ThingActionsScope(name = "lgwebos")
66 @NonNullByDefault
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;
71
72     @Override
73     public void setThingHandler(@Nullable ThingHandler handler) {
74         this.handler = (LGWebOSHandler) handler;
75     }
76
77     @Override
78     public @Nullable ThingHandler getThingHandler() {
79         return this.handler;
80     }
81
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.");
88         }
89         return lgWebOSHandler;
90     }
91
92     private enum Button {
93         UP,
94         DOWN,
95         LEFT,
96         RIGHT,
97         BACK,
98         DELETE,
99         ENTER,
100         HOME,
101         OK
102     }
103
104     @Override
105     @RuleAction(label = "@text/actionShowToastLabel", description = "@text/actionShowToastDesc")
106     public void showToast(
107             @ActionInput(name = "text", label = "@text/actionShowToastInputTextLabel", description = "@text/actionShowToastInputTextDesc") String text)
108             throws IOException {
109         getConnectedSocket().ifPresent(control -> control.showToast(text, createResponseListener()));
110     }
111
112     @Override
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)
117             throws IOException {
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()));
123         }
124     }
125
126     @Override
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()));
131     }
132
133     private List<AppInfo> getAppInfos() {
134         LGWebOSHandler lgWebOSHandler = getLGWebOSHandler();
135
136         if (!this.getConnectedSocket().isPresent()) {
137             return Collections.emptyList();
138         }
139
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();
145         }
146         return appInfos;
147     }
148
149     @Override
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()) {
155             getConnectedSocket()
156                     .ifPresent(control -> control.launchAppWithInfo(appInfo.get(), createResponseListener()));
157         } else {
158             logger.warn("Device with ThingID {} does not support any app with id: {}.",
159                     getLGWebOSHandler().getThing().getUID(), appId);
160         }
161     }
162
163     @Override
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) {
168         try {
169             JsonParser parser = new JsonParser();
170             JsonObject payload = (JsonObject) parser.parse(params);
171
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()));
176             } else {
177                 logger.warn("Device with ThingID {} does not support any app with id: {}.",
178                         getLGWebOSHandler().getThing().getUID(), appId);
179             }
180
181         } catch (JsonParseException ex) {
182             logger.warn("Parameters value ({}) is not in a valid JSON format. {}", params, ex.getMessage());
183             return;
184         }
185     }
186
187     @Override
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);
195         });
196     }
197
198     @Override
199     @RuleAction(label = "@text/actionSendButtonLabel", description = "@text/actionSendButtonDesc")
200     public void sendButton(
201             @ActionInput(name = "text", label = "@text/actionSendButtonInputButtonLabel", description = "@text/actionSendButtonInputButtonDesc") String button) {
202         try {
203             switch (Button.valueOf(button)) {
204                 case UP:
205                     getConnectedSocket().ifPresent(control -> control.executeMouse(s -> s.button(ButtonType.UP)));
206                     break;
207                 case DOWN:
208                     getConnectedSocket().ifPresent(control -> control.executeMouse(s -> s.button(ButtonType.DOWN)));
209                     break;
210                 case LEFT:
211                     getConnectedSocket().ifPresent(control -> control.executeMouse(s -> s.button(ButtonType.LEFT)));
212                     break;
213                 case RIGHT:
214                     getConnectedSocket().ifPresent(control -> control.executeMouse(s -> s.button(ButtonType.RIGHT)));
215                     break;
216                 case BACK:
217                     getConnectedSocket().ifPresent(control -> control.executeMouse(s -> s.button(ButtonType.BACK)));
218                     break;
219                 case DELETE:
220                     getConnectedSocket().ifPresent(control -> control.sendDelete());
221                     break;
222                 case ENTER:
223                     getConnectedSocket().ifPresent(control -> control.sendEnter());
224                     break;
225                 case HOME:
226                     getConnectedSocket().ifPresent(control -> control.executeMouse(s -> s.button("HOME")));
227                     break;
228                 case OK:
229                     getConnectedSocket().ifPresent(control -> control.executeMouse(s -> s.click()));
230                     break;
231             }
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(", ")));
235         }
236     }
237
238     @Override
239     @RuleAction(label = "@text/actionIncreaseChannelLabel", description = "@text/actionIncreaseChannelDesc")
240     public void increaseChannel() {
241         getConnectedSocket().ifPresent(control -> control.channelUp(createResponseListener()));
242     }
243
244     @Override
245     @RuleAction(label = "@text/actionDecreaseChannelLabel", description = "@text/actionDecreaseChannelDesc")
246     public void decreaseChannel() {
247         getConnectedSocket().ifPresent(control -> control.channelDown(createResponseListener()));
248     }
249
250     @Override
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)));
255     }
256
257     private Optional<LGWebOSTVSocket> getConnectedSocket() {
258         LGWebOSHandler lgWebOSHandler = getLGWebOSHandler();
259         final LGWebOSTVSocket socket = lgWebOSHandler.getSocket();
260
261         if (socket.getState() != State.REGISTERED) {
262             logger.warn("Device with ThingID {} is currently not connected.", lgWebOSHandler.getThing().getUID());
263             return Optional.empty();
264         }
265
266         return Optional.of(socket);
267     }
268
269     private ResponseListener<TextInputStatusInfo> createTextInputStatusListener() {
270         return new ResponseListener<TextInputStatusInfo>() {
271
272             @Override
273             public void onError(@Nullable String error) {
274                 logger.warn("Response: {}", error);
275             }
276
277             @Override
278             public void onSuccess(@Nullable TextInputStatusInfo info) {
279                 logger.debug("Response: {}", info == null ? "OK" : info.getRawData());
280             }
281         };
282     }
283
284     private <O> ResponseListener<O> createResponseListener() {
285         return new ResponseListener<O>() {
286
287             @Override
288             public void onError(@Nullable String error) {
289                 logger.warn("Response: {}", error);
290             }
291
292             @Override
293             public void onSuccess(@Nullable O object) {
294                 logger.debug("Response: {}", object == null ? "OK" : object.toString());
295             }
296         };
297     }
298
299     // delegation methods for "legacy" rule support
300
301     private static ILGWebOSActions invokeMethodOf(@Nullable ThingActions actions) {
302         if (actions == null) {
303             throw new IllegalArgumentException("actions cannot be null");
304         }
305         if (actions.getClass().getName().equals(LGWebOSActions.class.getName())) {
306             if (actions instanceof ILGWebOSActions) {
307                 return (ILGWebOSActions) actions;
308             } else {
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);
314                         });
315             }
316         }
317         throw new IllegalArgumentException("Actions is not an instance of LGWebOSActions");
318     }
319
320     public static void showToast(@Nullable ThingActions actions, String text) throws IOException {
321         invokeMethodOf(actions).showToast(text);
322     }
323
324     public static void showToast(@Nullable ThingActions actions, String icon, String text) throws IOException {
325         invokeMethodOf(actions).showToast(icon, text);
326     }
327
328     public static void launchBrowser(@Nullable ThingActions actions, String url) {
329         invokeMethodOf(actions).launchBrowser(url);
330     }
331
332     public static void launchApplication(@Nullable ThingActions actions, String appId) {
333         invokeMethodOf(actions).launchApplication(appId);
334     }
335
336     public static void launchApplication(@Nullable ThingActions actions, String appId, String param) {
337         invokeMethodOf(actions).launchApplication(appId, param);
338     }
339
340     public static void sendText(@Nullable ThingActions actions, String text) {
341         invokeMethodOf(actions).sendText(text);
342     }
343
344     public static void sendButton(@Nullable ThingActions actions, String button) {
345         invokeMethodOf(actions).sendButton(button);
346     }
347
348     public static void increaseChannel(@Nullable ThingActions actions) {
349         invokeMethodOf(actions).increaseChannel();
350     }
351
352     public static void decreaseChannel(@Nullable ThingActions actions) {
353         invokeMethodOf(actions).decreaseChannel();
354     }
355
356     public static void sendRCButton(@Nullable ThingActions actions, String rcButton) {
357         invokeMethodOf(actions).sendRCButton(rcButton);
358     }
359 }